From 43a97878ce14b72f0981164f87f2e35e14151312 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 11:22:09 +0200 Subject: Adding upstream version 110.0.1. Signed-off-by: Daniel Baumann --- remote/marionette/test/xpcshell/.eslintrc.js | 7 + remote/marionette/test/xpcshell/README | 16 + remote/marionette/test/xpcshell/head.js | 7 + remote/marionette/test/xpcshell/test_action.js | 745 +++++++++++++++++++++++ remote/marionette/test/xpcshell/test_actors.js | 61 ++ remote/marionette/test/xpcshell/test_browser.js | 25 + remote/marionette/test/xpcshell/test_cookie.js | 370 +++++++++++ remote/marionette/test/xpcshell/test_dom.js | 277 +++++++++ remote/marionette/test/xpcshell/test_element.js | 571 +++++++++++++++++ remote/marionette/test/xpcshell/test_json.js | 251 ++++++++ remote/marionette/test/xpcshell/test_message.js | 279 +++++++++ remote/marionette/test/xpcshell/test_modal.js | 119 ++++ remote/marionette/test/xpcshell/test_navigate.js | 96 +++ remote/marionette/test/xpcshell/test_prefs.js | 115 ++++ remote/marionette/test/xpcshell/test_sync.js | 400 ++++++++++++ remote/marionette/test/xpcshell/xpcshell.ini | 20 + 16 files changed, 3359 insertions(+) create mode 100644 remote/marionette/test/xpcshell/.eslintrc.js create mode 100644 remote/marionette/test/xpcshell/README create mode 100644 remote/marionette/test/xpcshell/head.js create mode 100644 remote/marionette/test/xpcshell/test_action.js create mode 100644 remote/marionette/test/xpcshell/test_actors.js create mode 100644 remote/marionette/test/xpcshell/test_browser.js create mode 100644 remote/marionette/test/xpcshell/test_cookie.js create mode 100644 remote/marionette/test/xpcshell/test_dom.js create mode 100644 remote/marionette/test/xpcshell/test_element.js create mode 100644 remote/marionette/test/xpcshell/test_json.js create mode 100644 remote/marionette/test/xpcshell/test_message.js create mode 100644 remote/marionette/test/xpcshell/test_modal.js create mode 100644 remote/marionette/test/xpcshell/test_navigate.js create mode 100644 remote/marionette/test/xpcshell/test_prefs.js create mode 100644 remote/marionette/test/xpcshell/test_sync.js create mode 100644 remote/marionette/test/xpcshell/xpcshell.ini (limited to 'remote/marionette/test/xpcshell') 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 + 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