diff options
Diffstat (limited to 'testing/marionette/test/unit')
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] |