Skip to content

Commit be054c4

Browse files
author
Elijah Sawyers
committed
Add the ability to load and test web extensions.
Validate this change by writing a few simple tests that verify that some APIs on browser.runtime behave as expected.
1 parent c67ae73 commit be054c4

File tree

14 files changed

+319
-5
lines changed

14 files changed

+319
-5
lines changed

docs/writing-tests/testharness.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,33 @@ dedicated worker tests and shared worker
199199
tests](testharness-api.html#determining-when-all-tests-are-complete), it is
200200
automatically invoked for tests defined using the "multi-global" pattern.
201201

202-
## Other features of `.window.js`, `.worker.js` and `.any.js`
202+
## Extension tests (`.extension.js`)
203+
204+
Create a JavaScript file whose name ends in `.extension.js` to have the necessary HTML boilerplate
205+
generated for you at `.extension.html`.
206+
207+
Extension tests leverage the `browser.test` API rather than interacting with the `testharness.js`
208+
framework directly.
209+
210+
For example, one could write a test for `browser.runtime.getURL()` by creating a
211+
`web-extensions/browser.runtime.extension.js` file as follows:
212+
213+
```js
214+
runTestsWithWebExtension("/resources/runtime/")
215+
// ==> this method assumes that the extension resources (manifest, scripts, etc.) exist at the path
216+
```
217+
218+
And by creating a `web-extensions/resources/runtime/background.js` file as follows:
219+
220+
```js
221+
browser.test.addTest(function getURLWithNoParameter() {
222+
browser.test.assertThrows(() => browser.runtime.getURL())
223+
})
224+
```
225+
226+
This test could then be run from `web-extensions/browser.runtime.extension.html`.
227+
228+
## Other features of `.window.js`, `.worker.js`, `.any.js` and `.extension.js`
203229

204230
### Specifying a test title
205231

resources/testdriver.js

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1302,7 +1302,47 @@
13021302
*/
13031303
remove_virtual_pressure_source: function(source_type, context=null) {
13041304
return window.test_driver_internal.remove_virtual_pressure_source(source_type, context);
1305-
}
1305+
},
1306+
1307+
/**
1308+
* Loads a WebExtension.
1309+
*
1310+
* Matches the `Load Web Extension
1311+
* <https://github.com/w3c/webextensions/blob/main/specification/webdriver-classic.bs>`_
1312+
* WebDriver command.
1313+
* In addition to the
1314+
* `webExtension.Install <https://www.w3.org/TR/webdriver-bidi/#command-webExtension-install> command`_
1315+
*
1316+
* @param {String} type - A type such as "path", "archviePath", or "base64".
1317+
*
1318+
* @param {String} path - The path to the extension's resources if type "path" or "archivePath" is specified.
1319+
*
1320+
* @param {String} value - The base64 encoded value of the extension's resources if type "base64" is specified.
1321+
*
1322+
* @returns {Promise} Returns the extension identifier as defined in the specs.
1323+
* Rejected if the extension fails to load.
1324+
*/
1325+
load_web_extension: function(path) {
1326+
return window.test_driver_internal.load_web_extension(path);
1327+
},
1328+
1329+
/**
1330+
* Unloads a WebExtension.
1331+
*
1332+
* Matches the `Unload Web Extension
1333+
* <https://github.com/w3c/webextensions/blob/main/specification/webdriver-classic.bs>`_
1334+
* WebDriver command.
1335+
* In addition to the
1336+
* `webExtension.Uninstall <https://www.w3.org/TR/webdriver-bidi/#command-webExtension-uninstall> command`_
1337+
*
1338+
* @param {String} extension_id - The extension idetifier.
1339+
*
1340+
* @returns {Promise} Fulfilled after the extension has been removed.
1341+
* Rejected in case the WebDriver command errors out.
1342+
*/
1343+
unload_web_extension: function(extension) {
1344+
return window.test_driver_internal.unload_web_extension(extension);
1345+
},
13061346
};
13071347

13081348
window.test_driver_internal = {

tools/manifest/sourcefile.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,12 @@ def name_is_window(self) -> bool:
379379
be a window js test file"""
380380
return "window" in self.meta_flags and self.ext == ".js"
381381

382+
@property
383+
def name_is_extension(self) -> bool:
384+
"""Check if the file name matches the conditions for the file to
385+
be a extension js test file"""
386+
return "extension" in self.meta_flags and self.ext == ".js"
387+
382388
@property
383389
def name_is_webdriver(self) -> bool:
384390
"""Check if the file name matches the conditions for the file to
@@ -467,7 +473,7 @@ def pac_nodes(self) -> List[ElementTree.Element]:
467473

468474
@cached_property
469475
def script_metadata(self) -> Optional[List[Tuple[Text, Text]]]:
470-
if self.name_is_worker or self.name_is_multi_global or self.name_is_window:
476+
if self.name_is_worker or self.name_is_multi_global or self.name_is_window or self.name_is_extension:
471477
regexp = js_meta_re
472478
elif self.name_is_webdriver:
473479
regexp = python_meta_re
@@ -865,6 +871,9 @@ def possible_types(self) -> Set[Text]:
865871
if self.name_is_window:
866872
return {TestharnessTest.item_type}
867873

874+
if self.name_is_extension:
875+
return {TestharnessTest.item_type}
876+
868877
if self.markup_type is None:
869878
return {SupportFile.item_type}
870879

@@ -1004,6 +1013,7 @@ def manifest_items(self) -> Tuple[Text, List[ManifestItem]]:
10041013
test_url + variant,
10051014
timeout=self.timeout,
10061015
pac=self.pac,
1016+
extension=self.extension,
10071017
script_metadata=self.script_metadata
10081018
)
10091019
for variant in self.test_variants
@@ -1026,6 +1036,22 @@ def manifest_items(self) -> Tuple[Text, List[ManifestItem]]:
10261036
]
10271037
rv = TestharnessTest.item_type, tests
10281038

1039+
elif self.name_is_extension:
1040+
test_url = replace_end(self.rel_url, ".extension.js", ".extension.html")
1041+
tests = [
1042+
TestharnessTest(
1043+
self.tests_root,
1044+
self.rel_path,
1045+
self.url_base,
1046+
test_url + variant,
1047+
timeout=self.timeout,
1048+
pac=self.pac,
1049+
script_metadata=self.script_metadata
1050+
)
1051+
for variant in self.test_variants
1052+
]
1053+
rv = TestharnessTest.item_type, tests
1054+
10291055
elif self.content_is_css_manual and not self.name_is_reference:
10301056
rv = ManualTest.item_type, [
10311057
ManualTest(

tools/serve/serve.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,64 @@ class WindowHandler(HtmlWrapperHandler):
314314
<script src="%(path)s"></script>
315315
"""
316316

317+
class ExtensionHandler(HtmlWrapperHandler):
318+
path_replace = [(".extension.html", ".extension.js")]
319+
wrapper = """<!doctype html>
320+
<meta charset=utf-8>
321+
%(meta)s
322+
<script src="/resources/testharness.js"></script>
323+
<script src="/resources/testharnessreport.js"></script>
324+
<script src="/resources/testdriver.js?feature=bidi"></script>
325+
<script src="/resources/testdriver-vendor.js"></script>
326+
%(script)s
327+
<div id=log></div>
328+
<script>
329+
setup({ explicit_done: true })
330+
331+
function runTestsWithWebExtension(extensionPath) {
332+
test_driver.load_web_extension({
333+
type: "path",
334+
path: extensionPath
335+
})
336+
.then((result) => {
337+
let test;
338+
339+
browser.test.onMessage.addListener((message, data) => {
340+
switch(message) {
341+
case "assert":
342+
test.step(() => {
343+
assert_true(data.result, data.message)
344+
})
345+
346+
break
347+
case "assert-equality":
348+
test.step(() => {
349+
assert_true(data.result, `Expected: ${data.expectedValue}; Actual: ${data.actualValue}; Description: ${data.message}`)
350+
})
351+
352+
break
353+
case "test-started":
354+
test = async_test(data.testName)
355+
356+
break
357+
case "test-finished":
358+
test.done()
359+
360+
if (!data.remainingTests)
361+
test_driver.unload_web_extension(result.extension)
362+
.then(() => {
363+
done()
364+
})
365+
366+
break
367+
}
368+
})
369+
})
370+
}
371+
</script>
372+
<script src="%(path)s"></script>
373+
"""
374+
317375

318376
class WindowModulesHandler(HtmlWrapperHandler):
319377
global_type = "window-module"
@@ -775,6 +833,7 @@ def add_mount_point(self, url_base, path):
775833
("GET", "*.worker.html", WorkersHandler),
776834
("GET", "*.worker-module.html", WorkerModulesHandler),
777835
("GET", "*.window.html", WindowHandler),
836+
("GET", "*.extension.html", ExtensionHandler),
778837
("GET", "*.any.html", AnyHtmlHandler),
779838
("GET", "*.any.sharedworker.html", SharedWorkersHandler),
780839
("GET", "*.any.sharedworker-module.html", SharedWorkerModulesHandler),
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from typing import Any, Mapping
2+
3+
from ._module import BidiModule, command
4+
5+
6+
class WebExtension(BidiModule):
7+
@command
8+
def install(self, params: Mapping[str, str]) -> Mapping[str, Any]:
9+
"""
10+
Represents a command `webExtension.install` specified in
11+
https://www.w3.org/TR/webdriver-bidi/#command-webExtension-install
12+
"""
13+
14+
return params
15+
16+
@command
17+
def uninstall(self, extension_id) -> Mapping[str, Any]:
18+
"""
19+
Represents a command `webExtension.Uninstall` specified in
20+
https://www.w3.org/TR/webdriver-bidi/#command-webExtension-uninstall
21+
"""
22+
23+
return {
24+
"extension": extension_id
25+
}

tools/webdriver/webdriver/client.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -763,6 +763,14 @@ def print(self,
763763
body[prop] = value
764764
return self.send_session_command("POST", "print", body)
765765

766+
@command
767+
def load_web_extension(self, extension):
768+
return self.send_session_command("POST", "webextension", extension)
769+
770+
@command
771+
def unload_web_extension(self, extension_id):
772+
return self.send_session_command("DELETE", "webextension/%s" % extension_id)
773+
766774

767775
class ShadowRoot:
768776
identifier = "shadow-6066-11e4-a52e-4f735466cecf"

tools/wptrunner/wptrunner/executors/actions.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,28 @@ def __call__(self, payload):
511511
source_type = payload["source_type"]
512512
return self.protocol.pressure.remove_virtual_pressure_source(source_type)
513513

514+
class LoadWebExtensionAction:
515+
name = "load_web_extension"
516+
517+
def __init__(self, logger, protocol):
518+
self.logger = logger
519+
self.protocol = protocol
520+
521+
def __call__(self, payload):
522+
self.logger.debug("loading web extension")
523+
return self.protocol.web_extensions.load_web_extension(payload["extension"])
524+
525+
class UnloadWebExtensionAction:
526+
name = "unload_web_extension"
527+
528+
def __init__(self, logger, protocol):
529+
self.logger = logger
530+
self.protocol = protocol
531+
532+
def __call__(self, payload):
533+
self.logger.debug("unloading web extension")
534+
return self.protocol.web_extensions.unload_web_extension(payload["extension_id"])
535+
514536
actions = [ClickAction,
515537
DeleteAllCookiesAction,
516538
GetAllCookiesAction,
@@ -550,4 +572,6 @@ def __call__(self, payload):
550572
RunBounceTrackingMitigationsAction,
551573
CreateVirtualPressureSourceAction,
552574
UpdateVirtualPressureSourceAction,
553-
RemoveVirtualPressureSourceAction]
575+
RemoveVirtualPressureSourceAction,
576+
LoadWebExtensionAction,
577+
UnloadWebExtensionAction]

tools/wptrunner/wptrunner/executors/executorwebdriver.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
DevicePostureProtocolPart,
4646
StorageProtocolPart,
4747
VirtualPressureSourceProtocolPart,
48+
WebExtensionsProtocolPart,
4849
merge_dicts)
4950

5051
from typing import List, Optional, Tuple
@@ -251,6 +252,23 @@ async def set_permission(self, descriptor, state, origin):
251252
return await self.webdriver.bidi_session.permissions.set_permission(
252253
descriptor=descriptor, state=state, origin=origin)
253254

255+
class WebDriverBidiWebExtensionsProtocolPart(WebExtensionsProtocolPart):
256+
def __init__(self, parent):
257+
super().__init__(parent)
258+
self.webdriver = None
259+
260+
def setup(self):
261+
self.webdriver = self.parent.webdriver
262+
263+
def load_web_extension(self, extension):
264+
if extension["type"] == "path" and self.parent.test_path:
265+
extension_path = self.parent.test_path[:self.parent.test_path.rfind('/')]
266+
extension["path"] = extension_path + extension.get("path")
267+
268+
return self.webdriver.loop.run_until_complete(self.webdriver.bidi_session.web_extension.install(extension))
269+
270+
def unload_web_extension(self, extension_id):
271+
return self.webdriver.loop.run_until_complete(self.webdriver.bidi_session.web_extension.uninstall(extension_id))
254272

255273
class WebDriverTestharnessProtocolPart(TestharnessProtocolPart):
256274
def setup(self):
@@ -669,6 +687,20 @@ def update_virtual_pressure_source(self, source_type, sample):
669687
def remove_virtual_pressure_source(self, source_type):
670688
return self.webdriver.send_session_command("DELETE", "pressuresource/%s" % source_type)
671689

690+
class WebDriverWebExtensionsProtocolPart(WebExtensionsProtocolPart):
691+
def setup(self):
692+
self.webdriver = self.parent.webdriver
693+
694+
def load_web_extension(self, extension):
695+
if extension["type"] == "path" and self.parent.test_path:
696+
extension_path = self.parent.test_path[:self.parent.test_path.rfind('/')]
697+
extension["path"] = extension_path + extension.get("path")
698+
699+
return self.webdriver.load_web_extension(extension)
700+
701+
def unload_web_extension(self, extension_id):
702+
return self.webdriver.unload_web_extension(extension_id)
703+
672704

673705
class WebDriverProtocol(Protocol):
674706
enable_bidi = False
@@ -693,7 +725,8 @@ class WebDriverProtocol(Protocol):
693725
WebDriverVirtualSensorPart,
694726
WebDriverDevicePostureProtocolPart,
695727
WebDriverStorageProtocolPart,
696-
WebDriverVirtualPressureSourceProtocolPart]
728+
WebDriverVirtualPressureSourceProtocolPart,
729+
WebDriverWebExtensionsProtocolPart]
697730

698731
def __init__(self, executor, browser, capabilities, **kwargs):
699732
super().__init__(executor, browser)
@@ -769,6 +802,7 @@ class WebDriverBidiProtocol(WebDriverProtocol):
769802
WebDriverBidiEventsProtocolPart,
770803
WebDriverBidiPermissionsProtocolPart,
771804
WebDriverBidiScriptProtocolPart,
805+
WebDriverBidiWebExtensionsProtocolPart,
772806
*(part for part in WebDriverProtocol.implements)
773807
]
774808

@@ -975,6 +1009,7 @@ def on_environment_change(self, new_environment):
9751009

9761010
def do_test(self, test):
9771011
url = self.test_url(test)
1012+
self.protocol.test_path = test.path
9781013

9791014
success, data = WebDriverRun(self.logger,
9801015
self.do_testharness,

0 commit comments

Comments
 (0)