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