summaryrefslogtreecommitdiffstats
path: root/testing/marionette/test/unit
diff options
context:
space:
mode:
Diffstat (limited to 'testing/marionette/test/unit')
-rw-r--r--testing/marionette/test/unit/.eslintrc.js7
-rw-r--r--testing/marionette/test/unit/README16
-rw-r--r--testing/marionette/test/unit/test_action.js712
-rw-r--r--testing/marionette/test/unit/test_actors.js49
-rw-r--r--testing/marionette/test/unit/test_assert.js207
-rw-r--r--testing/marionette/test/unit/test_browser.js25
-rw-r--r--testing/marionette/test/unit/test_capabilities.js609
-rw-r--r--testing/marionette/test/unit/test_cookie.js368
-rw-r--r--testing/marionette/test/unit/test_dom.js275
-rw-r--r--testing/marionette/test/unit/test_element.js609
-rw-r--r--testing/marionette/test/unit/test_error.js477
-rw-r--r--testing/marionette/test/unit/test_evaluate.js342
-rw-r--r--testing/marionette/test/unit/test_format.js118
-rw-r--r--testing/marionette/test/unit/test_message.js277
-rw-r--r--testing/marionette/test/unit/test_modal.js148
-rw-r--r--testing/marionette/test/unit/test_navigate.js88
-rw-r--r--testing/marionette/test/unit/test_prefs.js133
-rw-r--r--testing/marionette/test/unit/test_store.js220
-rw-r--r--testing/marionette/test/unit/test_sync.js521
-rw-r--r--testing/marionette/test/unit/xpcshell.ini24
20 files changed, 5225 insertions, 0 deletions
diff --git a/testing/marionette/test/unit/.eslintrc.js b/testing/marionette/test/unit/.eslintrc.js
new file mode 100644
index 0000000000..2ef179ab5e
--- /dev/null
+++ b/testing/marionette/test/unit/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ rules: {
+ camelcase: "off",
+ },
+};
diff --git a/testing/marionette/test/unit/README b/testing/marionette/test/unit/README
new file mode 100644
index 0000000000..06eca782e7
--- /dev/null
+++ b/testing/marionette/test/unit/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 testing/marionette/test/unit
+
+Or call out the harness specifically:
+
+ % ./mach xpcshell-test testing/marionette/test/unit
+
+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/testing/marionette/test/unit/test_action.js b/testing/marionette/test/unit/test_action.js
new file mode 100644
index 0000000000..1d515d6382
--- /dev/null
+++ b/testing/marionette/test/unit/test_action.js
@@ -0,0 +1,712 @@
+/* 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.import("chrome://marionette/content/action.js");
+
+const XHTMLNS = "http://www.w3.org/1999/xhtml";
+
+const domEl = {
+ nodeType: 1,
+ ELEMENT_NODE: 1,
+ namespaceURI: XHTMLNS,
+};
+
+action.inputStateMap = new Map();
+
+add_test(function test_createAction() {
+ Assert.throws(
+ () => new action.Action(),
+ /InvalidArgumentError/,
+ "Missing Action constructor args"
+ );
+ Assert.throws(
+ () => new action.Action(1, 2),
+ /InvalidArgumentError/,
+ "Missing Action constructor args"
+ );
+ Assert.throws(
+ () => new action.Action(1, 2, "sometype"),
+ /Expected string/,
+ "Non-string arguments."
+ );
+ ok(new action.Action("id", "sometype", "sometype"));
+
+ run_next_test();
+});
+
+add_test(function test_defaultPointerParameters() {
+ let defaultParameters = { pointerType: action.PointerType.Mouse };
+ deepEqual(action.PointerParameters.fromJSON(), defaultParameters);
+
+ run_next_test();
+});
+
+add_test(function test_processPointerParameters() {
+ let check = (regex, message, arg) =>
+ checkErrors(regex, action.PointerParameters.fromJSON, [arg], message);
+ let parametersData;
+ for (let d of ["foo", "", "get", "Get"]) {
+ parametersData = { pointerType: d };
+ let message = `parametersData: [pointerType: ${parametersData.pointerType}]`;
+ check(/Unknown pointerType/, message, parametersData);
+ }
+ parametersData.pointerType = "mouse"; // TODO "pen";
+ deepEqual(action.PointerParameters.fromJSON(parametersData), {
+ pointerType: "mouse",
+ }); // TODO action.PointerType.Pen});
+
+ run_next_test();
+});
+
+add_test(function test_processPointerUpDownAction() {
+ let actionItem = { type: "pointerDown" };
+ let actionSequence = { type: "pointer", id: "some_id" };
+ for (let d of [-1, "a"]) {
+ actionItem.button = d;
+ checkErrors(
+ /Expected 'button' \(.*\) to be >= 0/,
+ action.Action.fromJSON,
+ [actionSequence, actionItem],
+ `button: ${actionItem.button}`
+ );
+ }
+ actionItem.button = 5;
+ let act = action.Action.fromJSON(actionSequence, actionItem);
+ equal(act.button, actionItem.button);
+
+ run_next_test();
+});
+
+add_test(function test_validateActionDurationAndCoordinates() {
+ let actionItem = {};
+ let actionSequence = { id: "some_id" };
+ let check = function(type, subtype, message = undefined) {
+ message =
+ message || `duration: ${actionItem.duration}, subtype: ${subtype}`;
+ actionItem.type = subtype;
+ actionSequence.type = type;
+ checkErrors(
+ /Expected '.*' \(.*\) to be >= 0/,
+ action.Action.fromJSON,
+ [actionSequence, actionItem],
+ message
+ );
+ };
+ for (let d of [-1, "a"]) {
+ actionItem.duration = d;
+ check("none", "pause");
+ check("pointer", "pointerMove");
+ }
+ actionItem.duration = 5000;
+ for (let name of ["x", "y"]) {
+ actionItem[name] = "a";
+ actionItem.type = "pointerMove";
+ actionSequence.type = "pointer";
+ checkErrors(
+ /Expected '.*' \(.*\) to be an Integer/,
+ action.Action.fromJSON,
+ [actionSequence, actionItem],
+ `duration: ${actionItem.duration}, subtype: pointerMove`
+ );
+ }
+ run_next_test();
+});
+
+add_test(function test_processPointerMoveActionOriginValidation() {
+ let actionSequence = { type: "pointer", id: "some_id" };
+ let actionItem = { duration: 5000, type: "pointerMove" };
+ for (let d of [-1, { a: "blah" }, []]) {
+ actionItem.origin = d;
+
+ checkErrors(
+ /Expected \'origin\' to be undefined, "viewport", "pointer", or an element/,
+ action.Action.fromJSON,
+ [actionSequence, actionItem],
+ `actionItem.origin: (${getTypeString(d)})`
+ );
+ }
+
+ run_next_test();
+});
+
+add_test(function test_processPointerMoveActionOriginStringValidation() {
+ let actionSequence = { type: "pointer", id: "some_id" };
+ let actionItem = { duration: 5000, type: "pointerMove" };
+ for (let d of ["a", "", "get", "Get"]) {
+ actionItem.origin = d;
+ checkErrors(
+ /Unknown pointer-move origin/,
+ action.Action.fromJSON,
+ [actionSequence, actionItem],
+ `actionItem.origin: ${d}`
+ );
+ }
+
+ run_next_test();
+});
+
+add_test(function test_processPointerMoveActionElementOrigin() {
+ let actionSequence = { type: "pointer", id: "some_id" };
+ let actionItem = { duration: 5000, type: "pointerMove" };
+ actionItem.origin = domEl;
+ let a = action.Action.fromJSON(actionSequence, actionItem);
+ deepEqual(a.origin, actionItem.origin);
+ run_next_test();
+});
+
+add_test(function test_processPointerMoveActionDefaultOrigin() {
+ let actionSequence = { type: "pointer", id: "some_id" };
+ // origin left undefined
+ let actionItem = { duration: 5000, type: "pointerMove" };
+ let a = action.Action.fromJSON(actionSequence, actionItem);
+ deepEqual(a.origin, action.PointerOrigin.Viewport);
+ run_next_test();
+});
+
+add_test(function test_processPointerMoveAction() {
+ let actionSequence = { id: "some_id", type: "pointer" };
+ let actionItems = [
+ {
+ duration: 5000,
+ type: "pointerMove",
+ origin: undefined,
+ x: undefined,
+ y: undefined,
+ },
+ {
+ duration: undefined,
+ type: "pointerMove",
+ origin: domEl,
+ x: undefined,
+ y: undefined,
+ },
+ {
+ duration: 5000,
+ type: "pointerMove",
+ x: 0,
+ y: undefined,
+ origin: undefined,
+ },
+ {
+ duration: 5000,
+ type: "pointerMove",
+ x: 1,
+ y: 2,
+ origin: undefined,
+ },
+ ];
+ for (let expected of actionItems) {
+ let actual = action.Action.fromJSON(actionSequence, expected);
+ ok(actual instanceof action.Action);
+ equal(actual.duration, expected.duration);
+ equal(actual.x, expected.x);
+ equal(actual.y, expected.y);
+
+ let origin = expected.origin;
+ if (typeof origin == "undefined") {
+ origin = action.PointerOrigin.Viewport;
+ }
+ deepEqual(actual.origin, origin);
+ }
+ run_next_test();
+});
+
+add_test(function test_computePointerDestinationViewport() {
+ let act = { type: "pointerMove", x: 100, y: 200, origin: "viewport" };
+ let inputState = new action.InputState.Pointer(action.PointerType.Mouse);
+ // these values should not affect the outcome
+ inputState.x = "99";
+ inputState.y = "10";
+ let target = action.computePointerDestination(act, inputState);
+ equal(act.x, target.x);
+ equal(act.y, target.y);
+
+ run_next_test();
+});
+
+add_test(function test_computePointerDestinationPointer() {
+ let act = { type: "pointerMove", x: 100, y: 200, origin: "pointer" };
+ let inputState = new action.InputState.Pointer(action.PointerType.Mouse);
+ inputState.x = 10;
+ inputState.y = 99;
+ let target = action.computePointerDestination(act, inputState);
+ equal(act.x + inputState.x, target.x);
+ equal(act.y + inputState.y, target.y);
+
+ run_next_test();
+});
+
+add_test(function test_computePointerDestinationElement() {
+ // origin represents a web element
+ // using an object literal instead to test default case in computePointerDestination
+ let act = { type: "pointerMove", x: 100, y: 200, origin: {} };
+ let inputState = new action.InputState.Pointer(action.PointerType.Mouse);
+ let elementCenter = { x: 10, y: 99 };
+ let target = action.computePointerDestination(act, inputState, elementCenter);
+ equal(act.x + elementCenter.x, target.x);
+ equal(act.y + elementCenter.y, target.y);
+
+ Assert.throws(
+ () => action.computePointerDestination(act, inputState, { a: 1 }),
+ /InvalidArgumentError/,
+ "Invalid element center coordinates."
+ );
+
+ Assert.throws(
+ () => action.computePointerDestination(act, inputState, undefined),
+ /InvalidArgumentError/,
+ "Undefined element center coordinates."
+ );
+
+ run_next_test();
+});
+
+add_test(function test_processPointerAction() {
+ let actionSequence = {
+ type: "pointer",
+ id: "some_id",
+ parameters: {
+ pointerType: "mouse", // TODO "touch"
+ },
+ };
+ let actionItems = [
+ {
+ duration: 2000,
+ type: "pause",
+ },
+ {
+ type: "pointerMove",
+ duration: 2000,
+ },
+ {
+ type: "pointerUp",
+ button: 1,
+ },
+ ];
+ for (let expected of actionItems) {
+ let actual = action.Action.fromJSON(actionSequence, expected);
+ equal(actual.type, actionSequence.type);
+ 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(actual.pointerType, actionSequence.parameters.pointerType);
+ }
+ }
+
+ run_next_test();
+});
+
+add_test(function test_processPauseAction() {
+ let actionItem = { type: "pause", duration: 5000 };
+ let actionSequence = { id: "some_id" };
+ for (let type of ["none", "key", "pointer"]) {
+ actionSequence.type = type;
+ let act = action.Action.fromJSON(actionSequence, actionItem);
+ ok(act instanceof action.Action);
+ equal(act.type, type);
+ equal(act.subtype, actionItem.type);
+ equal(act.id, actionSequence.id);
+ equal(act.duration, actionItem.duration);
+ }
+ actionItem.duration = undefined;
+ let act = action.Action.fromJSON(actionSequence, actionItem);
+ equal(act.duration, actionItem.duration);
+
+ run_next_test();
+});
+
+add_test(function test_processActionSubtypeValidation() {
+ let actionItem = { type: "dancing" };
+ let actionSequence = { id: "some_id" };
+ let check = function(regex) {
+ let message = `type: ${actionSequence.type}, subtype: ${actionItem.type}`;
+ checkErrors(
+ regex,
+ action.Action.fromJSON,
+ [actionSequence, actionItem],
+ message
+ );
+ };
+ for (let type of ["none", "key", "pointer"]) {
+ actionSequence.type = type;
+ check(new RegExp(`Unknown subtype for ${type} action`));
+ }
+ run_next_test();
+});
+
+add_test(function test_processKeyActionUpDown() {
+ let actionSequence = { type: "key", id: "some_id" };
+ let actionItem = { type: "keyDown" };
+
+ for (let v of [-1, undefined, [], ["a"], { length: 1 }, null]) {
+ actionItem.value = v;
+ let message = `actionItem.value: (${getTypeString(v)})`;
+ Assert.throws(
+ () => action.Action.fromJSON(actionSequence, actionItem),
+ /InvalidArgumentError/,
+ message
+ );
+ Assert.throws(
+ () => action.Action.fromJSON(actionSequence, actionItem),
+ /Expected 'value' to be a string that represents single code point/,
+ message
+ );
+ }
+
+ actionItem.value = "\uE004";
+ let act = action.Action.fromJSON(actionSequence, actionItem);
+ ok(act instanceof action.Action);
+ equal(act.type, actionSequence.type);
+ equal(act.subtype, actionItem.type);
+ equal(act.id, actionSequence.id);
+ equal(act.value, actionItem.value);
+
+ run_next_test();
+});
+
+add_test(function test_processInputSourceActionSequenceValidation() {
+ let actionSequence = { type: "swim", id: "some id" };
+ let check = (message, regex) =>
+ checkErrors(regex, action.Sequence.fromJSON, [actionSequence], message);
+ check(`actionSequence.type: ${actionSequence.type}`, /Unknown action type/);
+ action.inputStateMap.clear();
+
+ actionSequence.type = "none";
+ actionSequence.id = -1;
+ check(
+ `actionSequence.id: ${getTypeString(actionSequence.id)}`,
+ /Expected 'id' to be a string/
+ );
+ action.inputStateMap.clear();
+
+ actionSequence.id = undefined;
+ check(
+ `actionSequence.id: ${getTypeString(actionSequence.id)}`,
+ /Expected 'id' to be defined/
+ );
+ action.inputStateMap.clear();
+
+ actionSequence.id = "some_id";
+ actionSequence.actions = -1;
+ check(
+ `actionSequence.actions: ${getTypeString(actionSequence.actions)}`,
+ /Expected 'actionSequence.actions' to be an array/
+ );
+ action.inputStateMap.clear();
+
+ run_next_test();
+});
+
+add_test(function test_processInputSourceActionSequence() {
+ let actionItem = { type: "pause", duration: 5 };
+ let actionSequence = {
+ type: "none",
+ id: "some id",
+ actions: [actionItem],
+ };
+ let expectedAction = new action.Action(
+ actionSequence.id,
+ "none",
+ actionItem.type
+ );
+ expectedAction.duration = actionItem.duration;
+ let actions = action.Sequence.fromJSON(actionSequence);
+ equal(actions.length, 1);
+ deepEqual(actions[0], expectedAction);
+ action.inputStateMap.clear();
+ run_next_test();
+});
+
+add_test(function test_processInputSourceActionSequencePointer() {
+ let actionItem = { type: "pointerDown", button: 1 };
+ let actionSequence = {
+ type: "pointer",
+ id: "9",
+ actions: [actionItem],
+ parameters: {
+ pointerType: "mouse", // TODO "pen"
+ },
+ };
+ let expectedAction = new action.Action(
+ actionSequence.id,
+ actionSequence.type,
+ actionItem.type
+ );
+ expectedAction.pointerType = actionSequence.parameters.pointerType;
+ expectedAction.button = actionItem.button;
+ let actions = action.Sequence.fromJSON(actionSequence);
+ equal(actions.length, 1);
+ deepEqual(actions[0], expectedAction);
+ action.inputStateMap.clear();
+ run_next_test();
+});
+
+add_test(function test_processInputSourceActionSequenceKey() {
+ let actionItem = { type: "keyUp", value: "a" };
+ let actionSequence = {
+ type: "key",
+ id: "9",
+ actions: [actionItem],
+ };
+ let expectedAction = new action.Action(
+ actionSequence.id,
+ actionSequence.type,
+ actionItem.type
+ );
+ expectedAction.value = actionItem.value;
+ let actions = action.Sequence.fromJSON(actionSequence);
+ equal(actions.length, 1);
+ deepEqual(actions[0], expectedAction);
+ action.inputStateMap.clear();
+ run_next_test();
+});
+
+add_test(function test_processInputSourceActionSequenceInputStateMap() {
+ let id = "1";
+ let actionItem = { type: "pause", duration: 5000 };
+ let actionSequence = {
+ type: "key",
+ id,
+ actions: [actionItem],
+ };
+ let wrongInputState = new action.InputState.Null();
+ action.inputStateMap.set(actionSequence.id, wrongInputState);
+ checkErrors(
+ /to be mapped to/,
+ action.Sequence.fromJSON,
+ [actionSequence],
+ `${actionSequence.type} using ${wrongInputState}`
+ );
+ action.inputStateMap.clear();
+ let rightInputState = new action.InputState.Key();
+ action.inputStateMap.set(id, rightInputState);
+ let acts = action.Sequence.fromJSON(actionSequence);
+ equal(acts.length, 1);
+ action.inputStateMap.clear();
+ run_next_test();
+});
+
+add_test(function test_processPointerActionInputStateMap() {
+ let actionItem = { type: "pointerDown" };
+ let id = "1";
+ let parameters = { pointerType: "mouse" };
+ let a = new action.Action(id, "pointer", actionItem.type);
+ let wrongInputState = new action.InputState.Key();
+ action.inputStateMap.set(id, wrongInputState);
+ checkErrors(
+ /to be mapped to InputState whose type is/,
+ action.processPointerAction,
+ [id, parameters, a],
+ `type "pointer" with ${wrongInputState.type} in inputState`
+ );
+ action.inputStateMap.clear();
+
+ // TODO - uncomment once pen is supported
+ // wrongInputState = new action.InputState.Pointer("pen");
+ // action.inputStateMap.set(id, wrongInputState);
+ // checkErrors(
+ // /to be mapped to InputState whose subtype is/, action.processPointerAction,
+ // [id, parameters, a],
+ // `subtype ${parameters.pointerType} with ${wrongInputState.subtype} in inputState`);
+ // action.inputStateMap.clear();
+
+ let rightInputState = new action.InputState.Pointer("mouse");
+ action.inputStateMap.set(id, rightInputState);
+ action.processPointerAction(id, parameters, a);
+ action.inputStateMap.clear();
+ run_next_test();
+});
+
+add_test(function test_createInputState() {
+ for (let kind in action.InputState) {
+ let state;
+ if (kind == "Pointer") {
+ state = new action.InputState[kind]("mouse");
+ } else {
+ state = new action.InputState[kind]();
+ }
+ ok(state);
+ if (kind === "Null") {
+ equal(state.type, "none");
+ } else {
+ equal(state.type, kind.toLowerCase());
+ }
+ }
+ Assert.throws(
+ () => new action.InputState.Pointer(),
+ /InvalidArgumentError/,
+ "Missing InputState.Pointer constructor arg"
+ );
+ Assert.throws(
+ () => new action.InputState.Pointer("foo"),
+ /InvalidArgumentError/,
+ "Invalid InputState.Pointer constructor arg"
+ );
+ run_next_test();
+});
+
+add_test(function test_extractActionChainValidation() {
+ for (let actions of [-1, "a", undefined, null]) {
+ let message = `actions: ${getTypeString(actions)}`;
+ Assert.throws(
+ () => action.Chain.fromJSON(actions),
+ /InvalidArgumentError/,
+ message
+ );
+ Assert.throws(
+ () => action.Chain.fromJSON(actions),
+ /Expected 'actions' to be an array/,
+ message
+ );
+ }
+ run_next_test();
+});
+
+add_test(function test_extractActionChainEmpty() {
+ deepEqual(action.Chain.fromJSON([]), []);
+ run_next_test();
+});
+
+add_test(function test_extractActionChain_oneTickOneInput() {
+ let actionItem = { type: "pause", duration: 5000 };
+ let actionSequence = {
+ type: "none",
+ id: "some id",
+ actions: [actionItem],
+ };
+ let expectedAction = new action.Action(
+ actionSequence.id,
+ "none",
+ actionItem.type
+ );
+ expectedAction.duration = actionItem.duration;
+ let actionsByTick = action.Chain.fromJSON([actionSequence]);
+ equal(1, actionsByTick.length);
+ equal(1, actionsByTick[0].length);
+ deepEqual(actionsByTick, [[expectedAction]]);
+ action.inputStateMap.clear();
+ run_next_test();
+});
+
+add_test(function test_extractActionChain_twoAndThreeTicks() {
+ let mouseActionItems = [
+ {
+ type: "pointerDown",
+ button: 2,
+ },
+ {
+ type: "pointerUp",
+ button: 2,
+ },
+ ];
+ let mouseActionSequence = {
+ type: "pointer",
+ id: "7",
+ actions: mouseActionItems,
+ parameters: {
+ pointerType: "mouse", // TODO "touch"
+ },
+ };
+ let 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([
+ 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);
+ let expectedAction = new action.Action(
+ keyActionSequence.id,
+ "key",
+ keyActionItems[2].type
+ );
+ expectedAction.value = keyActionItems[2].value;
+ deepEqual(actionsByTick[2][0], expectedAction);
+ action.inputStateMap.clear();
+
+ // one empty action sequence
+ actionsByTick = action.Chain.fromJSON([
+ keyActionSequence,
+ { type: "none", id: "some", actions: [] },
+ ]);
+ equal(keyActionItems.length, actionsByTick.length);
+ equal(1, actionsByTick[0].length);
+ action.inputStateMap.clear();
+ run_next_test();
+});
+
+add_test(function test_computeTickDuration() {
+ let expected = 8000;
+ let tickActions = [
+ { type: "none", subtype: "pause", duration: 5000 },
+ { type: "key", subtype: "pause", duration: 1000 },
+ { type: "pointer", subtype: "pointerMove", duration: 6000 },
+ // invalid because keyDown should not have duration, so duration should be ignored.
+ { type: "key", subtype: "keyDown", duration: 100000 },
+ { type: "pointer", subtype: "pause", duration: expected },
+ { type: "pointer", subtype: "pointerUp" },
+ ];
+ equal(expected, action.computeTickDuration(tickActions));
+ run_next_test();
+});
+
+add_test(function test_computeTickDuration_empty() {
+ equal(0, action.computeTickDuration([]));
+ run_next_test();
+});
+
+add_test(function test_computeTickDuration_noDurations() {
+ let tickActions = [
+ // invalid because keyDown should not have duration, so duration should be ignored.
+ { type: "key", subtype: "keyDown", duration: 100000 },
+ // undefined duration permitted
+ { type: "none", subtype: "pause" },
+ { type: "pointer", subtype: "pointerMove" },
+ { type: "pointer", subtype: "pointerDown" },
+ { type: "key", subtype: "keyUp" },
+ ];
+
+ equal(0, action.computeTickDuration(tickActions));
+ run_next_test();
+});
+
+// helpers
+function getTypeString(obj) {
+ return Object.prototype.toString.call(obj);
+}
+
+function checkErrors(regex, func, args, message) {
+ if (typeof message == "undefined") {
+ message = `actionFunc: ${func.name}; args: ${args}`;
+ }
+ Assert.throws(() => func.apply(this, args), /InvalidArgumentError/, message);
+ Assert.throws(() => func.apply(this, args), regex, message);
+}
diff --git a/testing/marionette/test/unit/test_actors.js b/testing/marionette/test/unit/test_actors.js
new file mode 100644
index 0000000000..584533f869
--- /dev/null
+++ b/testing/marionette/test/unit/test_actors.js
@@ -0,0 +1,49 @@
+/* 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 { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ EventDispatcher:
+ "chrome://marionette/content/actors/MarionetteEventsParent.jsm",
+ registerCommandsActor:
+ "chrome://marionette/content/actors/MarionetteCommandsParent.jsm",
+ registerEventsActor:
+ "chrome://marionette/content/actors/MarionetteEventsParent.jsm",
+ unregisterCommandsActor:
+ "chrome://marionette/content/actors/MarionetteCommandsParent.jsm",
+ unregisterEventsActor:
+ "chrome://marionette/content/actors/MarionetteEventsParent.jsm",
+});
+
+registerCleanupFunction(function() {
+ unregisterCommandsActor();
+ unregisterEventsActor();
+});
+
+add_test(function test_commandsActor_register() {
+ registerCommandsActor();
+ unregisterCommandsActor();
+
+ registerCommandsActor();
+ registerCommandsActor();
+ unregisterCommandsActor();
+
+ run_next_test();
+});
+
+add_test(function test_eventsActor_register() {
+ registerEventsActor();
+ unregisterEventsActor();
+
+ registerEventsActor();
+ registerEventsActor();
+ unregisterEventsActor();
+
+ run_next_test();
+});
diff --git a/testing/marionette/test/unit/test_assert.js b/testing/marionette/test/unit/test_assert.js
new file mode 100644
index 0000000000..aa93453139
--- /dev/null
+++ b/testing/marionette/test/unit/test_assert.js
@@ -0,0 +1,207 @@
+/* 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-new-object */
+
+const { assert } = ChromeUtils.import("chrome://marionette/content/assert.js");
+const { error } = ChromeUtils.import("chrome://marionette/content/error.js");
+
+add_test(function test_acyclic() {
+ assert.acyclic({});
+
+ Assert.throws(() => {
+ let obj = {};
+ obj.reference = obj;
+ assert.acyclic(obj);
+ }, /JavaScriptError/);
+
+ // custom message
+ let cyclic = {};
+ cyclic.reference = cyclic;
+ Assert.throws(() => assert.acyclic(cyclic, "", RangeError), RangeError);
+ Assert.throws(() => assert.acyclic(cyclic, "foo"), /JavaScriptError: foo/);
+ Assert.throws(
+ () => assert.acyclic(cyclic, "bar", RangeError),
+ /RangeError: bar/
+ );
+
+ run_next_test();
+});
+
+add_test(function test_session() {
+ assert.session({ sessionID: "foo" });
+ for (let typ of [null, undefined, ""]) {
+ Assert.throws(
+ () => assert.session({ sessionId: typ }),
+ /InvalidSessionIDError/
+ );
+ }
+
+ Assert.throws(() => assert.session({ sessionId: null }, "custom"), /custom/);
+
+ run_next_test();
+});
+
+add_test(function test_platforms() {
+ // at least one will fail
+ let raised;
+ for (let fn of [assert.firefox, assert.fennec]) {
+ try {
+ fn();
+ } catch (e) {
+ raised = e;
+ }
+ }
+ ok(raised instanceof error.UnsupportedOperationError);
+
+ run_next_test();
+});
+
+add_test(function test_noUserPrompt() {
+ assert.noUserPrompt(null);
+ assert.noUserPrompt(undefined);
+ Assert.throws(() => assert.noUserPrompt({}), /UnexpectedAlertOpenError/);
+ Assert.throws(() => assert.noUserPrompt({}, "custom"), /custom/);
+
+ run_next_test();
+});
+
+add_test(function test_defined() {
+ assert.defined({});
+ Assert.throws(() => assert.defined(undefined), /InvalidArgumentError/);
+ Assert.throws(() => assert.noUserPrompt({}, "custom"), /custom/);
+
+ run_next_test();
+});
+
+add_test(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/);
+
+ run_next_test();
+});
+
+add_test(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/);
+
+ run_next_test();
+});
+
+add_test(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/);
+
+ run_next_test();
+});
+
+add_test(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/);
+
+ run_next_test();
+});
+
+add_test(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/);
+
+ run_next_test();
+});
+
+add_test(function test_string() {
+ assert.string("foo");
+ assert.string(`bar`);
+ Assert.throws(() => assert.string(42), /InvalidArgumentError/);
+ Assert.throws(() => assert.string(42, "custom"), /custom/);
+
+ run_next_test();
+});
+
+add_test(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/);
+
+ run_next_test();
+});
+
+add_test(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/);
+
+ run_next_test();
+});
+
+add_test(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/);
+
+ run_next_test();
+});
+
+add_test(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/);
+
+ run_next_test();
+});
+
+add_test(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/);
+
+ run_next_test();
+});
+
+/* eslint-enable no-array-constructor, no-new-object */
diff --git a/testing/marionette/test/unit/test_browser.js b/testing/marionette/test/unit/test_browser.js
new file mode 100644
index 0000000000..3f89cd0b1a
--- /dev/null
+++ b/testing/marionette/test/unit/test_browser.js
@@ -0,0 +1,25 @@
+const { Context } = ChromeUtils.import(
+ "chrome://marionette/content/browser.js"
+);
+
+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/testing/marionette/test/unit/test_capabilities.js b/testing/marionette/test/unit/test_capabilities.js
new file mode 100644
index 0000000000..afc8e75d16
--- /dev/null
+++ b/testing/marionette/test/unit/test_capabilities.js
@@ -0,0 +1,609 @@
+/* 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 { Preferences } = ChromeUtils.import(
+ "resource://gre/modules/Preferences.jsm"
+);
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+const { error } = ChromeUtils.import("chrome://marionette/content/error.js");
+const {
+ Capabilities,
+ PageLoadStrategy,
+ Proxy,
+ Timeouts,
+ UnhandledPromptBehavior,
+} = ChromeUtils.import("chrome://marionette/content/capabilities.js");
+
+// FTP protocol handler is needed for ftpProxy tests
+registerCleanupFunction(function() {
+ Preferences.reset("network.ftp.enabled");
+});
+Preferences.set("network.ftp.enabled", true);
+
+add_test(function test_Timeouts_ctor() {
+ let ts = new Timeouts();
+ equal(ts.implicit, 0);
+ equal(ts.pageLoad, 300000);
+ equal(ts.script, 30000);
+
+ run_next_test();
+});
+
+add_test(function test_Timeouts_toString() {
+ equal(new Timeouts().toString(), "[object Timeouts]");
+
+ run_next_test();
+});
+
+add_test(function test_Timeouts_toJSON() {
+ let ts = new Timeouts();
+ deepEqual(ts.toJSON(), { implicit: 0, pageLoad: 300000, script: 30000 });
+
+ run_next_test();
+});
+
+add_test(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);
+
+ run_next_test();
+});
+
+add_test(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");
+ }
+
+ run_next_test();
+});
+
+add_test(function test_Timeouts_fromJSON_invalid_types() {
+ for (let value of [null, [], {}, false, "10", 2.5]) {
+ Assert.throws(
+ () => Timeouts.fromJSON({ implicit: value }),
+ /InvalidArgumentError/
+ );
+ }
+
+ run_next_test();
+});
+
+add_test(function test_Timeouts_fromJSON_bounds() {
+ for (let value of [-1, Number.MAX_SAFE_INTEGER + 1]) {
+ Assert.throws(
+ () => Timeouts.fromJSON({ script: value }),
+ /InvalidArgumentError/
+ );
+ }
+
+ run_next_test();
+});
+
+add_test(function test_PageLoadStrategy() {
+ equal(PageLoadStrategy.None, "none");
+ equal(PageLoadStrategy.Eager, "eager");
+ equal(PageLoadStrategy.Normal, "normal");
+
+ run_next_test();
+});
+
+add_test(function test_Proxy_ctor() {
+ let p = new Proxy();
+ let props = [
+ "proxyType",
+ "httpProxy",
+ "sslProxy",
+ "ftpProxy",
+ "socksProxy",
+ "socksVersion",
+ "proxyAutoconfigUrl",
+ ];
+ for (let prop of props) {
+ ok(prop in p, `${prop} in ${JSON.stringify(props)}`);
+ equal(p[prop], null);
+ }
+
+ run_next_test();
+});
+
+add_test(function test_Proxy_init() {
+ let p = new Proxy();
+
+ // no changed made, and 5 (system) is default
+ equal(p.init(), false);
+ equal(Preferences.get("network.proxy.type"), 5);
+
+ // pac
+ p.proxyType = "pac";
+ p.proxyAutoconfigUrl = "http://localhost:1234";
+ ok(p.init());
+
+ equal(Preferences.get("network.proxy.type"), 2);
+ equal(
+ Preferences.get("network.proxy.autoconfig_url"),
+ "http://localhost:1234"
+ );
+
+ // direct
+ p = new Proxy();
+ p.proxyType = "direct";
+ ok(p.init());
+ equal(Preferences.get("network.proxy.type"), 0);
+
+ // autodetect
+ p = new Proxy();
+ p.proxyType = "autodetect";
+ ok(p.init());
+ equal(Preferences.get("network.proxy.type"), 4);
+
+ // system
+ p = new Proxy();
+ p.proxyType = "system";
+ ok(p.init());
+ equal(Preferences.get("network.proxy.type"), 5);
+
+ // manual
+ for (let proxy of ["ftp", "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(Preferences.get("network.proxy.type"), 1);
+ equal(Preferences.get("network.proxy.no_proxies_on"), "foo, bar");
+ equal(Preferences.get(`network.proxy.${proxy}`), "foo");
+ equal(Preferences.get(`network.proxy.${proxy}_port`), 42);
+ if (proxy === "socks") {
+ equal(Preferences.get(`network.proxy.${proxy}_version`), 4);
+ }
+ }
+
+ // empty no proxy should reset default exclustions
+ p = new Proxy();
+ p.proxyType = "manual";
+ p.noProxy = [];
+ ok(p.init());
+ equal(Preferences.get("network.proxy.no_proxies_on"), "");
+
+ run_next_test();
+});
+
+add_test(function test_Proxy_toString() {
+ equal(new Proxy().toString(), "[object Proxy]");
+
+ run_next_test();
+});
+
+add_test(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 ["ftpProxy", "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);
+
+ run_next_test();
+});
+
+add_test(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", "ftpProxy", "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 = { ftpProxy: 21, 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/
+ );
+
+ // 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));
+
+ run_next_test();
+});
+
+add_test(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");
+
+ run_next_test();
+});
+
+add_test(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")));
+ ok(caps.has("platformVersion"));
+ 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"), !Services.androidBridge);
+ equal(caps.get("strictFileInteractability"), false);
+
+ ok(caps.has("rotatable"));
+
+ equal(false, caps.get("moz:accessibilityChecks"));
+ ok(caps.has("moz:buildID"));
+ ok(caps.has("moz:debuggerAddress"));
+ ok(caps.has("moz:processID"));
+ ok(caps.has("moz:profile"));
+ equal(false, caps.get("moz:useNonSpecCompliantPointerOrigin"));
+ equal(true, caps.get("moz:webdriverClick"));
+
+ run_next_test();
+});
+
+add_test(function test_Capabilities_toString() {
+ equal("[object Capabilities]", new Capabilities().toString());
+
+ run_next_test();
+});
+
+add_test(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("platformVersion"), json.platformVersion);
+ equal(caps.get("pageLoadStrategy"), json.pageLoadStrategy);
+ equal(caps.get("acceptInsecureCerts"), json.acceptInsecureCerts);
+ deepEqual(caps.get("timeouts").toJSON(), json.timeouts);
+ equal(undefined, json.proxy);
+ equal(caps.get("setWindowRect"), json.setWindowRect);
+ equal(caps.get("strictFileInteractability"), json.strictFileInteractability);
+
+ equal(caps.get("rotatable"), json.rotatable);
+
+ 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:processID"), json["moz:processID"]);
+ equal(caps.get("moz:profile"), json["moz:profile"]);
+ equal(
+ caps.get("moz:useNonSpecCompliantPointerOrigin"),
+ json["moz:useNonSpecCompliantPointerOrigin"]
+ );
+ equal(caps.get("moz:webdriverClick"), json["moz:webdriverClick"]);
+
+ run_next_test();
+});
+
+add_test(function test_Capabilities_fromJSON() {
+ const { fromJSON } = Capabilities;
+
+ // plain
+ for (let typ of [{}, null, undefined]) {
+ ok(fromJSON(typ).has("browserName"));
+ }
+ for (let typ of [true, 42, "foo", []]) {
+ Assert.throws(() => fromJSON(typ), /InvalidArgumentError/);
+ }
+
+ // matching
+ let caps = new Capabilities();
+
+ caps = fromJSON({ acceptInsecureCerts: true });
+ equal(true, caps.get("acceptInsecureCerts"));
+ caps = fromJSON({ acceptInsecureCerts: false });
+ equal(false, caps.get("acceptInsecureCerts"));
+ Assert.throws(
+ () => fromJSON({ acceptInsecureCerts: "foo" }),
+ /InvalidArgumentError/
+ );
+
+ for (let strategy of Object.values(PageLoadStrategy)) {
+ caps = fromJSON({ pageLoadStrategy: strategy });
+ equal(strategy, caps.get("pageLoadStrategy"));
+ }
+ Assert.throws(
+ () => fromJSON({ pageLoadStrategy: "foo" }),
+ /InvalidArgumentError/
+ );
+ Assert.throws(
+ () => fromJSON({ pageLoadStrategy: null }),
+ /InvalidArgumentError/
+ );
+
+ 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);
+
+ if (!Services.androidBridge) {
+ caps = fromJSON({ setWindowRect: true });
+ equal(true, caps.get("setWindowRect"));
+ Assert.throws(
+ () => fromJSON({ setWindowRect: false }),
+ /InvalidArgumentError/
+ );
+ } else {
+ Assert.throws(
+ () => fromJSON({ setWindowRect: true }),
+ /InvalidArgumentError/
+ );
+ }
+
+ caps = fromJSON({ strictFileInteractability: false });
+ equal(false, caps.get("strictFileInteractability"));
+ caps = fromJSON({ strictFileInteractability: true });
+ equal(true, caps.get("strictFileInteractability"));
+
+ caps = fromJSON({ "moz:accessibilityChecks": true });
+ equal(true, caps.get("moz:accessibilityChecks"));
+ caps = fromJSON({ "moz:accessibilityChecks": false });
+ equal(false, caps.get("moz:accessibilityChecks"));
+ Assert.throws(
+ () => fromJSON({ "moz:accessibilityChecks": "foo" }),
+ /InvalidArgumentError/
+ );
+ Assert.throws(
+ () => fromJSON({ "moz:accessibilityChecks": 1 }),
+ /InvalidArgumentError/
+ );
+
+ // 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:useNonSpecCompliantPointerOrigin": false });
+ equal(false, caps.get("moz:useNonSpecCompliantPointerOrigin"));
+ caps = fromJSON({ "moz:useNonSpecCompliantPointerOrigin": true });
+ equal(true, caps.get("moz:useNonSpecCompliantPointerOrigin"));
+ Assert.throws(
+ () => fromJSON({ "moz:useNonSpecCompliantPointerOrigin": "foo" }),
+ /InvalidArgumentError/
+ );
+ Assert.throws(
+ () => fromJSON({ "moz:useNonSpecCompliantPointerOrigin": 1 }),
+ /InvalidArgumentError/
+ );
+
+ caps = fromJSON({ "moz:webdriverClick": true });
+ equal(true, caps.get("moz:webdriverClick"));
+ caps = fromJSON({ "moz:webdriverClick": false });
+ equal(false, caps.get("moz:webdriverClick"));
+ Assert.throws(
+ () => fromJSON({ "moz:webdriverClick": "foo" }),
+ /InvalidArgumentError/
+ );
+ Assert.throws(
+ () => fromJSON({ "moz:webdriverClick": 1 }),
+ /InvalidArgumentError/
+ );
+
+ run_next_test();
+});
+
+// use Proxy.toJSON to test marshal
+add_test(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());
+
+ run_next_test();
+});
diff --git a/testing/marionette/test/unit/test_cookie.js b/testing/marionette/test/unit/test_cookie.js
new file mode 100644
index 0000000000..933b9f8ef8
--- /dev/null
+++ b/testing/marionette/test/unit/test_cookie.js
@@ -0,0 +1,368 @@
+/* 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.import("chrome://marionette/content/cookie.js");
+
+/* 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/testing/marionette/test/unit/test_dom.js b/testing/marionette/test/unit/test_dom.js
new file mode 100644
index 0000000000..ddb1c7e30b
--- /dev/null
+++ b/testing/marionette/test/unit/test_dom.js
@@ -0,0 +1,275 @@
+const {
+ ContentEventObserverService,
+ WebElementEventTarget,
+} = ChromeUtils.import("chrome://marionette/content/dom.js");
+
+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/testing/marionette/test/unit/test_element.js b/testing/marionette/test/unit/test_element.js
new file mode 100644
index 0000000000..1644ff9346
--- /dev/null
+++ b/testing/marionette/test/unit/test_element.js
@@ -0,0 +1,609 @@
+/* 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 {
+ ChromeWebElement,
+ ContentWebElement,
+ ContentWebFrame,
+ ContentWebWindow,
+ element,
+ WebElement,
+} = ChromeUtils.import("chrome://marionette/content/element.js");
+const { InvalidArgumentError } = ChromeUtils.import(
+ "chrome://marionette/content/error.js"
+);
+
+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";
+
+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("browser");
+const domElInXULDocument = new DOMElement("input", {
+ ownerDocument: {
+ documentElement: { namespaceURI: XUL_NS },
+ },
+});
+
+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(domElInXULDocument));
+ ok(element.isDOMElement(svgEl));
+ ok(!element.isDOMElement(xulEl));
+ 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(domElInXULDocument));
+ ok(!element.isXULElement(domEl));
+ ok(!element.isXULElement(svgEl));
+ ok(!element.isDOMElement(domWin));
+ ok(!element.isDOMElement(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(domElInXULDocument));
+ 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_WebElement_ctor() {
+ let el = new WebElement("foo");
+ equal(el.uuid, "foo");
+
+ for (let t of [42, true, [], {}, null, undefined]) {
+ Assert.throws(() => new WebElement(t), /to be a string/);
+ }
+
+ run_next_test();
+});
+
+add_test(function test_WebElemenet_is() {
+ let a = new WebElement("a");
+ let b = new WebElement("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_WebElement_from() {
+ ok(WebElement.from(domEl) instanceof ContentWebElement);
+ ok(WebElement.from(domWin) instanceof ContentWebWindow);
+ ok(WebElement.from(domFrame) instanceof ContentWebFrame);
+ ok(WebElement.from(xulEl) instanceof ChromeWebElement);
+ ok(WebElement.from(domElInXULDocument) instanceof ChromeWebElement);
+
+ Assert.throws(() => WebElement.from({}), /InvalidArgumentError/);
+
+ run_next_test();
+});
+
+add_test(function test_WebElement_fromJSON_ContentWebElement() {
+ const { Identifier } = ContentWebElement;
+
+ let ref = { [Identifier]: "foo" };
+ let webEl = WebElement.fromJSON(ref);
+ ok(webEl instanceof ContentWebElement);
+ equal(webEl.uuid, "foo");
+
+ let identifierPrecedence = {
+ [Identifier]: "identifier-uuid",
+ };
+ let precedenceEl = WebElement.fromJSON(identifierPrecedence);
+ ok(precedenceEl instanceof ContentWebElement);
+ equal(precedenceEl.uuid, "identifier-uuid");
+
+ run_next_test();
+});
+
+add_test(function test_WebElement_fromJSON_ContentWebWindow() {
+ let ref = { [ContentWebWindow.Identifier]: "foo" };
+ let win = WebElement.fromJSON(ref);
+ ok(win instanceof ContentWebWindow);
+ equal(win.uuid, "foo");
+
+ run_next_test();
+});
+
+add_test(function test_WebElement_fromJSON_ContentWebFrame() {
+ let ref = { [ContentWebFrame.Identifier]: "foo" };
+ let frame = WebElement.fromJSON(ref);
+ ok(frame instanceof ContentWebFrame);
+ equal(frame.uuid, "foo");
+
+ run_next_test();
+});
+
+add_test(function test_WebElement_fromJSON_ChromeWebElement() {
+ let ref = { [ChromeWebElement.Identifier]: "foo" };
+ let el = WebElement.fromJSON(ref);
+ ok(el instanceof ChromeWebElement);
+ equal(el.uuid, "foo");
+
+ run_next_test();
+});
+
+add_test(function test_WebElement_fromJSON_malformed() {
+ Assert.throws(() => WebElement.fromJSON({}), /InvalidArgumentError/);
+ Assert.throws(() => WebElement.fromJSON(null), /InvalidArgumentError/);
+ run_next_test();
+});
+
+add_test(function test_WebElement_fromUUID() {
+ let xulWebEl = WebElement.fromUUID("foo", "chrome");
+ ok(xulWebEl instanceof ChromeWebElement);
+ equal(xulWebEl.uuid, "foo");
+
+ let domWebEl = WebElement.fromUUID("bar", "content");
+ ok(domWebEl instanceof ContentWebElement);
+ equal(domWebEl.uuid, "bar");
+
+ Assert.throws(
+ () => WebElement.fromUUID("baz", "bah"),
+ /InvalidArgumentError/
+ );
+
+ run_next_test();
+});
+
+add_test(function test_WebElement_isReference() {
+ for (let t of [42, true, "foo", [], {}]) {
+ ok(!WebElement.isReference(t));
+ }
+
+ ok(WebElement.isReference({ [ContentWebElement.Identifier]: "foo" }));
+ ok(WebElement.isReference({ [ContentWebWindow.Identifier]: "foo" }));
+ ok(WebElement.isReference({ [ContentWebFrame.Identifier]: "foo" }));
+ ok(WebElement.isReference({ [ChromeWebElement.Identifier]: "foo" }));
+
+ run_next_test();
+});
+
+add_test(function test_WebElement_generateUUID() {
+ equal(typeof WebElement.generateUUID(), "string");
+ run_next_test();
+});
+
+add_test(function test_ContentWebElement_toJSON() {
+ const { Identifier } = ContentWebElement;
+
+ let el = new ContentWebElement("foo");
+ let json = el.toJSON();
+
+ ok(Identifier in json);
+ equal(json[Identifier], "foo");
+
+ run_next_test();
+});
+
+add_test(function test_ContentWebElement_fromJSON() {
+ const { Identifier } = ContentWebElement;
+
+ let el = ContentWebElement.fromJSON({ [Identifier]: "foo" });
+ ok(el instanceof ContentWebElement);
+ equal(el.uuid, "foo");
+
+ Assert.throws(() => ContentWebElement.fromJSON({}), /InvalidArgumentError/);
+
+ run_next_test();
+});
+
+add_test(function test_ContentWebWindow_toJSON() {
+ let win = new ContentWebWindow("foo");
+ let json = win.toJSON();
+ ok(ContentWebWindow.Identifier in json);
+ equal(json[ContentWebWindow.Identifier], "foo");
+
+ run_next_test();
+});
+
+add_test(function test_ContentWebWindow_fromJSON() {
+ let ref = { [ContentWebWindow.Identifier]: "foo" };
+ let win = ContentWebWindow.fromJSON(ref);
+ ok(win instanceof ContentWebWindow);
+ equal(win.uuid, "foo");
+
+ run_next_test();
+});
+
+add_test(function test_ContentWebFrame_toJSON() {
+ let frame = new ContentWebFrame("foo");
+ let json = frame.toJSON();
+ ok(ContentWebFrame.Identifier in json);
+ equal(json[ContentWebFrame.Identifier], "foo");
+
+ run_next_test();
+});
+
+add_test(function test_ContentWebFrame_fromJSON() {
+ let ref = { [ContentWebFrame.Identifier]: "foo" };
+ let win = ContentWebFrame.fromJSON(ref);
+ ok(win instanceof ContentWebFrame);
+ equal(win.uuid, "foo");
+
+ run_next_test();
+});
+
+add_test(function test_ChromeWebElement_toJSON() {
+ let el = new ChromeWebElement("foo");
+ let json = el.toJSON();
+ ok(ChromeWebElement.Identifier in json);
+ equal(json[ChromeWebElement.Identifier], "foo");
+
+ run_next_test();
+});
+
+add_test(function test_ChromeWebElement_fromJSON() {
+ let ref = { [ChromeWebElement.Identifier]: "foo" };
+ let win = ChromeWebElement.fromJSON(ref);
+ ok(win instanceof ChromeWebElement);
+ equal(win.uuid, "foo");
+
+ run_next_test();
+});
diff --git a/testing/marionette/test/unit/test_error.js b/testing/marionette/test/unit/test_error.js
new file mode 100644
index 0000000000..0be71dec65
--- /dev/null
+++ b/testing/marionette/test/unit/test_error.js
@@ -0,0 +1,477 @@
+/* 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.import("chrome://marionette/content/error.js");
+
+function notok(condition) {
+ ok(!condition);
+}
+
+add_test(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()));
+ ok(error.isError(new error.WebDriverError()));
+ ok(error.isError(new error.InvalidArgumentError()));
+
+ run_next_test();
+});
+
+add_test(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()));
+
+ ok(error.isWebDriverError(new error.WebDriverError()));
+ ok(error.isWebDriverError(new error.InvalidArgumentError()));
+ ok(error.isWebDriverError(new error.JavaScriptError()));
+
+ run_next_test();
+});
+
+add_test(function test_wrap() {
+ // webdriver-derived errors should not be wrapped
+ equal(error.wrap(new error.WebDriverError()).name, "WebDriverError");
+ ok(error.wrap(new error.WebDriverError()) instanceof error.WebDriverError);
+ equal(
+ error.wrap(new error.InvalidArgumentError()).name,
+ "InvalidArgumentError"
+ );
+ ok(
+ error.wrap(new error.InvalidArgumentError()) instanceof error.WebDriverError
+ );
+ ok(
+ error.wrap(new error.InvalidArgumentError()) instanceof
+ error.InvalidArgumentError
+ );
+
+ // JS errors should be wrapped in UnknownError
+ equal(error.wrap(new Error()).name, "UnknownError");
+ ok(error.wrap(new Error()) instanceof error.UnknownError);
+ equal(error.wrap(new EvalError()).name, "UnknownError");
+ equal(error.wrap(new InternalError()).name, "UnknownError");
+ equal(error.wrap(new RangeError()).name, "UnknownError");
+ equal(error.wrap(new ReferenceError()).name, "UnknownError");
+ equal(error.wrap(new SyntaxError()).name, "UnknownError");
+ equal(error.wrap(new TypeError()).name, "UnknownError");
+ equal(error.wrap(new URIError()).name, "UnknownError");
+
+ // wrapped JS errors should retain their type
+ // as part of the message field
+ equal(error.wrap(new error.WebDriverError("foo")).message, "foo");
+ equal(error.wrap(new TypeError("foo")).message, "TypeError: foo");
+
+ run_next_test();
+});
+
+add_test(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]);
+ equal(
+ "WebDriverError: foo",
+ error.stringify(new error.WebDriverError("foo")).split("\n")[0]
+ );
+ equal(
+ "InvalidArgumentError: foo",
+ error.stringify(new error.InvalidArgumentError("foo")).split("\n")[0]
+ );
+
+ run_next_test();
+});
+
+add_test(function test_stack() {
+ equal("string", typeof error.stack());
+ ok(error.stack().includes("test_stack"));
+ ok(!error.stack().includes("add_test"));
+
+ run_next_test();
+});
+
+add_test(function test_toJSON() {
+ let e0 = new error.WebDriverError();
+ let e0s = e0.toJSON();
+ equal(e0s.error, "webdriver error");
+ equal(e0s.message, "");
+ equal(e0s.stacktrace, e0.stack);
+
+ let e1 = new error.WebDriverError("a");
+ let e1s = e1.toJSON();
+ equal(e1s.message, e1.message);
+ equal(e1s.stacktrace, e1.stack);
+
+ let e2 = new error.JavaScriptError("foo");
+ let e2s = e2.toJSON();
+ equal(e2.status, e2s.error);
+ equal(e2.message, e2s.message);
+
+ run_next_test();
+});
+
+add_test(function test_fromJSON() {
+ Assert.throws(
+ () => error.WebDriverError.fromJSON({ error: "foo" }),
+ /Not of WebDriverError descent/
+ );
+ Assert.throws(
+ () => error.WebDriverError.fromJSON({ error: "Error" }),
+ /Not of WebDriverError descent/
+ );
+ Assert.throws(
+ () => error.WebDriverError.fromJSON({}),
+ /Undeserialisable error type/
+ );
+ Assert.throws(() => error.WebDriverError.fromJSON(undefined), /TypeError/);
+
+ // stacks will be different
+ let e1 = new error.WebDriverError("1");
+ let e1r = error.WebDriverError.fromJSON({
+ error: "webdriver error",
+ message: "1",
+ });
+ ok(e1r instanceof error.WebDriverError);
+ equal(e1r.name, e1.name);
+ equal(e1r.status, e1.status);
+ equal(e1r.message, e1.message);
+
+ // stacks will be different
+ let e2 = new error.InvalidArgumentError("2");
+ let e2r = error.WebDriverError.fromJSON({
+ error: "invalid argument",
+ message: "2",
+ });
+ ok(e2r instanceof error.WebDriverError);
+ ok(e2r instanceof error.InvalidArgumentError);
+ equal(e2r.name, e2.name);
+ equal(e2r.status, e2.status);
+ equal(e2r.message, e2.message);
+
+ // test stacks
+ let e3j = { error: "no such element", message: "3", stacktrace: "4" };
+ let e3r = error.WebDriverError.fromJSON(e3j);
+ ok(e3r instanceof error.WebDriverError);
+ ok(e3r instanceof error.NoSuchElementError);
+ equal(e3r.name, "NoSuchElementError");
+ equal(e3r.status, e3j.error);
+ equal(e3r.message, e3j.message);
+ equal(e3r.stack, e3j.stacktrace);
+
+ // parity with toJSON
+ let e4j = new error.JavaScriptError("foo").toJSON();
+ let e4 = error.WebDriverError.fromJSON(e4j);
+ equal(e4j.error, e4.status);
+ equal(e4j.message, e4.message);
+ equal(e4j.stacktrace, e4.stack);
+
+ run_next_test();
+});
+
+add_test(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);
+
+ run_next_test();
+});
+
+add_test(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(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(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
+ );
+
+ run_next_test();
+});
+
+add_test(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);
+
+ run_next_test();
+});
+
+add_test(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);
+
+ run_next_test();
+});
+
+add_test(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);
+
+ run_next_test();
+});
+
+add_test(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);
+
+ run_next_test();
+});
+
+add_test(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);
+
+ run_next_test();
+});
+
+add_test(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);
+
+ run_next_test();
+});
+
+add_test(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);
+
+ run_next_test();
+});
+
+add_test(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);
+
+ run_next_test();
+});
+
+add_test(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);
+
+ run_next_test();
+});
+
+add_test(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);
+
+ run_next_test();
+});
+
+add_test(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);
+
+ run_next_test();
+});
+
+add_test(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);
+
+ run_next_test();
+});
+
+add_test(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);
+
+ run_next_test();
+});
+
+add_test(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);
+
+ run_next_test();
+});
+
+add_test(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);
+
+ run_next_test();
+});
+
+add_test(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);
+
+ run_next_test();
+});
+
+add_test(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);
+
+ run_next_test();
+});
+
+add_test(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);
+
+ run_next_test();
+});
+
+add_test(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);
+
+ run_next_test();
+});
+
+add_test(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);
+
+ run_next_test();
+});
+
+add_test(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);
+
+ run_next_test();
+});
+
+add_test(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);
+
+ run_next_test();
+});
+
+add_test(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);
+
+ run_next_test();
+});
diff --git a/testing/marionette/test/unit/test_evaluate.js b/testing/marionette/test/unit/test_evaluate.js
new file mode 100644
index 0000000000..6b426e13fa
--- /dev/null
+++ b/testing/marionette/test/unit/test_evaluate.js
@@ -0,0 +1,342 @@
+const { element, ReferenceStore, WebElement } = ChromeUtils.import(
+ "chrome://marionette/content/element.js"
+);
+const { evaluate } = ChromeUtils.import(
+ "chrome://marionette/content/evaluate.js"
+);
+
+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";
+
+class Element {
+ constructor(tagName, attrs = {}) {
+ this.tagName = tagName;
+ this.localName = tagName;
+
+ // Set default properties
+ this.isConnected = true;
+ this.ownerDocument = { documentElement: {} };
+ this.ownerGlobal = { document: this.ownerDocument };
+
+ for (let attr in attrs) {
+ this[attr] = attrs[attr];
+ }
+ }
+
+ get nodeType() {
+ return 1;
+ }
+ get ELEMENT_NODE() {
+ return 1;
+ }
+}
+
+class DOMElement extends Element {
+ constructor(tagName, attrs = {}) {
+ super(tagName, attrs);
+ this.namespaceURI = XHTML_NS;
+ }
+}
+
+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;
+ }
+}
+
+const domEl = new DOMElement("p");
+const svgEl = new SVGElement("rect");
+const xulEl = new XULElement("browser");
+
+const domWebEl = WebElement.from(domEl);
+const svgWebEl = WebElement.from(svgEl);
+const xulWebEl = WebElement.from(xulEl);
+
+const domElId = { id: 1, browsingContextId: 4, webElRef: domWebEl.toJSON() };
+const svgElId = { id: 2, browsingContextId: 5, webElRef: svgWebEl.toJSON() };
+const xulElId = { id: 3, browsingContextId: 6, webElRef: xulWebEl.toJSON() };
+
+const seenEls = new element.Store();
+const elementIdCache = new element.ReferenceStore();
+
+add_test(function test_toJSON_types() {
+ // null
+ equal(null, evaluate.toJSON(undefined));
+ equal(null, evaluate.toJSON(null));
+
+ // primitives
+ equal(true, evaluate.toJSON(true));
+ equal(42, evaluate.toJSON(42));
+ equal("foo", evaluate.toJSON("foo"));
+
+ // collections
+ deepEqual([], evaluate.toJSON([]));
+
+ // elements
+ ok(evaluate.toJSON(domEl, seenEls) instanceof WebElement);
+ ok(evaluate.toJSON(svgEl, seenEls) instanceof WebElement);
+ ok(evaluate.toJSON(xulEl, seenEls) instanceof WebElement);
+
+ // toJSON
+ equal(
+ "foo",
+ evaluate.toJSON({
+ toJSON() {
+ return "foo";
+ },
+ })
+ );
+
+ // arbitrary object
+ deepEqual({ foo: "bar" }, evaluate.toJSON({ foo: "bar" }));
+
+ run_next_test();
+});
+
+add_test(function test_toJSON_types_ReferenceStore() {
+ // Temporarily add custom elements until xpcshell tests
+ // have access to real DOM nodes (including the Window Proxy)
+ elementIdCache.add(domElId);
+ elementIdCache.add(svgElId);
+ elementIdCache.add(xulElId);
+
+ deepEqual(evaluate.toJSON(domWebEl, elementIdCache), domElId);
+ deepEqual(evaluate.toJSON(svgWebEl, elementIdCache), svgElId);
+ deepEqual(evaluate.toJSON(xulWebEl, elementIdCache), xulElId);
+
+ Assert.throws(
+ () => evaluate.toJSON(domEl, elementIdCache),
+ /TypeError/,
+ "Reference store not usable for elements"
+ );
+
+ elementIdCache.clear();
+
+ run_next_test();
+});
+
+add_test(function test_toJSON_sequences() {
+ const input = [
+ null,
+ true,
+ [],
+ domEl,
+ {
+ toJSON() {
+ return "foo";
+ },
+ },
+ { bar: "baz" },
+ ];
+ const actual = evaluate.toJSON(input, seenEls);
+
+ equal(null, actual[0]);
+ equal(true, actual[1]);
+ deepEqual([], actual[2]);
+ ok(actual[3] instanceof WebElement);
+ equal("foo", actual[4]);
+ deepEqual({ bar: "baz" }, actual[5]);
+
+ run_next_test();
+});
+
+add_test(function test_toJSON_sequences_ReferenceStore() {
+ const input = [
+ null,
+ true,
+ [],
+ domWebEl,
+ {
+ toJSON() {
+ return "foo";
+ },
+ },
+ { bar: "baz" },
+ ];
+
+ Assert.throws(
+ () => evaluate.toJSON(input, elementIdCache),
+ /NoSuchElementError/,
+ "Expected no element"
+ );
+
+ elementIdCache.add(domElId);
+
+ const actual = evaluate.toJSON(input, elementIdCache);
+
+ equal(null, actual[0]);
+ equal(true, actual[1]);
+ deepEqual([], actual[2]);
+ deepEqual(actual[3], domElId);
+ equal("foo", actual[4]);
+ deepEqual({ bar: "baz" }, actual[5]);
+
+ elementIdCache.clear();
+
+ run_next_test();
+});
+
+add_test(function test_toJSON_objects() {
+ const input = {
+ null: null,
+ boolean: true,
+ array: [],
+ webElement: domEl,
+ elementId: domElId,
+ toJSON: {
+ toJSON() {
+ return "foo";
+ },
+ },
+ object: { bar: "baz" },
+ };
+ const actual = evaluate.toJSON(input, seenEls);
+
+ equal(null, actual.null);
+ equal(true, actual.boolean);
+ deepEqual([], actual.array);
+ ok(actual.webElement instanceof WebElement);
+ ok(actual.elementId instanceof WebElement);
+ equal("foo", actual.toJSON);
+ deepEqual({ bar: "baz" }, actual.object);
+
+ run_next_test();
+});
+
+add_test(function test_toJSON_objects_ReferenceStore() {
+ const input = {
+ null: null,
+ boolean: true,
+ array: [],
+ webElement: domWebEl,
+ elementId: domElId,
+ toJSON: {
+ toJSON() {
+ return "foo";
+ },
+ },
+ object: { bar: "baz" },
+ };
+
+ Assert.throws(
+ () => evaluate.toJSON(input, elementIdCache),
+ /NoSuchElementError/,
+ "Expected no element"
+ );
+
+ elementIdCache.add(domElId);
+
+ const actual = evaluate.toJSON(input, elementIdCache);
+
+ equal(null, actual.null);
+ equal(true, actual.boolean);
+ deepEqual([], actual.array);
+ deepEqual(actual.webElement, domElId);
+ deepEqual(actual.elementId, domElId);
+ equal("foo", actual.toJSON);
+ deepEqual({ bar: "baz" }, actual.object);
+
+ elementIdCache.clear();
+
+ run_next_test();
+});
+
+add_test(function test_fromJSON_ReferenceStore() {
+ // Add unknown element to reference store
+ let webEl = evaluate.fromJSON(domElId, elementIdCache);
+ deepEqual(webEl, domWebEl);
+ deepEqual(elementIdCache.get(webEl), domElId);
+
+ // Previously seen element is associated with original web element reference
+ const domElId2 = {
+ id: 1,
+ browsingContextId: 4,
+ webElRef: WebElement.from(domEl).toJSON(),
+ };
+ webEl = evaluate.fromJSON(domElId2, elementIdCache);
+ deepEqual(webEl, domWebEl);
+ deepEqual(elementIdCache.get(webEl), domElId);
+
+ // Store doesn't contain ElementIdentifiers
+ Assert.throws(
+ () => evaluate.fromJSON(domElId, seenEls),
+ /TypeError/,
+ "Expected element.ReferenceStore"
+ );
+
+ elementIdCache.clear();
+
+ run_next_test();
+});
+
+add_test(function test_fromJSON_Store() {
+ // Pass-through WebElements without adding it to the element store
+ let webEl = evaluate.fromJSON(domWebEl.toJSON());
+ deepEqual(webEl, domWebEl);
+ ok(!seenEls.has(domWebEl));
+
+ // Find element in the element store
+ webEl = seenEls.add(domEl);
+ const el = evaluate.fromJSON(webEl.toJSON(), seenEls);
+ deepEqual(el, domEl);
+
+ // Reference store doesn't contain web elements
+ Assert.throws(
+ () => evaluate.fromJSON(domWebEl.toJSON(), elementIdCache),
+ /TypeError/,
+ "Expected element.Store"
+ );
+
+ seenEls.clear();
+
+ run_next_test();
+});
+
+add_test(function test_isCyclic_noncyclic() {
+ for (let type of [true, 42, "foo", [], {}, null, undefined]) {
+ ok(!evaluate.isCyclic(type));
+ }
+
+ run_next_test();
+});
+
+add_test(function test_isCyclic_object() {
+ let obj = {};
+ obj.reference = obj;
+ ok(evaluate.isCyclic(obj));
+
+ run_next_test();
+});
+
+add_test(function test_isCyclic_array() {
+ let arr = [];
+ arr.push(arr);
+ ok(evaluate.isCyclic(arr));
+
+ run_next_test();
+});
+
+add_test(function test_isCyclic_arrayInObject() {
+ let arr = [];
+ arr.push(arr);
+ ok(evaluate.isCyclic({ arr }));
+
+ run_next_test();
+});
+
+add_test(function test_isCyclic_objectInArray() {
+ let obj = {};
+ obj.reference = obj;
+ ok(evaluate.isCyclic([obj]));
+
+ run_next_test();
+});
diff --git a/testing/marionette/test/unit/test_format.js b/testing/marionette/test/unit/test_format.js
new file mode 100644
index 0000000000..7cce50a231
--- /dev/null
+++ b/testing/marionette/test/unit/test_format.js
@@ -0,0 +1,118 @@
+/* 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 { pprint, truncate } = ChromeUtils.import(
+ "chrome://marionette/content/format.js"
+);
+
+const MAX_STRING_LENGTH = 250;
+const HALF = "x".repeat(MAX_STRING_LENGTH / 2);
+
+add_test(function test_pprint() {
+ equal('[object Object] {"foo":"bar"}', pprint`${{ foo: "bar" }}`);
+
+ equal("[object Number] 42", pprint`${42}`);
+ equal("[object Boolean] true", pprint`${true}`);
+ equal("[object Undefined] undefined", pprint`${undefined}`);
+ equal("[object Null] null", pprint`${null}`);
+
+ let complexObj = { toJSON: () => "foo" };
+ equal('[object Object] "foo"', pprint`${complexObj}`);
+
+ let cyclic = {};
+ cyclic.me = cyclic;
+ equal("[object Object] <cyclic object value>", pprint`${cyclic}`);
+
+ let el = {
+ hasAttribute: attr => attr in el,
+ getAttribute: attr => (attr in el ? el[attr] : null),
+ nodeType: 1,
+ localName: "input",
+ id: "foo",
+ class: "a b",
+ href: "#",
+ name: "bar",
+ src: "s",
+ type: "t",
+ };
+ equal(
+ '<input id="foo" class="a b" href="#" name="bar" src="s" type="t">',
+ pprint`${el}`
+ );
+
+ run_next_test();
+});
+
+add_test(function test_truncate_empty() {
+ equal(truncate``, "");
+ run_next_test();
+});
+
+add_test(function test_truncate_noFields() {
+ equal(truncate`foo bar`, "foo bar");
+ run_next_test();
+});
+
+add_test(function test_truncate_multipleFields() {
+ equal(truncate`${0}`, "0");
+ equal(truncate`${1}${2}${3}`, "123");
+ equal(truncate`a${1}b${2}c${3}`, "a1b2c3");
+ run_next_test();
+});
+
+add_test(function test_truncate_primitiveFields() {
+ equal(truncate`${123}`, "123");
+ equal(truncate`${true}`, "true");
+ equal(truncate`${null}`, "");
+ equal(truncate`${undefined}`, "");
+ run_next_test();
+});
+
+add_test(function test_truncate_string() {
+ equal(truncate`${"foo"}`, "foo");
+ equal(truncate`${"x".repeat(250)}`, "x".repeat(250));
+ equal(truncate`${"x".repeat(260)}`, `${HALF} ... ${HALF}`);
+ run_next_test();
+});
+
+add_test(function test_truncate_array() {
+ equal(truncate`${["foo"]}`, JSON.stringify(["foo"]));
+ equal(truncate`${"foo"} ${["bar"]}`, `foo ${JSON.stringify(["bar"])}`);
+ equal(
+ truncate`${["x".repeat(260)]}`,
+ JSON.stringify([`${HALF} ... ${HALF}`])
+ );
+
+ run_next_test();
+});
+
+add_test(function test_truncate_object() {
+ equal(truncate`${{}}`, JSON.stringify({}));
+ equal(truncate`${{ foo: "bar" }}`, JSON.stringify({ foo: "bar" }));
+ equal(
+ truncate`${{ foo: "x".repeat(260) }}`,
+ JSON.stringify({ foo: `${HALF} ... ${HALF}` })
+ );
+ equal(truncate`${{ foo: ["bar"] }}`, JSON.stringify({ foo: ["bar"] }));
+ equal(
+ truncate`${{ foo: ["bar", { baz: 42 }] }}`,
+ JSON.stringify({ foo: ["bar", { baz: 42 }] })
+ );
+
+ let complex = {
+ toString() {
+ return "hello world";
+ },
+ };
+ equal(truncate`${complex}`, "hello world");
+
+ let longComplex = {
+ toString() {
+ return "x".repeat(260);
+ },
+ };
+ equal(truncate`${longComplex}`, `${HALF} ... ${HALF}`);
+
+ run_next_test();
+});
diff --git a/testing/marionette/test/unit/test_message.js b/testing/marionette/test/unit/test_message.js
new file mode 100644
index 0000000000..4d3f09d2a5
--- /dev/null
+++ b/testing/marionette/test/unit/test_message.js
@@ -0,0 +1,277 @@
+/* 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.import("chrome://marionette/content/error.js");
+const { Command, Message, Response } = ChromeUtils.import(
+ "chrome://marionette/content/message.js"
+);
+
+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/testing/marionette/test/unit/test_modal.js b/testing/marionette/test/unit/test_modal.js
new file mode 100644
index 0000000000..0a7c365af0
--- /dev/null
+++ b/testing/marionette/test/unit/test_modal.js
@@ -0,0 +1,148 @@
+/* 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.import("chrome://marionette/content/modal.js");
+
+const mockModalDialog = {
+ opener: {
+ ownerGlobal: "foo",
+ },
+};
+
+const mockTabModalDialog = {
+ ownerGlobal: "foo",
+};
+
+add_test(function test_addCallback() {
+ let observer = new modal.DialogObserver();
+ 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();
+ 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();
+ 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, "toplevel-window-ready");
+});
+
+add_test(function test_handleCallbackOpenModalDialog() {
+ let observer = new modal.DialogObserver();
+
+ observer.add((action, target, win) => {
+ equal(action, modal.ACTION_OPENED, "'opened' action has been passed");
+ equal(
+ target.get(),
+ mockModalDialog,
+ "weak reference has been created for target"
+ );
+ equal(
+ win,
+ mockModalDialog.opener.ownerGlobal,
+ "chrome window has been passed"
+ );
+ run_next_test();
+ });
+ observer.observe(mockModalDialog, "common-dialog-loaded");
+});
+
+add_test(function test_handleCallbackCloseModalDialog() {
+ let observer = new modal.DialogObserver();
+
+ observer.add((action, target, win) => {
+ equal(action, modal.ACTION_CLOSED, "'closed' action has been passed");
+ equal(
+ target.get(),
+ mockModalDialog,
+ "weak reference has been created for target"
+ );
+ equal(
+ win,
+ mockModalDialog.opener.ownerGlobal,
+ "chrome window has been passed"
+ );
+ run_next_test();
+ });
+ observer.handleEvent({
+ type: "DOMModalDialogClosed",
+ target: mockModalDialog,
+ });
+});
+
+add_test(function test_handleCallbackOpenTabModalDialog() {
+ let observer = new modal.DialogObserver();
+
+ observer.add((action, target, win) => {
+ equal(action, modal.ACTION_OPENED, "'opened' action has been passed");
+ equal(
+ target.get(),
+ mockTabModalDialog,
+ "weak reference has been created for target"
+ );
+ equal(win, mockTabModalDialog.ownerGlobal, "chrome window has been passed");
+ run_next_test();
+ });
+ observer.observe(mockTabModalDialog, "tabmodal-dialog-loaded");
+});
+
+add_test(function test_handleCallbackCloseTabModalDialog() {
+ let observer = new modal.DialogObserver();
+
+ observer.add((action, target, win) => {
+ equal(action, modal.ACTION_CLOSED, "'closed' action has been passed");
+ equal(
+ target.get(),
+ mockTabModalDialog,
+ "weak reference has been created for target"
+ );
+ equal(win, mockTabModalDialog.ownerGlobal, "chrome window has been passed");
+ run_next_test();
+ });
+ observer.handleEvent({
+ type: "DOMModalDialogClosed",
+ target: mockTabModalDialog,
+ });
+});
diff --git a/testing/marionette/test/unit/test_navigate.js b/testing/marionette/test/unit/test_navigate.js
new file mode 100644
index 0000000000..1298d9e14b
--- /dev/null
+++ b/testing/marionette/test/unit/test_navigate.js
@@ -0,0 +1,88 @@
+/* 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 { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+XPCOMUtils.defineLazyGlobalGetters(this, ["URL"]);
+
+const { navigate } = ChromeUtils.import(
+ "chrome://marionette/content/navigate.js"
+);
+
+const topContext = {
+ id: 7,
+ get top() {
+ return this;
+ },
+};
+
+const nestedContext = {
+ id: 8,
+ parent: topContext,
+ top: topContext,
+};
+
+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: topContext, expected: true },
+ { cur: "http://a/", target: "_parent", bc: nestedContext, expected: false },
+ { cur: "http://a/", target: "_self", expected: true },
+ { cur: "http://a/", target: "_top", bc: topContext, expected: true },
+ { cur: "http://a/", target: "_top", bc: nestedContext, 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/testing/marionette/test/unit/test_prefs.js b/testing/marionette/test/unit/test_prefs.js
new file mode 100644
index 0000000000..cd3f38a657
--- /dev/null
+++ b/testing/marionette/test/unit/test_prefs.js
@@ -0,0 +1,133 @@
+/* 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 { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+const { Log } = ChromeUtils.import("resource://gre/modules/Log.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "env",
+ "@mozilla.org/process/environment;1",
+ "nsIEnvironment"
+);
+
+const { Branch, EnvironmentPrefs, MarionettePrefs } = ChromeUtils.import(
+ "chrome://marionette/content/prefs.js",
+ null
+);
+
+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",
+ };
+ env.set("FOO", JSON.stringify(prefsTable));
+
+ try {
+ for (let [key, value] of EnvironmentPrefs.from("FOO")) {
+ equal(prefsTable[key], value);
+ }
+ } finally {
+ env.set("FOO", null);
+ }
+
+ run_next_test();
+});
+
+add_test(function test_MarionettePrefs_getters() {
+ equal(false, MarionettePrefs.enabled);
+ equal(false, MarionettePrefs.clickToStart);
+ equal(false, MarionettePrefs.contentListener);
+ equal(2828, MarionettePrefs.port);
+ equal(Log.Level.Info, MarionettePrefs.logLevel);
+ equal(true, MarionettePrefs.recommendedPrefs);
+
+ run_next_test();
+});
+
+add_test(function test_MarionettePrefs_setters() {
+ try {
+ MarionettePrefs.contentListener = true;
+ MarionettePrefs.port = 777;
+ equal(true, MarionettePrefs.contentListener);
+ equal(777, MarionettePrefs.port);
+ } finally {
+ Services.prefs.clearUserPref("marionette.contentListener");
+ Services.prefs.clearUserPref("marionette.port");
+ }
+
+ run_next_test();
+});
diff --git a/testing/marionette/test/unit/test_store.js b/testing/marionette/test/unit/test_store.js
new file mode 100644
index 0000000000..81a51b577c
--- /dev/null
+++ b/testing/marionette/test/unit/test_store.js
@@ -0,0 +1,220 @@
+const { element, ReferenceStore, WebElement } = ChromeUtils.import(
+ "chrome://marionette/content/element.js"
+);
+
+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";
+
+class Element {
+ constructor(tagName, attrs = {}) {
+ this.tagName = tagName;
+ this.localName = tagName;
+
+ // Set default properties
+ this.isConnected = true;
+ this.ownerDocument = {};
+ this.ownerGlobal = { document: this.ownerDocument };
+
+ for (let attr in attrs) {
+ this[attr] = attrs[attr];
+ }
+ }
+
+ get nodeType() {
+ return 1;
+ }
+ get ELEMENT_NODE() {
+ return 1;
+ }
+}
+
+class DOMElement extends Element {
+ constructor(tagName, attrs = {}) {
+ super(tagName, attrs);
+ this.namespaceURI = XHTML_NS;
+ this.ownerDocument = { documentElement: { namespaceURI: XHTML_NS } };
+ }
+}
+
+class SVGElement extends Element {
+ constructor(tagName, attrs = {}) {
+ super(tagName, attrs);
+ this.namespaceURI = SVG_NS;
+ this.ownerDocument = { documentElement: { namespaceURI: SVG_NS } };
+ }
+}
+
+class XULElement extends Element {
+ constructor(tagName, attrs = {}) {
+ super(tagName, attrs);
+ this.namespaceURI = XUL_NS;
+ this.ownerDocument = { documentElement: { namespaceURI: XUL_NS } };
+ }
+}
+
+function makeIterator(items) {
+ return function*() {
+ for (const i of items) {
+ yield i;
+ }
+ };
+}
+
+const nestedBrowsingContext = {
+ id: 7,
+ getAllBrowsingContextsInSubtree: makeIterator([
+ { id: 7 },
+ { id: 71 },
+ { id: 72 },
+ ]),
+};
+
+const domEl = new DOMElement("p");
+const svgEl = new SVGElement("rect");
+const xulEl = new XULElement("browser");
+const frameEl = new DOMElement("iframe");
+const innerEl = new DOMElement("p", { id: "inner" });
+
+const domWebEl = WebElement.from(domEl);
+const svgWebEl = WebElement.from(svgEl);
+const xulWebEl = WebElement.from(xulEl);
+const frameWebEl = WebElement.from(frameEl);
+const innerWebEl = WebElement.from(innerEl);
+
+const domElId = { id: 1, browsingContextId: 4, webElRef: domWebEl.toJSON() };
+const svgElId = { id: 2, browsingContextId: 15, webElRef: svgWebEl.toJSON() };
+const xulElId = { id: 3, browsingContextId: 15, webElRef: xulWebEl.toJSON() };
+const frameElId = {
+ id: 10,
+ browsingContextId: 7,
+ webElRef: frameWebEl.toJSON(),
+};
+const innerElId = {
+ id: 11,
+ browsingContextId: 72,
+ webElRef: innerWebEl.toJSON(),
+};
+
+const elementIdCache = new element.ReferenceStore();
+
+registerCleanupFunction(() => {
+ elementIdCache.clear();
+});
+
+add_test(function test_add_element() {
+ elementIdCache.add(domElId);
+ equal(elementIdCache.refs.size, 1);
+ equal(elementIdCache.domRefs.size, 1);
+ deepEqual(elementIdCache.refs.get(domWebEl.uuid), domElId);
+ deepEqual(elementIdCache.domRefs.get(domElId.id), domWebEl.toJSON());
+
+ elementIdCache.add(domElId);
+ equal(elementIdCache.refs.size, 1);
+ equal(elementIdCache.domRefs.size, 1);
+
+ elementIdCache.add(xulElId);
+ equal(elementIdCache.refs.size, 2);
+ equal(elementIdCache.domRefs.size, 2);
+
+ elementIdCache.clear();
+ equal(elementIdCache.refs.size, 0);
+ equal(elementIdCache.domRefs.size, 0);
+
+ run_next_test();
+});
+
+add_test(function test_get_element() {
+ elementIdCache.add(domElId);
+ deepEqual(elementIdCache.get(domWebEl), domElId);
+
+ run_next_test();
+});
+
+add_test(function test_get_no_such_element() {
+ throws(() => elementIdCache.get(frameWebEl), /NoSuchElementError/);
+
+ elementIdCache.add(domElId);
+ throws(() => elementIdCache.get(frameWebEl), /NoSuchElementError/);
+
+ run_next_test();
+});
+
+add_test(function test_clear_by_unknown_browsing_context() {
+ const unknownContext = {
+ id: 1000,
+ getAllBrowsingContextsInSubtree: makeIterator([{ id: 1000 }]),
+ };
+ elementIdCache.add(domElId);
+ elementIdCache.add(svgElId);
+ elementIdCache.add(xulElId);
+ elementIdCache.add(frameElId);
+ elementIdCache.add(innerElId);
+
+ equal(elementIdCache.refs.size, 5);
+ equal(elementIdCache.domRefs.size, 5);
+
+ elementIdCache.clear(unknownContext);
+
+ equal(elementIdCache.refs.size, 5);
+ equal(elementIdCache.domRefs.size, 5);
+
+ run_next_test();
+});
+
+add_test(function test_clear_by_known_browsing_context() {
+ const context = {
+ id: 15,
+ getAllBrowsingContextsInSubtree: makeIterator([{ id: 15 }]),
+ };
+ const anotherContext = {
+ id: 4,
+ getAllBrowsingContextsInSubtree: makeIterator([{ id: 4 }]),
+ };
+ elementIdCache.add(domElId);
+ elementIdCache.add(svgElId);
+ elementIdCache.add(xulElId);
+ elementIdCache.add(frameElId);
+ elementIdCache.add(innerElId);
+
+ equal(elementIdCache.refs.size, 5);
+ equal(elementIdCache.domRefs.size, 5);
+
+ elementIdCache.clear(context);
+
+ equal(elementIdCache.refs.size, 3);
+ equal(elementIdCache.domRefs.size, 3);
+ ok(elementIdCache.has(domWebEl));
+ ok(!elementIdCache.has(svgWebEl));
+ ok(!elementIdCache.has(xulWebEl));
+
+ elementIdCache.clear(anotherContext);
+
+ equal(elementIdCache.refs.size, 2);
+ equal(elementIdCache.domRefs.size, 2);
+ ok(!elementIdCache.has(domWebEl));
+
+ run_next_test();
+});
+
+add_test(function test_clear_by_nested_browsing_context() {
+ elementIdCache.add(domElId);
+ elementIdCache.add(svgElId);
+ elementIdCache.add(xulElId);
+ elementIdCache.add(frameElId);
+ elementIdCache.add(innerElId);
+
+ equal(elementIdCache.refs.size, 5);
+ equal(elementIdCache.domRefs.size, 5);
+
+ elementIdCache.clear(nestedBrowsingContext);
+
+ equal(elementIdCache.refs.size, 3);
+ equal(elementIdCache.domRefs.size, 3);
+
+ ok(elementIdCache.has(domWebEl));
+ ok(!elementIdCache.has(frameWebEl));
+ ok(!elementIdCache.has(innerWebEl));
+
+ run_next_test();
+});
diff --git a/testing/marionette/test/unit/test_sync.js b/testing/marionette/test/unit/test_sync.js
new file mode 100644
index 0000000000..4120cafe91
--- /dev/null
+++ b/testing/marionette/test/unit/test_sync.js
@@ -0,0 +1,521 @@
+/* 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 { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+const {
+ DebounceCallback,
+ IdlePromise,
+ PollPromise,
+ Sleep,
+ TimedPromise,
+ waitForEvent,
+ waitForLoadEvent,
+ waitForMessage,
+ waitForObserverTopic,
+} = ChromeUtils.import("chrome://marionette/content/sync.js");
+
+const { EventDispatcher } = ChromeUtils.import(
+ "chrome://marionette/content/actors/MarionetteEventsParent.jsm"
+);
+
+/**
+ * Mimic a DOM node for listening for events.
+ */
+class MockElement {
+ constructor() {
+ this.capture = false;
+ this.func = null;
+ this.eventName = null;
+ this.untrusted = false;
+ }
+
+ addEventListener(name, func, capture, untrusted) {
+ this.eventName = name;
+ this.func = func;
+ if (capture != null) {
+ this.capture = capture;
+ }
+ if (untrusted != null) {
+ this.untrusted = untrusted;
+ }
+ }
+
+ click() {
+ if (this.func) {
+ let details = {
+ capture: this.capture,
+ target: this,
+ type: this.eventName,
+ untrusted: this.untrusted,
+ };
+ this.func(details);
+ }
+ }
+
+ removeEventListener(name, func) {
+ this.capture = false;
+ this.func = null;
+ this.eventName = null;
+ this.untrusted = false;
+ }
+}
+
+/**
+ * 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.import("chrome://marionette/content/sync.js", sync);
+
+ 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_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_waitForEvent_subjectAndEventNameTypes() {
+ let element = new MockElement();
+
+ for (let subject of ["foo", 42, null, undefined, true, [], {}]) {
+ Assert.throws(() => waitForEvent(subject, "click"), /TypeError/);
+ }
+
+ for (let eventName of [42, null, undefined, true, [], {}]) {
+ Assert.throws(() => waitForEvent(element, eventName), /TypeError/);
+ }
+
+ let clicked = waitForEvent(element, "click");
+ element.click();
+ let event = await clicked;
+ equal(element, event.target);
+});
+
+add_task(async function test_waitForEvent_captureTypes() {
+ let element = new MockElement();
+
+ for (let capture of ["foo", 42, [], {}]) {
+ Assert.throws(
+ () => waitForEvent(element, "click", { capture }),
+ /TypeError/
+ );
+ }
+
+ for (let capture of [null, undefined, false, true]) {
+ let expected_capture = capture == null ? false : capture;
+
+ element = new MockElement();
+ let clicked = waitForEvent(element, "click", { capture });
+ element.click();
+ let event = await clicked;
+ equal(element, event.target);
+ equal(expected_capture, event.capture);
+ }
+});
+
+add_task(async function test_waitForEvent_checkFnTypes() {
+ let element = new MockElement();
+
+ for (let checkFn of ["foo", 42, true, [], {}]) {
+ Assert.throws(
+ () => waitForEvent(element, "click", { checkFn }),
+ /TypeError/
+ );
+ }
+
+ let count;
+ for (let checkFn of [null, undefined, event => count++ > 0]) {
+ let expected_count = checkFn == null ? 0 : 2;
+ count = 0;
+
+ element = new MockElement();
+ let clicked = waitForEvent(element, "click", { checkFn });
+ element.click();
+ element.click();
+ let event = await clicked;
+ equal(element, event.target);
+ equal(expected_count, count);
+ }
+});
+
+add_task(async function test_waitForEvent_wantsUntrustedTypes() {
+ let element = new MockElement();
+
+ for (let wantsUntrusted of ["foo", 42, [], {}]) {
+ Assert.throws(
+ () => waitForEvent(element, "click", { wantsUntrusted }),
+ /TypeError/
+ );
+ }
+
+ for (let wantsUntrusted of [null, undefined, false, true]) {
+ let expected_untrusted = wantsUntrusted == null ? false : wantsUntrusted;
+
+ element = new MockElement();
+ let clicked = waitForEvent(element, "click", { wantsUntrusted });
+ element.click();
+ let event = await clicked;
+ equal(element, event.target);
+ equal(expected_untrusted, event.untrusted);
+ }
+});
+
+add_task(async function test_waitForLoadEvent() {
+ const mockBrowsingContext = {};
+ const onLoad = waitForLoadEvent("pageshow", () => mockBrowsingContext);
+
+ // Fake a page load by emitting the expected event on the EventDispatcher.
+ EventDispatcher.emit("page-load", {
+ type: "pageshow",
+ browsingContext: mockBrowsingContext,
+ });
+
+ const loadEvent = await onLoad;
+ equal(loadEvent.type, "pageshow");
+ equal(loadEvent.browsingContext, mockBrowsingContext);
+});
+
+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(() => waitForEvent(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/testing/marionette/test/unit/xpcshell.ini b/testing/marionette/test/unit/xpcshell.ini
new file mode 100644
index 0000000000..d804ec6d39
--- /dev/null
+++ b/testing/marionette/test/unit/xpcshell.ini
@@ -0,0 +1,24 @@
+# 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]
+skip-if = appname == "thunderbird"
+
+[test_action.js]
+[test_actors.js]
+[test_assert.js]
+[test_browser.js]
+[test_capabilities.js]
+[test_cookie.js]
+[test_dom.js]
+[test_element.js]
+[test_error.js]
+[test_evaluate.js]
+[test_format.js]
+[test_message.js]
+[test_modal.js]
+[test_navigate.js]
+[test_prefs.js]
+[test_store.js]
+[test_sync.js]