Skip to content

Commit 4c8dc60

Browse files
Elijah Sawyerskiaraarose
authored andcommitted
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 fc4f94d commit 4c8dc60

File tree

18 files changed

+314
-13
lines changed

18 files changed

+314
-13
lines changed

docs/writing-tests/testharness.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,35 @@ 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.runTests([
222+
function getURLWithNoParameter() {
223+
browser.test.assertThrows(() => browser.runtime.getURL())
224+
}
225+
])
226+
```
227+
228+
This test could then be run from `web-extensions/browser.runtime.extension.html`.
229+
230+
## Other features of `.window.js`, `.worker.js`, `.any.js` and `.extension.js`
203231

204232
### Specifying a test title
205233

resources/testdriver.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2169,6 +2169,43 @@
21692169
*/
21702170
clear_display_features: function(context=null) {
21712171
return window.test_driver_internal.clear_display_features(context);
2172+
},
2173+
2174+
/**
2175+
* Installs a WebExtension.
2176+
*
2177+
* Matches the `Install WebExtension
2178+
* <https://github.com/w3c/webextensions/blob/main/specification/webdriver-classic.bs>`_
2179+
* WebDriver command.
2180+
*
2181+
* @param {Object} params - Parameters for loading the extension.
2182+
* @param {String} params.type - A type such as "path", "archivePath", or "base64".
2183+
*
2184+
* @param {String} params.path - The path to the extension's resources if type "path" or "archivePath" is specified.
2185+
*
2186+
* @param {String} params.value - The base64 encoded value of the extension's resources if type "base64" is specified.
2187+
*
2188+
* @returns {Promise} Returns the extension identifier as defined in the spec.
2189+
* Rejected if the extension fails to load.
2190+
*/
2191+
install_web_extension: function(params) {
2192+
return window.test_driver_internal.install_web_extension(params);
2193+
},
2194+
2195+
/**
2196+
* Uninstalls a WebExtension.
2197+
*
2198+
* Matches the `Uninstall WebExtension
2199+
* <https://github.com/w3c/webextensions/blob/main/specification/webdriver-classic.bs>`_
2200+
* WebDriver command.
2201+
*
2202+
* @param {String} extension_id - The extension identifier.
2203+
*
2204+
* @returns {Promise} Fulfilled after the extension has been removed.
2205+
* Rejected in case the WebDriver command errors out.
2206+
*/
2207+
uninstall_web_extension: function(extension_id) {
2208+
return window.test_driver_internal.uninstall_web_extension(extension_id);
21722209
}
21732210
};
21742211

resources/web-extensions-helper.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// testharness file with WebExtensions utilities
2+
3+
/**
4+
* Loads the WebExtension at the path specified and runs the tests defined in the extension's resources.
5+
* Listens to messages sent from the user agent and converts the `browser.test` assertions
6+
* into testharness.js assertions.
7+
*
8+
* @param {string} extensionPath - a path to the extension's resources.
9+
*/
10+
11+
setup({ explicit_done: true })
12+
globalThis.runTestsWithWebExtension = function(extensionPath) {
13+
test_driver.install_web_extension({
14+
type: "path",
15+
path: extensionPath
16+
})
17+
.then((result) => {
18+
let test;
19+
browser.test.onTestStarted.addListener((data) => {
20+
test = async_test(data.testName)
21+
})
22+
23+
browser.test.onTestFinished.addListener((data) => {
24+
test.step(() => {
25+
let description = data.message ? `${data.assertionDescription}. ${data.message}` : data.assertionDescription
26+
assert_true(data.result, description)
27+
})
28+
29+
test.done()
30+
31+
if (!data.result) {
32+
test.set_status(test.FAIL)
33+
}
34+
35+
if (!data.remainingTests) {
36+
test_driver.uninstall_web_extension(result.extension).then(() => { done() })
37+
}
38+
})
39+
})
40+
}

tools/lint/lint.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -600,7 +600,7 @@ def is_query_string_correct(script: Text, src: Text,
600600
if not is_path_correct("testdriver.js", src):
601601
errors.append(rules.TestdriverPath.error(path))
602602
if not is_query_string_correct("testdriver.js", src,
603-
{'feature': ['bidi']}):
603+
{'feature': ['bidi', 'extensions']}):
604604
errors.append(rules.TestdriverUnsupportedQueryParameter.error(path))
605605

606606
if (not is_path_correct("testdriver-vendor.js", src) or

tools/manifest/sourcefile.py

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

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

467473
@cached_property
468474
def script_metadata(self) -> Optional[List[Tuple[Text, Text]]]:
469-
if self.name_is_worker or self.name_is_multi_global or self.name_is_window:
475+
if self.name_is_worker or self.name_is_multi_global or self.name_is_window or self.name_is_extension:
470476
regexp = js_meta_re
471477
elif self.name_is_webdriver:
472478
regexp = python_meta_re
@@ -911,6 +917,9 @@ def possible_types(self) -> Set[Text]:
911917
if self.name_is_window:
912918
return {TestharnessTest.item_type}
913919

920+
if self.name_is_extension:
921+
return {TestharnessTest.item_type}
922+
914923
if self.markup_type is None:
915924
return {SupportFile.item_type}
916925

@@ -1075,6 +1084,22 @@ def manifest_items(self) -> Tuple[Text, List[ManifestItem]]:
10751084
]
10761085
rv = TestharnessTest.item_type, tests
10771086

1087+
elif self.name_is_extension:
1088+
test_url = replace_end(self.rel_url, ".extension.js", ".extension.html")
1089+
tests = [
1090+
TestharnessTest(
1091+
self.tests_root,
1092+
self.rel_path,
1093+
self.url_base,
1094+
test_url + variant,
1095+
timeout=self.timeout,
1096+
pac=self.pac,
1097+
script_metadata=self.script_metadata
1098+
)
1099+
for variant in self.test_variants
1100+
]
1101+
rv = TestharnessTest.item_type, tests
1102+
10781103
elif self.content_is_css_manual and not self.name_is_reference:
10791104
rv = ManualTest.item_type, [
10801105
ManualTest(

tools/serve/serve.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,21 @@ class WindowHandler(HtmlWrapperHandler):
311311
<script src="%(path)s"></script>
312312
"""
313313

314+
class ExtensionHandler(HtmlWrapperHandler):
315+
path_replace = [(".extension.html", ".extension.js")]
316+
wrapper = """<!doctype html>
317+
<meta charset=utf-8>
318+
%(meta)s
319+
<script src="/resources/testharness.js"></script>
320+
<script src="/resources/testharnessreport.js"></script>
321+
<script src="/resources/testdriver.js?feature=extensions"></script>
322+
<script src="/resources/testdriver-vendor.js"></script>
323+
<script src="/resources/web-extensions-helper.js"></script>
324+
%(script)s
325+
<div id=log></div>
326+
<script src="%(path)s"></script>
327+
"""
328+
314329

315330
class WindowModulesHandler(HtmlWrapperHandler):
316331
global_type = "window-module"
@@ -772,6 +787,7 @@ def add_mount_point(self, url_base, path):
772787
("GET", "*.worker.html", WorkersHandler),
773788
("GET", "*.worker-module.html", WorkerModulesHandler),
774789
("GET", "*.window.html", WindowHandler),
790+
("GET", "*.extension.html", ExtensionHandler),
775791
("GET", "*.any.html", AnyHtmlHandler),
776792
("GET", "*.any.sharedworker.html", SharedWorkersHandler),
777793
("GET", "*.any.sharedworker-module.html", SharedWorkerModulesHandler),

tools/webdriver/webdriver/client.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,7 @@ def __init__(self,
407407
self.find = Find(self)
408408
self.alert = UserPrompt(self)
409409
self.actions = Actions(self)
410+
self.web_extensions = WebExtensions(self)
410411

411412
def __repr__(self):
412413
return "<%s %s>" % (self.__class__.__name__, self.session_id or "(disconnected)")
@@ -850,6 +851,21 @@ def property(self, name):
850851
return self.send_element_command("GET", "property/%s" % name)
851852

852853

854+
class WebExtensions:
855+
def __init__(self, session):
856+
self.session = session
857+
858+
def install(self, type, path=None, value=None):
859+
body = {"type": type}
860+
if path is not None:
861+
body["path"] = path
862+
elif value is not None:
863+
body["value"] = value
864+
return self.session.send_session_command("POST", "webextension", body)
865+
866+
def uninstall(self, extension_id):
867+
return self.session.send_session_command("DELETE", "webextension/%s" % extension_id)
868+
853869
class WebFrame:
854870
identifier = "frame-075b-4da1-b6ba-e579c2d3230a"
855871

tools/wptrunner/wptrunner/browsers/chrome.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,12 @@ def require_webdriver_bidi(self) -> Optional[bool]:
294294
def settings(self, test: Test) -> BrowserSettings:
295295
""" Required to store `require_webdriver_bidi` in browser settings."""
296296
settings = super().settings(test)
297-
self._require_webdriver_bidi = test.testdriver_features is not None and 'bidi' in test.testdriver_features
297+
self._require_webdriver_bidi = (
298+
test.testdriver_features is not None and (
299+
'bidi' in test.testdriver_features or
300+
'extensions' in test.testdriver_features
301+
)
302+
)
298303

299304
return {
300305
**settings,

tools/wptrunner/wptrunner/executors/actions.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -544,6 +544,32 @@ def __init__(self, logger, protocol):
544544
def __call__(self, payload):
545545
return self.protocol.display_features.clear_display_features()
546546

547+
class WebExtensionInstallAction:
548+
name = "install_web_extension"
549+
550+
def __init__(self, logger, protocol):
551+
self.logger = logger
552+
self.protocol = protocol
553+
554+
def __call__(self, payload):
555+
self.logger.debug("installing web extension")
556+
type = payload["type"]
557+
path = payload.get("path")
558+
value = payload.get("value")
559+
return self.protocol.web_extensions.install_web_extension(type, path, value)
560+
561+
class WebExtensionUninstallAction:
562+
name = "uninstall_web_extension"
563+
564+
def __init__(self, logger, protocol):
565+
self.logger = logger
566+
self.protocol = protocol
567+
568+
def __call__(self, payload):
569+
self.logger.debug("uninstalling web extension")
570+
extension_id = payload["extension_id"]
571+
return self.protocol.web_extensions.uninstall_web_extension(extension_id)
572+
547573
actions = [ClickAction,
548574
DeleteAllCookiesAction,
549575
GetAllCookiesAction,
@@ -586,4 +612,6 @@ def __call__(self, payload):
586612
RemoveVirtualPressureSourceAction,
587613
SetProtectedAudienceKAnonymityAction,
588614
SetDisplayFeaturesAction,
589-
ClearDisplayFeaturesAction]
615+
ClearDisplayFeaturesAction,
616+
WebExtensionInstallAction,
617+
WebExtensionUninstallAction]

tools/wptrunner/wptrunner/executors/asyncactions.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,4 +314,6 @@ async def __call__(self, payload):
314314
BidiEmulationSetScreenOrientationOverrideAction,
315315
BidiPermissionsSetPermissionAction,
316316
BidiSessionSubscribeAction,
317-
BidiSessionUnsubscribeAction]
317+
BidiSessionUnsubscribeAction,
318+
BidiPermissionsSetPermissionAction,
319+
BidiSessionSubscribeAction]

0 commit comments

Comments
 (0)