summaryrefslogtreecommitdiffstats
path: root/remote/shared/webdriver/test/xpcshell
diff options
context:
space:
mode:
Diffstat (limited to 'remote/shared/webdriver/test/xpcshell')
-rw-r--r--remote/shared/webdriver/test/xpcshell/head.js15
-rw-r--r--remote/shared/webdriver/test/xpcshell/test_Actions.js758
-rw-r--r--remote/shared/webdriver/test/xpcshell/test_Assert.js183
-rw-r--r--remote/shared/webdriver/test/xpcshell/test_Capabilities.js700
-rw-r--r--remote/shared/webdriver/test/xpcshell/test_Errors.js543
-rw-r--r--remote/shared/webdriver/test/xpcshell/test_NodeCache.js265
-rw-r--r--remote/shared/webdriver/test/xpcshell/test_Session.js72
-rw-r--r--remote/shared/webdriver/test/xpcshell/test_URLPattern_invalid.js129
-rw-r--r--remote/shared/webdriver/test/xpcshell/test_URLPattern_matchURLPattern.js607
-rw-r--r--remote/shared/webdriver/test/xpcshell/test_URLPattern_parseURLPattern.js369
-rw-r--r--remote/shared/webdriver/test/xpcshell/xpcshell.toml20
11 files changed, 3661 insertions, 0 deletions
diff --git a/remote/shared/webdriver/test/xpcshell/head.js b/remote/shared/webdriver/test/xpcshell/head.js
new file mode 100644
index 0000000000..ddc5573d78
--- /dev/null
+++ b/remote/shared/webdriver/test/xpcshell/head.js
@@ -0,0 +1,15 @@
+async function doGC() {
+ // Run GC and CC a few times to make sure that as much as possible is freed.
+ const numCycles = 3;
+ for (let i = 0; i < numCycles; i++) {
+ Cu.forceGC();
+ Cu.forceCC();
+ await new Promise(resolve => Cu.schedulePreciseShrinkingGC(resolve));
+ }
+
+ const MemoryReporter = Cc[
+ "@mozilla.org/memory-reporter-manager;1"
+ ].getService(Ci.nsIMemoryReporterManager);
+
+ await new Promise(resolve => MemoryReporter.minimizeMemoryUsage(resolve));
+}
diff --git a/remote/shared/webdriver/test/xpcshell/test_Actions.js b/remote/shared/webdriver/test/xpcshell/test_Actions.js
new file mode 100644
index 0000000000..24eac2e09d
--- /dev/null
+++ b/remote/shared/webdriver/test/xpcshell/test_Actions.js
@@ -0,0 +1,758 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { action, CLICK_INTERVAL, ClickTracker } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/webdriver/Actions.sys.mjs"
+);
+
+const { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+const XHTMLNS = "http://www.w3.org/1999/xhtml";
+
+const domEl = {
+ nodeType: 1,
+ ELEMENT_NODE: 1,
+ namespaceURI: XHTMLNS,
+};
+
+add_task(function test_createInputState() {
+ for (let type of ["none", "key", "pointer" /*"wheel"*/]) {
+ const state = new action.State();
+ const id = "device";
+ const actionSequence = {
+ type,
+ id,
+ actions: [],
+ };
+ action.Chain.fromJSON(state, [actionSequence]);
+ equal(state.inputStateMap.size, 1);
+ equal(state.inputStateMap.get(id).constructor.type, type);
+ }
+});
+
+add_task(function test_defaultPointerParameters() {
+ let state = new action.State();
+ const inputTickActions = [
+ { type: "pointer", subtype: "pointerDown", button: 0 },
+ ];
+ const chain = action.Chain.fromJSON(state, chainForTick(inputTickActions));
+ const pointerAction = chain[0][0];
+ equal(
+ state.getInputSource(pointerAction.id).pointer.constructor.type,
+ "mouse"
+ );
+});
+
+add_task(function test_processPointerParameters() {
+ for (let subtype of ["pointerDown", "pointerUp"]) {
+ for (let pointerType of [2, true, {}, []]) {
+ const inputTickActions = [
+ {
+ type: "pointer",
+ parameters: { pointerType },
+ subtype,
+ button: 0,
+ },
+ ];
+ let message = `Action sequence with parameters: {pointerType: ${pointerType} subtype: ${subtype}}`;
+ checkFromJSONErrors(
+ inputTickActions,
+ /Expected "pointerType" to be a string/,
+ message
+ );
+ }
+
+ for (let pointerType of ["", "foo"]) {
+ const inputTickActions = [
+ {
+ type: "pointer",
+ parameters: { pointerType },
+ subtype,
+ button: 0,
+ },
+ ];
+ let message = `Action sequence with parameters: {pointerType: ${pointerType} subtype: ${subtype}}`;
+ checkFromJSONErrors(
+ inputTickActions,
+ /Expected "pointerType" to be one of/,
+ message
+ );
+ }
+ }
+
+ for (let pointerType of ["mouse" /*"touch"*/]) {
+ let state = new action.State();
+ const inputTickActions = [
+ {
+ type: "pointer",
+ parameters: { pointerType },
+ subtype: "pointerDown",
+ button: 0,
+ },
+ ];
+ const chain = action.Chain.fromJSON(state, chainForTick(inputTickActions));
+ const pointerAction = chain[0][0];
+ equal(
+ state.getInputSource(pointerAction.id).pointer.constructor.type,
+ pointerType
+ );
+ }
+});
+
+add_task(function test_processPointerDownAction() {
+ for (let button of [-1, "a"]) {
+ const inputTickActions = [
+ { type: "pointer", subtype: "pointerDown", button },
+ ];
+ checkFromJSONErrors(
+ inputTickActions,
+ /Expected "button" to be a positive integer/,
+ `pointerDown with {button: ${button}}`
+ );
+ }
+ let state = new action.State();
+ const inputTickActions = [
+ { type: "pointer", subtype: "pointerDown", button: 5 },
+ ];
+ const chain = action.Chain.fromJSON(state, chainForTick(inputTickActions));
+ equal(chain[0][0].button, 5);
+});
+
+add_task(function test_validateActionDurationAndCoordinates() {
+ for (let [type, subtype] of [
+ ["none", "pause"],
+ ["pointer", "pointerMove"],
+ ]) {
+ for (let duration of [-1, "a"]) {
+ const inputTickActions = [{ type, subtype, duration }];
+ checkFromJSONErrors(
+ inputTickActions,
+ /Expected "duration" to be a positive integer/,
+ `{subtype} with {duration: ${duration}}`
+ );
+ }
+ }
+ for (let name of ["x", "y"]) {
+ const actionItem = {
+ type: "pointer",
+ subtype: "pointerMove",
+ duration: 5000,
+ };
+ actionItem[name] = "a";
+ checkFromJSONErrors(
+ [actionItem],
+ /Expected ".*" to be an integer/,
+ `${name}: "a", subtype: pointerMove`
+ );
+ }
+});
+
+add_task(function test_processPointerMoveActionOriginValidation() {
+ for (let origin of [-1, { a: "blah" }, []]) {
+ const inputTickActions = [
+ { type: "pointer", duration: 5000, subtype: "pointerMove", origin },
+ ];
+ checkFromJSONErrors(
+ inputTickActions,
+ /Expected "origin" to be undefined, "viewport", "pointer", or an element/,
+ `actionItem.origin: (${getTypeString(origin)})`
+ );
+ }
+});
+
+add_task(function test_processPointerMoveActionOriginStringValidation() {
+ for (let origin of ["", "viewports", "pointers"]) {
+ const inputTickActions = [
+ { type: "pointer", duration: 5000, subtype: "pointerMove", origin },
+ ];
+ checkFromJSONErrors(
+ inputTickActions,
+ /Expected "origin" to be undefined, "viewport", "pointer", or an element/,
+ `actionItem.origin: ${origin}`
+ );
+ }
+});
+
+add_task(function test_processPointerMoveActionElementOrigin() {
+ let state = new action.State();
+ const inputTickActions = [
+ {
+ type: "pointer",
+ duration: 5000,
+ subtype: "pointerMove",
+ origin: domEl,
+ x: 0,
+ y: 0,
+ },
+ ];
+ const chain = action.Chain.fromJSON(state, chainForTick(inputTickActions));
+ deepEqual(chain[0][0].origin.element, domEl);
+});
+
+add_task(function test_processPointerMoveActionDefaultOrigin() {
+ let state = new action.State();
+ const inputTickActions = [
+ { type: "pointer", duration: 5000, subtype: "pointerMove", x: 0, y: 0 },
+ ];
+ const chain = action.Chain.fromJSON(state, chainForTick(inputTickActions));
+ // The default is viewport coordinates which have an origin at [0,0] and don't depend on inputSource
+ deepEqual(chain[0][0].origin.getOriginCoordinates(null, null), {
+ x: 0,
+ y: 0,
+ });
+});
+
+add_task(function test_processPointerMoveAction() {
+ let state = new action.State();
+ const actionItems = [
+ {
+ duration: 5000,
+ type: "pointerMove",
+ origin: undefined,
+ x: 0,
+ y: 0,
+ },
+ {
+ duration: undefined,
+ type: "pointerMove",
+ origin: domEl,
+ x: 0,
+ y: 0,
+ },
+ {
+ duration: 5000,
+ type: "pointerMove",
+ x: 1,
+ y: 2,
+ origin: undefined,
+ },
+ ];
+ const actionSequence = {
+ id: "some_id",
+ type: "pointer",
+ actions: actionItems,
+ };
+ let chain = action.Chain.fromJSON(state, [actionSequence]);
+ equal(chain.length, actionItems.length);
+ for (let i = 0; i < actionItems.length; i++) {
+ let actual = chain[i][0];
+ let expected = actionItems[i];
+ equal(actual.duration, expected.duration);
+ equal(actual.x, expected.x);
+ equal(actual.y, expected.y);
+
+ let originClass;
+ if (expected.origin === undefined || expected.origin == "viewport") {
+ originClass = "ViewportOrigin";
+ } else if (expected.origin === "pointer") {
+ originClass = "PointerOrigin";
+ } else {
+ originClass = "ElementOrigin";
+ }
+ deepEqual(actual.origin.constructor.name, originClass);
+ }
+});
+
+add_task(function test_computePointerDestinationViewport() {
+ const state = new action.State();
+ const inputTickActions = [
+ {
+ type: "pointer",
+ subtype: "pointerMove",
+ x: 100,
+ y: 200,
+ origin: "viewport",
+ },
+ ];
+ const chain = action.Chain.fromJSON(state, chainForTick(inputTickActions));
+ const actionItem = chain[0][0];
+ const inputSource = state.getInputSource(actionItem.id);
+ // these values should not affect the outcome
+ inputSource.x = "99";
+ inputSource.y = "10";
+ const target = actionItem.origin.getTargetCoordinates(
+ inputSource,
+ [actionItem.x, actionItem.y],
+ null
+ );
+ equal(actionItem.x, target[0]);
+ equal(actionItem.y, target[1]);
+});
+
+add_task(function test_computePointerDestinationPointer() {
+ const state = new action.State();
+ const inputTickActions = [
+ {
+ type: "pointer",
+ subtype: "pointerMove",
+ x: 100,
+ y: 200,
+ origin: "pointer",
+ },
+ ];
+ const chain = action.Chain.fromJSON(state, chainForTick(inputTickActions));
+ const actionItem = chain[0][0];
+ const inputSource = state.getInputSource(actionItem.id);
+ inputSource.x = 10;
+ inputSource.y = 99;
+ const target = actionItem.origin.getTargetCoordinates(
+ inputSource,
+ [actionItem.x, actionItem.y],
+ null
+ );
+ equal(actionItem.x + inputSource.x, target[0]);
+ equal(actionItem.y + inputSource.y, target[1]);
+});
+
+add_task(function test_processPointerAction() {
+ for (let pointerType of ["mouse", "touch"]) {
+ const actionItems = [
+ {
+ duration: 2000,
+ type: "pause",
+ },
+ {
+ type: "pointerMove",
+ duration: 2000,
+ x: 0,
+ y: 0,
+ },
+ {
+ type: "pointerUp",
+ button: 1,
+ },
+ ];
+ let actionSequence = {
+ type: "pointer",
+ id: "some_id",
+ parameters: {
+ pointerType,
+ },
+ actions: actionItems,
+ };
+ const state = new action.State();
+ const chain = action.Chain.fromJSON(state, [actionSequence]);
+ equal(chain.length, actionItems.length);
+ for (let i = 0; i < actionItems.length; i++) {
+ const actual = chain[i][0];
+ const expected = actionItems[i];
+ equal(actual.type, expected.type === "pause" ? "none" : "pointer");
+ equal(actual.subtype, expected.type);
+ equal(actual.id, actionSequence.id);
+ if (expected.type === "pointerUp") {
+ equal(actual.button, expected.button);
+ } else {
+ equal(actual.duration, expected.duration);
+ }
+ if (expected.type !== "pause") {
+ equal(
+ state.getInputSource(actual.id).pointer.constructor.type,
+ pointerType
+ );
+ }
+ }
+ }
+});
+
+add_task(function test_processPauseAction() {
+ for (let type of ["none", "key", "pointer"]) {
+ const state = new action.State();
+ const actionSequence = {
+ type,
+ id: "some_id",
+ actions: [{ type: "pause", duration: 5000 }],
+ };
+ const actionItem = action.Chain.fromJSON(state, [actionSequence])[0][0];
+ equal(actionItem.type, "none");
+ equal(actionItem.subtype, "pause");
+ equal(actionItem.id, "some_id");
+ equal(actionItem.duration, 5000);
+ }
+ const state = new action.State();
+ const actionSequence = {
+ type: "none",
+ id: "some_id",
+ actions: [{ type: "pause" }],
+ };
+ const actionItem = action.Chain.fromJSON(state, [actionSequence])[0][0];
+ equal(actionItem.duration, undefined);
+});
+
+add_task(function test_processActionSubtypeValidation() {
+ for (let type of ["none", "key", "pointer"]) {
+ const message = `type: ${type}, subtype: dancing`;
+ const inputTickActions = [{ type, subtype: "dancing" }];
+ checkFromJSONErrors(
+ inputTickActions,
+ new RegExp(`Expected known subtype for type`),
+ message
+ );
+ }
+});
+
+add_task(function test_processKeyActionDown() {
+ for (let value of [-1, undefined, [], ["a"], { length: 1 }, null]) {
+ const inputTickActions = [{ type: "key", subtype: "keyDown", value }];
+ const message = `actionItem.value: (${getTypeString(value)})`;
+ checkFromJSONErrors(
+ inputTickActions,
+ /Expected "value" to be a string that represents single code point/,
+ message
+ );
+ }
+
+ const state = new action.State();
+ const actionSequence = {
+ type: "key",
+ id: "keyboard",
+ actions: [{ type: "keyDown", value: "\uE004" }],
+ };
+ const actionItem = action.Chain.fromJSON(state, [actionSequence])[0][0];
+
+ equal(actionItem.type, "key");
+ equal(actionItem.id, "keyboard");
+ equal(actionItem.subtype, "keyDown");
+ equal(actionItem.value, "\ue004");
+});
+
+add_task(function test_processInputSourceActionSequenceValidation() {
+ checkFromJSONErrors(
+ [{ type: "swim", subtype: "pause", id: "some id" }],
+ /Expected known action type/,
+ "actionSequence type: swim"
+ );
+
+ checkFromJSONErrors(
+ [{ type: "none", subtype: "pause", id: -1 }],
+ /Expected "id" to be a string/,
+ "actionSequence id: -1"
+ );
+
+ checkFromJSONErrors(
+ [{ type: "none", subtype: "pause", id: undefined }],
+ /Expected "id" to be a string/,
+ "actionSequence id: undefined"
+ );
+
+ const state = new action.State();
+ const actionSequence = [
+ { type: "none", subtype: "pause", id: "some_id", actions: -1 },
+ ];
+ const errorRegex = /Expected "actionSequence.actions" to be an array/;
+ const message = "actionSequence actions: -1";
+
+ Assert.throws(
+ () => action.Chain.fromJSON(state, actionSequence),
+ /InvalidArgumentError/,
+ message
+ );
+ Assert.throws(
+ () => action.Chain.fromJSON(state, actionSequence),
+ errorRegex,
+ message
+ );
+});
+
+add_task(function test_processInputSourceActionSequence() {
+ const state = new action.State();
+ const actionItem = { type: "pause", duration: 5 };
+ const actionSequence = {
+ type: "none",
+ id: "some id",
+ actions: [actionItem],
+ };
+ const chain = action.Chain.fromJSON(state, [actionSequence]);
+ equal(chain.length, 1);
+ const tickActions = chain[0];
+ equal(tickActions.length, 1);
+ equal(tickActions[0].type, "none");
+ equal(tickActions[0].subtype, "pause");
+ equal(tickActions[0].duration, 5);
+ equal(tickActions[0].id, "some id");
+});
+
+add_task(function test_processInputSourceActionSequencePointer() {
+ const state = new action.State();
+ const actionItem = { type: "pointerDown", button: 1 };
+ const actionSequence = {
+ type: "pointer",
+ id: "9",
+ actions: [actionItem],
+ parameters: {
+ pointerType: "mouse", // TODO "pen"
+ },
+ };
+ const chain = action.Chain.fromJSON(state, [actionSequence]);
+ equal(chain.length, 1);
+ const tickActions = chain[0];
+ equal(tickActions.length, 1);
+ equal(tickActions[0].type, "pointer");
+ equal(tickActions[0].subtype, "pointerDown");
+ equal(tickActions[0].button, 1);
+ equal(tickActions[0].id, "9");
+ const inputSource = state.getInputSource(tickActions[0].id);
+ equal(inputSource.constructor.type, "pointer");
+ equal(inputSource.pointer.constructor.type, "mouse");
+});
+
+add_task(function test_processInputSourceActionSequenceKey() {
+ const state = new action.State();
+ const actionItem = { type: "keyUp", value: "a" };
+ const actionSequence = {
+ type: "key",
+ id: "9",
+ actions: [actionItem],
+ };
+ const chain = action.Chain.fromJSON(state, [actionSequence]);
+ equal(chain.length, 1);
+ const tickActions = chain[0];
+ equal(tickActions.length, 1);
+ equal(tickActions[0].type, "key");
+ equal(tickActions[0].subtype, "keyUp");
+ equal(tickActions[0].value, "a");
+ equal(tickActions[0].id, "9");
+});
+
+add_task(function test_processInputSourceActionSequenceInputStateMap() {
+ const state = new action.State();
+ const id = "1";
+ const actionItem = { type: "pause", duration: 5000 };
+ const actionSequence = {
+ type: "key",
+ id,
+ actions: [actionItem],
+ };
+ action.Chain.fromJSON(state, [actionSequence]);
+ equal(state.inputStateMap.size, 1);
+ equal(state.inputStateMap.get(id).constructor.type, "key");
+
+ // Construct a different state with the same input id
+ const state1 = new action.State();
+ const actionItem1 = { type: "pointerDown", button: 0 };
+ const actionSequence1 = {
+ type: "pointer",
+ id,
+ actions: [actionItem1],
+ };
+ action.Chain.fromJSON(state1, [actionSequence1]);
+ equal(state1.inputStateMap.size, 1);
+
+ // Overwrite the state in the initial map with one of a different type
+ state.inputStateMap.set(id, state1.inputStateMap.get(id));
+ equal(state.inputStateMap.get(id).constructor.type, "pointer");
+
+ const message = "Wrong state for input id type";
+ Assert.throws(
+ () => action.Chain.fromJSON(state, [actionSequence]),
+ /InvalidArgumentError/,
+ message
+ );
+ Assert.throws(
+ () => action.Chain.fromJSON(state, [actionSequence]),
+ /Expected input source \[object String\] "1" to be type pointer/,
+ message
+ );
+});
+
+add_task(function test_extractActionChainValidation() {
+ for (let actions of [-1, "a", undefined, null]) {
+ const state = new action.State();
+ let message = `actions: ${getTypeString(actions)}`;
+ Assert.throws(
+ () => action.Chain.fromJSON(state, actions),
+ /InvalidArgumentError/,
+ message
+ );
+ Assert.throws(
+ () => action.Chain.fromJSON(state, actions),
+ /Expected "actions" to be an array/,
+ message
+ );
+ }
+});
+
+add_task(function test_extractActionChainEmpty() {
+ const state = new action.State();
+ deepEqual(action.Chain.fromJSON(state, []), []);
+});
+
+add_task(function test_extractActionChain_oneTickOneInput() {
+ const state = new action.State();
+ const actionItem = { type: "pause", duration: 5000 };
+ const actionSequence = {
+ type: "none",
+ id: "some id",
+ actions: [actionItem],
+ };
+ const actionsByTick = action.Chain.fromJSON(state, [actionSequence]);
+ equal(1, actionsByTick.length);
+ equal(1, actionsByTick[0].length);
+ equal(actionsByTick[0][0].id, actionSequence.id);
+ equal(actionsByTick[0][0].type, "none");
+ equal(actionsByTick[0][0].subtype, "pause");
+ equal(actionsByTick[0][0].duration, actionItem.duration);
+});
+
+add_task(function test_extractActionChain_twoAndThreeTicks() {
+ const state = new action.State();
+ const mouseActionItems = [
+ {
+ type: "pointerDown",
+ button: 2,
+ },
+ {
+ type: "pointerUp",
+ button: 2,
+ },
+ ];
+ const mouseActionSequence = {
+ type: "pointer",
+ id: "7",
+ actions: mouseActionItems,
+ parameters: {
+ pointerType: "mouse",
+ },
+ };
+ const keyActionItems = [
+ {
+ type: "keyDown",
+ value: "a",
+ },
+ {
+ type: "pause",
+ duration: 4,
+ },
+ {
+ type: "keyUp",
+ value: "a",
+ },
+ ];
+ let keyActionSequence = {
+ type: "key",
+ id: "1",
+ actions: keyActionItems,
+ };
+ let actionsByTick = action.Chain.fromJSON(state, [
+ keyActionSequence,
+ mouseActionSequence,
+ ]);
+ // number of ticks is same as longest action sequence
+ equal(keyActionItems.length, actionsByTick.length);
+ equal(2, actionsByTick[0].length);
+ equal(2, actionsByTick[1].length);
+ equal(1, actionsByTick[2].length);
+
+ equal(actionsByTick[2][0].id, keyActionSequence.id);
+ equal(actionsByTick[2][0].type, "key");
+ equal(actionsByTick[2][0].subtype, "keyUp");
+});
+
+add_task(function test_computeTickDuration() {
+ const state = new action.State();
+ const expected = 8000;
+ const inputTickActions = [
+ { type: "none", subtype: "pause", duration: 5000 },
+ { type: "key", subtype: "pause", duration: 1000 },
+ { type: "pointer", subtype: "pointerMove", duration: 6000, x: 0, y: 0 },
+ // invalid because keyDown should not have duration, so duration should be ignored.
+ { type: "key", subtype: "keyDown", duration: 100000, value: "a" },
+ { type: "pointer", subtype: "pause", duration: expected },
+ { type: "pointer", subtype: "pointerUp", button: 0 },
+ ];
+ const chain = action.Chain.fromJSON(state, chainForTick(inputTickActions));
+ equal(1, chain.length);
+ const tickActions = chain[0];
+ equal(expected, tickActions.getDuration());
+});
+
+add_task(function test_computeTickDuration_noDurations() {
+ const state = new action.State();
+ const inputTickActions = [
+ // invalid because keyDown should not have duration, so duration should be ignored.
+ { type: "key", subtype: "keyDown", duration: 100000, value: "a" },
+ // undefined duration permitted
+ { type: "none", subtype: "pause" },
+ { type: "pointer", subtype: "pointerMove", button: 0, x: 0, y: 0 },
+ { type: "pointer", subtype: "pointerDown", button: 0 },
+ { type: "key", subtype: "keyUp", value: "a" },
+ ];
+ const chain = action.Chain.fromJSON(state, chainForTick(inputTickActions));
+ equal(0, chain[0].getDuration());
+});
+
+add_task(function test_ClickTracker_setClick() {
+ const clickTracker = new ClickTracker();
+ const button1 = 1;
+ const button2 = 2;
+
+ clickTracker.setClick(button1);
+ equal(1, clickTracker.count);
+
+ // Make sure that clicking different mouse buttons doesn't increase the count.
+ clickTracker.setClick(button2);
+ equal(1, clickTracker.count);
+
+ clickTracker.setClick(button2);
+ equal(2, clickTracker.count);
+
+ clickTracker.reset();
+ equal(0, clickTracker.count);
+});
+
+add_task(function test_ClickTracker_reset_after_timeout() {
+ const clickTracker = new ClickTracker();
+
+ clickTracker.setClick(1);
+ equal(1, clickTracker.count);
+
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ setTimeout(() => equal(0, clickTracker.count), CLICK_INTERVAL + 10);
+});
+
+// helpers
+function getTypeString(obj) {
+ return Object.prototype.toString.call(obj);
+}
+
+function checkFromJSONErrors(inputTickActions, regex, message) {
+ const state = new action.State();
+
+ if (typeof message == "undefined") {
+ message = `fromJSON`;
+ }
+ Assert.throws(
+ () => action.Chain.fromJSON(state, chainForTick(inputTickActions)),
+ /InvalidArgumentError/,
+ message
+ );
+ Assert.throws(
+ () => action.Chain.fromJSON(state, chainForTick(inputTickActions)),
+ regex,
+ message
+ );
+}
+
+function chainForTick(tickActions) {
+ const actions = [];
+ let lastId = 0;
+ for (let { type, subtype, parameters, ...props } of tickActions) {
+ let id;
+ if (!props.hasOwnProperty("id")) {
+ id = `${type}_${lastId++}`;
+ } else {
+ id = props.id;
+ delete props.id;
+ }
+ const inputAction = { type, id, actions: [{ type: subtype, ...props }] };
+ if (parameters !== undefined) {
+ inputAction.parameters = parameters;
+ }
+ actions.push(inputAction);
+ }
+ return actions;
+}
diff --git a/remote/shared/webdriver/test/xpcshell/test_Assert.js b/remote/shared/webdriver/test/xpcshell/test_Assert.js
new file mode 100644
index 0000000000..cf474868b6
--- /dev/null
+++ b/remote/shared/webdriver/test/xpcshell/test_Assert.js
@@ -0,0 +1,183 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+/* eslint-disable no-array-constructor, no-object-constructor */
+
+const { assert } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/webdriver/Assert.sys.mjs"
+);
+const { error } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/webdriver/Errors.sys.mjs"
+);
+
+add_task(function test_session() {
+ assert.session({ id: "foo" });
+
+ const invalidTypes = [
+ null,
+ undefined,
+ [],
+ {},
+ { id: undefined },
+ { id: null },
+ { id: true },
+ { id: 1 },
+ { id: [] },
+ { id: {} },
+ ];
+
+ for (const invalidType of invalidTypes) {
+ Assert.throws(() => assert.session(invalidType), /InvalidSessionIDError/);
+ }
+
+ Assert.throws(() => assert.session({ id: null }, "custom"), /custom/);
+});
+
+add_task(function test_platforms() {
+ // at least one will fail
+ let raised;
+ for (let fn of [assert.desktop, assert.mobile]) {
+ try {
+ fn();
+ } catch (e) {
+ raised = e;
+ }
+ }
+ ok(raised instanceof error.UnsupportedOperationError);
+});
+
+add_task(function test_noUserPrompt() {
+ assert.noUserPrompt(null);
+ assert.noUserPrompt(undefined);
+ Assert.throws(() => assert.noUserPrompt({}), /UnexpectedAlertOpenError/);
+ Assert.throws(() => assert.noUserPrompt({}, "custom"), /custom/);
+});
+
+add_task(function test_defined() {
+ assert.defined({});
+ Assert.throws(() => assert.defined(undefined), /InvalidArgumentError/);
+ Assert.throws(() => assert.noUserPrompt({}, "custom"), /custom/);
+});
+
+add_task(function test_number() {
+ assert.number(1);
+ assert.number(0);
+ assert.number(-1);
+ assert.number(1.2);
+ for (let i of ["foo", "1", {}, [], NaN, Infinity, undefined]) {
+ Assert.throws(() => assert.number(i), /InvalidArgumentError/);
+ }
+
+ Assert.throws(() => assert.number("foo", "custom"), /custom/);
+});
+
+add_task(function test_callable() {
+ assert.callable(function () {});
+ assert.callable(() => {});
+
+ for (let typ of [undefined, "", true, {}, []]) {
+ Assert.throws(() => assert.callable(typ), /InvalidArgumentError/);
+ }
+
+ Assert.throws(() => assert.callable("foo", "custom"), /custom/);
+});
+
+add_task(function test_integer() {
+ assert.integer(1);
+ assert.integer(0);
+ assert.integer(-1);
+ Assert.throws(() => assert.integer("foo"), /InvalidArgumentError/);
+ Assert.throws(() => assert.integer(1.2), /InvalidArgumentError/);
+
+ Assert.throws(() => assert.integer("foo", "custom"), /custom/);
+});
+
+add_task(function test_positiveInteger() {
+ assert.positiveInteger(1);
+ assert.positiveInteger(0);
+ Assert.throws(() => assert.positiveInteger(-1), /InvalidArgumentError/);
+ Assert.throws(() => assert.positiveInteger("foo"), /InvalidArgumentError/);
+ Assert.throws(() => assert.positiveInteger("foo", "custom"), /custom/);
+});
+
+add_task(function test_positiveNumber() {
+ assert.positiveNumber(1);
+ assert.positiveNumber(0);
+ assert.positiveNumber(1.1);
+ assert.positiveNumber(Number.MAX_VALUE);
+ // eslint-disable-next-line no-loss-of-precision
+ Assert.throws(() => assert.positiveNumber(1.8e308), /InvalidArgumentError/);
+ Assert.throws(() => assert.positiveNumber(-1), /InvalidArgumentError/);
+ Assert.throws(() => assert.positiveNumber(Infinity), /InvalidArgumentError/);
+ Assert.throws(() => assert.positiveNumber("foo"), /InvalidArgumentError/);
+ Assert.throws(() => assert.positiveNumber("foo", "custom"), /custom/);
+});
+
+add_task(function test_boolean() {
+ assert.boolean(true);
+ assert.boolean(false);
+ Assert.throws(() => assert.boolean("false"), /InvalidArgumentError/);
+ Assert.throws(() => assert.boolean(undefined), /InvalidArgumentError/);
+ Assert.throws(() => assert.boolean(undefined, "custom"), /custom/);
+});
+
+add_task(function test_string() {
+ assert.string("foo");
+ assert.string(`bar`);
+ Assert.throws(() => assert.string(42), /InvalidArgumentError/);
+ Assert.throws(() => assert.string(42, "custom"), /custom/);
+});
+
+add_task(function test_open() {
+ assert.open({ currentWindowGlobal: {} });
+
+ for (let typ of [null, undefined, { currentWindowGlobal: null }]) {
+ Assert.throws(() => assert.open(typ), /NoSuchWindowError/);
+ }
+
+ Assert.throws(() => assert.open(null, "custom"), /custom/);
+});
+
+add_task(function test_object() {
+ assert.object({});
+ assert.object(new Object());
+ for (let typ of [42, "foo", true, null, undefined]) {
+ Assert.throws(() => assert.object(typ), /InvalidArgumentError/);
+ }
+
+ Assert.throws(() => assert.object(null, "custom"), /custom/);
+});
+
+add_task(function test_in() {
+ assert.in("foo", { foo: 42 });
+ for (let typ of [{}, 42, true, null, undefined]) {
+ Assert.throws(() => assert.in("foo", typ), /InvalidArgumentError/);
+ }
+
+ Assert.throws(() => assert.in("foo", { bar: 42 }, "custom"), /custom/);
+});
+
+add_task(function test_array() {
+ assert.array([]);
+ assert.array(new Array());
+ Assert.throws(() => assert.array(42), /InvalidArgumentError/);
+ Assert.throws(() => assert.array({}), /InvalidArgumentError/);
+
+ Assert.throws(() => assert.array(42, "custom"), /custom/);
+});
+
+add_task(function test_that() {
+ equal(1, assert.that(n => n + 1)(1));
+ Assert.throws(() => assert.that(() => false)(), /InvalidArgumentError/);
+ Assert.throws(() => assert.that(val => val)(false), /InvalidArgumentError/);
+ Assert.throws(
+ () => assert.that(val => val, "foo", error.SessionNotCreatedError)(false),
+ /SessionNotCreatedError/
+ );
+
+ Assert.throws(() => assert.that(() => false, "custom")(), /custom/);
+});
+
+/* eslint-enable no-array-constructor, no-new-object */
diff --git a/remote/shared/webdriver/test/xpcshell/test_Capabilities.js b/remote/shared/webdriver/test/xpcshell/test_Capabilities.js
new file mode 100644
index 0000000000..19401dd463
--- /dev/null
+++ b/remote/shared/webdriver/test/xpcshell/test_Capabilities.js
@@ -0,0 +1,700 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { AppInfo } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/AppInfo.sys.mjs"
+);
+const { error } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/webdriver/Errors.sys.mjs"
+);
+const {
+ Capabilities,
+ mergeCapabilities,
+ PageLoadStrategy,
+ processCapabilities,
+ Proxy,
+ Timeouts,
+ UnhandledPromptBehavior,
+ validateCapabilities,
+} = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/webdriver/Capabilities.sys.mjs"
+);
+
+add_task(function test_Timeouts_ctor() {
+ let ts = new Timeouts();
+ equal(ts.implicit, 0);
+ equal(ts.pageLoad, 300000);
+ equal(ts.script, 30000);
+});
+
+add_task(function test_Timeouts_toString() {
+ equal(new Timeouts().toString(), "[object Timeouts]");
+});
+
+add_task(function test_Timeouts_toJSON() {
+ let ts = new Timeouts();
+ deepEqual(ts.toJSON(), { implicit: 0, pageLoad: 300000, script: 30000 });
+});
+
+add_task(function test_Timeouts_fromJSON() {
+ let json = {
+ implicit: 0,
+ pageLoad: 2.0,
+ script: Number.MAX_SAFE_INTEGER,
+ };
+ let ts = Timeouts.fromJSON(json);
+ equal(ts.implicit, json.implicit);
+ equal(ts.pageLoad, json.pageLoad);
+ equal(ts.script, json.script);
+});
+
+add_task(function test_Timeouts_fromJSON_unrecognised_field() {
+ let json = {
+ sessionId: "foobar",
+ };
+ try {
+ Timeouts.fromJSON(json);
+ } catch (e) {
+ equal(e.name, error.InvalidArgumentError.name);
+ equal(e.message, "Unrecognised timeout: sessionId");
+ }
+});
+
+add_task(function test_Timeouts_fromJSON_invalid_types() {
+ for (let value of [null, [], {}, false, "10", 2.5]) {
+ Assert.throws(
+ () => Timeouts.fromJSON({ implicit: value }),
+ /InvalidArgumentError/
+ );
+ }
+});
+
+add_task(function test_Timeouts_fromJSON_bounds() {
+ for (let value of [-1, Number.MAX_SAFE_INTEGER + 1]) {
+ Assert.throws(
+ () => Timeouts.fromJSON({ script: value }),
+ /InvalidArgumentError/
+ );
+ }
+});
+
+add_task(function test_PageLoadStrategy() {
+ equal(PageLoadStrategy.None, "none");
+ equal(PageLoadStrategy.Eager, "eager");
+ equal(PageLoadStrategy.Normal, "normal");
+});
+
+add_task(function test_Proxy_ctor() {
+ let p = new Proxy();
+ let props = [
+ "proxyType",
+ "httpProxy",
+ "sslProxy",
+ "socksProxy",
+ "socksVersion",
+ "proxyAutoconfigUrl",
+ ];
+ for (let prop of props) {
+ ok(prop in p, `${prop} in ${JSON.stringify(props)}`);
+ equal(p[prop], null);
+ }
+});
+
+add_task(function test_Proxy_init() {
+ let p = new Proxy();
+
+ // no changed made, and 5 (system) is default
+ equal(p.init(), false);
+ equal(Services.prefs.getIntPref("network.proxy.type"), 5);
+
+ // pac
+ p.proxyType = "pac";
+ p.proxyAutoconfigUrl = "http://localhost:1234";
+ ok(p.init());
+
+ equal(Services.prefs.getIntPref("network.proxy.type"), 2);
+ equal(
+ Services.prefs.getStringPref("network.proxy.autoconfig_url"),
+ "http://localhost:1234"
+ );
+
+ // direct
+ p = new Proxy();
+ p.proxyType = "direct";
+ ok(p.init());
+ equal(Services.prefs.getIntPref("network.proxy.type"), 0);
+
+ // autodetect
+ p = new Proxy();
+ p.proxyType = "autodetect";
+ ok(p.init());
+ equal(Services.prefs.getIntPref("network.proxy.type"), 4);
+
+ // system
+ p = new Proxy();
+ p.proxyType = "system";
+ ok(p.init());
+ equal(Services.prefs.getIntPref("network.proxy.type"), 5);
+
+ // manual
+ for (let proxy of ["http", "ssl", "socks"]) {
+ p = new Proxy();
+ p.proxyType = "manual";
+ p.noProxy = ["foo", "bar"];
+ p[`${proxy}Proxy`] = "foo";
+ p[`${proxy}ProxyPort`] = 42;
+ if (proxy === "socks") {
+ p[`${proxy}Version`] = 4;
+ }
+
+ ok(p.init());
+ equal(Services.prefs.getIntPref("network.proxy.type"), 1);
+ equal(
+ Services.prefs.getStringPref("network.proxy.no_proxies_on"),
+ "foo, bar"
+ );
+ equal(Services.prefs.getStringPref(`network.proxy.${proxy}`), "foo");
+ equal(Services.prefs.getIntPref(`network.proxy.${proxy}_port`), 42);
+ if (proxy === "socks") {
+ equal(Services.prefs.getIntPref(`network.proxy.${proxy}_version`), 4);
+ }
+ }
+
+ // empty no proxy should reset default exclustions
+ p = new Proxy();
+ p.proxyType = "manual";
+ p.noProxy = [];
+ ok(p.init());
+ equal(Services.prefs.getStringPref("network.proxy.no_proxies_on"), "");
+});
+
+add_task(function test_Proxy_toString() {
+ equal(new Proxy().toString(), "[object Proxy]");
+});
+
+add_task(function test_Proxy_toJSON() {
+ let p = new Proxy();
+ deepEqual(p.toJSON(), {});
+
+ // autoconfig url
+ p = new Proxy();
+ p.proxyType = "pac";
+ p.proxyAutoconfigUrl = "foo";
+ deepEqual(p.toJSON(), { proxyType: "pac", proxyAutoconfigUrl: "foo" });
+
+ // manual proxy
+ p = new Proxy();
+ p.proxyType = "manual";
+ deepEqual(p.toJSON(), { proxyType: "manual" });
+
+ for (let proxy of ["httpProxy", "sslProxy", "socksProxy"]) {
+ let expected = { proxyType: "manual" };
+
+ p = new Proxy();
+ p.proxyType = "manual";
+
+ if (proxy == "socksProxy") {
+ p.socksVersion = 5;
+ expected.socksVersion = 5;
+ }
+
+ // without port
+ p[proxy] = "foo";
+ expected[proxy] = "foo";
+ deepEqual(p.toJSON(), expected);
+
+ // with port
+ p[proxy] = "foo";
+ p[`${proxy}Port`] = 0;
+ expected[proxy] = "foo:0";
+ deepEqual(p.toJSON(), expected);
+
+ p[`${proxy}Port`] = 42;
+ expected[proxy] = "foo:42";
+ deepEqual(p.toJSON(), expected);
+
+ // add brackets for IPv6 address as proxy hostname
+ p[proxy] = "2001:db8::1";
+ p[`${proxy}Port`] = 42;
+ expected[proxy] = "foo:42";
+ expected[proxy] = "[2001:db8::1]:42";
+ deepEqual(p.toJSON(), expected);
+ }
+
+ // noProxy: add brackets for IPv6 address
+ p = new Proxy();
+ p.proxyType = "manual";
+ p.noProxy = ["2001:db8::1"];
+ let expected = { proxyType: "manual", noProxy: "[2001:db8::1]" };
+ deepEqual(p.toJSON(), expected);
+});
+
+add_task(function test_Proxy_fromJSON() {
+ let p = new Proxy();
+ deepEqual(p, Proxy.fromJSON(undefined));
+ deepEqual(p, Proxy.fromJSON(null));
+
+ for (let typ of [true, 42, "foo", []]) {
+ Assert.throws(() => Proxy.fromJSON(typ), /InvalidArgumentError/);
+ }
+
+ // must contain a valid proxyType
+ Assert.throws(() => Proxy.fromJSON({}), /InvalidArgumentError/);
+ Assert.throws(
+ () => Proxy.fromJSON({ proxyType: "foo" }),
+ /InvalidArgumentError/
+ );
+
+ // autoconfig url
+ for (let url of [true, 42, [], {}]) {
+ Assert.throws(
+ () => Proxy.fromJSON({ proxyType: "pac", proxyAutoconfigUrl: url }),
+ /InvalidArgumentError/
+ );
+ }
+
+ p = new Proxy();
+ p.proxyType = "pac";
+ p.proxyAutoconfigUrl = "foo";
+ deepEqual(p, Proxy.fromJSON({ proxyType: "pac", proxyAutoconfigUrl: "foo" }));
+
+ // manual proxy
+ p = new Proxy();
+ p.proxyType = "manual";
+ deepEqual(p, Proxy.fromJSON({ proxyType: "manual" }));
+
+ for (let proxy of ["httpProxy", "sslProxy", "socksProxy"]) {
+ let manual = { proxyType: "manual" };
+
+ // invalid hosts
+ for (let host of [
+ true,
+ 42,
+ [],
+ {},
+ null,
+ "http://foo",
+ "foo:-1",
+ "foo:65536",
+ "foo/test",
+ "foo#42",
+ "foo?foo=bar",
+ "2001:db8::1",
+ ]) {
+ manual[proxy] = host;
+ Assert.throws(() => Proxy.fromJSON(manual), /InvalidArgumentError/);
+ }
+
+ p = new Proxy();
+ p.proxyType = "manual";
+ if (proxy == "socksProxy") {
+ manual.socksVersion = 5;
+ p.socksVersion = 5;
+ }
+
+ let host_map = {
+ "foo:1": { hostname: "foo", port: 1 },
+ "foo:21": { hostname: "foo", port: 21 },
+ "foo:80": { hostname: "foo", port: 80 },
+ "foo:443": { hostname: "foo", port: 443 },
+ "foo:65535": { hostname: "foo", port: 65535 },
+ "127.0.0.1:42": { hostname: "127.0.0.1", port: 42 },
+ "[2001:db8::1]:42": { hostname: "2001:db8::1", port: "42" },
+ };
+
+ // valid proxy hosts with port
+ for (let host in host_map) {
+ manual[proxy] = host;
+
+ p[`${proxy}`] = host_map[host].hostname;
+ p[`${proxy}Port`] = host_map[host].port;
+
+ deepEqual(p, Proxy.fromJSON(manual));
+ }
+
+ // Without a port the default port of the scheme is used
+ for (let host of ["foo", "foo:"]) {
+ manual[proxy] = host;
+
+ // For socks no default port is available
+ p[proxy] = `foo`;
+ if (proxy === "socksProxy") {
+ p[`${proxy}Port`] = null;
+ } else {
+ let default_ports = { httpProxy: 80, sslProxy: 443 };
+
+ p[`${proxy}Port`] = default_ports[proxy];
+ }
+
+ deepEqual(p, Proxy.fromJSON(manual));
+ }
+ }
+
+ // missing required socks version
+ Assert.throws(
+ () => Proxy.fromJSON({ proxyType: "manual", socksProxy: "foo:1234" }),
+ /InvalidArgumentError/
+ );
+
+ // Bug 1703805: Since Firefox 90 ftpProxy is no longer supported
+ Assert.throws(
+ () => Proxy.fromJSON({ proxyType: "manual", ftpProxy: "foo:21" }),
+ /InvalidArgumentError/
+ );
+
+ // noProxy: invalid settings
+ for (let noProxy of [true, 42, {}, null, "foo", [true], [42], [{}], [null]]) {
+ Assert.throws(
+ () => Proxy.fromJSON({ proxyType: "manual", noProxy }),
+ /InvalidArgumentError/
+ );
+ }
+
+ // noProxy: valid settings
+ p = new Proxy();
+ p.proxyType = "manual";
+ for (let noProxy of [[], ["foo"], ["foo", "bar"], ["127.0.0.1"]]) {
+ let manual = { proxyType: "manual", noProxy };
+ p.noProxy = noProxy;
+ deepEqual(p, Proxy.fromJSON(manual));
+ }
+
+ // noProxy: IPv6 needs brackets removed
+ p = new Proxy();
+ p.proxyType = "manual";
+ p.noProxy = ["2001:db8::1"];
+ let manual = { proxyType: "manual", noProxy: ["[2001:db8::1]"] };
+ deepEqual(p, Proxy.fromJSON(manual));
+});
+
+add_task(function test_UnhandledPromptBehavior() {
+ equal(UnhandledPromptBehavior.Accept, "accept");
+ equal(UnhandledPromptBehavior.AcceptAndNotify, "accept and notify");
+ equal(UnhandledPromptBehavior.Dismiss, "dismiss");
+ equal(UnhandledPromptBehavior.DismissAndNotify, "dismiss and notify");
+ equal(UnhandledPromptBehavior.Ignore, "ignore");
+});
+
+add_task(function test_Capabilities_ctor() {
+ let caps = new Capabilities();
+ ok(caps.has("browserName"));
+ ok(caps.has("browserVersion"));
+ ok(caps.has("platformName"));
+ ok(["linux", "mac", "windows", "android"].includes(caps.get("platformName")));
+ equal(PageLoadStrategy.Normal, caps.get("pageLoadStrategy"));
+ equal(false, caps.get("acceptInsecureCerts"));
+ ok(caps.get("timeouts") instanceof Timeouts);
+ ok(caps.get("proxy") instanceof Proxy);
+ equal(caps.get("setWindowRect"), !AppInfo.isAndroid);
+ equal(caps.get("strictFileInteractability"), false);
+ equal(caps.get("webSocketUrl"), null);
+
+ equal(false, caps.get("moz:accessibilityChecks"));
+ ok(caps.has("moz:buildID"));
+ ok(caps.has("moz:debuggerAddress"));
+ ok(caps.has("moz:platformVersion"));
+ ok(caps.has("moz:processID"));
+ ok(caps.has("moz:profile"));
+ equal(true, caps.get("moz:webdriverClick"));
+
+ // No longer supported capabilities
+ ok(!caps.has("moz:useNonSpecCompliantPointerOrigin"));
+});
+
+add_task(function test_Capabilities_toString() {
+ equal("[object Capabilities]", new Capabilities().toString());
+});
+
+add_task(function test_Capabilities_toJSON() {
+ let caps = new Capabilities();
+ let json = caps.toJSON();
+
+ equal(caps.get("browserName"), json.browserName);
+ equal(caps.get("browserVersion"), json.browserVersion);
+ equal(caps.get("platformName"), json.platformName);
+ equal(caps.get("pageLoadStrategy"), json.pageLoadStrategy);
+ equal(caps.get("acceptInsecureCerts"), json.acceptInsecureCerts);
+ deepEqual(caps.get("proxy").toJSON(), json.proxy);
+ deepEqual(caps.get("timeouts").toJSON(), json.timeouts);
+ equal(caps.get("setWindowRect"), json.setWindowRect);
+ equal(caps.get("strictFileInteractability"), json.strictFileInteractability);
+ equal(caps.get("webSocketUrl"), json.webSocketUrl);
+
+ equal(caps.get("moz:accessibilityChecks"), json["moz:accessibilityChecks"]);
+ equal(caps.get("moz:buildID"), json["moz:buildID"]);
+ equal(caps.get("moz:debuggerAddress"), json["moz:debuggerAddress"]);
+ equal(caps.get("moz:platformVersion"), json["moz:platformVersion"]);
+ equal(caps.get("moz:processID"), json["moz:processID"]);
+ equal(caps.get("moz:profile"), json["moz:profile"]);
+ equal(caps.get("moz:webdriverClick"), json["moz:webdriverClick"]);
+});
+
+add_task(function test_Capabilities_fromJSON() {
+ const { fromJSON } = Capabilities;
+
+ // plain
+ for (let typ of [{}, null, undefined]) {
+ ok(fromJSON(typ).has("browserName"));
+ }
+
+ // matching
+ let caps = new Capabilities();
+
+ caps = fromJSON({ acceptInsecureCerts: true });
+ equal(true, caps.get("acceptInsecureCerts"));
+ caps = fromJSON({ acceptInsecureCerts: false });
+ equal(false, caps.get("acceptInsecureCerts"));
+
+ for (let strategy of Object.values(PageLoadStrategy)) {
+ caps = fromJSON({ pageLoadStrategy: strategy });
+ equal(strategy, caps.get("pageLoadStrategy"));
+ }
+
+ let proxyConfig = { proxyType: "manual" };
+ caps = fromJSON({ proxy: proxyConfig });
+ equal("manual", caps.get("proxy").proxyType);
+
+ let timeoutsConfig = { implicit: 123 };
+ caps = fromJSON({ timeouts: timeoutsConfig });
+ equal(123, caps.get("timeouts").implicit);
+
+ caps = fromJSON({ strictFileInteractability: false });
+ equal(false, caps.get("strictFileInteractability"));
+ caps = fromJSON({ strictFileInteractability: true });
+ equal(true, caps.get("strictFileInteractability"));
+
+ caps = fromJSON({ webSocketUrl: true });
+ equal(true, caps.get("webSocketUrl"));
+
+ caps = fromJSON({ "webauthn:virtualAuthenticators": true });
+ equal(true, caps.get("webauthn:virtualAuthenticators"));
+ caps = fromJSON({ "webauthn:virtualAuthenticators": false });
+ equal(false, caps.get("webauthn:virtualAuthenticators"));
+ Assert.throws(
+ () => fromJSON({ "webauthn:virtualAuthenticators": "foo" }),
+ /InvalidArgumentError/
+ );
+
+ caps = fromJSON({ "webauthn:extension:uvm": true });
+ equal(true, caps.get("webauthn:extension:uvm"));
+ caps = fromJSON({ "webauthn:extension:uvm": false });
+ equal(false, caps.get("webauthn:extension:uvm"));
+ Assert.throws(
+ () => fromJSON({ "webauthn:extension:uvm": "foo" }),
+ /InvalidArgumentError/
+ );
+
+ caps = fromJSON({ "webauthn:extension:prf": true });
+ equal(true, caps.get("webauthn:extension:prf"));
+ caps = fromJSON({ "webauthn:extension:prf": false });
+ equal(false, caps.get("webauthn:extension:prf"));
+ Assert.throws(
+ () => fromJSON({ "webauthn:extension:prf": "foo" }),
+ /InvalidArgumentError/
+ );
+
+ caps = fromJSON({ "webauthn:extension:largeBlob": true });
+ equal(true, caps.get("webauthn:extension:largeBlob"));
+ caps = fromJSON({ "webauthn:extension:largeBlob": false });
+ equal(false, caps.get("webauthn:extension:largeBlob"));
+ Assert.throws(
+ () => fromJSON({ "webauthn:extension:largeBlob": "foo" }),
+ /InvalidArgumentError/
+ );
+
+ caps = fromJSON({ "webauthn:extension:credBlob": true });
+ equal(true, caps.get("webauthn:extension:credBlob"));
+ caps = fromJSON({ "webauthn:extension:credBlob": false });
+ equal(false, caps.get("webauthn:extension:credBlob"));
+ Assert.throws(
+ () => fromJSON({ "webauthn:extension:credBlob": "foo" }),
+ /InvalidArgumentError/
+ );
+
+ caps = fromJSON({ "moz:accessibilityChecks": true });
+ equal(true, caps.get("moz:accessibilityChecks"));
+ caps = fromJSON({ "moz:accessibilityChecks": false });
+ equal(false, caps.get("moz:accessibilityChecks"));
+
+ // capability is always populated with null if remote agent is not listening
+ caps = fromJSON({});
+ equal(null, caps.get("moz:debuggerAddress"));
+ caps = fromJSON({ "moz:debuggerAddress": "foo" });
+ equal(null, caps.get("moz:debuggerAddress"));
+ caps = fromJSON({ "moz:debuggerAddress": true });
+ equal(null, caps.get("moz:debuggerAddress"));
+
+ caps = fromJSON({ "moz:webdriverClick": true });
+ equal(true, caps.get("moz:webdriverClick"));
+ caps = fromJSON({ "moz:webdriverClick": false });
+ equal(false, caps.get("moz:webdriverClick"));
+
+ // No longer supported capabilities
+ Assert.throws(
+ () => fromJSON({ "moz:useNonSpecCompliantPointerOrigin": false }),
+ /InvalidArgumentError/
+ );
+ Assert.throws(
+ () => fromJSON({ "moz:useNonSpecCompliantPointerOrigin": true }),
+ /InvalidArgumentError/
+ );
+});
+
+add_task(function test_mergeCapabilities() {
+ // Shadowed values.
+ Assert.throws(
+ () =>
+ mergeCapabilities(
+ { acceptInsecureCerts: true },
+ { acceptInsecureCerts: false }
+ ),
+ /InvalidArgumentError/
+ );
+
+ deepEqual(
+ { acceptInsecureCerts: true },
+ mergeCapabilities({ acceptInsecureCerts: true }, undefined)
+ );
+ deepEqual(
+ { acceptInsecureCerts: true, browserName: "Firefox" },
+ mergeCapabilities({ acceptInsecureCerts: true }, { browserName: "Firefox" })
+ );
+});
+
+add_task(function test_validateCapabilities_invalid() {
+ const invalidCapabilities = [
+ true,
+ 42,
+ "foo",
+ [],
+ { acceptInsecureCerts: "foo" },
+ { browserName: true },
+ { browserVersion: true },
+ { platformName: true },
+ { pageLoadStrategy: "foo" },
+ { proxy: false },
+ { strictFileInteractability: "foo" },
+ { timeouts: false },
+ { unhandledPromptBehavior: false },
+ { webSocketUrl: false },
+ { webSocketUrl: "foo" },
+ { "moz:firefoxOptions": "foo" },
+ { "moz:accessibilityChecks": "foo" },
+ { "moz:webdriverClick": "foo" },
+ { "moz:webdriverClick": 1 },
+ { "moz:useNonSpecCompliantPointerOrigin": false },
+ { "moz:debuggerAddress": "foo" },
+ { "moz:someRandomString": {} },
+ ];
+ for (const capabilities of invalidCapabilities) {
+ Assert.throws(
+ () => validateCapabilities(capabilities),
+ /InvalidArgumentError/
+ );
+ }
+});
+
+add_task(function test_validateCapabilities_valid() {
+ // Ignore null value.
+ deepEqual({}, validateCapabilities({ test: null }));
+
+ const validCapabilities = [
+ { acceptInsecureCerts: true },
+ { browserName: "firefox" },
+ { browserVersion: "12" },
+ { platformName: "linux" },
+ { pageLoadStrategy: "eager" },
+ { proxy: { proxyType: "manual", httpProxy: "test.com" } },
+ { strictFileInteractability: true },
+ { timeouts: { pageLoad: 500 } },
+ { unhandledPromptBehavior: "accept" },
+ { webSocketUrl: true },
+ { "moz:firefoxOptions": {} },
+ { "moz:accessibilityChecks": true },
+ { "moz:webdriverClick": true },
+ { "moz:debuggerAddress": true },
+ { "test:extension": "foo" },
+ ];
+ for (const validCapability of validCapabilities) {
+ deepEqual(validCapability, validateCapabilities(validCapability));
+ }
+});
+
+add_task(function test_processCapabilities() {
+ for (const invalidValue of [
+ { capabilities: null },
+ { capabilities: undefined },
+ { capabilities: "foo" },
+ { capabilities: true },
+ { capabilities: [] },
+ { capabilities: { alwaysMatch: null } },
+ { capabilities: { alwaysMatch: "foo" } },
+ { capabilities: { alwaysMatch: true } },
+ { capabilities: { alwaysMatch: [] } },
+ { capabilities: { firstMatch: null } },
+ { capabilities: { firstMatch: "foo" } },
+ { capabilities: { firstMatch: true } },
+ { capabilities: { firstMatch: {} } },
+ { capabilities: { firstMatch: [] } },
+ ]) {
+ Assert.throws(
+ () => processCapabilities(invalidValue),
+ /InvalidArgumentError/
+ );
+ }
+
+ deepEqual(
+ { acceptInsecureCerts: true },
+ processCapabilities({
+ capabilities: { alwaysMatch: { acceptInsecureCerts: true } },
+ })
+ );
+ deepEqual(
+ { browserName: "Firefox" },
+ processCapabilities({
+ capabilities: { firstMatch: [{ browserName: "Firefox" }] },
+ })
+ );
+ deepEqual(
+ { acceptInsecureCerts: true, browserName: "Firefox" },
+ processCapabilities({
+ capabilities: {
+ alwaysMatch: { acceptInsecureCerts: true },
+ firstMatch: [{ browserName: "Firefox" }],
+ },
+ })
+ );
+});
+
+// use Proxy.toJSON to test marshal
+add_task(function test_marshal() {
+ let proxy = new Proxy();
+
+ // drop empty fields
+ deepEqual({}, proxy.toJSON());
+ proxy.proxyType = "manual";
+ deepEqual({ proxyType: "manual" }, proxy.toJSON());
+ proxy.proxyType = null;
+ deepEqual({}, proxy.toJSON());
+ proxy.proxyType = undefined;
+ deepEqual({}, proxy.toJSON());
+
+ // iterate over object literals
+ proxy.proxyType = { foo: "bar" };
+ deepEqual({ proxyType: { foo: "bar" } }, proxy.toJSON());
+
+ // iterate over complex object that implement toJSON
+ proxy.proxyType = new Proxy();
+ deepEqual({}, proxy.toJSON());
+ proxy.proxyType.proxyType = "manual";
+ deepEqual({ proxyType: { proxyType: "manual" } }, proxy.toJSON());
+
+ // drop objects with no entries
+ proxy.proxyType = { foo: {} };
+ deepEqual({}, proxy.toJSON());
+ proxy.proxyType = { foo: new Proxy() };
+ deepEqual({}, proxy.toJSON());
+});
diff --git a/remote/shared/webdriver/test/xpcshell/test_Errors.js b/remote/shared/webdriver/test/xpcshell/test_Errors.js
new file mode 100644
index 0000000000..22e3526039
--- /dev/null
+++ b/remote/shared/webdriver/test/xpcshell/test_Errors.js
@@ -0,0 +1,543 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { error } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/webdriver/Errors.sys.mjs"
+);
+
+const errors = [
+ error.WebDriverError,
+
+ error.DetachedShadowRootError,
+ error.ElementClickInterceptedError,
+ error.ElementNotAccessibleError,
+ error.ElementNotInteractableError,
+ error.InsecureCertificateError,
+ error.InvalidArgumentError,
+ error.InvalidCookieDomainError,
+ error.InvalidElementStateError,
+ error.InvalidSelectorError,
+ error.InvalidSessionIDError,
+ error.JavaScriptError,
+ error.MoveTargetOutOfBoundsError,
+ error.NoSuchAlertError,
+ error.NoSuchElementError,
+ error.NoSuchFrameError,
+ error.NoSuchHandleError,
+ error.NoSuchInterceptError,
+ error.NoSuchNodeError,
+ error.NoSuchRequestError,
+ error.NoSuchScriptError,
+ error.NoSuchShadowRootError,
+ error.NoSuchWindowError,
+ error.ScriptTimeoutError,
+ error.SessionNotCreatedError,
+ error.StaleElementReferenceError,
+ error.TimeoutError,
+ error.UnableToSetCookieError,
+ error.UnexpectedAlertOpenError,
+ error.UnknownCommandError,
+ error.UnknownError,
+ error.UnsupportedOperationError,
+];
+
+function notok(condition) {
+ ok(!condition);
+}
+
+add_task(function test_isError() {
+ notok(error.isError(null));
+ notok(error.isError([]));
+ notok(error.isError(new Date()));
+
+ ok(error.isError(new Components.Exception()));
+ ok(error.isError(new Error()));
+ ok(error.isError(new EvalError()));
+ ok(error.isError(new InternalError()));
+ ok(error.isError(new RangeError()));
+ ok(error.isError(new ReferenceError()));
+ ok(error.isError(new SyntaxError()));
+ ok(error.isError(new TypeError()));
+ ok(error.isError(new URIError()));
+
+ errors.forEach(err => ok(error.isError(new err())));
+});
+
+add_task(function test_isWebDriverError() {
+ notok(error.isWebDriverError(new Components.Exception()));
+ notok(error.isWebDriverError(new Error()));
+ notok(error.isWebDriverError(new EvalError()));
+ notok(error.isWebDriverError(new InternalError()));
+ notok(error.isWebDriverError(new RangeError()));
+ notok(error.isWebDriverError(new ReferenceError()));
+ notok(error.isWebDriverError(new SyntaxError()));
+ notok(error.isWebDriverError(new TypeError()));
+ notok(error.isWebDriverError(new URIError()));
+
+ errors.forEach(err => ok(error.isWebDriverError(new err())));
+});
+
+add_task(function test_wrap() {
+ // webdriver-derived errors should not be wrapped
+ errors.forEach(err => {
+ const unwrappedError = new err("foo");
+ const wrappedError = error.wrap(unwrappedError);
+
+ ok(wrappedError instanceof error.WebDriverError);
+ ok(wrappedError instanceof err);
+ equal(wrappedError.name, unwrappedError.name);
+ equal(wrappedError.status, unwrappedError.status);
+ equal(wrappedError.message, "foo");
+ });
+
+ // JS errors should be wrapped in UnknownError and retain their type
+ // as part of the message field.
+ const jsErrors = [
+ Error,
+ EvalError,
+ InternalError,
+ RangeError,
+ ReferenceError,
+ SyntaxError,
+ TypeError,
+ URIError,
+ ];
+
+ jsErrors.forEach(err => {
+ const originalError = new err("foo");
+ const wrappedError = error.wrap(originalError);
+
+ ok(wrappedError instanceof error.UnknownError);
+ equal(wrappedError.name, "UnknownError");
+ equal(wrappedError.status, "unknown error");
+ equal(wrappedError.message, `${originalError.name}: foo`);
+ });
+});
+
+add_task(function test_stringify() {
+ equal("<unprintable error>", error.stringify());
+ equal("<unprintable error>", error.stringify("foo"));
+ equal("[object Object]", error.stringify({}));
+ equal("[object Object]\nfoo", error.stringify({ stack: "foo" }));
+ equal("Error: foo", error.stringify(new Error("foo")).split("\n")[0]);
+
+ errors.forEach(err => {
+ const e = new err("foo");
+
+ equal(`${e.name}: foo`, error.stringify(e).split("\n")[0]);
+ });
+});
+
+add_task(function test_constructor_from_error() {
+ const data = { a: 3, b: "bar" };
+ const origError = new error.WebDriverError("foo", data);
+
+ errors.forEach(err => {
+ const newError = new err(origError);
+
+ ok(newError instanceof err);
+ equal(newError.message, origError.message);
+ equal(newError.stack, origError.stack);
+ equal(newError.data, origError.data);
+ });
+});
+
+add_task(function test_stack() {
+ equal("string", typeof error.stack());
+ ok(error.stack().includes("test_stack"));
+ ok(!error.stack().includes("add_task"));
+});
+
+add_task(function test_toJSON() {
+ errors.forEach(err => {
+ const e0 = new err();
+ const e0_json = e0.toJSON();
+ equal(e0_json.error, e0.status);
+ equal(e0_json.message, "");
+ equal(e0_json.stacktrace, e0.stack);
+ equal(e0_json.data, undefined);
+
+ // message property
+ const e1 = new err("a");
+ const e1_json = e1.toJSON();
+
+ equal(e1_json.message, e1.message);
+ equal(e1_json.stacktrace, e1.stack);
+ equal(e1_json.data, undefined);
+
+ // message and optional data property
+ const data = { a: 3, b: "bar" };
+ const e2 = new err("foo", data);
+ const e2_json = e2.toJSON();
+
+ equal(e2.status, e2_json.error);
+ equal(e2.message, e2_json.message);
+ equal(e2_json.data, data);
+ });
+});
+
+add_task(function test_fromJSON() {
+ errors.forEach(err => {
+ Assert.throws(
+ () => err.fromJSON({ error: "foo" }),
+ /Not of WebDriverError descent/
+ );
+ Assert.throws(
+ () => err.fromJSON({ error: "Error" }),
+ /Not of WebDriverError descent/
+ );
+ Assert.throws(() => err.fromJSON({}), /Undeserialisable error type/);
+ Assert.throws(() => err.fromJSON(undefined), /TypeError/);
+
+ // message and stack
+ const e1 = new err("1");
+ const e1_json = { error: e1.status, message: "3", stacktrace: "4" };
+ const e1_fromJSON = error.WebDriverError.fromJSON(e1_json);
+
+ ok(e1_fromJSON instanceof error.WebDriverError);
+ ok(e1_fromJSON instanceof err);
+ equal(e1_fromJSON.name, e1.name);
+ equal(e1_fromJSON.status, e1_json.error);
+ equal(e1_fromJSON.message, e1_json.message);
+ equal(e1_fromJSON.stack, e1_json.stacktrace);
+
+ // message and optional data
+ const e2_data = { a: 3, b: "bar" };
+ const e2 = new err("1", e2_data);
+ const e2_json = { error: e1.status, message: "3", data: e2_data };
+ const e2_fromJSON = error.WebDriverError.fromJSON(e2_json);
+
+ ok(e2_fromJSON instanceof error.WebDriverError);
+ ok(e2_fromJSON instanceof err);
+ equal(e2_fromJSON.name, e2.name);
+ equal(e2_fromJSON.status, e2_json.error);
+ equal(e2_fromJSON.message, e2_json.message);
+ equal(e2_fromJSON.data, e2_json.data);
+
+ // parity with toJSON
+ const e3_data = { a: 3, b: "bar" };
+ const e3 = new err("1", e3_data);
+ const e3_json = e3.toJSON();
+ const e3_fromJSON = error.WebDriverError.fromJSON(e3_json);
+
+ equal(e3_json.error, e3_fromJSON.status);
+ equal(e3_json.message, e3_fromJSON.message);
+ equal(e3_json.stacktrace, e3_fromJSON.stack);
+ });
+});
+
+add_task(function test_WebDriverError() {
+ let err = new error.WebDriverError("foo");
+ equal("WebDriverError", err.name);
+ equal("foo", err.message);
+ equal("webdriver error", err.status);
+ ok(err instanceof error.WebDriverError);
+});
+
+add_task(function test_DetachedShadowRootError() {
+ let err = new error.DetachedShadowRootError("foo");
+ equal("DetachedShadowRootError", err.name);
+ equal("foo", err.message);
+ equal("detached shadow root", err.status);
+ ok(err instanceof error.WebDriverError);
+});
+
+add_task(function test_ElementClickInterceptedError() {
+ let otherEl = {
+ hasAttribute: attr => attr in otherEl,
+ getAttribute: attr => (attr in otherEl ? otherEl[attr] : null),
+ nodeType: 1,
+ localName: "a",
+ };
+ let obscuredEl = {
+ hasAttribute: attr => attr in obscuredEl,
+ getAttribute: attr => (attr in obscuredEl ? obscuredEl[attr] : null),
+ nodeType: 1,
+ localName: "b",
+ ownerDocument: {
+ elementFromPoint() {
+ return otherEl;
+ },
+ },
+ style: {
+ pointerEvents: "auto",
+ },
+ };
+
+ let err1 = new error.ElementClickInterceptedError(
+ undefined,
+ undefined,
+ obscuredEl,
+ { x: 1, y: 2 }
+ );
+ equal("ElementClickInterceptedError", err1.name);
+ equal(
+ "Element <b> is not clickable at point (1,2) " +
+ "because another element <a> obscures it",
+ err1.message
+ );
+ equal("element click intercepted", err1.status);
+ ok(err1 instanceof error.WebDriverError);
+
+ obscuredEl.style.pointerEvents = "none";
+ let err2 = new error.ElementClickInterceptedError(
+ undefined,
+ undefined,
+ obscuredEl,
+ { x: 1, y: 2 }
+ );
+ equal(
+ "Element <b> is not clickable at point (1,2) " +
+ "because it does not have pointer events enabled, " +
+ "and element <a> would receive the click instead",
+ err2.message
+ );
+});
+
+add_task(function test_ElementNotAccessibleError() {
+ let err = new error.ElementNotAccessibleError("foo");
+ equal("ElementNotAccessibleError", err.name);
+ equal("foo", err.message);
+ equal("element not accessible", err.status);
+ ok(err instanceof error.WebDriverError);
+});
+
+add_task(function test_ElementNotInteractableError() {
+ let err = new error.ElementNotInteractableError("foo");
+ equal("ElementNotInteractableError", err.name);
+ equal("foo", err.message);
+ equal("element not interactable", err.status);
+ ok(err instanceof error.WebDriverError);
+});
+
+add_task(function test_InsecureCertificateError() {
+ let err = new error.InsecureCertificateError("foo");
+ equal("InsecureCertificateError", err.name);
+ equal("foo", err.message);
+ equal("insecure certificate", err.status);
+ ok(err instanceof error.WebDriverError);
+});
+
+add_task(function test_InvalidArgumentError() {
+ let err = new error.InvalidArgumentError("foo");
+ equal("InvalidArgumentError", err.name);
+ equal("foo", err.message);
+ equal("invalid argument", err.status);
+ ok(err instanceof error.WebDriverError);
+});
+
+add_task(function test_InvalidCookieDomainError() {
+ let err = new error.InvalidCookieDomainError("foo");
+ equal("InvalidCookieDomainError", err.name);
+ equal("foo", err.message);
+ equal("invalid cookie domain", err.status);
+ ok(err instanceof error.WebDriverError);
+});
+
+add_task(function test_InvalidElementStateError() {
+ let err = new error.InvalidElementStateError("foo");
+ equal("InvalidElementStateError", err.name);
+ equal("foo", err.message);
+ equal("invalid element state", err.status);
+ ok(err instanceof error.WebDriverError);
+});
+
+add_task(function test_InvalidSelectorError() {
+ let err = new error.InvalidSelectorError("foo");
+ equal("InvalidSelectorError", err.name);
+ equal("foo", err.message);
+ equal("invalid selector", err.status);
+ ok(err instanceof error.WebDriverError);
+});
+
+add_task(function test_InvalidSessionIDError() {
+ let err = new error.InvalidSessionIDError("foo");
+ equal("InvalidSessionIDError", err.name);
+ equal("foo", err.message);
+ equal("invalid session id", err.status);
+ ok(err instanceof error.WebDriverError);
+});
+
+add_task(function test_JavaScriptError() {
+ let err = new error.JavaScriptError("foo");
+ equal("JavaScriptError", err.name);
+ equal("foo", err.message);
+ equal("javascript error", err.status);
+ ok(err instanceof error.WebDriverError);
+
+ equal("", new error.JavaScriptError(undefined).message);
+
+ let superErr = new RangeError("foo");
+ let inheritedErr = new error.JavaScriptError(superErr);
+ equal("RangeError: foo", inheritedErr.message);
+ equal(superErr.stack, inheritedErr.stack);
+});
+
+add_task(function test_MoveTargetOutOfBoundsError() {
+ let err = new error.MoveTargetOutOfBoundsError("foo");
+ equal("MoveTargetOutOfBoundsError", err.name);
+ equal("foo", err.message);
+ equal("move target out of bounds", err.status);
+ ok(err instanceof error.WebDriverError);
+});
+
+add_task(function test_NoSuchAlertError() {
+ let err = new error.NoSuchAlertError("foo");
+ equal("NoSuchAlertError", err.name);
+ equal("foo", err.message);
+ equal("no such alert", err.status);
+ ok(err instanceof error.WebDriverError);
+});
+
+add_task(function test_NoSuchElementError() {
+ let err = new error.NoSuchElementError("foo");
+ equal("NoSuchElementError", err.name);
+ equal("foo", err.message);
+ equal("no such element", err.status);
+ ok(err instanceof error.WebDriverError);
+});
+
+add_task(function test_NoSuchFrameError() {
+ let err = new error.NoSuchFrameError("foo");
+ equal("NoSuchFrameError", err.name);
+ equal("foo", err.message);
+ equal("no such frame", err.status);
+ ok(err instanceof error.WebDriverError);
+});
+
+add_task(function test_NoSuchHandleError() {
+ let err = new error.NoSuchHandleError("foo");
+ equal("NoSuchHandleError", err.name);
+ equal("foo", err.message);
+ equal("no such handle", err.status);
+ ok(err instanceof error.WebDriverError);
+});
+
+add_task(function test_NoSuchInterceptError() {
+ let err = new error.NoSuchInterceptError("foo");
+ equal("NoSuchInterceptError", err.name);
+ equal("foo", err.message);
+ equal("no such intercept", err.status);
+ ok(err instanceof error.WebDriverError);
+});
+
+add_task(function test_NoSuchNodeError() {
+ let err = new error.NoSuchNodeError("foo");
+ equal("NoSuchNodeError", err.name);
+ equal("foo", err.message);
+ equal("no such node", err.status);
+ ok(err instanceof error.WebDriverError);
+});
+
+add_task(function test_NoSuchRequestError() {
+ let err = new error.NoSuchRequestError("foo");
+ equal("NoSuchRequestError", err.name);
+ equal("foo", err.message);
+ equal("no such request", err.status);
+ ok(err instanceof error.WebDriverError);
+});
+
+add_task(function test_NoSuchScriptError() {
+ let err = new error.NoSuchScriptError("foo");
+ equal("NoSuchScriptError", err.name);
+ equal("foo", err.message);
+ equal("no such script", err.status);
+ ok(err instanceof error.WebDriverError);
+});
+
+add_task(function test_NoSuchShadowRootError() {
+ let err = new error.NoSuchShadowRootError("foo");
+ equal("NoSuchShadowRootError", err.name);
+ equal("foo", err.message);
+ equal("no such shadow root", err.status);
+ ok(err instanceof error.WebDriverError);
+});
+
+add_task(function test_NoSuchUserContextError() {
+ let err = new error.NoSuchUserContextError("foo");
+ equal("NoSuchUserContextError", err.name);
+ equal("foo", err.message);
+ equal("no such user context", err.status);
+ ok(err instanceof error.WebDriverError);
+});
+
+add_task(function test_NoSuchWindowError() {
+ let err = new error.NoSuchWindowError("foo");
+ equal("NoSuchWindowError", err.name);
+ equal("foo", err.message);
+ equal("no such window", err.status);
+ ok(err instanceof error.WebDriverError);
+});
+
+add_task(function test_ScriptTimeoutError() {
+ let err = new error.ScriptTimeoutError("foo");
+ equal("ScriptTimeoutError", err.name);
+ equal("foo", err.message);
+ equal("script timeout", err.status);
+ ok(err instanceof error.WebDriverError);
+});
+
+add_task(function test_SessionNotCreatedError() {
+ let err = new error.SessionNotCreatedError("foo");
+ equal("SessionNotCreatedError", err.name);
+ equal("foo", err.message);
+ equal("session not created", err.status);
+ ok(err instanceof error.WebDriverError);
+});
+
+add_task(function test_StaleElementReferenceError() {
+ let err = new error.StaleElementReferenceError("foo");
+ equal("StaleElementReferenceError", err.name);
+ equal("foo", err.message);
+ equal("stale element reference", err.status);
+ ok(err instanceof error.WebDriverError);
+});
+
+add_task(function test_TimeoutError() {
+ let err = new error.TimeoutError("foo");
+ equal("TimeoutError", err.name);
+ equal("foo", err.message);
+ equal("timeout", err.status);
+ ok(err instanceof error.WebDriverError);
+});
+
+add_task(function test_UnableToSetCookieError() {
+ let err = new error.UnableToSetCookieError("foo");
+ equal("UnableToSetCookieError", err.name);
+ equal("foo", err.message);
+ equal("unable to set cookie", err.status);
+ ok(err instanceof error.WebDriverError);
+});
+
+add_task(function test_UnexpectedAlertOpenError() {
+ let err = new error.UnexpectedAlertOpenError("foo");
+ equal("UnexpectedAlertOpenError", err.name);
+ equal("foo", err.message);
+ equal("unexpected alert open", err.status);
+ ok(err instanceof error.WebDriverError);
+});
+
+add_task(function test_UnknownCommandError() {
+ let err = new error.UnknownCommandError("foo");
+ equal("UnknownCommandError", err.name);
+ equal("foo", err.message);
+ equal("unknown command", err.status);
+ ok(err instanceof error.WebDriverError);
+});
+
+add_task(function test_UnknownError() {
+ let err = new error.UnknownError("foo");
+ equal("UnknownError", err.name);
+ equal("foo", err.message);
+ equal("unknown error", err.status);
+ ok(err instanceof error.WebDriverError);
+});
+
+add_task(function test_UnsupportedOperationError() {
+ let err = new error.UnsupportedOperationError("foo");
+ equal("UnsupportedOperationError", err.name);
+ equal("foo", err.message);
+ equal("unsupported operation", err.status);
+ ok(err instanceof error.WebDriverError);
+});
diff --git a/remote/shared/webdriver/test/xpcshell/test_NodeCache.js b/remote/shared/webdriver/test/xpcshell/test_NodeCache.js
new file mode 100644
index 0000000000..4efe9fba3a
--- /dev/null
+++ b/remote/shared/webdriver/test/xpcshell/test_NodeCache.js
@@ -0,0 +1,265 @@
+const { NodeCache } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/webdriver/NodeCache.sys.mjs"
+);
+
+function setupTest() {
+ const browser = Services.appShell.createWindowlessBrowser(false);
+
+ browser.document.body.innerHTML = `
+ <div id="foo" style="margin: 50px">
+ <iframe></iframe>
+ <video></video>
+ <svg xmlns="http://www.w3.org/2000/svg"></svg>
+ <textarea></textarea>
+ </div>
+ <div id="with-comment"><!-- Comment --></div>
+ `;
+
+ const divEl = browser.document.querySelector("div");
+ const svgEl = browser.document.querySelector("svg");
+ const textareaEl = browser.document.querySelector("textarea");
+ const videoEl = browser.document.querySelector("video");
+
+ const iframeEl = browser.document.querySelector("iframe");
+ const childEl = iframeEl.contentDocument.createElement("div");
+ iframeEl.contentDocument.body.appendChild(childEl);
+
+ const shadowRoot = videoEl.openOrClosedShadowRoot;
+
+ return {
+ browser,
+ nodeCache: new NodeCache(),
+ childEl,
+ divEl,
+ iframeEl,
+ shadowRoot,
+ seenNodeIds: new Map(),
+ svgEl,
+ textareaEl,
+ videoEl,
+ };
+}
+
+add_task(function getOrCreateNodeReference_invalid() {
+ const { nodeCache, seenNodeIds } = setupTest();
+
+ const invalidValues = [null, undefined, "foo", 42, true, [], {}];
+
+ for (const value of invalidValues) {
+ info(`Testing value: ${value}`);
+ Assert.throws(
+ () => nodeCache.getOrCreateNodeReference(value, seenNodeIds),
+ /TypeError/
+ );
+ }
+});
+
+add_task(function getOrCreateNodeReference_supportedNodeTypes() {
+ const { browser, divEl, nodeCache, seenNodeIds } = setupTest();
+
+ // Bug 1820734: No ownerGlobal is available in XPCShell tests
+ // const xmlDocument = new DOMParser().parseFromString(
+ // "<xml></xml>",
+ // "application/xml"
+ // );
+
+ const values = [
+ { node: divEl, type: Node.ELEMENT_NODE },
+ { node: divEl.attributes[0], type: Node.ATTRIBUTE_NODE },
+ { node: browser.document.createTextNode("foo"), type: Node.TEXT_NODE },
+ // Bug 1820734: No ownerGlobal is available in XPCShell tests
+ // {
+ // node: xmlDocument.createCDATASection("foo"),
+ // type: Node.CDATA_SECTION_NODE,
+ // },
+ {
+ node: browser.document.createProcessingInstruction(
+ "xml-stylesheet",
+ "href='foo.css'"
+ ),
+ type: Node.PROCESSING_INSTRUCTION_NODE_NODE,
+ },
+ { node: browser.document.createComment("foo"), type: Node.COMMENT_NODE },
+ { node: browser.document, type: Node.Document_NODE },
+ {
+ node: browser.document.implementation.createDocumentType(
+ "foo",
+ "bar",
+ "dtd"
+ ),
+ type: Node.DOCUMENT_TYPE_NODE_NODE,
+ },
+ {
+ node: browser.document.createDocumentFragment(),
+ type: Node.DOCUMENT_FRAGMENT_NODE,
+ },
+ ];
+
+ values.forEach((value, index) => {
+ info(`Testing value: ${value.type}`);
+ const nodeRef = nodeCache.getOrCreateNodeReference(value.node, seenNodeIds);
+ equal(nodeCache.size, index + 1);
+ equal(typeof nodeRef, "string");
+ ok(seenNodeIds.get(browser.browsingContext).includes(nodeRef));
+ });
+});
+
+add_task(function getOrCreateNodeReference_referenceAlreadyCreated() {
+ const { browser, divEl, nodeCache, seenNodeIds } = setupTest();
+
+ const divElRef = nodeCache.getOrCreateNodeReference(divEl, seenNodeIds);
+ const divElRefOther = nodeCache.getOrCreateNodeReference(divEl, seenNodeIds);
+
+ equal(divElRefOther, divElRef);
+ equal(nodeCache.size, 1);
+ equal(seenNodeIds.size, 1);
+ ok(seenNodeIds.get(browser.browsingContext).includes(divElRef));
+});
+
+add_task(function getOrCreateNodeReference_differentReference() {
+ const { browser, divEl, nodeCache, seenNodeIds, shadowRoot } = setupTest();
+
+ const divElRef = nodeCache.getOrCreateNodeReference(divEl, seenNodeIds);
+ equal(nodeCache.size, 1);
+ equal(seenNodeIds.size, 1);
+ ok(seenNodeIds.get(browser.browsingContext).includes(divElRef));
+
+ const shadowRootRef = nodeCache.getOrCreateNodeReference(
+ shadowRoot,
+ seenNodeIds
+ );
+ equal(nodeCache.size, 2);
+ equal(seenNodeIds.size, 1);
+ ok(seenNodeIds.get(browser.browsingContext).includes(divElRef));
+ ok(seenNodeIds.get(browser.browsingContext).includes(shadowRootRef));
+
+ notEqual(divElRef, shadowRootRef);
+});
+
+add_task(function getOrCreateNodeReference_differentReferencePerNodeCache() {
+ const { browser, divEl, nodeCache, seenNodeIds } = setupTest();
+ const nodeCache2 = new NodeCache();
+
+ const divElRef1 = nodeCache.getOrCreateNodeReference(divEl, seenNodeIds);
+ const divElRef2 = nodeCache2.getOrCreateNodeReference(divEl, seenNodeIds);
+
+ notEqual(divElRef1, divElRef2);
+ equal(
+ nodeCache.getNode(browser.browsingContext, divElRef1),
+ nodeCache2.getNode(browser.browsingContext, divElRef2)
+ );
+
+ equal(seenNodeIds.size, 1);
+ ok(seenNodeIds.get(browser.browsingContext).includes(divElRef1));
+ ok(seenNodeIds.get(browser.browsingContext).includes(divElRef2));
+
+ equal(nodeCache.getNode(browser.browsingContext, divElRef2), null);
+});
+
+add_task(function clear() {
+ const { browser, divEl, nodeCache, seenNodeIds, svgEl } = setupTest();
+
+ nodeCache.getOrCreateNodeReference(divEl, seenNodeIds);
+ nodeCache.getOrCreateNodeReference(svgEl, seenNodeIds);
+ equal(nodeCache.size, 2);
+ equal(seenNodeIds.size, 1);
+
+ // Clear requires explicit arguments.
+ Assert.throws(() => nodeCache.clear(), /Error/);
+
+ // Clear references for a different browsing context
+ const browser2 = Services.appShell.createWindowlessBrowser(false);
+ const imgEl = browser2.document.createElement("img");
+ const imgElRef = nodeCache.getOrCreateNodeReference(imgEl, seenNodeIds);
+ equal(nodeCache.size, 3);
+ equal(seenNodeIds.size, 2);
+
+ nodeCache.clear({ browsingContext: browser.browsingContext });
+ equal(nodeCache.size, 1);
+ equal(nodeCache.getNode(browser2.browsingContext, imgElRef), imgEl);
+
+ // Clear all references
+ nodeCache.getOrCreateNodeReference(divEl, seenNodeIds);
+ equal(nodeCache.size, 2);
+ equal(seenNodeIds.size, 2);
+
+ nodeCache.clear({ all: true });
+ equal(nodeCache.size, 0);
+});
+
+add_task(function getNode_multiple_nodes() {
+ const { browser, divEl, nodeCache, seenNodeIds, svgEl } = setupTest();
+
+ const divElRef = nodeCache.getOrCreateNodeReference(divEl, seenNodeIds);
+ const svgElRef = nodeCache.getOrCreateNodeReference(svgEl, seenNodeIds);
+
+ equal(nodeCache.getNode(browser.browsingContext, svgElRef), svgEl);
+ equal(nodeCache.getNode(browser.browsingContext, divElRef), divEl);
+});
+
+add_task(function getNode_differentBrowsingContextInSameGroup() {
+ const { iframeEl, divEl, nodeCache, seenNodeIds } = setupTest();
+
+ const divElRef = nodeCache.getOrCreateNodeReference(divEl, seenNodeIds);
+ equal(nodeCache.size, 1);
+
+ equal(
+ nodeCache.getNode(iframeEl.contentWindow.browsingContext, divElRef),
+ divEl
+ );
+});
+
+add_task(function getNode_differentBrowsingContextInOtherGroup() {
+ const { divEl, nodeCache, seenNodeIds } = setupTest();
+
+ const divElRef = nodeCache.getOrCreateNodeReference(divEl, seenNodeIds);
+ equal(nodeCache.size, 1);
+
+ const browser2 = Services.appShell.createWindowlessBrowser(false);
+ equal(nodeCache.getNode(browser2.browsingContext, divElRef), null);
+});
+
+add_task(async function getNode_nodeDeleted() {
+ const { browser, nodeCache, seenNodeIds } = setupTest();
+ let el = browser.document.createElement("div");
+
+ const elRef = nodeCache.getOrCreateNodeReference(el, seenNodeIds);
+
+ // Delete element and force a garbage collection
+ el = null;
+
+ await doGC();
+
+ equal(nodeCache.getNode(browser.browsingContext, elRef), null);
+});
+
+add_task(function getNodeDetails_forTopBrowsingContext() {
+ const { browser, divEl, nodeCache, seenNodeIds } = setupTest();
+
+ const divElRef = nodeCache.getOrCreateNodeReference(divEl, seenNodeIds);
+
+ const nodeDetails = nodeCache.getReferenceDetails(divElRef);
+ equal(nodeDetails.browserId, browser.browsingContext.browserId);
+ equal(nodeDetails.browsingContextGroupId, browser.browsingContext.group.id);
+ equal(nodeDetails.browsingContextId, browser.browsingContext.id);
+ ok(nodeDetails.isTopBrowsingContext);
+ ok(nodeDetails.nodeWeakRef);
+ equal(nodeDetails.nodeWeakRef.get(), divEl);
+});
+
+add_task(async function getNodeDetails_forChildBrowsingContext() {
+ const { browser, iframeEl, childEl, nodeCache, seenNodeIds } = setupTest();
+
+ const childElRef = nodeCache.getOrCreateNodeReference(childEl, seenNodeIds);
+
+ const nodeDetails = nodeCache.getReferenceDetails(childElRef);
+ equal(nodeDetails.browserId, browser.browsingContext.browserId);
+ equal(nodeDetails.browsingContextGroupId, browser.browsingContext.group.id);
+ equal(
+ nodeDetails.browsingContextId,
+ iframeEl.contentWindow.browsingContext.id
+ );
+ ok(!nodeDetails.isTopBrowsingContext);
+ ok(nodeDetails.nodeWeakRef);
+ equal(nodeDetails.nodeWeakRef.get(), childEl);
+});
diff --git a/remote/shared/webdriver/test/xpcshell/test_Session.js b/remote/shared/webdriver/test/xpcshell/test_Session.js
new file mode 100644
index 0000000000..3b3d893319
--- /dev/null
+++ b/remote/shared/webdriver/test/xpcshell/test_Session.js
@@ -0,0 +1,72 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { Capabilities, Timeouts } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/webdriver/Capabilities.sys.mjs"
+);
+const { getWebDriverSessionById, WebDriverSession } =
+ ChromeUtils.importESModule(
+ "chrome://remote/content/shared/webdriver/Session.sys.mjs"
+ );
+
+add_task(function test_WebDriverSession_ctor() {
+ const session = new WebDriverSession();
+
+ equal(typeof session.id, "string");
+ ok(session.capabilities instanceof Capabilities);
+});
+
+add_task(function test_WebDriverSession_destroy() {
+ const session = new WebDriverSession();
+
+ session.destroy();
+});
+
+add_task(function test_WebDriverSession_getters() {
+ const session = new WebDriverSession();
+
+ equal(
+ session.a11yChecks,
+ session.capabilities.get("moz:accessibilityChecks")
+ );
+ equal(session.pageLoadStrategy, session.capabilities.get("pageLoadStrategy"));
+ equal(session.proxy, session.capabilities.get("proxy"));
+ equal(
+ session.strictFileInteractability,
+ session.capabilities.get("strictFileInteractability")
+ );
+ equal(session.timeouts, session.capabilities.get("timeouts"));
+ equal(
+ session.unhandledPromptBehavior,
+ session.capabilities.get("unhandledPromptBehavior")
+ );
+});
+
+add_task(function test_WebDriverSession_setters() {
+ const session = new WebDriverSession();
+
+ const timeouts = new Timeouts();
+ timeouts.pageLoad = 45;
+
+ session.timeouts = timeouts;
+ equal(session.timeouts, session.capabilities.get("timeouts"));
+});
+
+add_task(function test_getWebDriverSessionById() {
+ const session1 = new WebDriverSession();
+ const session2 = new WebDriverSession();
+
+ equal(getWebDriverSessionById(session1.id), session1);
+ equal(getWebDriverSessionById(session2.id), session2);
+
+ session1.destroy();
+ equal(getWebDriverSessionById(session1.id), undefined);
+ equal(getWebDriverSessionById(session2.id), session2);
+
+ session2.destroy();
+ equal(getWebDriverSessionById(session1.id), undefined);
+ equal(getWebDriverSessionById(session2.id), undefined);
+});
diff --git a/remote/shared/webdriver/test/xpcshell/test_URLPattern_invalid.js b/remote/shared/webdriver/test/xpcshell/test_URLPattern_invalid.js
new file mode 100644
index 0000000000..0e537a210f
--- /dev/null
+++ b/remote/shared/webdriver/test/xpcshell/test_URLPattern_invalid.js
@@ -0,0 +1,129 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { parseURLPattern } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/webdriver/URLPattern.sys.mjs"
+);
+
+add_task(
+ async function test_parseURLPattern_patternPattern_unescapedCharacters() {
+ const properties = ["protocol", "hostname", "port", "pathname", "search"];
+ const values = ["*", "(", ")", "{", "}"];
+ for (const property of properties) {
+ for (const value of values) {
+ Assert.throws(
+ () => parseURLPattern({ type: "pattern", [property]: value }),
+ /InvalidArgumentError/
+ );
+ }
+ }
+ }
+);
+
+add_task(async function test_parseURLPattern_patternPattern_protocol() {
+ const values = [
+ "",
+ "http/",
+ "http\\*",
+ "http\\(",
+ "http\\)",
+ "http\\{",
+ "http\\}",
+ "http#",
+ "http@",
+ "http%",
+ ];
+ for (const value of values) {
+ Assert.throws(
+ () => parseURLPattern({ type: "pattern", protocol: value }),
+ /InvalidArgumentError/
+ );
+ }
+});
+
+add_task(
+ async function test_parseURLPattern_patternPattern_unsupported_protocol() {
+ const values = ["ftp", "abc", "webpack"];
+ for (const value of values) {
+ Assert.throws(
+ () => parseURLPattern({ type: "pattern", protocol: value }),
+ /UnsupportedOperationError/
+ );
+ }
+ }
+);
+
+add_task(async function test_parseURLPattern_patternPattern_hostname() {
+ const values = ["", "abc/com/", "abc?com", "abc#com", "abc:com"];
+ for (const value of values) {
+ Assert.throws(
+ () => parseURLPattern({ type: "pattern", hostname: value }),
+ /InvalidArgumentError/
+ );
+ }
+});
+
+add_task(async function test_parseURLPattern_patternPattern_port() {
+ const values = ["", "abcd", "-1", "80 ", "1.3", ":80", "65536"];
+ for (const value of values) {
+ Assert.throws(
+ () => parseURLPattern({ type: "pattern", port: value }),
+ /InvalidArgumentError/
+ );
+ }
+});
+
+add_task(async function test_parseURLPattern_patternPattern_pathname() {
+ const values = ["path?", "path#"];
+ for (const value of values) {
+ Assert.throws(
+ () => parseURLPattern({ type: "pattern", pathname: value }),
+ /InvalidArgumentError/
+ );
+ }
+});
+
+add_task(async function test_parseURLPattern_patternPattern_search() {
+ const values = ["search#"];
+ for (const value of values) {
+ Assert.throws(
+ () => parseURLPattern({ type: "pattern", search: value }),
+ /InvalidArgumentError/
+ );
+ }
+});
+
+add_task(async function test_parseURLPattern_stringPattern_invalid_url() {
+ const values = ["", "invalid", "http:invalid:url", "[1::", "127.0..1"];
+ for (const value of values) {
+ Assert.throws(
+ () => parseURLPattern({ type: "string", pattern: value }),
+ /InvalidArgumentError/
+ );
+ }
+});
+
+add_task(
+ async function test_parseURLPattern_stringPattern_unescaped_characters() {
+ const values = ["*", "(", ")", "{", "}"];
+ for (const value of values) {
+ Assert.throws(
+ () => parseURLPattern({ type: "string", pattern: value }),
+ /InvalidArgumentError/
+ );
+ }
+ }
+);
+
+add_task(
+ async function test_parseURLPattern_stringPattern_unsupported_protocol() {
+ const values = ["ftp://some/path", "abc:pathplaceholder", "webpack://test"];
+ for (const value of values) {
+ Assert.throws(
+ () => parseURLPattern({ type: "string", pattern: value }),
+ /UnsupportedOperationError/
+ );
+ }
+ }
+);
diff --git a/remote/shared/webdriver/test/xpcshell/test_URLPattern_matchURLPattern.js b/remote/shared/webdriver/test/xpcshell/test_URLPattern_matchURLPattern.js
new file mode 100644
index 0000000000..f4831d583f
--- /dev/null
+++ b/remote/shared/webdriver/test/xpcshell/test_URLPattern_matchURLPattern.js
@@ -0,0 +1,607 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { matchURLPattern, parseURLPattern } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/webdriver/URLPattern.sys.mjs"
+);
+
+// Test several variations which should match a string based http://example.com
+// pattern.
+add_task(async function test_matchURLPattern_url_variations() {
+ const pattern = parseURLPattern({
+ type: "string",
+ pattern: "http://example.com",
+ });
+
+ const urls = [
+ "http://example.com",
+ "http://EXAMPLE.com",
+ "http://user:password@example.com",
+ "http://example.com:80",
+ "http://example.com/",
+ "http://example.com/#some-hash",
+ "http:example.com",
+ "http:/example.com",
+ "http://example.com?",
+ "http://example.com/?",
+ ];
+ for (const url of urls) {
+ ok(
+ matchURLPattern(pattern, url),
+ `url "${url}" should match pattern "http://example.com"`
+ );
+ }
+
+ // Test URLs close to http://example.com but which should not match.
+ const failingUrls = [
+ "https://example.com",
+ "http://example.com:88",
+ "http://example.com/a",
+ "http://example.com/?abc",
+ ];
+ for (const url of failingUrls) {
+ ok(
+ !matchURLPattern(pattern, url),
+ `url "${url}" should not match pattern "http://example.com"`
+ );
+ }
+});
+
+add_task(async function test_matchURLPattern_stringPatterns() {
+ const tests = [
+ {
+ pattern: "http://example.com",
+ url: "http://example.com",
+ match: true,
+ },
+ {
+ pattern: "HTTP://example.com:80",
+ url: "http://example.com",
+ match: true,
+ },
+ {
+ pattern: "http://example.com:80",
+ url: "http://example.com",
+ match: true,
+ },
+ {
+ pattern: "http://example.com/path",
+ url: "http://example.com/path",
+ match: true,
+ },
+ {
+ pattern: "http://example.com/PATH_CASE",
+ url: "http://example.com/path_case",
+ match: false,
+ },
+ {
+ pattern: "http://example.com/path_single_segment",
+ url: "http://example.com/path_single_segment/",
+ match: false,
+ },
+ {
+ pattern: "http://example.com/path",
+ url: "http://example.com/path_continued",
+ match: false,
+ },
+ {
+ pattern: "http://example.com/path_two_segments/",
+ url: "http://example.com/path_two_segments/",
+ match: true,
+ },
+ {
+ pattern: "http://example.com/path_two_segments/",
+ url: "http://example.com/path_two_segments",
+ match: false,
+ },
+ {
+ pattern: "http://example.com/emptysearch?",
+ url: "http://example.com/emptysearch?",
+ match: true,
+ },
+ {
+ pattern: "http://example.com/emptysearch?",
+ url: "http://example.com/emptysearch",
+ match: true,
+ },
+ {
+ pattern: "http://example.com/emptysearch?",
+ url: "http://example.com/emptysearch??",
+ match: false,
+ },
+ {
+ pattern: "http://example.com/emptysearch?",
+ url: "http://example.com/emptysearch?a",
+ match: false,
+ },
+ {
+ pattern: "http://example.com/search?param",
+ url: "http://example.com/search?param",
+ match: true,
+ },
+ {
+ pattern: "http://example.com/search?param",
+ url: "http://example.com/search?param=value",
+ match: false,
+ },
+ {
+ pattern: "http://example.com/search?param=value",
+ url: "http://example.com/search?param=value",
+ match: true,
+ },
+ {
+ pattern: "http://example.com/search?a=b&c=d",
+ url: "http://example.com/search?a=b&c=d",
+ match: true,
+ },
+ {
+ pattern: "http://example.com/search?a=b&c=d",
+ url: "http://example.com/search?c=d&a=b",
+ match: false,
+ },
+ {
+ pattern: "http://example.com/search?param",
+ url: "http://example.com/search?param#ref",
+ match: true,
+ },
+ {
+ pattern: "http://example.com/search?param#ref",
+ url: "http://example.com/search?param#ref",
+ match: true,
+ },
+ {
+ pattern: "http://example.com/search?param#ref",
+ url: "http://example.com/search?param",
+ match: true,
+ },
+ {
+ pattern: "http://example.com/search?param",
+ url: "http://example.com/search?parameter",
+ match: false,
+ },
+ {
+ pattern: "http://example.com/search?parameter",
+ url: "http://example.com/search?param",
+ match: false,
+ },
+ {
+ pattern: "https://example.com:80",
+ url: "https://example.com",
+ match: false,
+ },
+ {
+ pattern: "https://example.com:443",
+ url: "https://example.com",
+ match: true,
+ },
+ {
+ pattern: "ws://example.com",
+ url: "ws://example.com:80",
+ match: true,
+ },
+ ];
+
+ runMatchPatternTests(tests, "string");
+});
+
+add_task(async function test_patternPatterns_no_property() {
+ const tests = [
+ // Test protocol
+ {
+ pattern: {},
+ url: "https://example.com",
+ match: true,
+ },
+ {
+ pattern: {},
+ url: "https://example.com",
+ match: true,
+ },
+ {
+ pattern: {},
+ url: "https://example.com:1234",
+ match: true,
+ },
+ {
+ pattern: {},
+ url: "https://example.com/a",
+ match: true,
+ },
+ {
+ pattern: {},
+ url: "https://example.com/a?test",
+ match: true,
+ },
+ ];
+
+ runMatchPatternTests(tests, "pattern");
+});
+
+add_task(async function test_patternPatterns_protocol() {
+ const tests = [
+ // Test protocol
+ {
+ pattern: {
+ protocol: "http",
+ },
+ url: "http://example.com",
+ match: true,
+ },
+ {
+ pattern: {
+ protocol: "HTTP",
+ },
+ url: "http://example.com",
+ match: true,
+ },
+ {
+ pattern: {
+ protocol: "http",
+ },
+ url: "http://example.com:80",
+ match: true,
+ },
+ {
+ pattern: {
+ protocol: "http",
+ },
+ url: "http://example.com:1234",
+ match: true,
+ },
+ {
+ pattern: {
+ protocol: "http",
+ port: "80",
+ },
+ url: "http://example.com:80",
+ match: true,
+ },
+ {
+ pattern: {
+ protocol: "http",
+ port: "1234",
+ },
+ url: "http://example.com:1234",
+ match: true,
+ },
+ {
+ pattern: {
+ protocol: "http",
+ port: "1234",
+ },
+ url: "http://example.com",
+ match: false,
+ },
+ {
+ pattern: {
+ protocol: "http",
+ },
+ url: "https://wrong-scheme.com",
+ match: false,
+ },
+ {
+ pattern: {
+ protocol: "http",
+ },
+ url: "http://whatever.com/?search#ref",
+ match: true,
+ },
+ {
+ pattern: {
+ protocol: "http",
+ },
+ url: "http://example.com/a",
+ match: true,
+ },
+ {
+ pattern: {
+ protocol: "http",
+ },
+ url: "http://whatever.com/path?search#ref",
+ match: true,
+ },
+ ];
+
+ runMatchPatternTests(tests, "pattern");
+});
+
+add_task(async function test_patternPatterns_port() {
+ const tests = [
+ {
+ pattern: {
+ protocol: "http",
+ port: "80",
+ },
+ url: "http://abc.com/",
+ match: true,
+ },
+ {
+ pattern: {
+ port: "1234",
+ },
+ url: "http://a.com:1234",
+ match: true,
+ },
+ {
+ pattern: {
+ port: "1234",
+ },
+ url: "https://a.com:1234",
+ match: true,
+ },
+ ];
+
+ runMatchPatternTests(tests, "pattern");
+});
+
+add_task(async function test_patternPatterns_hostname() {
+ const tests = [
+ {
+ pattern: {
+ hostname: "example.com",
+ },
+ url: "http://example.com",
+ match: true,
+ },
+ {
+ pattern: {
+ hostname: "example.com",
+ },
+ url: "http://example.com:80",
+ match: true,
+ },
+ {
+ pattern: {
+ hostname: "example.com",
+ },
+ url: "https://example.com",
+ match: true,
+ },
+ {
+ pattern: {
+ hostname: "example.com",
+ },
+ url: "https://example.com:443",
+ match: true,
+ },
+ {
+ pattern: {
+ hostname: "example.com",
+ },
+ url: "ws://example.com",
+ match: true,
+ },
+ {
+ pattern: {
+ hostname: "example.com",
+ },
+ url: "ws://example.com:80",
+ match: true,
+ },
+ {
+ pattern: {
+ hostname: "example.com",
+ },
+ url: "http://example.com/path",
+ match: true,
+ },
+ {
+ pattern: {
+ hostname: "example.com",
+ },
+ url: "http://example.com/?search",
+ match: true,
+ },
+ {
+ pattern: {
+ hostname: "example\\{.com",
+ },
+ url: "http://example{.com/",
+ match: true,
+ },
+ {
+ pattern: {
+ hostname: "example\\{.com",
+ },
+ url: "http://example\\{.com/",
+ match: false,
+ },
+ {
+ pattern: {
+ hostname: "127.0.0.1",
+ },
+ url: "http://127.0.0.1/",
+ match: true,
+ },
+ {
+ pattern: {
+ hostname: "127.0.0.1",
+ },
+ url: "http://127.0.0.2/",
+ match: false,
+ },
+ {
+ pattern: {
+ hostname: "[2001:db8::1]",
+ },
+ url: "http://[2001:db8::1]/",
+ match: true,
+ },
+ {
+ pattern: {
+ hostname: "[::AB:1]",
+ },
+ url: "http://[::ab:1]/",
+ match: true,
+ },
+ ];
+
+ runMatchPatternTests(tests, "pattern");
+});
+
+add_task(async function test_patternPatterns_pathname() {
+ const tests = [
+ {
+ pattern: {
+ pathname: "/",
+ },
+ url: "http://example.com",
+ match: true,
+ },
+ {
+ pattern: {
+ pathname: "/",
+ },
+ url: "http://example.com/",
+ match: true,
+ },
+ {
+ pattern: {
+ pathname: "/",
+ },
+ url: "http://example.com/?",
+ match: true,
+ },
+ {
+ pattern: {
+ pathname: "path",
+ },
+ url: "http://example.com/path",
+ match: true,
+ },
+ {
+ pattern: {
+ pathname: "/path",
+ },
+ url: "http://example.com/path",
+ match: true,
+ },
+ {
+ pattern: {
+ pathname: "path",
+ },
+ url: "http://example.com/path/",
+ match: false,
+ },
+ {
+ pattern: {
+ pathname: "path",
+ },
+ url: "http://example.com/path_continued",
+ match: false,
+ },
+ {
+ pattern: {
+ pathname: "/",
+ },
+ url: "http://example.com/path",
+ match: false,
+ },
+ ];
+
+ runMatchPatternTests(tests, "pattern");
+});
+
+add_task(async function test_patternPatterns_search() {
+ const tests = [
+ {
+ pattern: {
+ search: "",
+ },
+ url: "http://example.com/?",
+ match: true,
+ },
+ {
+ pattern: {
+ search: "",
+ },
+ url: "http://example.com/",
+ match: true,
+ },
+ {
+ pattern: {
+ search: "",
+ },
+ url: "http://example.com/?#",
+ match: true,
+ },
+ {
+ pattern: {
+ search: "?",
+ },
+ url: "http://example.com/?",
+ match: true,
+ },
+ {
+ pattern: {
+ search: "?a",
+ },
+ url: "http://example.com/?a",
+ match: true,
+ },
+ {
+ pattern: {
+ search: "?",
+ },
+ url: "http://example.com/??",
+ match: false,
+ },
+ {
+ pattern: {
+ search: "query",
+ },
+ url: "http://example.com/?query",
+ match: true,
+ },
+ {
+ pattern: {
+ search: "?query",
+ },
+ url: "http://example.com/?query",
+ match: true,
+ },
+ {
+ pattern: {
+ search: "query=value",
+ },
+ url: "http://example.com/?query=value",
+ match: true,
+ },
+ {
+ pattern: {
+ search: "query",
+ },
+ url: "http://example.com/?query=value",
+ match: false,
+ },
+ {
+ pattern: {
+ search: "query",
+ },
+ url: "http://example.com/?query#value",
+ match: true,
+ },
+ ];
+
+ runMatchPatternTests(tests, "pattern");
+});
+
+function runMatchPatternTests(tests, type) {
+ for (const test of tests) {
+ let pattern;
+ if (type == "pattern") {
+ pattern = parseURLPattern({ type: "pattern", ...test.pattern });
+ } else {
+ pattern = parseURLPattern({ type: "string", pattern: test.pattern });
+ }
+
+ equal(
+ matchURLPattern(pattern, test.url),
+ test.match,
+ `url "${test.url}" ${
+ test.match ? "should" : "should not"
+ } match pattern ${JSON.stringify(test.pattern)}`
+ );
+ }
+}
diff --git a/remote/shared/webdriver/test/xpcshell/test_URLPattern_parseURLPattern.js b/remote/shared/webdriver/test/xpcshell/test_URLPattern_parseURLPattern.js
new file mode 100644
index 0000000000..d4bf3c5fdf
--- /dev/null
+++ b/remote/shared/webdriver/test/xpcshell/test_URLPattern_parseURLPattern.js
@@ -0,0 +1,369 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { parseURLPattern } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/webdriver/URLPattern.sys.mjs"
+);
+
+add_task(async function test_parseURLPattern_stringPatterns() {
+ const STRING_PATTERN_TESTS = [
+ {
+ input: "http://example.com",
+ protocol: "http",
+ hostname: "example.com",
+ port: "",
+ pathname: "/",
+ search: "",
+ },
+ {
+ input: "http://example.com/",
+ protocol: "http",
+ hostname: "example.com",
+ port: "",
+ pathname: "/",
+ search: "",
+ },
+ {
+ input: "http://EXAMPLE.com",
+ protocol: "http",
+ hostname: "example.com",
+ port: "",
+ pathname: "/",
+ search: "",
+ },
+ {
+ input: "http://example%2Ecom",
+ protocol: "http",
+ hostname: "example.com",
+ port: "",
+ pathname: "/",
+ search: "",
+ },
+
+ {
+ input: "http://example.com:80",
+ protocol: "http",
+ hostname: "example.com",
+ port: "",
+ pathname: "/",
+ search: "",
+ },
+ {
+ input: "http://example.com:8888",
+ protocol: "http",
+ hostname: "example.com",
+ port: "8888",
+ pathname: "/",
+ search: "",
+ },
+ {
+ input: "http://example.com/a////b",
+ protocol: "http",
+ hostname: "example.com",
+ port: "",
+ pathname: "/a////b",
+ search: "",
+ },
+ {
+ input: "http://example.com/?",
+ protocol: "http",
+ hostname: "example.com",
+ port: "",
+ pathname: "/",
+ search: "",
+ },
+ {
+ input: "http://example.com/??",
+ protocol: "http",
+ hostname: "example.com",
+ port: "",
+ pathname: "/",
+ search: "?",
+ },
+ {
+ input: "http://example.com/?/",
+ protocol: "http",
+ hostname: "example.com",
+ port: "",
+ pathname: "/",
+ search: "/",
+ },
+ {
+ input: "file:///testfolder/test.zip",
+ protocol: "file",
+ hostname: "",
+ port: null,
+ pathname: "/testfolder/test.zip",
+ search: "",
+ },
+ {
+ input: "http://example\\{.com/",
+ protocol: "http",
+ hostname: "example{.com",
+ port: "",
+ pathname: "/",
+ search: "",
+ },
+ {
+ input: "http://[2001:db8::1]/",
+ protocol: "http",
+ hostname: "[2001:db8::1]",
+ port: "",
+ pathname: "/",
+ search: "",
+ },
+ {
+ input: "http://127.0.0.1/",
+ protocol: "http",
+ hostname: "127.0.0.1",
+ port: "",
+ pathname: "/",
+ search: "",
+ },
+ ];
+
+ for (const test of STRING_PATTERN_TESTS) {
+ const pattern = parseURLPattern({
+ type: "string",
+ pattern: test.input,
+ });
+
+ equal(pattern.protocol, "protocol" in test ? test.protocol : null);
+ equal(pattern.hostname, "hostname" in test ? test.hostname : null);
+ equal(pattern.port, "port" in test ? test.port : null);
+ equal(pattern.pathname, "pathname" in test ? test.pathname : null);
+ equal(pattern.search, "search" in test ? test.search : null);
+ }
+});
+
+add_task(async function test_parseURLPattern_patternPatterns() {
+ const PATTERN_PATTERN_TESTS = [
+ {
+ pattern: {
+ protocol: "http",
+ },
+ protocol: "http",
+ hostname: null,
+ port: null,
+ pathname: null,
+ search: null,
+ },
+ {
+ pattern: {
+ protocol: "HTTP",
+ },
+ protocol: "http",
+ hostname: null,
+ port: null,
+ pathname: null,
+ search: null,
+ },
+ {
+ pattern: {
+ hostname: "example.com",
+ },
+ protocol: null,
+ hostname: "example.com",
+ port: null,
+ pathname: null,
+ search: null,
+ },
+ {
+ pattern: {
+ hostname: "EXAMPLE.com",
+ },
+ protocol: null,
+ hostname: "example.com",
+ port: null,
+ pathname: null,
+ search: null,
+ },
+ {
+ pattern: {
+ hostname: "127.0.0.1",
+ },
+ protocol: null,
+ hostname: "127.0.0.1",
+ port: null,
+ pathname: null,
+ search: null,
+ },
+ {
+ pattern: {
+ hostname: "[2001:db8::1]",
+ },
+ protocol: null,
+ hostname: "[2001:db8::1]",
+ port: null,
+ pathname: null,
+ search: null,
+ },
+ {
+ pattern: {
+ port: "80",
+ },
+ protocol: null,
+ hostname: null,
+ port: "",
+ pathname: null,
+ search: null,
+ },
+ {
+ pattern: {
+ port: "1234",
+ },
+ protocol: null,
+ hostname: null,
+ port: "1234",
+ pathname: null,
+ search: null,
+ },
+ {
+ pattern: {
+ pathname: "path/to",
+ },
+ protocol: null,
+ hostname: null,
+ port: null,
+ pathname: "/path/to",
+ search: null,
+ },
+ {
+ pattern: {
+ pathname: "/path/to",
+ },
+ protocol: null,
+ hostname: null,
+ port: null,
+ pathname: "/path/to",
+ search: null,
+ },
+ {
+ pattern: {
+ pathname: "/path/to/",
+ },
+ protocol: null,
+ hostname: null,
+ port: null,
+ pathname: "/path/to/",
+ search: null,
+ },
+ {
+ pattern: {
+ search: "?search",
+ },
+ protocol: null,
+ hostname: null,
+ port: null,
+ pathname: null,
+ search: "search",
+ },
+ {
+ pattern: {
+ search: "search",
+ },
+ protocol: null,
+ hostname: null,
+ port: null,
+ pathname: null,
+ search: "search",
+ },
+ {
+ pattern: {
+ search: "?search=something",
+ },
+ protocol: null,
+ hostname: null,
+ port: null,
+ pathname: null,
+ search: "search=something",
+ },
+ {
+ pattern: {
+ search: "search=something",
+ },
+ protocol: null,
+ hostname: null,
+ port: null,
+ pathname: null,
+ search: "search=something",
+ },
+ ];
+
+ for (const test of PATTERN_PATTERN_TESTS) {
+ const pattern = parseURLPattern({
+ type: "pattern",
+ ...test.pattern,
+ });
+
+ equal(pattern.protocol, "protocol" in test ? test.protocol : null);
+ equal(pattern.hostname, "hostname" in test ? test.hostname : null);
+ equal(pattern.port, "port" in test ? test.port : null);
+ equal(pattern.pathname, "pathname" in test ? test.pathname : null);
+ equal(pattern.search, "search" in test ? test.search : null);
+ }
+});
+
+add_task(async function test_parseURLPattern_invalid_type() {
+ const values = [null, undefined, 1, [], "string"];
+ for (const value of values) {
+ Assert.throws(() => parseURLPattern(value), /InvalidArgumentError/);
+ }
+});
+
+add_task(async function test_parseURLPattern_invalid_type_type() {
+ const values = [null, undefined, 1, {}, []];
+ for (const type of values) {
+ Assert.throws(() => parseURLPattern({ type }), /InvalidArgumentError/);
+ }
+});
+
+add_task(async function test_parseURLPattern_invalid_type_value() {
+ const values = ["", "unknownType"];
+ for (const type of values) {
+ Assert.throws(() => parseURLPattern({ type }), /InvalidArgumentError/);
+ }
+});
+
+add_task(async function test_parseURLPattern_invalid_stringPatternType() {
+ const values = [null, undefined, 1, {}, []];
+ for (const pattern of values) {
+ Assert.throws(
+ () => parseURLPattern({ type: "string", pattern }),
+ /InvalidArgumentError/
+ );
+ }
+});
+
+add_task(async function test_parseURLPattern_invalid_stringPattern() {
+ const values = [
+ "foo",
+ "*",
+ "(",
+ ")",
+ "{",
+ "}",
+ "http\\{s\\}://example.com",
+ "https://example.com:port/",
+ ];
+ for (const pattern of values) {
+ Assert.throws(
+ () => parseURLPattern({ type: "string", pattern }),
+ /InvalidArgumentError/
+ );
+ }
+});
+
+add_task(async function test_parseURLPattern_invalid_patternPattern_type() {
+ const properties = ["protocol", "hostname", "port", "pathname", "search"];
+ const values = [false, 42, [], {}];
+ for (const property of properties) {
+ for (const value of values) {
+ Assert.throws(
+ () => parseURLPattern({ type: "pattern", [property]: value }),
+ /InvalidArgumentError/
+ );
+ }
+ }
+});
diff --git a/remote/shared/webdriver/test/xpcshell/xpcshell.toml b/remote/shared/webdriver/test/xpcshell/xpcshell.toml
new file mode 100644
index 0000000000..1cdd1eb47c
--- /dev/null
+++ b/remote/shared/webdriver/test/xpcshell/xpcshell.toml
@@ -0,0 +1,20 @@
+[DEFAULT]
+head = "head.js"
+
+["test_Actions.js"]
+
+["test_Assert.js"]
+
+["test_Capabilities.js"]
+
+["test_Errors.js"]
+
+["test_NodeCache.js"]
+
+["test_Session.js"]
+
+["test_URLPattern_invalid.js"]
+
+["test_URLPattern_matchURLPattern.js"]
+
+["test_URLPattern_parseURLPattern.js"]