Skip to content

Commit 8f96b02

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 0a2208a commit 8f96b02

File tree

17 files changed

+320
-7
lines changed

17 files changed

+320
-7
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: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1497,6 +1497,42 @@
14971497
*/
14981498
set_protected_audience_k_anonymity: function(owner, name, hashes, context = null) {
14991499
return window.test_driver_internal.set_protected_audience_k_anonymity(owner, name, hashes, context);
1500+
},
1501+
1502+
/**
1503+
* Loads a WebExtension.
1504+
*
1505+
* Matches the `Load Web Extension
1506+
* <https://github.com/w3c/webextensions/blob/main/specification/webdriver-classic.bs>`_
1507+
* WebDriver command.
1508+
*
1509+
* @param {String} type - A type such as "path", "archivePath", or "base64".
1510+
*
1511+
* @param {String} path - The path to the extension's resources if type "path" or "archivePath" is specified.
1512+
*
1513+
* @param {String} value - The base64 encoded value of the extension's resources if type "base64" is specified.
1514+
*
1515+
* @returns {Promise} Returns the extension identifier as defined in the spec.
1516+
* Rejected if the extension fails to load.
1517+
*/
1518+
install_web_extension: function(path) {
1519+
return window.test_driver_internal.install_web_extension(path);
1520+
},
1521+
1522+
/**
1523+
* Unloads a WebExtension.
1524+
*
1525+
* Matches the `Unload Web Extension
1526+
* <https://github.com/w3c/webextensions/blob/main/specification/webdriver-classic.bs>`_
1527+
* WebDriver command.
1528+
*
1529+
* @param {String} extension_id - The extension idetifier.
1530+
*
1531+
* @returns {Promise} Fulfilled after the extension has been removed.
1532+
* Rejected in case the WebDriver command errors out.
1533+
*/
1534+
uninstall_web_extension: function(extension) {
1535+
return window.test_driver_internal.uninstall_web_extension(extension);
15001536
}
15011537
};
15021538

resources/web-extensions-helper.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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+
13+
globalThis.runTestsWithWebExtension = function(extensionPath) {
14+
test_driver.install_web_extension({
15+
type: "path",
16+
path: extensionPath
17+
})
18+
.then((result) => {
19+
let test;
20+
21+
browser.test.onTestStarted.addListener((message, data) => {
22+
test = async_test(data.testName)
23+
})
24+
25+
browser.test.onTestStarted.addListener((message, data) => {
26+
})
27+
28+
browser.test.onTestFinished.addListener((message, data) => {
29+
test.step(() => {
30+
let description = data.message ? `${data.assertionDescription}. ${data.message}` : data.assertionDescription
31+
assert_true(data.result, description)
32+
})
33+
test.done()
34+
35+
if (!data.result)
36+
test.set_status(test.FAIL)
37+
38+
if (!data.remainingTests)
39+
test_driver.uninstall_web_extension(result.extension).then(() => { done() })
40+
})
41+
})
42+
}

tools/lint/lint.py

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

605605
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
@@ -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
@@ -912,6 +918,9 @@ def possible_types(self) -> Set[Text]:
912918
if self.name_is_window:
913919
return {TestharnessTest.item_type}
914920

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

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

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

tools/serve/serve.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,21 @@ 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=extensions"></script>
325+
<script src="/resources/testdriver-vendor.js"></script>
326+
<script src="/resources/web-extensions-helper.js"></script>
327+
%(script)s
328+
<div id=log></div>
329+
<script src="%(path)s"></script>
330+
"""
331+
317332

318333
class WindowModulesHandler(HtmlWrapperHandler):
319334
global_type = "window-module"
@@ -775,6 +790,7 @@ def add_mount_point(self, url_base, path):
775790
("GET", "*.worker.html", WorkersHandler),
776791
("GET", "*.worker-module.html", WorkerModulesHandler),
777792
("GET", "*.window.html", WindowHandler),
793+
("GET", "*.extension.html", ExtensionHandler),
778794
("GET", "*.any.html", AnyHtmlHandler),
779795
("GET", "*.any.sharedworker.html", SharedWorkersHandler),
780796
("GET", "*.any.sharedworker-module.html", SharedWorkerModulesHandler),

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 install_web_extension(self, extension):
768+
return self.send_session_command("POST", "webextension", extension)
769+
770+
@command
771+
def uninstall_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/browsers/chrome.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ def require_webdriver_bidi(self) -> Optional[bool]:
278278
def settings(self, test: Test) -> BrowserSettings:
279279
""" Required to store `require_webdriver_bidi` in browser settings."""
280280
settings = super().settings(test)
281-
self._require_webdriver_bidi = test.testdriver_features is not None and 'bidi' in test.testdriver_features
281+
self._require_webdriver_bidi = (test.testdriver_features is not None and ('bidi' in test.testdriver_features or 'extensions' in test.testdriver_features))
282282

283283
return {
284284
**settings,

tools/wptrunner/wptrunner/executors/actions.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,28 @@ def __call__(self, payload):
523523
owner, name, hashes = payload["owner"], payload["name"], payload["hashes"]
524524
return self.protocol.protected_audience.set_k_anonymity(owner, name, hashes)
525525

526+
class InstallWebExtensionAction:
527+
name = "install_web_extension"
528+
529+
def __init__(self, logger, protocol):
530+
self.logger = logger
531+
self.protocol = protocol
532+
533+
def __call__(self, payload):
534+
self.logger.debug("installing web extension")
535+
return self.protocol.web_extensions.install_web_extension(payload["extension"])
536+
537+
class UninstallWebExtensionAction:
538+
name = "uninstall_web_extension"
539+
540+
def __init__(self, logger, protocol):
541+
self.logger = logger
542+
self.protocol = protocol
543+
544+
def __call__(self, payload):
545+
self.logger.debug("uninstalling web extension")
546+
return self.protocol.web_extensions.uninstall_web_extension(payload["extension_id"])
547+
526548
actions = [ClickAction,
527549
DeleteAllCookiesAction,
528550
GetAllCookiesAction,
@@ -563,4 +585,6 @@ def __call__(self, payload):
563585
CreateVirtualPressureSourceAction,
564586
UpdateVirtualPressureSourceAction,
565587
RemoveVirtualPressureSourceAction,
566-
SetProtectedAudienceKAnonymityAction]
588+
SetProtectedAudienceKAnonymityAction,
589+
InstallWebExtensionAction,
590+
UninstallWebExtensionAction]

tools/wptrunner/wptrunner/executors/asyncactions.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,10 +128,35 @@ async def __call__(self, payload):
128128
state,
129129
origin)
130130

131+
class BidiInstallWebExtensionAction:
132+
name = "bidi.web_extensions.install_web_extension"
133+
134+
def __init__(self, logger, protocol):
135+
do_delayed_imports()
136+
self.logger = logger
137+
self.protocol = protocol
138+
139+
def __call__(self, payload):
140+
return self.protocol.bidi_web_extensions.install_web_extension(payload['extension'])
141+
142+
143+
class BidiUninstallWebExtensionAction:
144+
name = "bidi.web_extensions.uninstall_web_extension"
145+
146+
def __init__(self, logger, protocol):
147+
do_delayed_imports()
148+
self.logger = logger
149+
self.protocol = protocol
150+
151+
def __call__(self, payload):
152+
return self.protocol.bidi_web_extensions.uninstall_web_extension(payload['extension'])
153+
131154

132155
async_actions = [
133156
BidiBluetoothHandleRequestDevicePrompt,
134157
BidiBluetoothSimulateAdapterAction,
135158
BidiBluetoothSimulatePreconnectedPeripheralAction,
159+
BidiInstallWebExtensionAction,
136160
BidiPermissionsSetPermissionAction,
137-
BidiSessionSubscribeAction]
161+
BidiSessionSubscribeAction,
162+
BidiUninstallWebExtensionAction]

0 commit comments

Comments
 (0)