summaryrefslogtreecommitdiffstats
path: root/remote/marionette/test
diff options
context:
space:
mode:
Diffstat (limited to 'remote/marionette/test')
-rw-r--r--remote/marionette/test/README1
-rw-r--r--remote/marionette/test/xpcshell/.eslintrc.js7
-rw-r--r--remote/marionette/test/xpcshell/README16
-rw-r--r--remote/marionette/test/xpcshell/head.js7
-rw-r--r--remote/marionette/test/xpcshell/test_action.js745
-rw-r--r--remote/marionette/test/xpcshell/test_actors.js61
-rw-r--r--remote/marionette/test/xpcshell/test_browser.js25
-rw-r--r--remote/marionette/test/xpcshell/test_cookie.js370
-rw-r--r--remote/marionette/test/xpcshell/test_dom.js277
-rw-r--r--remote/marionette/test/xpcshell/test_element.js571
-rw-r--r--remote/marionette/test/xpcshell/test_json.js251
-rw-r--r--remote/marionette/test/xpcshell/test_message.js279
-rw-r--r--remote/marionette/test/xpcshell/test_modal.js119
-rw-r--r--remote/marionette/test/xpcshell/test_navigate.js96
-rw-r--r--remote/marionette/test/xpcshell/test_prefs.js115
-rw-r--r--remote/marionette/test/xpcshell/test_sync.js400
-rw-r--r--remote/marionette/test/xpcshell/xpcshell.ini20
17 files changed, 3360 insertions, 0 deletions
diff --git a/remote/marionette/test/README b/remote/marionette/test/README
new file mode 100644
index 0000000000..9305b92cab
--- /dev/null
+++ b/remote/marionette/test/README
@@ -0,0 +1 @@
+See ../doc/Testing.md \ No newline at end of file
diff --git a/remote/marionette/test/xpcshell/.eslintrc.js b/remote/marionette/test/xpcshell/.eslintrc.js
new file mode 100644
index 0000000000..2ef179ab5e
--- /dev/null
+++ b/remote/marionette/test/xpcshell/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ rules: {
+ camelcase: "off",
+ },
+};
diff --git a/remote/marionette/test/xpcshell/README b/remote/marionette/test/xpcshell/README
new file mode 100644
index 0000000000..ce516d17ca
--- /dev/null
+++ b/remote/marionette/test/xpcshell/README
@@ -0,0 +1,16 @@
+To run the tests in this directory, from the top source directory,
+either invoke the test despatcher in mach:
+
+ % ./mach test remote/marionette/test/xpcshell
+
+Or call out the harness specifically:
+
+ % ./mach xpcshell-test remote/marionette/test/xpcshell
+
+The latter gives you the --sequential option which can be useful
+when debugging to prevent tests from running in parallel.
+
+When adding new tests you must make sure they are listed in
+xpcshell.ini, otherwise they will not run on try.
+
+See also ../../doc/Testing.md for more advice on our other types of tests.
diff --git a/remote/marionette/test/xpcshell/head.js b/remote/marionette/test/xpcshell/head.js
new file mode 100644
index 0000000000..4ff0e0dfa0
--- /dev/null
+++ b/remote/marionette/test/xpcshell/head.js
@@ -0,0 +1,7 @@
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+const SVG_NS = "http://www.w3.org/2000/svg";
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+const browser = Services.appShell.createWindowlessBrowser(false);
diff --git a/remote/marionette/test/xpcshell/test_action.js b/remote/marionette/test/xpcshell/test_action.js
new file mode 100644
index 0000000000..963a3337ec
--- /dev/null
+++ b/remote/marionette/test/xpcshell/test_action.js
@@ -0,0 +1,745 @@
+/* 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 } = ChromeUtils.importESModule(
+ "chrome://remote/content/marionette/action.sys.mjs"
+);
+
+const XHTMLNS = "http://www.w3.org/1999/xhtml";
+
+const domEl = {
+ nodeType: 1,
+ ELEMENT_NODE: 1,
+ namespaceURI: XHTMLNS,
+};
+
+add_test(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);
+ }
+ run_next_test();
+});
+
+add_test(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"
+ );
+
+ run_next_test();
+});
+
+add_test(function test_processPointerParameters() {
+ for (let subtype of ["pointerDown", "pointerUp"]) {
+ for (let pointerType of ["foo", "", "get", "Get", 2, {}]) {
+ const inputTickActions = [
+ {
+ type: "pointer",
+ parameters: { pointerType },
+ subtype,
+ button: 0,
+ },
+ ];
+ let message = `Action sequence with parameters: {pointerType: ${pointerType} subtype: ${subtype}}`;
+ checkFromJSONErrors(inputTickActions, /Unknown pointerType/, 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
+ );
+ }
+
+ run_next_test();
+});
+
+add_test(function test_processPointerDownAction() {
+ for (let button of [-1, "a"]) {
+ const inputTickActions = [
+ { type: "pointer", subtype: "pointerDown", button },
+ ];
+ checkFromJSONErrors(
+ inputTickActions,
+ /Expected 'button' .* to be >= 0/,
+ `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);
+
+ run_next_test();
+});
+
+add_test(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 >= 0/,
+ `{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`
+ );
+ }
+ run_next_test();
+});
+
+add_test(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)})`
+ );
+ }
+
+ run_next_test();
+});
+
+add_test(function test_processPointerMoveActionOriginStringValidation() {
+ for (let origin of ["a", "", "get", "Get"]) {
+ const inputTickActions = [
+ { type: "pointer", duration: 5000, subtype: "pointerMove", origin },
+ ];
+ checkFromJSONErrors(
+ inputTickActions,
+ /Expected 'origin' to be undefined, "viewport", "pointer", or an element/,
+ `actionItem.origin: ${origin}`
+ );
+ }
+
+ run_next_test();
+});
+
+add_test(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);
+ run_next_test();
+});
+
+add_test(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(state, null, null), {
+ x: 0,
+ y: 0,
+ });
+ run_next_test();
+});
+
+add_test(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);
+ }
+ run_next_test();
+});
+
+add_test(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(
+ state,
+ inputSource,
+ [actionItem.x, actionItem.y],
+ null
+ );
+ equal(actionItem.x, target[0]);
+ equal(actionItem.y, target[1]);
+
+ run_next_test();
+});
+
+add_test(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(
+ state,
+ inputSource,
+ [actionItem.x, actionItem.y],
+ null
+ );
+ equal(actionItem.x + inputSource.x, target[0]);
+ equal(actionItem.y + inputSource.y, target[1]);
+
+ run_next_test();
+});
+
+add_test(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
+ );
+ }
+ }
+ }
+ run_next_test();
+});
+
+add_test(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);
+
+ run_next_test();
+});
+
+add_test(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(`Unknown subtype dancing for type ${type}`),
+ message
+ );
+ }
+ run_next_test();
+});
+
+add_test(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");
+
+ run_next_test();
+});
+
+add_test(function test_processInputSourceActionSequenceValidation() {
+ checkFromJSONErrors(
+ [{ type: "swim", subtype: "pause", id: "some id" }],
+ /Unknown 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
+ );
+
+ run_next_test();
+});
+
+add_test(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");
+ run_next_test();
+});
+
+add_test(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");
+ run_next_test();
+});
+
+add_test(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");
+ run_next_test();
+});
+
+add_test(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 1 to be type pointer, got key/,
+ message
+ );
+
+ run_next_test();
+});
+
+add_test(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
+ );
+ }
+ run_next_test();
+});
+
+add_test(function test_extractActionChainEmpty() {
+ const state = new action.State();
+ deepEqual(action.Chain.fromJSON(state, []), []);
+ run_next_test();
+});
+
+add_test(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);
+
+ run_next_test();
+});
+
+add_test(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");
+ run_next_test();
+});
+
+add_test(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());
+ run_next_test();
+});
+
+add_test(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());
+ run_next_test();
+});
+
+// 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/marionette/test/xpcshell/test_actors.js b/remote/marionette/test/xpcshell/test_actors.js
new file mode 100644
index 0000000000..6514ceebb6
--- /dev/null
+++ b/remote/marionette/test/xpcshell/test_actors.js
@@ -0,0 +1,61 @@
+/* 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 {
+ getMarionetteCommandsActorProxy,
+ registerCommandsActor,
+ unregisterCommandsActor,
+} = ChromeUtils.importESModule(
+ "chrome://remote/content/marionette/actors/MarionetteCommandsParent.sys.mjs"
+);
+const { enableEventsActor, disableEventsActor } = ChromeUtils.importESModule(
+ "chrome://remote/content/marionette/actors/MarionetteEventsParent.sys.mjs"
+);
+
+registerCleanupFunction(function() {
+ unregisterCommandsActor();
+ disableEventsActor();
+});
+
+add_test(function test_commandsActor_register() {
+ registerCommandsActor();
+ unregisterCommandsActor();
+
+ registerCommandsActor();
+ registerCommandsActor();
+ unregisterCommandsActor();
+
+ run_next_test();
+});
+
+add_test(async function test_commandsActor_getActorProxy_noBrowsingContext() {
+ registerCommandsActor();
+
+ try {
+ await getMarionetteCommandsActorProxy(() => null).sendQuery("foo", "bar");
+ ok(false, "Expected NoBrowsingContext error not raised");
+ } catch (e) {
+ ok(
+ e.message.includes("No BrowsingContext found"),
+ "Expected default error message found"
+ );
+ }
+
+ unregisterCommandsActor();
+
+ run_next_test();
+});
+
+add_test(function test_eventsActor_enable_disable() {
+ enableEventsActor();
+ disableEventsActor();
+
+ enableEventsActor();
+ enableEventsActor();
+ disableEventsActor();
+
+ run_next_test();
+});
diff --git a/remote/marionette/test/xpcshell/test_browser.js b/remote/marionette/test/xpcshell/test_browser.js
new file mode 100644
index 0000000000..c00a7063e3
--- /dev/null
+++ b/remote/marionette/test/xpcshell/test_browser.js
@@ -0,0 +1,25 @@
+const { Context } = ChromeUtils.importESModule(
+ "chrome://remote/content/marionette/browser.sys.mjs"
+);
+
+add_test(function test_Context() {
+ ok(Context.hasOwnProperty("Chrome"));
+ ok(Context.hasOwnProperty("Content"));
+ equal(typeof Context.Chrome, "string");
+ equal(typeof Context.Content, "string");
+ equal(Context.Chrome, "chrome");
+ equal(Context.Content, "content");
+
+ run_next_test();
+});
+
+add_test(function test_Context_fromString() {
+ equal(Context.fromString("chrome"), Context.Chrome);
+ equal(Context.fromString("content"), Context.Content);
+
+ for (let typ of ["", "foo", true, 42, [], {}, null, undefined]) {
+ Assert.throws(() => Context.fromString(typ), /TypeError/);
+ }
+
+ run_next_test();
+});
diff --git a/remote/marionette/test/xpcshell/test_cookie.js b/remote/marionette/test/xpcshell/test_cookie.js
new file mode 100644
index 0000000000..08d0f41bbf
--- /dev/null
+++ b/remote/marionette/test/xpcshell/test_cookie.js
@@ -0,0 +1,370 @@
+/* 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 { cookie } = ChromeUtils.importESModule(
+ "chrome://remote/content/marionette/cookie.sys.mjs"
+);
+
+/* eslint-disable mozilla/use-chromeutils-generateqi */
+
+cookie.manager = {
+ cookies: [],
+
+ add(
+ domain,
+ path,
+ name,
+ value,
+ secure,
+ httpOnly,
+ session,
+ expiry,
+ originAttributes,
+ sameSite
+ ) {
+ if (name === "fail") {
+ throw new Error("An error occurred while adding cookie");
+ }
+ let newCookie = {
+ host: domain,
+ path,
+ name,
+ value,
+ isSecure: secure,
+ isHttpOnly: httpOnly,
+ isSession: session,
+ expiry,
+ originAttributes,
+ sameSite,
+ };
+ cookie.manager.cookies.push(newCookie);
+ },
+
+ remove(host, name, path) {
+ for (let i = 0; i < this.cookies.length; ++i) {
+ let candidate = this.cookies[i];
+ if (
+ candidate.host === host &&
+ candidate.name === name &&
+ candidate.path === path
+ ) {
+ return this.cookies.splice(i, 1);
+ }
+ }
+ return false;
+ },
+
+ getCookiesFromHost(host) {
+ let hostCookies = this.cookies.filter(
+ c => c.host === host || c.host === "." + host
+ );
+
+ return hostCookies;
+ },
+};
+
+add_test(function test_fromJSON() {
+ // object
+ for (let invalidType of ["foo", 42, true, [], null, undefined]) {
+ Assert.throws(() => cookie.fromJSON(invalidType), /Expected cookie object/);
+ }
+
+ // name and value
+ for (let invalidType of [42, true, [], {}, null, undefined]) {
+ Assert.throws(
+ () => cookie.fromJSON({ name: invalidType }),
+ /Cookie name must be string/
+ );
+ Assert.throws(
+ () => cookie.fromJSON({ name: "foo", value: invalidType }),
+ /Cookie value must be string/
+ );
+ }
+
+ // domain
+ for (let invalidType of [42, true, [], {}, null]) {
+ let domainTest = {
+ name: "foo",
+ value: "bar",
+ domain: invalidType,
+ };
+ Assert.throws(
+ () => cookie.fromJSON(domainTest),
+ /Cookie domain must be string/
+ );
+ }
+ let domainTest = {
+ name: "foo",
+ value: "bar",
+ domain: "domain",
+ };
+ let parsedCookie = cookie.fromJSON(domainTest);
+ equal(parsedCookie.domain, "domain");
+
+ // path
+ for (let invalidType of [42, true, [], {}, null]) {
+ let pathTest = {
+ name: "foo",
+ value: "bar",
+ path: invalidType,
+ };
+ Assert.throws(
+ () => cookie.fromJSON(pathTest),
+ /Cookie path must be string/
+ );
+ }
+
+ // secure
+ for (let invalidType of ["foo", 42, [], {}, null]) {
+ let secureTest = {
+ name: "foo",
+ value: "bar",
+ secure: invalidType,
+ };
+ Assert.throws(
+ () => cookie.fromJSON(secureTest),
+ /Cookie secure flag must be boolean/
+ );
+ }
+
+ // httpOnly
+ for (let invalidType of ["foo", 42, [], {}, null]) {
+ let httpOnlyTest = {
+ name: "foo",
+ value: "bar",
+ httpOnly: invalidType,
+ };
+ Assert.throws(
+ () => cookie.fromJSON(httpOnlyTest),
+ /Cookie httpOnly flag must be boolean/
+ );
+ }
+
+ // expiry
+ for (let invalidType of [
+ -1,
+ Number.MAX_SAFE_INTEGER + 1,
+ "foo",
+ true,
+ [],
+ {},
+ null,
+ ]) {
+ let expiryTest = {
+ name: "foo",
+ value: "bar",
+ expiry: invalidType,
+ };
+ Assert.throws(
+ () => cookie.fromJSON(expiryTest),
+ /Cookie expiry must be a positive integer/
+ );
+ }
+
+ // sameSite
+ for (let invalidType of ["foo", 42, [], {}, null]) {
+ const sameSiteTest = {
+ name: "foo",
+ value: "bar",
+ sameSite: invalidType,
+ };
+ Assert.throws(
+ () => cookie.fromJSON(sameSiteTest),
+ /Cookie SameSite flag must be one of None, Lax, or Strict/
+ );
+ }
+
+ // bare requirements
+ let bare = cookie.fromJSON({ name: "name", value: "value" });
+ equal("name", bare.name);
+ equal("value", bare.value);
+ for (let missing of [
+ "path",
+ "secure",
+ "httpOnly",
+ "session",
+ "expiry",
+ "sameSite",
+ ]) {
+ ok(!bare.hasOwnProperty(missing));
+ }
+
+ // everything
+ let full = cookie.fromJSON({
+ name: "name",
+ value: "value",
+ domain: ".domain",
+ path: "path",
+ secure: true,
+ httpOnly: true,
+ expiry: 42,
+ sameSite: "Lax",
+ });
+ equal("name", full.name);
+ equal("value", full.value);
+ equal(".domain", full.domain);
+ equal("path", full.path);
+ equal(true, full.secure);
+ equal(true, full.httpOnly);
+ equal(42, full.expiry);
+ equal("Lax", full.sameSite);
+
+ run_next_test();
+});
+
+add_test(function test_add() {
+ cookie.manager.cookies = [];
+
+ for (let invalidType of [42, true, [], {}, null, undefined]) {
+ Assert.throws(
+ () => cookie.add({ name: invalidType }),
+ /Cookie name must be string/
+ );
+ Assert.throws(
+ () => cookie.add({ name: "name", value: invalidType }),
+ /Cookie value must be string/
+ );
+ Assert.throws(
+ () => cookie.add({ name: "name", value: "value", domain: invalidType }),
+ /Cookie domain must be string/
+ );
+ }
+
+ cookie.add({
+ name: "name",
+ value: "value",
+ domain: "domain",
+ });
+ equal(1, cookie.manager.cookies.length);
+ equal("name", cookie.manager.cookies[0].name);
+ equal("value", cookie.manager.cookies[0].value);
+ equal(".domain", cookie.manager.cookies[0].host);
+ equal("/", cookie.manager.cookies[0].path);
+ ok(cookie.manager.cookies[0].expiry > new Date(Date.now()).getTime() / 1000);
+
+ cookie.add({
+ name: "name2",
+ value: "value2",
+ domain: "domain2",
+ });
+ equal(2, cookie.manager.cookies.length);
+
+ Assert.throws(() => {
+ let biscuit = { name: "name3", value: "value3", domain: "domain3" };
+ cookie.add(biscuit, { restrictToHost: "other domain" });
+ }, /Cookies may only be set for the current domain/);
+
+ cookie.add({
+ name: "name4",
+ value: "value4",
+ domain: "my.domain:1234",
+ });
+ equal(".my.domain", cookie.manager.cookies[2].host);
+
+ cookie.add({
+ name: "name5",
+ value: "value5",
+ domain: "domain5",
+ path: "/foo/bar",
+ });
+ equal("/foo/bar", cookie.manager.cookies[3].path);
+
+ cookie.add({
+ name: "name6",
+ value: "value",
+ domain: ".domain",
+ });
+ equal(".domain", cookie.manager.cookies[4].host);
+
+ const sameSiteMap = new Map([
+ ["None", Ci.nsICookie.SAMESITE_NONE],
+ ["Lax", Ci.nsICookie.SAMESITE_LAX],
+ ["Strict", Ci.nsICookie.SAMESITE_STRICT],
+ ]);
+
+ Array.from(sameSiteMap.keys()).forEach((entry, index) => {
+ cookie.add({
+ name: "name" + index,
+ value: "value",
+ domain: ".domain",
+ sameSite: entry,
+ });
+ equal(sameSiteMap.get(entry), cookie.manager.cookies[5 + index].sameSite);
+ });
+
+ Assert.throws(() => {
+ cookie.add({ name: "fail", value: "value6", domain: "domain6" });
+ }, /UnableToSetCookieError/);
+
+ run_next_test();
+});
+
+add_test(function test_remove() {
+ cookie.manager.cookies = [];
+
+ let crumble = {
+ name: "test_remove",
+ value: "value",
+ domain: "domain",
+ path: "/custom/path",
+ };
+
+ equal(0, cookie.manager.cookies.length);
+ cookie.add(crumble);
+ equal(1, cookie.manager.cookies.length);
+
+ cookie.remove(crumble);
+ equal(0, cookie.manager.cookies.length);
+ equal(undefined, cookie.manager.cookies[0]);
+
+ run_next_test();
+});
+
+add_test(function test_iter() {
+ cookie.manager.cookies = [];
+ let tomorrow = new Date();
+ tomorrow.setHours(tomorrow.getHours() + 24);
+
+ cookie.add({
+ expiry: tomorrow,
+ name: "0",
+ value: "",
+ domain: "foo.example.com",
+ });
+ cookie.add({
+ expiry: tomorrow,
+ name: "1",
+ value: "",
+ domain: "bar.example.com",
+ });
+
+ let fooCookies = [...cookie.iter("foo.example.com")];
+ equal(1, fooCookies.length);
+ equal(".foo.example.com", fooCookies[0].domain);
+ equal(true, fooCookies[0].hasOwnProperty("expiry"));
+
+ cookie.add({
+ name: "aSessionCookie",
+ value: "",
+ domain: "session.com",
+ });
+
+ let sessionCookies = [...cookie.iter("session.com")];
+ equal(1, sessionCookies.length);
+ equal("aSessionCookie", sessionCookies[0].name);
+ equal(false, sessionCookies[0].hasOwnProperty("expiry"));
+
+ cookie.add({
+ name: "2",
+ value: "",
+ domain: "samesite.example.com",
+ sameSite: "Lax",
+ });
+
+ let sameSiteCookies = [...cookie.iter("samesite.example.com")];
+ equal(1, sameSiteCookies.length);
+ equal("Lax", sameSiteCookies[0].sameSite);
+
+ run_next_test();
+});
diff --git a/remote/marionette/test/xpcshell/test_dom.js b/remote/marionette/test/xpcshell/test_dom.js
new file mode 100644
index 0000000000..83dc9de3ab
--- /dev/null
+++ b/remote/marionette/test/xpcshell/test_dom.js
@@ -0,0 +1,277 @@
+const {
+ ContentEventObserverService,
+ WebElementEventTarget,
+} = ChromeUtils.importESModule(
+ "chrome://remote/content/marionette/dom.sys.mjs"
+);
+
+class MessageSender {
+ constructor() {
+ this.listeners = {};
+ this.sent = [];
+ }
+
+ addMessageListener(name, listener) {
+ this.listeners[name] = listener;
+ }
+
+ sendAsyncMessage(name, data) {
+ this.sent.push({ name, data });
+ }
+}
+
+class Window {
+ constructor() {
+ this.events = [];
+ }
+
+ addEventListener(type) {
+ this.events.push(type);
+ }
+
+ removeEventListener(type) {
+ for (let i = 0; i < this.events.length; ++i) {
+ if (this.events[i] === type) {
+ this.events.splice(i, 1);
+ return;
+ }
+ }
+ }
+}
+
+add_test(function test_WebElementEventTarget_addEventListener_init() {
+ let ipc = new MessageSender();
+ let eventTarget = new WebElementEventTarget(ipc);
+ equal(Object.keys(eventTarget.listeners).length, 0);
+ equal(Object.keys(ipc.listeners).length, 1);
+
+ run_next_test();
+});
+
+add_test(function test_addEventListener() {
+ let ipc = new MessageSender();
+ let eventTarget = new WebElementEventTarget(ipc);
+
+ let listener = () => {};
+ eventTarget.addEventListener("click", listener);
+
+ // click listener was appended
+ equal(Object.keys(eventTarget.listeners).length, 1);
+ ok("click" in eventTarget.listeners);
+ equal(eventTarget.listeners.click.length, 1);
+ equal(eventTarget.listeners.click[0], listener);
+
+ // should have sent a registration message
+ deepEqual(ipc.sent[0], {
+ name: "Marionette:DOM:AddEventListener",
+ data: { type: "click" },
+ });
+
+ run_next_test();
+});
+
+add_test(function test_addEventListener_sameReference() {
+ let ipc = new MessageSender();
+ let eventTarget = new WebElementEventTarget(ipc);
+
+ let listener = () => {};
+ eventTarget.addEventListener("click", listener);
+ eventTarget.addEventListener("click", listener);
+ equal(eventTarget.listeners.click.length, 1);
+
+ run_next_test();
+});
+
+add_test(function test_WebElementEventTarget_addEventListener_once() {
+ let ipc = new MessageSender();
+ let eventTarget = new WebElementEventTarget(ipc);
+
+ eventTarget.addEventListener("click", () => {}, { once: true });
+ equal(eventTarget.listeners.click[0].once, true);
+
+ eventTarget.dispatchEvent({ type: "click" });
+ equal(eventTarget.listeners.click.length, 0);
+ deepEqual(ipc.sent[1], {
+ name: "Marionette:DOM:RemoveEventListener",
+ data: { type: "click" },
+ });
+
+ run_next_test();
+});
+
+add_test(function test_WebElementEventTarget_removeEventListener() {
+ let ipc = new MessageSender();
+ let eventTarget = new WebElementEventTarget(ipc);
+
+ equal(Object.keys(eventTarget.listeners).length, 0);
+ eventTarget.removeEventListener("click", () => {});
+ equal(Object.keys(eventTarget.listeners).length, 0);
+
+ let firstListener = () => {};
+ eventTarget.addEventListener("click", firstListener);
+ equal(eventTarget.listeners.click.length, 1);
+ ok(eventTarget.listeners.click[0] === firstListener);
+
+ let secondListener = () => {};
+ eventTarget.addEventListener("click", secondListener);
+ equal(eventTarget.listeners.click.length, 2);
+ ok(eventTarget.listeners.click[1] === secondListener);
+
+ ok(eventTarget.listeners.click[0] !== eventTarget.listeners.click[1]);
+
+ eventTarget.removeEventListener("click", secondListener);
+ equal(eventTarget.listeners.click.length, 1);
+ ok(eventTarget.listeners.click[0] === firstListener);
+
+ // event should not have been unregistered
+ // because there still exists another click event
+ equal(ipc.sent[ipc.sent.length - 1].name, "Marionette:DOM:AddEventListener");
+
+ eventTarget.removeEventListener("click", firstListener);
+ equal(eventTarget.listeners.click.length, 0);
+ deepEqual(ipc.sent[ipc.sent.length - 1], {
+ name: "Marionette:DOM:RemoveEventListener",
+ data: { type: "click" },
+ });
+
+ run_next_test();
+});
+
+add_test(function test_WebElementEventTarget_dispatchEvent() {
+ let ipc = new MessageSender();
+ let eventTarget = new WebElementEventTarget(ipc);
+
+ let listenerCalled = false;
+ let listener = () => (listenerCalled = true);
+ eventTarget.addEventListener("click", listener);
+ eventTarget.dispatchEvent({ type: "click" });
+ ok(listenerCalled);
+
+ run_next_test();
+});
+
+add_test(function test_WebElementEventTarget_dispatchEvent_multipleListeners() {
+ let ipc = new MessageSender();
+ let eventTarget = new WebElementEventTarget(ipc);
+
+ let clicksA = 0;
+ let clicksB = 0;
+ let listenerA = () => ++clicksA;
+ let listenerB = () => ++clicksB;
+
+ // the same listener should only be added, and consequently fire, once
+ eventTarget.addEventListener("click", listenerA);
+ eventTarget.addEventListener("click", listenerA);
+ eventTarget.addEventListener("click", listenerB);
+ eventTarget.dispatchEvent({ type: "click" });
+ equal(clicksA, 1);
+ equal(clicksB, 1);
+
+ run_next_test();
+});
+
+add_test(function test_ContentEventObserverService_add() {
+ let ipc = new MessageSender();
+ let win = new Window();
+ let obs = new ContentEventObserverService(
+ win,
+ ipc.sendAsyncMessage.bind(ipc)
+ );
+
+ equal(obs.events.size, 0);
+ equal(win.events.length, 0);
+
+ obs.add("foo");
+ equal(obs.events.size, 1);
+ equal(win.events.length, 1);
+ equal(obs.events.values().next().value, "foo");
+ equal(win.events[0], "foo");
+
+ obs.add("foo");
+ equal(obs.events.size, 1);
+ equal(win.events.length, 1);
+
+ run_next_test();
+});
+
+add_test(function test_ContentEventObserverService_remove() {
+ let ipc = new MessageSender();
+ let win = new Window();
+ let obs = new ContentEventObserverService(
+ win,
+ ipc.sendAsyncMessage.bind(ipc)
+ );
+
+ obs.remove("foo");
+ equal(obs.events.size, 0);
+ equal(win.events.length, 0);
+
+ obs.add("bar");
+ equal(obs.events.size, 1);
+ equal(win.events.length, 1);
+
+ obs.remove("bar");
+ equal(obs.events.size, 0);
+ equal(win.events.length, 0);
+
+ obs.add("baz");
+ obs.add("baz");
+ equal(obs.events.size, 1);
+ equal(win.events.length, 1);
+
+ obs.add("bah");
+ equal(obs.events.size, 2);
+ equal(win.events.length, 2);
+
+ obs.remove("baz");
+ equal(obs.events.size, 1);
+ equal(win.events.length, 1);
+
+ obs.remove("bah");
+ equal(obs.events.size, 0);
+ equal(win.events.length, 0);
+
+ run_next_test();
+});
+
+add_test(function test_ContentEventObserverService_clear() {
+ let ipc = new MessageSender();
+ let win = new Window();
+ let obs = new ContentEventObserverService(
+ win,
+ ipc.sendAsyncMessage.bind(ipc)
+ );
+
+ obs.clear();
+ equal(obs.events.size, 0);
+ equal(win.events.length, 0);
+
+ obs.add("foo");
+ obs.add("foo");
+ obs.add("bar");
+ equal(obs.events.size, 2);
+ equal(win.events.length, 2);
+
+ obs.clear();
+ equal(obs.events.size, 0);
+ equal(win.events.length, 0);
+
+ run_next_test();
+});
+
+add_test(function test_ContentEventObserverService_handleEvent() {
+ let ipc = new MessageSender();
+ let win = new Window();
+ let obs = new ContentEventObserverService(
+ win,
+ ipc.sendAsyncMessage.bind(ipc)
+ );
+
+ obs.handleEvent({ type: "click", target: win });
+ deepEqual(ipc.sent[0], {
+ name: "Marionette:DOM:OnEvent",
+ data: { type: "click" },
+ });
+
+ run_next_test();
+});
diff --git a/remote/marionette/test/xpcshell/test_element.js b/remote/marionette/test/xpcshell/test_element.js
new file mode 100644
index 0000000000..de0cbfb2fa
--- /dev/null
+++ b/remote/marionette/test/xpcshell/test_element.js
@@ -0,0 +1,571 @@
+/* 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 {
+ element,
+ WebElement,
+ WebFrame,
+ WebReference,
+ WebWindow,
+} = ChromeUtils.importESModule(
+ "chrome://remote/content/marionette/element.sys.mjs"
+);
+
+class Element {
+ constructor(tagName, attrs = {}) {
+ this.tagName = tagName;
+ this.localName = tagName;
+
+ for (let attr in attrs) {
+ this[attr] = attrs[attr];
+ }
+ }
+
+ get nodeType() {
+ return 1;
+ }
+ get ELEMENT_NODE() {
+ return 1;
+ }
+
+ // this is a severely limited CSS selector
+ // that only supports lists of tag names
+ matches(selector) {
+ let tags = selector.split(",");
+ return tags.includes(this.localName);
+ }
+}
+
+class DOMElement extends Element {
+ constructor(tagName, attrs = {}) {
+ super(tagName, attrs);
+
+ if (typeof this.namespaceURI == "undefined") {
+ this.namespaceURI = XHTML_NS;
+ }
+ if (typeof this.ownerDocument == "undefined") {
+ this.ownerDocument = { designMode: "off" };
+ }
+ if (typeof this.ownerDocument.documentElement == "undefined") {
+ this.ownerDocument.documentElement = { namespaceURI: XHTML_NS };
+ }
+
+ if (typeof this.type == "undefined") {
+ this.type = "text";
+ }
+
+ if (this.localName == "option") {
+ this.selected = false;
+ }
+
+ if (
+ this.localName == "input" &&
+ ["checkbox", "radio"].includes(this.type)
+ ) {
+ this.checked = false;
+ }
+ }
+
+ getBoundingClientRect() {
+ return {
+ top: 0,
+ left: 0,
+ width: 100,
+ height: 100,
+ };
+ }
+}
+
+class SVGElement extends Element {
+ constructor(tagName, attrs = {}) {
+ super(tagName, attrs);
+ this.namespaceURI = SVG_NS;
+ }
+}
+
+class XULElement extends Element {
+ constructor(tagName, attrs = {}) {
+ super(tagName, attrs);
+ this.namespaceURI = XUL_NS;
+
+ if (typeof this.ownerDocument == "undefined") {
+ this.ownerDocument = {};
+ }
+ if (typeof this.ownerDocument.documentElement == "undefined") {
+ this.ownerDocument.documentElement = { namespaceURI: XUL_NS };
+ }
+ }
+}
+
+const domEl = new DOMElement("p");
+const svgEl = new SVGElement("rect");
+const xulEl = new XULElement("text");
+
+const domElInPrivilegedDocument = new Element("input", {
+ nodePrincipal: { isSystemPrincipal: true },
+});
+const xulElInPrivilegedDocument = new XULElement("text", {
+ nodePrincipal: { isSystemPrincipal: true },
+});
+
+class WindowProxy {
+ get parent() {
+ return this;
+ }
+ get self() {
+ return this;
+ }
+ toString() {
+ return "[object Window]";
+ }
+}
+const domWin = new WindowProxy();
+const domFrame = new (class extends WindowProxy {
+ get parent() {
+ return domWin;
+ }
+})();
+
+add_test(function test_findClosest() {
+ equal(element.findClosest(domEl, "foo"), null);
+
+ let foo = new DOMElement("foo");
+ let bar = new DOMElement("bar");
+ bar.parentNode = foo;
+ equal(element.findClosest(bar, "foo"), foo);
+
+ run_next_test();
+});
+
+add_test(function test_isSelected() {
+ let checkbox = new DOMElement("input", { type: "checkbox" });
+ ok(!element.isSelected(checkbox));
+ checkbox.checked = true;
+ ok(element.isSelected(checkbox));
+
+ // selected is not a property of <input type=checkbox>
+ checkbox.selected = true;
+ checkbox.checked = false;
+ ok(!element.isSelected(checkbox));
+
+ let option = new DOMElement("option");
+ ok(!element.isSelected(option));
+ option.selected = true;
+ ok(element.isSelected(option));
+
+ // checked is not a property of <option>
+ option.checked = true;
+ option.selected = false;
+ ok(!element.isSelected(option));
+
+ // anything else should not be selected
+ for (let typ of [domEl, undefined, null, "foo", true, [], {}]) {
+ ok(!element.isSelected(typ));
+ }
+
+ run_next_test();
+});
+
+add_test(function test_isElement() {
+ ok(element.isElement(domEl));
+ ok(element.isElement(svgEl));
+ ok(element.isElement(xulEl));
+ ok(!element.isElement(domWin));
+ ok(!element.isElement(domFrame));
+ for (let typ of [true, 42, {}, [], undefined, null]) {
+ ok(!element.isElement(typ));
+ }
+
+ run_next_test();
+});
+
+add_test(function test_isDOMElement() {
+ ok(element.isDOMElement(domEl));
+ ok(element.isDOMElement(domElInPrivilegedDocument));
+ ok(element.isDOMElement(svgEl));
+ ok(!element.isDOMElement(xulEl));
+ ok(!element.isDOMElement(xulElInPrivilegedDocument));
+ ok(!element.isDOMElement(domWin));
+ ok(!element.isDOMElement(domFrame));
+ for (let typ of [true, 42, {}, [], undefined, null]) {
+ ok(!element.isDOMElement(typ));
+ }
+
+ run_next_test();
+});
+
+add_test(function test_isXULElement() {
+ ok(element.isXULElement(xulEl));
+ ok(element.isXULElement(xulElInPrivilegedDocument));
+ ok(!element.isXULElement(domElInPrivilegedDocument));
+ ok(!element.isXULElement(domEl));
+ ok(!element.isXULElement(svgEl));
+ ok(!element.isXULElement(domWin));
+ ok(!element.isXULElement(domFrame));
+ for (let typ of [true, 42, {}, [], undefined, null]) {
+ ok(!element.isXULElement(typ));
+ }
+
+ run_next_test();
+});
+
+add_test(function test_isDOMWindow() {
+ ok(element.isDOMWindow(domWin));
+ ok(element.isDOMWindow(domFrame));
+ ok(!element.isDOMWindow(domEl));
+ ok(!element.isDOMWindow(domElInPrivilegedDocument));
+ ok(!element.isDOMWindow(svgEl));
+ ok(!element.isDOMWindow(xulEl));
+ for (let typ of [true, 42, {}, [], undefined, null]) {
+ ok(!element.isDOMWindow(typ));
+ }
+
+ run_next_test();
+});
+
+add_test(function test_isReadOnly() {
+ ok(!element.isReadOnly(null));
+ ok(!element.isReadOnly(domEl));
+ ok(!element.isReadOnly(new DOMElement("p", { readOnly: true })));
+ ok(element.isReadOnly(new DOMElement("input", { readOnly: true })));
+ ok(element.isReadOnly(new DOMElement("textarea", { readOnly: true })));
+
+ run_next_test();
+});
+
+add_test(function test_isDisabled() {
+ ok(!element.isDisabled(new DOMElement("p")));
+ ok(!element.isDisabled(new SVGElement("rect", { disabled: true })));
+ ok(!element.isDisabled(new XULElement("browser", { disabled: true })));
+
+ let select = new DOMElement("select", { disabled: true });
+ let option = new DOMElement("option");
+ option.parentNode = select;
+ ok(element.isDisabled(option));
+
+ let optgroup = new DOMElement("optgroup", { disabled: true });
+ option.parentNode = optgroup;
+ optgroup.parentNode = select;
+ select.disabled = false;
+ ok(element.isDisabled(option));
+
+ ok(element.isDisabled(new DOMElement("button", { disabled: true })));
+ ok(element.isDisabled(new DOMElement("input", { disabled: true })));
+ ok(element.isDisabled(new DOMElement("select", { disabled: true })));
+ ok(element.isDisabled(new DOMElement("textarea", { disabled: true })));
+
+ run_next_test();
+});
+
+add_test(function test_isEditingHost() {
+ ok(!element.isEditingHost(null));
+ ok(element.isEditingHost(new DOMElement("p", { isContentEditable: true })));
+ ok(
+ element.isEditingHost(
+ new DOMElement("p", { ownerDocument: { designMode: "on" } })
+ )
+ );
+
+ run_next_test();
+});
+
+add_test(function test_isEditable() {
+ ok(!element.isEditable(null));
+ ok(!element.isEditable(domEl));
+ ok(!element.isEditable(new DOMElement("textarea", { readOnly: true })));
+ ok(!element.isEditable(new DOMElement("textarea", { disabled: true })));
+
+ for (let type of [
+ "checkbox",
+ "radio",
+ "hidden",
+ "submit",
+ "button",
+ "image",
+ ]) {
+ ok(!element.isEditable(new DOMElement("input", { type })));
+ }
+ ok(element.isEditable(new DOMElement("input", { type: "text" })));
+ ok(element.isEditable(new DOMElement("input")));
+
+ ok(element.isEditable(new DOMElement("textarea")));
+ ok(
+ element.isEditable(
+ new DOMElement("p", { ownerDocument: { designMode: "on" } })
+ )
+ );
+ ok(element.isEditable(new DOMElement("p", { isContentEditable: true })));
+
+ run_next_test();
+});
+
+add_test(function test_isMutableFormControlElement() {
+ ok(!element.isMutableFormControl(null));
+ ok(
+ !element.isMutableFormControl(
+ new DOMElement("textarea", { readOnly: true })
+ )
+ );
+ ok(
+ !element.isMutableFormControl(
+ new DOMElement("textarea", { disabled: true })
+ )
+ );
+
+ const mutableStates = new Set([
+ "color",
+ "date",
+ "datetime-local",
+ "email",
+ "file",
+ "month",
+ "number",
+ "password",
+ "range",
+ "search",
+ "tel",
+ "text",
+ "url",
+ "week",
+ ]);
+ for (let type of mutableStates) {
+ ok(element.isMutableFormControl(new DOMElement("input", { type })));
+ }
+ ok(element.isMutableFormControl(new DOMElement("textarea")));
+
+ ok(
+ !element.isMutableFormControl(new DOMElement("input", { type: "hidden" }))
+ );
+ ok(!element.isMutableFormControl(new DOMElement("p")));
+ ok(
+ !element.isMutableFormControl(
+ new DOMElement("p", { isContentEditable: true })
+ )
+ );
+ ok(
+ !element.isMutableFormControl(
+ new DOMElement("p", { ownerDocument: { designMode: "on" } })
+ )
+ );
+
+ run_next_test();
+});
+
+add_test(function test_coordinates() {
+ let p = element.coordinates(domEl);
+ ok(p.hasOwnProperty("x"));
+ ok(p.hasOwnProperty("y"));
+ equal("number", typeof p.x);
+ equal("number", typeof p.y);
+
+ deepEqual({ x: 50, y: 50 }, element.coordinates(domEl));
+ deepEqual({ x: 10, y: 10 }, element.coordinates(domEl, 10, 10));
+ deepEqual({ x: -5, y: -5 }, element.coordinates(domEl, -5, -5));
+
+ Assert.throws(() => element.coordinates(null), /node is null/);
+
+ Assert.throws(
+ () => element.coordinates(domEl, "string", undefined),
+ /Offset must be a number/
+ );
+ Assert.throws(
+ () => element.coordinates(domEl, undefined, "string"),
+ /Offset must be a number/
+ );
+ Assert.throws(
+ () => element.coordinates(domEl, "string", "string"),
+ /Offset must be a number/
+ );
+ Assert.throws(
+ () => element.coordinates(domEl, {}, undefined),
+ /Offset must be a number/
+ );
+ Assert.throws(
+ () => element.coordinates(domEl, undefined, {}),
+ /Offset must be a number/
+ );
+ Assert.throws(
+ () => element.coordinates(domEl, {}, {}),
+ /Offset must be a number/
+ );
+ Assert.throws(
+ () => element.coordinates(domEl, [], undefined),
+ /Offset must be a number/
+ );
+ Assert.throws(
+ () => element.coordinates(domEl, undefined, []),
+ /Offset must be a number/
+ );
+ Assert.throws(
+ () => element.coordinates(domEl, [], []),
+ /Offset must be a number/
+ );
+
+ run_next_test();
+});
+
+add_test(function test_WebReference_ctor() {
+ let el = new WebReference("foo");
+ equal(el.uuid, "foo");
+
+ for (let t of [42, true, [], {}, null, undefined]) {
+ Assert.throws(() => new WebReference(t), /to be a string/);
+ }
+
+ run_next_test();
+});
+
+add_test(function test_WebElemenet_is() {
+ let a = new WebReference("a");
+ let b = new WebReference("b");
+
+ ok(a.is(a));
+ ok(b.is(b));
+ ok(!a.is(b));
+ ok(!b.is(a));
+
+ ok(!a.is({}));
+
+ run_next_test();
+});
+
+add_test(function test_WebReference_from() {
+ ok(WebReference.from(domEl) instanceof WebElement);
+ ok(WebReference.from(xulEl) instanceof WebElement);
+ ok(WebReference.from(domWin) instanceof WebWindow);
+ ok(WebReference.from(domFrame) instanceof WebFrame);
+ ok(WebReference.from(domElInPrivilegedDocument) instanceof WebElement);
+ ok(WebReference.from(xulElInPrivilegedDocument) instanceof WebElement);
+
+ Assert.throws(() => WebReference.from({}), /InvalidArgumentError/);
+
+ run_next_test();
+});
+
+add_test(function test_WebReference_fromJSON_WebElement() {
+ const { Identifier } = WebElement;
+
+ let ref = { [Identifier]: "foo" };
+ let webEl = WebReference.fromJSON(ref);
+ ok(webEl instanceof WebElement);
+ equal(webEl.uuid, "foo");
+
+ let identifierPrecedence = {
+ [Identifier]: "identifier-uuid",
+ };
+ let precedenceEl = WebReference.fromJSON(identifierPrecedence);
+ ok(precedenceEl instanceof WebElement);
+ equal(precedenceEl.uuid, "identifier-uuid");
+
+ run_next_test();
+});
+
+add_test(function test_WebReference_fromJSON_WebWindow() {
+ let ref = { [WebWindow.Identifier]: "foo" };
+ let win = WebReference.fromJSON(ref);
+ ok(win instanceof WebWindow);
+ equal(win.uuid, "foo");
+
+ run_next_test();
+});
+
+add_test(function test_WebReference_fromJSON_WebFrame() {
+ let ref = { [WebFrame.Identifier]: "foo" };
+ let frame = WebReference.fromJSON(ref);
+ ok(frame instanceof WebFrame);
+ equal(frame.uuid, "foo");
+
+ run_next_test();
+});
+
+add_test(function test_WebReference_fromJSON_malformed() {
+ Assert.throws(() => WebReference.fromJSON({}), /InvalidArgumentError/);
+ Assert.throws(() => WebReference.fromJSON(null), /InvalidArgumentError/);
+ run_next_test();
+});
+
+add_test(function test_WebReference_fromUUID() {
+ let domWebEl = WebReference.fromUUID("bar");
+ ok(domWebEl instanceof WebElement);
+ equal(domWebEl.uuid, "bar");
+
+ run_next_test();
+});
+
+add_test(function test_WebReference_isReference() {
+ for (let t of [42, true, "foo", [], {}]) {
+ ok(!WebReference.isReference(t));
+ }
+
+ ok(WebReference.isReference({ [WebElement.Identifier]: "foo" }));
+ ok(WebReference.isReference({ [WebWindow.Identifier]: "foo" }));
+ ok(WebReference.isReference({ [WebFrame.Identifier]: "foo" }));
+
+ run_next_test();
+});
+
+add_test(function test_generateUUID() {
+ equal(typeof element.generateUUID(), "string");
+ run_next_test();
+});
+
+add_test(function test_WebElement_toJSON() {
+ const { Identifier } = WebElement;
+
+ let el = new WebElement("foo");
+ let json = el.toJSON();
+
+ ok(Identifier in json);
+ equal(json[Identifier], "foo");
+
+ run_next_test();
+});
+
+add_test(function test_WebElement_fromJSON() {
+ const { Identifier } = WebElement;
+
+ let el = WebElement.fromJSON({ [Identifier]: "foo" });
+ ok(el instanceof WebElement);
+ equal(el.uuid, "foo");
+
+ Assert.throws(() => WebElement.fromJSON({}), /InvalidArgumentError/);
+
+ run_next_test();
+});
+
+add_test(function test_WebWindow_toJSON() {
+ let win = new WebWindow("foo");
+ let json = win.toJSON();
+ ok(WebWindow.Identifier in json);
+ equal(json[WebWindow.Identifier], "foo");
+
+ run_next_test();
+});
+
+add_test(function test_WebWindow_fromJSON() {
+ let ref = { [WebWindow.Identifier]: "foo" };
+ let win = WebWindow.fromJSON(ref);
+ ok(win instanceof WebWindow);
+ equal(win.uuid, "foo");
+
+ run_next_test();
+});
+
+add_test(function test_WebFrame_toJSON() {
+ let frame = new WebFrame("foo");
+ let json = frame.toJSON();
+ ok(WebFrame.Identifier in json);
+ equal(json[WebFrame.Identifier], "foo");
+
+ run_next_test();
+});
+
+add_test(function test_WebFrame_fromJSON() {
+ let ref = { [WebFrame.Identifier]: "foo" };
+ let win = WebFrame.fromJSON(ref);
+ ok(win instanceof WebFrame);
+ equal(win.uuid, "foo");
+
+ run_next_test();
+});
diff --git a/remote/marionette/test/xpcshell/test_json.js b/remote/marionette/test/xpcshell/test_json.js
new file mode 100644
index 0000000000..b2956677c6
--- /dev/null
+++ b/remote/marionette/test/xpcshell/test_json.js
@@ -0,0 +1,251 @@
+const { WebElement, WebReference } = ChromeUtils.importESModule(
+ "chrome://remote/content/marionette/element.sys.mjs"
+);
+const { json } = ChromeUtils.importESModule(
+ "chrome://remote/content/marionette/json.sys.mjs"
+);
+const { NodeCache } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/webdriver/NodeCache.sys.mjs"
+);
+
+const MemoryReporter = Cc["@mozilla.org/memory-reporter-manager;1"].getService(
+ Ci.nsIMemoryReporterManager
+);
+
+const nodeCache = new NodeCache();
+
+const domEl = browser.document.createElement("div");
+const svgEl = browser.document.createElementNS(SVG_NS, "rect");
+
+browser.document.body.appendChild(domEl);
+browser.document.body.appendChild(svgEl);
+
+const win = domEl.ownerGlobal;
+
+add_test(function test_clone_generalTypes() {
+ // null
+ equal(json.clone(undefined, nodeCache), null);
+ equal(json.clone(null, nodeCache), null);
+
+ // primitives
+ equal(json.clone(true, nodeCache), true);
+ equal(json.clone(42, nodeCache), 42);
+ equal(json.clone("foo", nodeCache), "foo");
+
+ // toJSON
+ equal(
+ json.clone({
+ toJSON() {
+ return "foo";
+ },
+ }),
+ "foo"
+ );
+
+ nodeCache.clear({ all: true });
+ run_next_test();
+});
+
+add_test(function test_clone_WebElements() {
+ const domElSharedId = nodeCache.add(domEl);
+ deepEqual(
+ json.clone(domEl, nodeCache),
+ WebReference.from(domEl, domElSharedId).toJSON()
+ );
+
+ const svgElSharedId = nodeCache.add(svgEl);
+ deepEqual(
+ json.clone(svgEl, nodeCache),
+ WebReference.from(svgEl, svgElSharedId).toJSON()
+ );
+
+ nodeCache.clear({ all: true });
+ run_next_test();
+});
+
+add_test(function test_clone_Sequences() {
+ const domElSharedId = nodeCache.add(domEl);
+
+ const input = [
+ null,
+ true,
+ [],
+ domEl,
+ {
+ toJSON() {
+ return "foo";
+ },
+ },
+ { bar: "baz" },
+ ];
+
+ const actual = json.clone(input, nodeCache);
+
+ equal(actual[0], null);
+ equal(actual[1], true);
+ deepEqual(actual[2], []);
+ deepEqual(actual[3], { [WebElement.Identifier]: domElSharedId });
+ equal(actual[4], "foo");
+ deepEqual(actual[5], { bar: "baz" });
+
+ nodeCache.clear({ all: true });
+ run_next_test();
+});
+
+add_test(function test_clone_objects() {
+ const domElSharedId = nodeCache.add(domEl);
+
+ const input = {
+ null: null,
+ boolean: true,
+ array: [42],
+ element: domEl,
+ toJSON: {
+ toJSON() {
+ return "foo";
+ },
+ },
+ object: { bar: "baz" },
+ };
+
+ const actual = json.clone(input, nodeCache);
+
+ equal(actual.null, null);
+ equal(actual.boolean, true);
+ deepEqual(actual.array, [42]);
+ deepEqual(actual.element, { [WebElement.Identifier]: domElSharedId });
+ equal(actual.toJSON, "foo");
+ deepEqual(actual.object, { bar: "baz" });
+
+ nodeCache.clear({ all: true });
+ run_next_test();
+});
+
+add_test(function test_clone_сyclicReference() {
+ // object
+ Assert.throws(() => {
+ const obj = {};
+ obj.reference = obj;
+ json.clone(obj, nodeCache);
+ }, /JavaScriptError/);
+
+ // array
+ Assert.throws(() => {
+ const array = [];
+ array.push(array);
+ json.clone(array, nodeCache);
+ }, /JavaScriptError/);
+
+ // array in object
+ Assert.throws(() => {
+ const array = [];
+ array.push(array);
+ json.clone({ array }, nodeCache);
+ }, /JavaScriptError/);
+
+ // object in array
+ Assert.throws(() => {
+ const obj = {};
+ obj.reference = obj;
+ json.clone([obj], nodeCache);
+ }, /JavaScriptError/);
+
+ run_next_test();
+});
+
+add_test(function test_deserialize_generalTypes() {
+ // null
+ equal(json.deserialize(undefined, nodeCache, win), undefined);
+ equal(json.deserialize(null, nodeCache, win), null);
+
+ // primitives
+ equal(json.deserialize(true, nodeCache, win), true);
+ equal(json.deserialize(42, nodeCache, win), 42);
+ equal(json.deserialize("foo", nodeCache, win), "foo");
+
+ nodeCache.clear({ all: true });
+ run_next_test();
+});
+
+add_test(function test_deserialize_WebElements() {
+ // Fails to resolve for unknown elements
+ const unknownWebElId = { [WebElement.Identifier]: "foo" };
+ Assert.throws(() => {
+ json.deserialize(unknownWebElId, nodeCache, win);
+ }, /NoSuchElementError/);
+
+ const domElSharedId = nodeCache.add(domEl);
+ const domWebEl = { [WebElement.Identifier]: domElSharedId };
+
+ // Fails to resolve for missing window reference
+ Assert.throws(() => json.deserialize(domWebEl, nodeCache), /TypeError/);
+
+ // Previously seen element is associated with original web element reference
+ const el = json.deserialize(domWebEl, nodeCache, win);
+ deepEqual(el, domEl);
+ deepEqual(el, nodeCache.resolve(domElSharedId));
+
+ // Fails with stale element reference for removed element
+ let imgEl = browser.document.createElement("img");
+ const imgElSharedId = nodeCache.add(imgEl);
+ const imgWebEl = { [WebElement.Identifier]: imgElSharedId };
+
+ // Delete element and force a garbage collection
+ imgEl = null;
+
+ MemoryReporter.minimizeMemoryUsage(() => {
+ Assert.throws(
+ () => json.deserialize(imgWebEl, nodeCache, win),
+ /StaleElementReferenceError:/
+ );
+
+ nodeCache.clear({ all: true });
+ run_next_test();
+ });
+});
+
+add_test(function test_deserialize_Sequences() {
+ const domElSharedId = nodeCache.add(domEl);
+
+ const input = [
+ null,
+ true,
+ [42],
+ { [WebElement.Identifier]: domElSharedId },
+ { bar: "baz" },
+ ];
+
+ const actual = json.deserialize(input, nodeCache, win);
+
+ equal(actual[0], null);
+ equal(actual[1], true);
+ deepEqual(actual[2], [42]);
+ deepEqual(actual[3], domEl);
+ deepEqual(actual[4], { bar: "baz" });
+
+ nodeCache.clear({ all: true });
+ run_next_test();
+});
+
+add_test(function test_deserialize_objects() {
+ const domElSharedId = nodeCache.add(domEl);
+
+ const input = {
+ null: null,
+ boolean: true,
+ array: [42],
+ element: { [WebElement.Identifier]: domElSharedId },
+ object: { bar: "baz" },
+ };
+
+ const actual = json.deserialize(input, nodeCache, win);
+
+ equal(actual.null, null);
+ equal(actual.boolean, true);
+ deepEqual(actual.array, [42]);
+ deepEqual(actual.element, domEl);
+ deepEqual(actual.object, { bar: "baz" });
+
+ nodeCache.clear({ all: true });
+ run_next_test();
+});
diff --git a/remote/marionette/test/xpcshell/test_message.js b/remote/marionette/test/xpcshell/test_message.js
new file mode 100644
index 0000000000..5cf717d295
--- /dev/null
+++ b/remote/marionette/test/xpcshell/test_message.js
@@ -0,0 +1,279 @@
+/* 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 { Command, Message, Response } = ChromeUtils.importESModule(
+ "chrome://remote/content/marionette/message.sys.mjs"
+);
+
+add_test(function test_Message_Origin() {
+ equal(0, Message.Origin.Client);
+ equal(1, Message.Origin.Server);
+
+ run_next_test();
+});
+
+add_test(function test_Message_fromPacket() {
+ let cmd = new Command(4, "foo");
+ let resp = new Response(5, () => {});
+ resp.error = "foo";
+
+ ok(Message.fromPacket(cmd.toPacket()) instanceof Command);
+ ok(Message.fromPacket(resp.toPacket()) instanceof Response);
+ Assert.throws(
+ () => Message.fromPacket([3, 4, 5, 6]),
+ /Unrecognised message type in packet/
+ );
+
+ run_next_test();
+});
+
+add_test(function test_Command() {
+ let cmd = new Command(42, "foo", { bar: "baz" });
+ equal(42, cmd.id);
+ equal("foo", cmd.name);
+ deepEqual({ bar: "baz" }, cmd.parameters);
+ equal(null, cmd.onerror);
+ equal(null, cmd.onresult);
+ equal(Message.Origin.Client, cmd.origin);
+ equal(false, cmd.sent);
+
+ run_next_test();
+});
+
+add_test(function test_Command_onresponse() {
+ let onerrorOk = false;
+ let onresultOk = false;
+
+ let cmd = new Command(7, "foo");
+ cmd.onerror = () => (onerrorOk = true);
+ cmd.onresult = () => (onresultOk = true);
+
+ let errorResp = new Response(8, () => {});
+ errorResp.error = new error.WebDriverError("foo");
+
+ let bodyResp = new Response(9, () => {});
+ bodyResp.body = "bar";
+
+ cmd.onresponse(errorResp);
+ equal(true, onerrorOk);
+ equal(false, onresultOk);
+
+ cmd.onresponse(bodyResp);
+ equal(true, onresultOk);
+
+ run_next_test();
+});
+
+add_test(function test_Command_ctor() {
+ let cmd = new Command(42, "bar", { bar: "baz" });
+ let msg = cmd.toPacket();
+
+ equal(Command.Type, msg[0]);
+ equal(cmd.id, msg[1]);
+ equal(cmd.name, msg[2]);
+ equal(cmd.parameters, msg[3]);
+
+ run_next_test();
+});
+
+add_test(function test_Command_toString() {
+ let cmd = new Command(42, "foo", { bar: "baz" });
+ equal(JSON.stringify(cmd.toPacket()), cmd.toString());
+
+ run_next_test();
+});
+
+add_test(function test_Command_fromPacket() {
+ let c1 = new Command(42, "foo", { bar: "baz" });
+
+ let msg = c1.toPacket();
+ let c2 = Command.fromPacket(msg);
+
+ equal(c1.id, c2.id);
+ equal(c1.name, c2.name);
+ equal(c1.parameters, c2.parameters);
+
+ Assert.throws(
+ () => Command.fromPacket([null, 2, "foo", {}]),
+ /InvalidArgumentError/
+ );
+ Assert.throws(
+ () => Command.fromPacket([1, 2, "foo", {}]),
+ /InvalidArgumentError/
+ );
+ Assert.throws(
+ () => Command.fromPacket([0, null, "foo", {}]),
+ /InvalidArgumentError/
+ );
+ Assert.throws(
+ () => Command.fromPacket([0, 2, null, {}]),
+ /InvalidArgumentError/
+ );
+ Assert.throws(
+ () => Command.fromPacket([0, 2, "foo", false]),
+ /InvalidArgumentError/
+ );
+
+ let nullParams = Command.fromPacket([0, 2, "foo", null]);
+ equal(
+ "[object Object]",
+ Object.prototype.toString.call(nullParams.parameters)
+ );
+
+ run_next_test();
+});
+
+add_test(function test_Command_Type() {
+ equal(0, Command.Type);
+ run_next_test();
+});
+
+add_test(function test_Response_ctor() {
+ let handler = () => run_next_test();
+
+ let resp = new Response(42, handler);
+ equal(42, resp.id);
+ equal(null, resp.error);
+ ok("origin" in resp);
+ equal(Message.Origin.Server, resp.origin);
+ equal(false, resp.sent);
+ equal(handler, resp.respHandler_);
+
+ run_next_test();
+});
+
+add_test(function test_Response_sendConditionally() {
+ let fired = false;
+ let resp = new Response(42, () => (fired = true));
+ resp.sendConditionally(() => false);
+ equal(false, resp.sent);
+ equal(false, fired);
+ resp.sendConditionally(() => true);
+ equal(true, resp.sent);
+ equal(true, fired);
+
+ run_next_test();
+});
+
+add_test(function test_Response_send() {
+ let fired = false;
+ let resp = new Response(42, () => (fired = true));
+ resp.send();
+ equal(true, resp.sent);
+ equal(true, fired);
+
+ run_next_test();
+});
+
+add_test(function test_Response_sendError_sent() {
+ let resp = new Response(42, r => equal(false, r.sent));
+ resp.sendError(new error.WebDriverError());
+ ok(resp.sent);
+ Assert.throws(() => resp.send(), /already been sent/);
+
+ run_next_test();
+});
+
+add_test(function test_Response_sendError_body() {
+ let resp = new Response(42, r => equal(null, r.body));
+ resp.sendError(new error.WebDriverError());
+
+ run_next_test();
+});
+
+add_test(function test_Response_sendError_errorSerialisation() {
+ let err1 = new error.WebDriverError();
+ let resp1 = new Response(42);
+ resp1.sendError(err1);
+ equal(err1.status, resp1.error.error);
+ deepEqual(err1.toJSON(), resp1.error);
+
+ let err2 = new error.InvalidArgumentError();
+ let resp2 = new Response(43);
+ resp2.sendError(err2);
+ equal(err2.status, resp2.error.error);
+ deepEqual(err2.toJSON(), resp2.error);
+
+ run_next_test();
+});
+
+add_test(function test_Response_sendError_wrapInternalError() {
+ let err = new ReferenceError("foo");
+
+ // errors that originate from JavaScript (i.e. Marionette implementation
+ // issues) should be converted to UnknownError for transport
+ let resp = new Response(42, r => {
+ equal("unknown error", r.error.error);
+ equal(false, resp.sent);
+ });
+
+ // they should also throw after being sent
+ Assert.throws(() => resp.sendError(err), /foo/);
+ equal(true, resp.sent);
+
+ run_next_test();
+});
+
+add_test(function test_Response_toPacket() {
+ let resp = new Response(42, () => {});
+ let msg = resp.toPacket();
+
+ equal(Response.Type, msg[0]);
+ equal(resp.id, msg[1]);
+ equal(resp.error, msg[2]);
+ equal(resp.body, msg[3]);
+
+ run_next_test();
+});
+
+add_test(function test_Response_toString() {
+ let resp = new Response(42, () => {});
+ resp.error = "foo";
+ resp.body = "bar";
+
+ equal(JSON.stringify(resp.toPacket()), resp.toString());
+
+ run_next_test();
+});
+
+add_test(function test_Response_fromPacket() {
+ let r1 = new Response(42, () => {});
+ r1.error = "foo";
+ r1.body = "bar";
+
+ let msg = r1.toPacket();
+ let r2 = Response.fromPacket(msg);
+
+ equal(r1.id, r2.id);
+ equal(r1.error, r2.error);
+ equal(r1.body, r2.body);
+
+ Assert.throws(
+ () => Response.fromPacket([null, 2, "foo", {}]),
+ /InvalidArgumentError/
+ );
+ Assert.throws(
+ () => Response.fromPacket([0, 2, "foo", {}]),
+ /InvalidArgumentError/
+ );
+ Assert.throws(
+ () => Response.fromPacket([1, null, "foo", {}]),
+ /InvalidArgumentError/
+ );
+ Assert.throws(
+ () => Response.fromPacket([1, 2, null, {}]),
+ /InvalidArgumentError/
+ );
+ Response.fromPacket([1, 2, "foo", null]);
+
+ run_next_test();
+});
+
+add_test(function test_Response_Type() {
+ equal(1, Response.Type);
+ run_next_test();
+});
diff --git a/remote/marionette/test/xpcshell/test_modal.js b/remote/marionette/test/xpcshell/test_modal.js
new file mode 100644
index 0000000000..ac1f020353
--- /dev/null
+++ b/remote/marionette/test/xpcshell/test_modal.js
@@ -0,0 +1,119 @@
+/* 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 { modal } = ChromeUtils.importESModule(
+ "chrome://remote/content/marionette/modal.sys.mjs"
+);
+
+const chromeWindow = {};
+
+const mockModalDialog = {
+ docShell: {
+ chromeEventHandler: null,
+ },
+ opener: {
+ ownerGlobal: chromeWindow,
+ },
+ Dialog: {
+ args: {
+ modalType: Services.prompt.MODAL_TYPE_WINDOW,
+ },
+ },
+};
+
+const mockCurBrowser = {
+ window: chromeWindow,
+};
+
+add_test(function test_addCallback() {
+ let observer = new modal.DialogObserver(() => mockCurBrowser);
+ let cb1 = () => true;
+ let cb2 = () => false;
+
+ equal(observer.callbacks.size, 0);
+ observer.add(cb1);
+ equal(observer.callbacks.size, 1);
+ observer.add(cb1);
+ equal(observer.callbacks.size, 1);
+ observer.add(cb2);
+ equal(observer.callbacks.size, 2);
+
+ run_next_test();
+});
+
+add_test(function test_removeCallback() {
+ let observer = new modal.DialogObserver(() => mockCurBrowser);
+ let cb1 = () => true;
+ let cb2 = () => false;
+
+ equal(observer.callbacks.size, 0);
+ observer.add(cb1);
+ observer.add(cb2);
+
+ equal(observer.callbacks.size, 2);
+ observer.remove(cb1);
+ equal(observer.callbacks.size, 1);
+ observer.remove(cb1);
+ equal(observer.callbacks.size, 1);
+ observer.remove(cb2);
+ equal(observer.callbacks.size, 0);
+
+ run_next_test();
+});
+
+add_test(function test_registerDialogClosedEventHandler() {
+ let observer = new modal.DialogObserver(() => mockCurBrowser);
+ let mockChromeWindow = {
+ addEventListener(event, cb) {
+ equal(
+ event,
+ "DOMModalDialogClosed",
+ "registered event for closing modal"
+ );
+ equal(cb, observer, "set itself as handler");
+ run_next_test();
+ },
+ };
+
+ observer.observe(mockChromeWindow, "domwindowopened");
+});
+
+add_test(function test_handleCallbackOpenModalDialog() {
+ let observer = new modal.DialogObserver(() => mockCurBrowser);
+
+ observer.add((action, dialog) => {
+ equal(action, modal.ACTION_OPENED, "'opened' action has been passed");
+ equal(dialog, mockModalDialog, "dialog has been passed");
+ run_next_test();
+ });
+ observer.observe(mockModalDialog, "common-dialog-loaded");
+});
+
+add_test(function test_handleCallbackCloseModalDialog() {
+ let observer = new modal.DialogObserver(() => mockCurBrowser);
+
+ observer.add((action, dialog) => {
+ equal(action, modal.ACTION_CLOSED, "'closed' action has been passed");
+ equal(dialog, mockModalDialog, "dialog has been passed");
+ run_next_test();
+ });
+ observer.handleEvent({
+ type: "DOMModalDialogClosed",
+ target: mockModalDialog,
+ });
+});
+
+add_test(function test_dialogClosed() {
+ let observer = new modal.DialogObserver(() => mockCurBrowser);
+
+ observer.dialogClosed().then(() => {
+ run_next_test();
+ });
+ observer.handleEvent({
+ type: "DOMModalDialogClosed",
+ target: mockModalDialog,
+ });
+});
diff --git a/remote/marionette/test/xpcshell/test_navigate.js b/remote/marionette/test/xpcshell/test_navigate.js
new file mode 100644
index 0000000000..0bb6573d21
--- /dev/null
+++ b/remote/marionette/test/xpcshell/test_navigate.js
@@ -0,0 +1,96 @@
+/* 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 { navigate } = ChromeUtils.importESModule(
+ "chrome://remote/content/marionette/navigate.sys.mjs"
+);
+
+const mockTopContext = {
+ get children() {
+ return [mockNestedContext];
+ },
+ id: 7,
+ get top() {
+ return this;
+ },
+};
+
+const mockNestedContext = {
+ id: 8,
+ parent: mockTopContext,
+ top: mockTopContext,
+};
+
+add_test(function test_isLoadEventExpectedForCurrent() {
+ Assert.throws(
+ () => navigate.isLoadEventExpected(undefined),
+ /Expected at least one URL/
+ );
+
+ ok(navigate.isLoadEventExpected(new URL("http://a/")));
+
+ run_next_test();
+});
+
+add_test(function test_isLoadEventExpectedForFuture() {
+ const data = [
+ { current: "http://a/", future: undefined, expected: true },
+ { current: "http://a/", future: "http://a/", expected: true },
+ { current: "http://a/", future: "http://a/#", expected: true },
+ { current: "http://a/#", future: "http://a/", expected: true },
+ { current: "http://a/#a", future: "http://a/#A", expected: true },
+ { current: "http://a/#a", future: "http://a/#a", expected: false },
+ { current: "http://a/", future: "javascript:whatever", expected: false },
+ ];
+
+ for (const entry of data) {
+ const current = new URL(entry.current);
+ const future = entry.future ? new URL(entry.future) : undefined;
+ equal(navigate.isLoadEventExpected(current, { future }), entry.expected);
+ }
+
+ run_next_test();
+});
+
+add_test(function test_isLoadEventExpectedForTarget() {
+ for (const target of ["_parent", "_top"]) {
+ Assert.throws(
+ () => navigate.isLoadEventExpected(new URL("http://a"), { target }),
+ /Expected browsingContext when target is _parent or _top/
+ );
+ }
+
+ const data = [
+ { cur: "http://a/", target: "", expected: true },
+ { cur: "http://a/", target: "_blank", expected: false },
+ { cur: "http://a/", target: "_parent", bc: mockTopContext, expected: true },
+ {
+ cur: "http://a/",
+ target: "_parent",
+ bc: mockNestedContext,
+ expected: false,
+ },
+ { cur: "http://a/", target: "_self", expected: true },
+ { cur: "http://a/", target: "_top", bc: mockTopContext, expected: true },
+ {
+ cur: "http://a/",
+ target: "_top",
+ bc: mockNestedContext,
+ expected: false,
+ },
+ ];
+
+ for (const entry of data) {
+ const current = entry.cur ? new URL(entry.cur) : undefined;
+ equal(
+ navigate.isLoadEventExpected(current, {
+ target: entry.target,
+ browsingContext: entry.bc,
+ }),
+ entry.expected
+ );
+ }
+
+ run_next_test();
+});
diff --git a/remote/marionette/test/xpcshell/test_prefs.js b/remote/marionette/test/xpcshell/test_prefs.js
new file mode 100644
index 0000000000..85d1875e99
--- /dev/null
+++ b/remote/marionette/test/xpcshell/test_prefs.js
@@ -0,0 +1,115 @@
+/* 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 {
+ Branch,
+ EnvironmentPrefs,
+ MarionettePrefs,
+} = ChromeUtils.importESModule(
+ "chrome://remote/content/marionette/prefs.sys.mjs"
+);
+
+function reset() {
+ Services.prefs.setBoolPref("test.bool", false);
+ Services.prefs.setStringPref("test.string", "foo");
+ Services.prefs.setIntPref("test.int", 777);
+}
+
+// Give us something to work with:
+reset();
+
+add_test(function test_Branch_get_root() {
+ let root = new Branch(null);
+ equal(false, root.get("test.bool"));
+ equal("foo", root.get("test.string"));
+ equal(777, root.get("test.int"));
+ Assert.throws(() => root.get("doesnotexist"), /TypeError/);
+
+ run_next_test();
+});
+
+add_test(function test_Branch_get_branch() {
+ let test = new Branch("test.");
+ equal(false, test.get("bool"));
+ equal("foo", test.get("string"));
+ equal(777, test.get("int"));
+ Assert.throws(() => test.get("doesnotexist"), /TypeError/);
+
+ run_next_test();
+});
+
+add_test(function test_Branch_set_root() {
+ let root = new Branch(null);
+
+ try {
+ root.set("test.string", "bar");
+ root.set("test.in", 777);
+ root.set("test.bool", true);
+
+ equal("bar", Services.prefs.getStringPref("test.string"));
+ equal(777, Services.prefs.getIntPref("test.int"));
+ equal(true, Services.prefs.getBoolPref("test.bool"));
+ } finally {
+ reset();
+ }
+
+ run_next_test();
+});
+
+add_test(function test_Branch_set_branch() {
+ let test = new Branch("test.");
+
+ try {
+ test.set("string", "bar");
+ test.set("int", 888);
+ test.set("bool", true);
+
+ equal("bar", Services.prefs.getStringPref("test.string"));
+ equal(888, Services.prefs.getIntPref("test.int"));
+ equal(true, Services.prefs.getBoolPref("test.bool"));
+ } finally {
+ reset();
+ }
+
+ run_next_test();
+});
+
+add_test(function test_EnvironmentPrefs_from() {
+ let prefsTable = {
+ "test.bool": true,
+ "test.int": 888,
+ "test.string": "bar",
+ };
+ Services.env.set("FOO", JSON.stringify(prefsTable));
+
+ try {
+ for (let [key, value] of EnvironmentPrefs.from("FOO")) {
+ equal(prefsTable[key], value);
+ }
+ } finally {
+ Services.env.set("FOO", null);
+ }
+
+ run_next_test();
+});
+
+add_test(function test_MarionettePrefs_getters() {
+ equal(false, MarionettePrefs.clickToStart);
+ equal(2828, MarionettePrefs.port);
+
+ run_next_test();
+});
+
+add_test(function test_MarionettePrefs_setters() {
+ try {
+ MarionettePrefs.port = 777;
+ equal(777, MarionettePrefs.port);
+ } finally {
+ Services.prefs.clearUserPref("marionette.port");
+ }
+
+ run_next_test();
+});
diff --git a/remote/marionette/test/xpcshell/test_sync.js b/remote/marionette/test/xpcshell/test_sync.js
new file mode 100644
index 0000000000..e074327a9b
--- /dev/null
+++ b/remote/marionette/test/xpcshell/test_sync.js
@@ -0,0 +1,400 @@
+/* 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 {
+ DebounceCallback,
+ IdlePromise,
+ PollPromise,
+ Sleep,
+ TimedPromise,
+ waitForMessage,
+ waitForObserverTopic,
+} = ChromeUtils.importESModule(
+ "chrome://remote/content/marionette/sync.sys.mjs"
+);
+
+/**
+ * Mimic a message manager for sending messages.
+ */
+class MessageManager {
+ constructor() {
+ this.func = null;
+ this.message = null;
+ }
+
+ addMessageListener(message, func) {
+ this.func = func;
+ this.message = message;
+ }
+
+ removeMessageListener(message) {
+ this.func = null;
+ this.message = null;
+ }
+
+ send(message, data) {
+ if (this.func) {
+ this.func({
+ data,
+ message,
+ target: this,
+ });
+ }
+ }
+}
+
+/**
+ * Mimics nsITimer, but instead of using a system clock you can
+ * preprogram it to invoke the callback after a given number of ticks.
+ */
+class MockTimer {
+ constructor(ticksBeforeFiring) {
+ this.goal = ticksBeforeFiring;
+ this.ticks = 0;
+ this.cancelled = false;
+ }
+
+ initWithCallback(cb, timeout, type) {
+ this.ticks++;
+ if (this.ticks >= this.goal) {
+ cb();
+ }
+ }
+
+ cancel() {
+ this.cancelled = true;
+ }
+}
+
+add_test(function test_executeSoon_callback() {
+ // executeSoon() is already defined for xpcshell in head.js. As such import
+ // our implementation into a custom namespace.
+ let sync = ChromeUtils.importESModule(
+ "chrome://remote/content/marionette/sync.sys.mjs"
+ );
+
+ for (let func of ["foo", null, true, [], {}]) {
+ Assert.throws(() => sync.executeSoon(func), /TypeError/);
+ }
+
+ let a;
+ sync.executeSoon(() => {
+ a = 1;
+ });
+ executeSoon(() => equal(1, a));
+
+ run_next_test();
+});
+
+add_test(function test_PollPromise_funcTypes() {
+ for (let type of ["foo", 42, null, undefined, true, [], {}]) {
+ Assert.throws(() => new PollPromise(type), /TypeError/);
+ }
+ new PollPromise(() => {});
+ new PollPromise(function() {});
+
+ run_next_test();
+});
+
+add_test(function test_PollPromise_timeoutTypes() {
+ for (let timeout of ["foo", true, [], {}]) {
+ Assert.throws(() => new PollPromise(() => {}, { timeout }), /TypeError/);
+ }
+ for (let timeout of [1.2, -1]) {
+ Assert.throws(() => new PollPromise(() => {}, { timeout }), /RangeError/);
+ }
+ for (let timeout of [null, undefined, 42]) {
+ new PollPromise(resolve => resolve(1), { timeout });
+ }
+
+ run_next_test();
+});
+
+add_test(function test_PollPromise_intervalTypes() {
+ for (let interval of ["foo", null, true, [], {}]) {
+ Assert.throws(() => new PollPromise(() => {}, { interval }), /TypeError/);
+ }
+ for (let interval of [1.2, -1]) {
+ Assert.throws(() => new PollPromise(() => {}, { interval }), /RangeError/);
+ }
+ new PollPromise(() => {}, { interval: 42 });
+
+ run_next_test();
+});
+
+add_task(async function test_PollPromise_retvalTypes() {
+ for (let typ of [true, false, "foo", 42, [], {}]) {
+ strictEqual(typ, await new PollPromise(resolve => resolve(typ)));
+ }
+});
+
+add_task(async function test_PollPromise_rethrowError() {
+ let nevals = 0;
+ let err;
+ try {
+ await PollPromise(() => {
+ ++nevals;
+ throw new Error();
+ });
+ } catch (e) {
+ err = e;
+ }
+ equal(1, nevals);
+ ok(err instanceof Error);
+});
+
+add_task(async function test_PollPromise_noTimeout() {
+ let nevals = 0;
+ await new PollPromise((resolve, reject) => {
+ ++nevals;
+ nevals < 100 ? reject() : resolve();
+ });
+ equal(100, nevals);
+});
+
+add_task(async function test_PollPromise_zeroTimeout() {
+ // run at least once when timeout is 0
+ let nevals = 0;
+ let start = new Date().getTime();
+ await new PollPromise(
+ (resolve, reject) => {
+ ++nevals;
+ reject();
+ },
+ { timeout: 0 }
+ );
+ let end = new Date().getTime();
+ equal(1, nevals);
+ less(end - start, 500);
+});
+
+add_task(async function test_PollPromise_timeoutElapse() {
+ let nevals = 0;
+ let start = new Date().getTime();
+ await new PollPromise(
+ (resolve, reject) => {
+ ++nevals;
+ reject();
+ },
+ { timeout: 100 }
+ );
+ let end = new Date().getTime();
+ lessOrEqual(nevals, 11);
+ greaterOrEqual(end - start, 100);
+});
+
+add_task(async function test_PollPromise_interval() {
+ let nevals = 0;
+ await new PollPromise(
+ (resolve, reject) => {
+ ++nevals;
+ reject();
+ },
+ { timeout: 100, interval: 100 }
+ );
+ equal(2, nevals);
+});
+
+add_test(function test_TimedPromise_funcTypes() {
+ for (let type of ["foo", 42, null, undefined, true, [], {}]) {
+ Assert.throws(() => new TimedPromise(type), /TypeError/);
+ }
+ new TimedPromise(resolve => resolve());
+ new TimedPromise(function(resolve) {
+ resolve();
+ });
+
+ run_next_test();
+});
+
+add_test(function test_TimedPromise_timeoutTypes() {
+ for (let timeout of ["foo", null, true, [], {}]) {
+ Assert.throws(
+ () => new TimedPromise(resolve => resolve(), { timeout }),
+ /TypeError/
+ );
+ }
+ for (let timeout of [1.2, -1]) {
+ Assert.throws(
+ () => new TimedPromise(resolve => resolve(), { timeout }),
+ /RangeError/
+ );
+ }
+ new TimedPromise(resolve => resolve(), { timeout: 42 });
+
+ run_next_test();
+});
+
+add_test(async function test_TimedPromise_errorMessage() {
+ try {
+ await new TimedPromise(resolve => {}, { timeout: 0 });
+ ok(false, "Expected Timeout error not raised");
+ } catch (e) {
+ ok(
+ e.message.includes("TimedPromise timed out after"),
+ "Expected default error message found"
+ );
+ }
+
+ try {
+ await new TimedPromise(resolve => {}, {
+ errorMessage: "Not found",
+ timeout: 0,
+ });
+ ok(false, "Expected Timeout error not raised");
+ } catch (e) {
+ ok(
+ e.message.includes("Not found after"),
+ "Expected custom error message found"
+ );
+ }
+
+ run_next_test();
+});
+
+add_task(async function test_Sleep() {
+ await Sleep(0);
+ for (let type of ["foo", true, null, undefined]) {
+ Assert.throws(() => new Sleep(type), /TypeError/);
+ }
+ Assert.throws(() => new Sleep(1.2), /RangeError/);
+ Assert.throws(() => new Sleep(-1), /RangeError/);
+});
+
+add_task(async function test_IdlePromise() {
+ let called = false;
+ let win = {
+ requestAnimationFrame(callback) {
+ called = true;
+ callback();
+ },
+ };
+ await IdlePromise(win);
+ ok(called);
+});
+
+add_task(async function test_IdlePromiseAbortWhenWindowClosed() {
+ let win = {
+ closed: true,
+ requestAnimationFrame() {},
+ };
+ await IdlePromise(win);
+});
+
+add_test(function test_DebounceCallback_constructor() {
+ for (let cb of [42, "foo", true, null, undefined, [], {}]) {
+ Assert.throws(() => new DebounceCallback(cb), /TypeError/);
+ }
+ for (let timeout of ["foo", true, [], {}, () => {}]) {
+ Assert.throws(
+ () => new DebounceCallback(() => {}, { timeout }),
+ /TypeError/
+ );
+ }
+ for (let timeout of [-1, 2.3, NaN]) {
+ Assert.throws(
+ () => new DebounceCallback(() => {}, { timeout }),
+ /RangeError/
+ );
+ }
+
+ run_next_test();
+});
+
+add_task(async function test_DebounceCallback_repeatedCallback() {
+ let uniqueEvent = {};
+ let ncalls = 0;
+
+ let cb = ev => {
+ ncalls++;
+ equal(ev, uniqueEvent);
+ };
+ let debouncer = new DebounceCallback(cb);
+ debouncer.timer = new MockTimer(3);
+
+ // flood the debouncer with events,
+ // we only expect the last one to fire
+ debouncer.handleEvent(uniqueEvent);
+ debouncer.handleEvent(uniqueEvent);
+ debouncer.handleEvent(uniqueEvent);
+
+ equal(ncalls, 1);
+ ok(debouncer.timer.cancelled);
+});
+
+add_task(async function test_waitForMessage_messageManagerAndMessageTypes() {
+ let messageManager = new MessageManager();
+
+ for (let manager of ["foo", 42, null, undefined, true, [], {}]) {
+ Assert.throws(() => waitForMessage(manager, "message"), /TypeError/);
+ }
+
+ for (let message of [42, null, undefined, true, [], {}]) {
+ Assert.throws(() => waitForMessage(messageManager, message), /TypeError/);
+ }
+
+ let data = { foo: "bar" };
+ let sent = waitForMessage(messageManager, "message");
+ messageManager.send("message", data);
+ equal(data, await sent);
+});
+
+add_task(async function test_waitForMessage_checkFnTypes() {
+ let messageManager = new MessageManager();
+
+ for (let checkFn of ["foo", 42, true, [], {}]) {
+ Assert.throws(
+ () => waitForMessage(messageManager, "message", { checkFn }),
+ /TypeError/
+ );
+ }
+
+ let data1 = { fo: "bar" };
+ let data2 = { foo: "bar" };
+
+ for (let checkFn of [null, undefined, msg => "foo" in msg.data]) {
+ let expected_data = checkFn == null ? data1 : data2;
+
+ messageManager = new MessageManager();
+ let sent = waitForMessage(messageManager, "message", { checkFn });
+ messageManager.send("message", data1);
+ messageManager.send("message", data2);
+ equal(expected_data, await sent);
+ }
+});
+
+add_task(async function test_waitForObserverTopic_topicTypes() {
+ for (let topic of [42, null, undefined, true, [], {}]) {
+ Assert.throws(() => waitForObserverTopic(topic), /TypeError/);
+ }
+
+ let data = { foo: "bar" };
+ let sent = waitForObserverTopic("message");
+ Services.obs.notifyObservers(this, "message", data);
+ let result = await sent;
+ equal(this, result.subject);
+ equal(data, result.data);
+});
+
+add_task(async function test_waitForObserverTopic_checkFnTypes() {
+ for (let checkFn of ["foo", 42, true, [], {}]) {
+ Assert.throws(
+ () => waitForObserverTopic("message", { checkFn }),
+ /TypeError/
+ );
+ }
+
+ let data1 = { fo: "bar" };
+ let data2 = { foo: "bar" };
+
+ for (let checkFn of [null, undefined, (subject, data) => data == data2]) {
+ let expected_data = checkFn == null ? data1 : data2;
+
+ let sent = waitForObserverTopic("message");
+ Services.obs.notifyObservers(this, "message", data1);
+ Services.obs.notifyObservers(this, "message", data2);
+ let result = await sent;
+ equal(expected_data, result.data);
+ }
+});
diff --git a/remote/marionette/test/xpcshell/xpcshell.ini b/remote/marionette/test/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..94ec3f82fa
--- /dev/null
+++ b/remote/marionette/test/xpcshell/xpcshell.ini
@@ -0,0 +1,20 @@
+# 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/.
+
+[DEFAULT]
+head = head.js
+skip-if = appname == "thunderbird"
+
+[test_action.js]
+[test_actors.js]
+[test_browser.js]
+[test_cookie.js]
+[test_dom.js]
+[test_element.js]
+[test_json.js]
+[test_message.js]
+[test_modal.js]
+[test_navigate.js]
+[test_prefs.js]
+[test_sync.js]