Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion docs/writing-tests/testharness.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,35 @@ dedicated worker tests and shared worker
tests](testharness-api.html#determining-when-all-tests-are-complete), it is
automatically invoked for tests defined using the "multi-global" pattern.

## Other features of `.window.js`, `.worker.js` and `.any.js`
## Extension tests (`.extension.js`)

Create a JavaScript file whose name ends in `.extension.js` to have the necessary HTML boilerplate
generated for you at `.extension.html`.

Extension tests leverage the `browser.test` API rather than interacting with the `testharness.js`
framework directly.

For example, one could write a test for `browser.runtime.getURL()` by creating a
`web-extensions/browser.runtime.extension.js` file as follows:

```js
runTestsWithWebExtension("/resources/runtime/")
// ==> this method assumes that the extension resources (manifest, scripts, etc.) exist at the path
```

And by creating a `web-extensions/resources/runtime/background.js` file as follows:

```js
browser.test.runTests([
function getURLWithNoParameter() {
browser.test.assertThrows(() => browser.runtime.getURL())
}
])
```

This test could then be run from `web-extensions/browser.runtime.extension.html`.

## Other features of `.window.js`, `.worker.js`, `.any.js` and `.extension.js`

### Specifying a test title

Expand Down
37 changes: 37 additions & 0 deletions resources/testdriver.js
Original file line number Diff line number Diff line change
Expand Up @@ -2169,6 +2169,43 @@
*/
clear_display_features: function(context=null) {
return window.test_driver_internal.clear_display_features(context);
},

/**
* Installs a WebExtension.
*
* Matches the `Install WebExtension
* <https://github.com/w3c/webextensions/blob/main/specification/webdriver-classic.bs>`_
* WebDriver command.
*
* @param {Object} params - Parameters for loading the extension.
* @param {String} params.type - A type such as "path", "archivePath", or "base64".
*
* @param {String} params.path - The path to the extension's resources if type "path" or "archivePath" is specified.
*
* @param {String} params.value - The base64 encoded value of the extension's resources if type "base64" is specified.
*
* @returns {Promise} Returns the extension identifier as defined in the spec.
* Rejected if the extension fails to load.
*/
install_web_extension: function(params) {
return window.test_driver_internal.install_web_extension(params);
},

/**
* Uninstalls a WebExtension.
*
* Matches the `Uninstall WebExtension
* <https://github.com/w3c/webextensions/blob/main/specification/webdriver-classic.bs>`_
* WebDriver command.
*
* @param {String} extension_id - The extension identifier.
*
* @returns {Promise} Fulfilled after the extension has been removed.
* Rejected in case the WebDriver command errors out.
*/
uninstall_web_extension: function(extension_id) {
return window.test_driver_internal.uninstall_web_extension(extension_id);
}
};

Expand Down
40 changes: 40 additions & 0 deletions resources/web-extensions-helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// testharness file with WebExtensions utilities

/**
* Loads the WebExtension at the path specified and runs the tests defined in the extension's resources.
* Listens to messages sent from the user agent and converts the `browser.test` assertions
* into testharness.js assertions.
*
* @param {string} extensionPath - a path to the extension's resources.
*/

setup({ explicit_done: true })
globalThis.runTestsWithWebExtension = function(extensionPath) {
test_driver.install_web_extension({
type: "path",
path: extensionPath
})
.then((result) => {
let test;
browser.test.onTestStarted.addListener((data) => {
test = async_test(data.testName)
})

browser.test.onTestFinished.addListener((data) => {
test.step(() => {
let description = data.message ? `${data.assertionDescription}. ${data.message}` : data.assertionDescription
assert_true(data.result, description)
})

test.done()

if (!data.result) {
test.set_status(test.FAIL)
}

if (!data.remainingTests) {
test_driver.uninstall_web_extension(result.extension).then(() => { done() })
}
})
})
}
2 changes: 1 addition & 1 deletion tools/lint/lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -600,7 +600,7 @@ def is_query_string_correct(script: Text, src: Text,
if not is_path_correct("testdriver.js", src):
errors.append(rules.TestdriverPath.error(path))
if not is_query_string_correct("testdriver.js", src,
{'feature': ['bidi']}):
{'feature': ['bidi', 'extensions']}):
errors.append(rules.TestdriverUnsupportedQueryParameter.error(path))

if (not is_path_correct("testdriver-vendor.js", src) or
Expand Down
27 changes: 26 additions & 1 deletion tools/manifest/sourcefile.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,12 @@ def name_is_window(self) -> bool:
be a window js test file"""
return "window" in self.meta_flags and self.ext == ".js"

@property
def name_is_extension(self) -> bool:
"""Check if the file name matches the conditions for the file to
be a extension js test file"""
return "extension" in self.meta_flags and self.ext == ".js"

@property
def name_is_webdriver(self) -> bool:
"""Check if the file name matches the conditions for the file to
Expand Down Expand Up @@ -466,7 +472,7 @@ def pac_nodes(self) -> List[ElementTree.Element]:

@cached_property
def script_metadata(self) -> Optional[List[Tuple[Text, Text]]]:
if self.name_is_worker or self.name_is_multi_global or self.name_is_window:
if self.name_is_worker or self.name_is_multi_global or self.name_is_window or self.name_is_extension:
regexp = js_meta_re
elif self.name_is_webdriver:
regexp = python_meta_re
Expand Down Expand Up @@ -911,6 +917,9 @@ def possible_types(self) -> Set[Text]:
if self.name_is_window:
return {TestharnessTest.item_type}

if self.name_is_extension:
return {TestharnessTest.item_type}

if self.markup_type is None:
return {SupportFile.item_type}

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

elif self.name_is_extension:
test_url = replace_end(self.rel_url, ".extension.js", ".extension.html")
tests = [
TestharnessTest(
self.tests_root,
self.rel_path,
self.url_base,
test_url + variant,
timeout=self.timeout,
pac=self.pac,
script_metadata=self.script_metadata
)
for variant in self.test_variants
]
rv = TestharnessTest.item_type, tests

elif self.content_is_css_manual and not self.name_is_reference:
rv = ManualTest.item_type, [
ManualTest(
Expand Down
16 changes: 16 additions & 0 deletions tools/serve/serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,21 @@ class WindowHandler(HtmlWrapperHandler):
<script src="%(path)s"></script>
"""

class ExtensionHandler(HtmlWrapperHandler):
path_replace = [(".extension.html", ".extension.js")]
wrapper = """<!doctype html>
<meta charset=utf-8>
%(meta)s
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/resources/testdriver.js?feature=extensions"></script>
<script src="/resources/testdriver-vendor.js"></script>
<script src="/resources/web-extensions-helper.js"></script>
%(script)s
<div id=log></div>
<script src="%(path)s"></script>
"""


class WindowModulesHandler(HtmlWrapperHandler):
global_type = "window-module"
Expand Down Expand Up @@ -772,6 +787,7 @@ def add_mount_point(self, url_base, path):
("GET", "*.worker.html", WorkersHandler),
("GET", "*.worker-module.html", WorkerModulesHandler),
("GET", "*.window.html", WindowHandler),
("GET", "*.extension.html", ExtensionHandler),
("GET", "*.any.html", AnyHtmlHandler),
("GET", "*.any.sharedworker.html", SharedWorkersHandler),
("GET", "*.any.sharedworker-module.html", SharedWorkerModulesHandler),
Expand Down
16 changes: 16 additions & 0 deletions tools/webdriver/webdriver/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,7 @@ def __init__(self,
self.find = Find(self)
self.alert = UserPrompt(self)
self.actions = Actions(self)
self.web_extensions = WebExtensions(self)

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


class WebExtensions:
def __init__(self, session):
self.session = session

def install(self, type, path=None, value=None):
body = {"type": type}
if path is not None:
body["path"] = path
elif value is not None:
body["value"] = value
return self.session.send_session_command("POST", "webextension", body)

def uninstall(self, extension_id):
return self.session.send_session_command("DELETE", "webextension/%s" % extension_id)

class WebFrame:
identifier = "frame-075b-4da1-b6ba-e579c2d3230a"

Expand Down
7 changes: 6 additions & 1 deletion tools/wptrunner/wptrunner/browsers/chrome.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,12 @@ def require_webdriver_bidi(self) -> Optional[bool]:
def settings(self, test: Test) -> BrowserSettings:
""" Required to store `require_webdriver_bidi` in browser settings."""
settings = super().settings(test)
self._require_webdriver_bidi = test.testdriver_features is not None and 'bidi' in test.testdriver_features
self._require_webdriver_bidi = (
test.testdriver_features is not None and (
'bidi' in test.testdriver_features or
'extensions' in test.testdriver_features
)
)

return {
**settings,
Expand Down
30 changes: 29 additions & 1 deletion tools/wptrunner/wptrunner/executors/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,32 @@ def __init__(self, logger, protocol):
def __call__(self, payload):
return self.protocol.display_features.clear_display_features()

class WebExtensionInstallAction:
name = "install_web_extension"

def __init__(self, logger, protocol):
self.logger = logger
self.protocol = protocol

def __call__(self, payload):
self.logger.debug("installing web extension")
type = payload["type"]
path = payload.get("path")
value = payload.get("value")
return self.protocol.web_extensions.install_web_extension(type, path, value)

class WebExtensionUninstallAction:
name = "uninstall_web_extension"

def __init__(self, logger, protocol):
self.logger = logger
self.protocol = protocol

def __call__(self, payload):
self.logger.debug("uninstalling web extension")
extension_id = payload["extension_id"]
return self.protocol.web_extensions.uninstall_web_extension(extension_id)

actions = [ClickAction,
DeleteAllCookiesAction,
GetAllCookiesAction,
Expand Down Expand Up @@ -586,4 +612,6 @@ def __call__(self, payload):
RemoveVirtualPressureSourceAction,
SetProtectedAudienceKAnonymityAction,
SetDisplayFeaturesAction,
ClearDisplayFeaturesAction]
ClearDisplayFeaturesAction,
WebExtensionInstallAction,
WebExtensionUninstallAction]
4 changes: 3 additions & 1 deletion tools/wptrunner/wptrunner/executors/asyncactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,4 +314,6 @@ async def __call__(self, payload):
BidiEmulationSetScreenOrientationOverrideAction,
BidiPermissionsSetPermissionAction,
BidiSessionSubscribeAction,
BidiSessionUnsubscribeAction]
BidiSessionUnsubscribeAction,
BidiPermissionsSetPermissionAction,
BidiSessionSubscribeAction]
10 changes: 5 additions & 5 deletions tools/wptrunner/wptrunner/executors/executormarionette.py
Original file line number Diff line number Diff line change
Expand Up @@ -765,12 +765,12 @@ class MarionetteWebExtensionsProtocolPart(WebExtensionsProtocolPart):
def setup(self):
self.addons = Addons(self.parent.marionette)

def install_web_extension(self, extension):
if extension["type"] == "base64":
extension_id = self.addons.install(data=extension["value"], temp=True)
def install_web_extension(self, type, path, value):
if type == "base64":
extension_id = self.addons.install(data=value, temp=True)
else:
path = self.parent.test_dir + extension["path"]
extension_id = self.addons.install(path, temp=True)
extension_path = self.parent.test_dir + path
extension_id = self.addons.install(extension_path, temp=True)

return {'extension': extension_id}

Expand Down
Loading
Loading