diff options
Diffstat (limited to '')
11 files changed, 3661 insertions, 0 deletions
diff --git a/remote/shared/webdriver/test/xpcshell/head.js b/remote/shared/webdriver/test/xpcshell/head.js new file mode 100644 index 0000000000..ddc5573d78 --- /dev/null +++ b/remote/shared/webdriver/test/xpcshell/head.js @@ -0,0 +1,15 @@ +async function doGC() { + // Run GC and CC a few times to make sure that as much as possible is freed. + const numCycles = 3; + for (let i = 0; i < numCycles; i++) { + Cu.forceGC(); + Cu.forceCC(); + await new Promise(resolve => Cu.schedulePreciseShrinkingGC(resolve)); + } + + const MemoryReporter = Cc[ + "@mozilla.org/memory-reporter-manager;1" + ].getService(Ci.nsIMemoryReporterManager); + + await new Promise(resolve => MemoryReporter.minimizeMemoryUsage(resolve)); +} diff --git a/remote/shared/webdriver/test/xpcshell/test_Actions.js b/remote/shared/webdriver/test/xpcshell/test_Actions.js new file mode 100644 index 0000000000..24eac2e09d --- /dev/null +++ b/remote/shared/webdriver/test/xpcshell/test_Actions.js @@ -0,0 +1,758 @@ +/* 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, CLICK_INTERVAL, ClickTracker } = ChromeUtils.importESModule( + "chrome://remote/content/shared/webdriver/Actions.sys.mjs" +); + +const { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +const XHTMLNS = "http://www.w3.org/1999/xhtml"; + +const domEl = { + nodeType: 1, + ELEMENT_NODE: 1, + namespaceURI: XHTMLNS, +}; + +add_task(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); + } +}); + +add_task(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" + ); +}); + +add_task(function test_processPointerParameters() { + for (let subtype of ["pointerDown", "pointerUp"]) { + for (let pointerType of [2, true, {}, []]) { + const inputTickActions = [ + { + type: "pointer", + parameters: { pointerType }, + subtype, + button: 0, + }, + ]; + let message = `Action sequence with parameters: {pointerType: ${pointerType} subtype: ${subtype}}`; + checkFromJSONErrors( + inputTickActions, + /Expected "pointerType" to be a string/, + message + ); + } + + for (let pointerType of ["", "foo"]) { + const inputTickActions = [ + { + type: "pointer", + parameters: { pointerType }, + subtype, + button: 0, + }, + ]; + let message = `Action sequence with parameters: {pointerType: ${pointerType} subtype: ${subtype}}`; + checkFromJSONErrors( + inputTickActions, + /Expected "pointerType" to be one of/, + 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 + ); + } +}); + +add_task(function test_processPointerDownAction() { + for (let button of [-1, "a"]) { + const inputTickActions = [ + { type: "pointer", subtype: "pointerDown", button }, + ]; + checkFromJSONErrors( + inputTickActions, + /Expected "button" to be a positive integer/, + `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); +}); + +add_task(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 a positive integer/, + `{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` + ); + } +}); + +add_task(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)})` + ); + } +}); + +add_task(function test_processPointerMoveActionOriginStringValidation() { + for (let origin of ["", "viewports", "pointers"]) { + const inputTickActions = [ + { type: "pointer", duration: 5000, subtype: "pointerMove", origin }, + ]; + checkFromJSONErrors( + inputTickActions, + /Expected "origin" to be undefined, "viewport", "pointer", or an element/, + `actionItem.origin: ${origin}` + ); + } +}); + +add_task(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); +}); + +add_task(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(null, null), { + x: 0, + y: 0, + }); +}); + +add_task(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); + } +}); + +add_task(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( + inputSource, + [actionItem.x, actionItem.y], + null + ); + equal(actionItem.x, target[0]); + equal(actionItem.y, target[1]); +}); + +add_task(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( + inputSource, + [actionItem.x, actionItem.y], + null + ); + equal(actionItem.x + inputSource.x, target[0]); + equal(actionItem.y + inputSource.y, target[1]); +}); + +add_task(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 + ); + } + } + } +}); + +add_task(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); +}); + +add_task(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(`Expected known subtype for type`), + message + ); + } +}); + +add_task(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"); +}); + +add_task(function test_processInputSourceActionSequenceValidation() { + checkFromJSONErrors( + [{ type: "swim", subtype: "pause", id: "some id" }], + /Expected known 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 + ); +}); + +add_task(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"); +}); + +add_task(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"); +}); + +add_task(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"); +}); + +add_task(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 \[object String\] "1" to be type pointer/, + message + ); +}); + +add_task(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 + ); + } +}); + +add_task(function test_extractActionChainEmpty() { + const state = new action.State(); + deepEqual(action.Chain.fromJSON(state, []), []); +}); + +add_task(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); +}); + +add_task(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"); +}); + +add_task(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()); +}); + +add_task(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()); +}); + +add_task(function test_ClickTracker_setClick() { + const clickTracker = new ClickTracker(); + const button1 = 1; + const button2 = 2; + + clickTracker.setClick(button1); + equal(1, clickTracker.count); + + // Make sure that clicking different mouse buttons doesn't increase the count. + clickTracker.setClick(button2); + equal(1, clickTracker.count); + + clickTracker.setClick(button2); + equal(2, clickTracker.count); + + clickTracker.reset(); + equal(0, clickTracker.count); +}); + +add_task(function test_ClickTracker_reset_after_timeout() { + const clickTracker = new ClickTracker(); + + clickTracker.setClick(1); + equal(1, clickTracker.count); + + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + setTimeout(() => equal(0, clickTracker.count), CLICK_INTERVAL + 10); +}); + +// 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/shared/webdriver/test/xpcshell/test_Assert.js b/remote/shared/webdriver/test/xpcshell/test_Assert.js new file mode 100644 index 0000000000..cf474868b6 --- /dev/null +++ b/remote/shared/webdriver/test/xpcshell/test_Assert.js @@ -0,0 +1,183 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; +/* eslint-disable no-array-constructor, no-object-constructor */ + +const { assert } = ChromeUtils.importESModule( + "chrome://remote/content/shared/webdriver/Assert.sys.mjs" +); +const { error } = ChromeUtils.importESModule( + "chrome://remote/content/shared/webdriver/Errors.sys.mjs" +); + +add_task(function test_session() { + assert.session({ id: "foo" }); + + const invalidTypes = [ + null, + undefined, + [], + {}, + { id: undefined }, + { id: null }, + { id: true }, + { id: 1 }, + { id: [] }, + { id: {} }, + ]; + + for (const invalidType of invalidTypes) { + Assert.throws(() => assert.session(invalidType), /InvalidSessionIDError/); + } + + Assert.throws(() => assert.session({ id: null }, "custom"), /custom/); +}); + +add_task(function test_platforms() { + // at least one will fail + let raised; + for (let fn of [assert.desktop, assert.mobile]) { + try { + fn(); + } catch (e) { + raised = e; + } + } + ok(raised instanceof error.UnsupportedOperationError); +}); + +add_task(function test_noUserPrompt() { + assert.noUserPrompt(null); + assert.noUserPrompt(undefined); + Assert.throws(() => assert.noUserPrompt({}), /UnexpectedAlertOpenError/); + Assert.throws(() => assert.noUserPrompt({}, "custom"), /custom/); +}); + +add_task(function test_defined() { + assert.defined({}); + Assert.throws(() => assert.defined(undefined), /InvalidArgumentError/); + Assert.throws(() => assert.noUserPrompt({}, "custom"), /custom/); +}); + +add_task(function test_number() { + assert.number(1); + assert.number(0); + assert.number(-1); + assert.number(1.2); + for (let i of ["foo", "1", {}, [], NaN, Infinity, undefined]) { + Assert.throws(() => assert.number(i), /InvalidArgumentError/); + } + + Assert.throws(() => assert.number("foo", "custom"), /custom/); +}); + +add_task(function test_callable() { + assert.callable(function () {}); + assert.callable(() => {}); + + for (let typ of [undefined, "", true, {}, []]) { + Assert.throws(() => assert.callable(typ), /InvalidArgumentError/); + } + + Assert.throws(() => assert.callable("foo", "custom"), /custom/); +}); + +add_task(function test_integer() { + assert.integer(1); + assert.integer(0); + assert.integer(-1); + Assert.throws(() => assert.integer("foo"), /InvalidArgumentError/); + Assert.throws(() => assert.integer(1.2), /InvalidArgumentError/); + + Assert.throws(() => assert.integer("foo", "custom"), /custom/); +}); + +add_task(function test_positiveInteger() { + assert.positiveInteger(1); + assert.positiveInteger(0); + Assert.throws(() => assert.positiveInteger(-1), /InvalidArgumentError/); + Assert.throws(() => assert.positiveInteger("foo"), /InvalidArgumentError/); + Assert.throws(() => assert.positiveInteger("foo", "custom"), /custom/); +}); + +add_task(function test_positiveNumber() { + assert.positiveNumber(1); + assert.positiveNumber(0); + assert.positiveNumber(1.1); + assert.positiveNumber(Number.MAX_VALUE); + // eslint-disable-next-line no-loss-of-precision + Assert.throws(() => assert.positiveNumber(1.8e308), /InvalidArgumentError/); + Assert.throws(() => assert.positiveNumber(-1), /InvalidArgumentError/); + Assert.throws(() => assert.positiveNumber(Infinity), /InvalidArgumentError/); + Assert.throws(() => assert.positiveNumber("foo"), /InvalidArgumentError/); + Assert.throws(() => assert.positiveNumber("foo", "custom"), /custom/); +}); + +add_task(function test_boolean() { + assert.boolean(true); + assert.boolean(false); + Assert.throws(() => assert.boolean("false"), /InvalidArgumentError/); + Assert.throws(() => assert.boolean(undefined), /InvalidArgumentError/); + Assert.throws(() => assert.boolean(undefined, "custom"), /custom/); +}); + +add_task(function test_string() { + assert.string("foo"); + assert.string(`bar`); + Assert.throws(() => assert.string(42), /InvalidArgumentError/); + Assert.throws(() => assert.string(42, "custom"), /custom/); +}); + +add_task(function test_open() { + assert.open({ currentWindowGlobal: {} }); + + for (let typ of [null, undefined, { currentWindowGlobal: null }]) { + Assert.throws(() => assert.open(typ), /NoSuchWindowError/); + } + + Assert.throws(() => assert.open(null, "custom"), /custom/); +}); + +add_task(function test_object() { + assert.object({}); + assert.object(new Object()); + for (let typ of [42, "foo", true, null, undefined]) { + Assert.throws(() => assert.object(typ), /InvalidArgumentError/); + } + + Assert.throws(() => assert.object(null, "custom"), /custom/); +}); + +add_task(function test_in() { + assert.in("foo", { foo: 42 }); + for (let typ of [{}, 42, true, null, undefined]) { + Assert.throws(() => assert.in("foo", typ), /InvalidArgumentError/); + } + + Assert.throws(() => assert.in("foo", { bar: 42 }, "custom"), /custom/); +}); + +add_task(function test_array() { + assert.array([]); + assert.array(new Array()); + Assert.throws(() => assert.array(42), /InvalidArgumentError/); + Assert.throws(() => assert.array({}), /InvalidArgumentError/); + + Assert.throws(() => assert.array(42, "custom"), /custom/); +}); + +add_task(function test_that() { + equal(1, assert.that(n => n + 1)(1)); + Assert.throws(() => assert.that(() => false)(), /InvalidArgumentError/); + Assert.throws(() => assert.that(val => val)(false), /InvalidArgumentError/); + Assert.throws( + () => assert.that(val => val, "foo", error.SessionNotCreatedError)(false), + /SessionNotCreatedError/ + ); + + Assert.throws(() => assert.that(() => false, "custom")(), /custom/); +}); + +/* eslint-enable no-array-constructor, no-new-object */ diff --git a/remote/shared/webdriver/test/xpcshell/test_Capabilities.js b/remote/shared/webdriver/test/xpcshell/test_Capabilities.js new file mode 100644 index 0000000000..19401dd463 --- /dev/null +++ b/remote/shared/webdriver/test/xpcshell/test_Capabilities.js @@ -0,0 +1,700 @@ +/* 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 { AppInfo } = ChromeUtils.importESModule( + "chrome://remote/content/shared/AppInfo.sys.mjs" +); +const { error } = ChromeUtils.importESModule( + "chrome://remote/content/shared/webdriver/Errors.sys.mjs" +); +const { + Capabilities, + mergeCapabilities, + PageLoadStrategy, + processCapabilities, + Proxy, + Timeouts, + UnhandledPromptBehavior, + validateCapabilities, +} = ChromeUtils.importESModule( + "chrome://remote/content/shared/webdriver/Capabilities.sys.mjs" +); + +add_task(function test_Timeouts_ctor() { + let ts = new Timeouts(); + equal(ts.implicit, 0); + equal(ts.pageLoad, 300000); + equal(ts.script, 30000); +}); + +add_task(function test_Timeouts_toString() { + equal(new Timeouts().toString(), "[object Timeouts]"); +}); + +add_task(function test_Timeouts_toJSON() { + let ts = new Timeouts(); + deepEqual(ts.toJSON(), { implicit: 0, pageLoad: 300000, script: 30000 }); +}); + +add_task(function test_Timeouts_fromJSON() { + let json = { + implicit: 0, + pageLoad: 2.0, + script: Number.MAX_SAFE_INTEGER, + }; + let ts = Timeouts.fromJSON(json); + equal(ts.implicit, json.implicit); + equal(ts.pageLoad, json.pageLoad); + equal(ts.script, json.script); +}); + +add_task(function test_Timeouts_fromJSON_unrecognised_field() { + let json = { + sessionId: "foobar", + }; + try { + Timeouts.fromJSON(json); + } catch (e) { + equal(e.name, error.InvalidArgumentError.name); + equal(e.message, "Unrecognised timeout: sessionId"); + } +}); + +add_task(function test_Timeouts_fromJSON_invalid_types() { + for (let value of [null, [], {}, false, "10", 2.5]) { + Assert.throws( + () => Timeouts.fromJSON({ implicit: value }), + /InvalidArgumentError/ + ); + } +}); + +add_task(function test_Timeouts_fromJSON_bounds() { + for (let value of [-1, Number.MAX_SAFE_INTEGER + 1]) { + Assert.throws( + () => Timeouts.fromJSON({ script: value }), + /InvalidArgumentError/ + ); + } +}); + +add_task(function test_PageLoadStrategy() { + equal(PageLoadStrategy.None, "none"); + equal(PageLoadStrategy.Eager, "eager"); + equal(PageLoadStrategy.Normal, "normal"); +}); + +add_task(function test_Proxy_ctor() { + let p = new Proxy(); + let props = [ + "proxyType", + "httpProxy", + "sslProxy", + "socksProxy", + "socksVersion", + "proxyAutoconfigUrl", + ]; + for (let prop of props) { + ok(prop in p, `${prop} in ${JSON.stringify(props)}`); + equal(p[prop], null); + } +}); + +add_task(function test_Proxy_init() { + let p = new Proxy(); + + // no changed made, and 5 (system) is default + equal(p.init(), false); + equal(Services.prefs.getIntPref("network.proxy.type"), 5); + + // pac + p.proxyType = "pac"; + p.proxyAutoconfigUrl = "http://localhost:1234"; + ok(p.init()); + + equal(Services.prefs.getIntPref("network.proxy.type"), 2); + equal( + Services.prefs.getStringPref("network.proxy.autoconfig_url"), + "http://localhost:1234" + ); + + // direct + p = new Proxy(); + p.proxyType = "direct"; + ok(p.init()); + equal(Services.prefs.getIntPref("network.proxy.type"), 0); + + // autodetect + p = new Proxy(); + p.proxyType = "autodetect"; + ok(p.init()); + equal(Services.prefs.getIntPref("network.proxy.type"), 4); + + // system + p = new Proxy(); + p.proxyType = "system"; + ok(p.init()); + equal(Services.prefs.getIntPref("network.proxy.type"), 5); + + // manual + for (let proxy of ["http", "ssl", "socks"]) { + p = new Proxy(); + p.proxyType = "manual"; + p.noProxy = ["foo", "bar"]; + p[`${proxy}Proxy`] = "foo"; + p[`${proxy}ProxyPort`] = 42; + if (proxy === "socks") { + p[`${proxy}Version`] = 4; + } + + ok(p.init()); + equal(Services.prefs.getIntPref("network.proxy.type"), 1); + equal( + Services.prefs.getStringPref("network.proxy.no_proxies_on"), + "foo, bar" + ); + equal(Services.prefs.getStringPref(`network.proxy.${proxy}`), "foo"); + equal(Services.prefs.getIntPref(`network.proxy.${proxy}_port`), 42); + if (proxy === "socks") { + equal(Services.prefs.getIntPref(`network.proxy.${proxy}_version`), 4); + } + } + + // empty no proxy should reset default exclustions + p = new Proxy(); + p.proxyType = "manual"; + p.noProxy = []; + ok(p.init()); + equal(Services.prefs.getStringPref("network.proxy.no_proxies_on"), ""); +}); + +add_task(function test_Proxy_toString() { + equal(new Proxy().toString(), "[object Proxy]"); +}); + +add_task(function test_Proxy_toJSON() { + let p = new Proxy(); + deepEqual(p.toJSON(), {}); + + // autoconfig url + p = new Proxy(); + p.proxyType = "pac"; + p.proxyAutoconfigUrl = "foo"; + deepEqual(p.toJSON(), { proxyType: "pac", proxyAutoconfigUrl: "foo" }); + + // manual proxy + p = new Proxy(); + p.proxyType = "manual"; + deepEqual(p.toJSON(), { proxyType: "manual" }); + + for (let proxy of ["httpProxy", "sslProxy", "socksProxy"]) { + let expected = { proxyType: "manual" }; + + p = new Proxy(); + p.proxyType = "manual"; + + if (proxy == "socksProxy") { + p.socksVersion = 5; + expected.socksVersion = 5; + } + + // without port + p[proxy] = "foo"; + expected[proxy] = "foo"; + deepEqual(p.toJSON(), expected); + + // with port + p[proxy] = "foo"; + p[`${proxy}Port`] = 0; + expected[proxy] = "foo:0"; + deepEqual(p.toJSON(), expected); + + p[`${proxy}Port`] = 42; + expected[proxy] = "foo:42"; + deepEqual(p.toJSON(), expected); + + // add brackets for IPv6 address as proxy hostname + p[proxy] = "2001:db8::1"; + p[`${proxy}Port`] = 42; + expected[proxy] = "foo:42"; + expected[proxy] = "[2001:db8::1]:42"; + deepEqual(p.toJSON(), expected); + } + + // noProxy: add brackets for IPv6 address + p = new Proxy(); + p.proxyType = "manual"; + p.noProxy = ["2001:db8::1"]; + let expected = { proxyType: "manual", noProxy: "[2001:db8::1]" }; + deepEqual(p.toJSON(), expected); +}); + +add_task(function test_Proxy_fromJSON() { + let p = new Proxy(); + deepEqual(p, Proxy.fromJSON(undefined)); + deepEqual(p, Proxy.fromJSON(null)); + + for (let typ of [true, 42, "foo", []]) { + Assert.throws(() => Proxy.fromJSON(typ), /InvalidArgumentError/); + } + + // must contain a valid proxyType + Assert.throws(() => Proxy.fromJSON({}), /InvalidArgumentError/); + Assert.throws( + () => Proxy.fromJSON({ proxyType: "foo" }), + /InvalidArgumentError/ + ); + + // autoconfig url + for (let url of [true, 42, [], {}]) { + Assert.throws( + () => Proxy.fromJSON({ proxyType: "pac", proxyAutoconfigUrl: url }), + /InvalidArgumentError/ + ); + } + + p = new Proxy(); + p.proxyType = "pac"; + p.proxyAutoconfigUrl = "foo"; + deepEqual(p, Proxy.fromJSON({ proxyType: "pac", proxyAutoconfigUrl: "foo" })); + + // manual proxy + p = new Proxy(); + p.proxyType = "manual"; + deepEqual(p, Proxy.fromJSON({ proxyType: "manual" })); + + for (let proxy of ["httpProxy", "sslProxy", "socksProxy"]) { + let manual = { proxyType: "manual" }; + + // invalid hosts + for (let host of [ + true, + 42, + [], + {}, + null, + "http://foo", + "foo:-1", + "foo:65536", + "foo/test", + "foo#42", + "foo?foo=bar", + "2001:db8::1", + ]) { + manual[proxy] = host; + Assert.throws(() => Proxy.fromJSON(manual), /InvalidArgumentError/); + } + + p = new Proxy(); + p.proxyType = "manual"; + if (proxy == "socksProxy") { + manual.socksVersion = 5; + p.socksVersion = 5; + } + + let host_map = { + "foo:1": { hostname: "foo", port: 1 }, + "foo:21": { hostname: "foo", port: 21 }, + "foo:80": { hostname: "foo", port: 80 }, + "foo:443": { hostname: "foo", port: 443 }, + "foo:65535": { hostname: "foo", port: 65535 }, + "127.0.0.1:42": { hostname: "127.0.0.1", port: 42 }, + "[2001:db8::1]:42": { hostname: "2001:db8::1", port: "42" }, + }; + + // valid proxy hosts with port + for (let host in host_map) { + manual[proxy] = host; + + p[`${proxy}`] = host_map[host].hostname; + p[`${proxy}Port`] = host_map[host].port; + + deepEqual(p, Proxy.fromJSON(manual)); + } + + // Without a port the default port of the scheme is used + for (let host of ["foo", "foo:"]) { + manual[proxy] = host; + + // For socks no default port is available + p[proxy] = `foo`; + if (proxy === "socksProxy") { + p[`${proxy}Port`] = null; + } else { + let default_ports = { httpProxy: 80, sslProxy: 443 }; + + p[`${proxy}Port`] = default_ports[proxy]; + } + + deepEqual(p, Proxy.fromJSON(manual)); + } + } + + // missing required socks version + Assert.throws( + () => Proxy.fromJSON({ proxyType: "manual", socksProxy: "foo:1234" }), + /InvalidArgumentError/ + ); + + // Bug 1703805: Since Firefox 90 ftpProxy is no longer supported + Assert.throws( + () => Proxy.fromJSON({ proxyType: "manual", ftpProxy: "foo:21" }), + /InvalidArgumentError/ + ); + + // noProxy: invalid settings + for (let noProxy of [true, 42, {}, null, "foo", [true], [42], [{}], [null]]) { + Assert.throws( + () => Proxy.fromJSON({ proxyType: "manual", noProxy }), + /InvalidArgumentError/ + ); + } + + // noProxy: valid settings + p = new Proxy(); + p.proxyType = "manual"; + for (let noProxy of [[], ["foo"], ["foo", "bar"], ["127.0.0.1"]]) { + let manual = { proxyType: "manual", noProxy }; + p.noProxy = noProxy; + deepEqual(p, Proxy.fromJSON(manual)); + } + + // noProxy: IPv6 needs brackets removed + p = new Proxy(); + p.proxyType = "manual"; + p.noProxy = ["2001:db8::1"]; + let manual = { proxyType: "manual", noProxy: ["[2001:db8::1]"] }; + deepEqual(p, Proxy.fromJSON(manual)); +}); + +add_task(function test_UnhandledPromptBehavior() { + equal(UnhandledPromptBehavior.Accept, "accept"); + equal(UnhandledPromptBehavior.AcceptAndNotify, "accept and notify"); + equal(UnhandledPromptBehavior.Dismiss, "dismiss"); + equal(UnhandledPromptBehavior.DismissAndNotify, "dismiss and notify"); + equal(UnhandledPromptBehavior.Ignore, "ignore"); +}); + +add_task(function test_Capabilities_ctor() { + let caps = new Capabilities(); + ok(caps.has("browserName")); + ok(caps.has("browserVersion")); + ok(caps.has("platformName")); + ok(["linux", "mac", "windows", "android"].includes(caps.get("platformName"))); + equal(PageLoadStrategy.Normal, caps.get("pageLoadStrategy")); + equal(false, caps.get("acceptInsecureCerts")); + ok(caps.get("timeouts") instanceof Timeouts); + ok(caps.get("proxy") instanceof Proxy); + equal(caps.get("setWindowRect"), !AppInfo.isAndroid); + equal(caps.get("strictFileInteractability"), false); + equal(caps.get("webSocketUrl"), null); + + equal(false, caps.get("moz:accessibilityChecks")); + ok(caps.has("moz:buildID")); + ok(caps.has("moz:debuggerAddress")); + ok(caps.has("moz:platformVersion")); + ok(caps.has("moz:processID")); + ok(caps.has("moz:profile")); + equal(true, caps.get("moz:webdriverClick")); + + // No longer supported capabilities + ok(!caps.has("moz:useNonSpecCompliantPointerOrigin")); +}); + +add_task(function test_Capabilities_toString() { + equal("[object Capabilities]", new Capabilities().toString()); +}); + +add_task(function test_Capabilities_toJSON() { + let caps = new Capabilities(); + let json = caps.toJSON(); + + equal(caps.get("browserName"), json.browserName); + equal(caps.get("browserVersion"), json.browserVersion); + equal(caps.get("platformName"), json.platformName); + equal(caps.get("pageLoadStrategy"), json.pageLoadStrategy); + equal(caps.get("acceptInsecureCerts"), json.acceptInsecureCerts); + deepEqual(caps.get("proxy").toJSON(), json.proxy); + deepEqual(caps.get("timeouts").toJSON(), json.timeouts); + equal(caps.get("setWindowRect"), json.setWindowRect); + equal(caps.get("strictFileInteractability"), json.strictFileInteractability); + equal(caps.get("webSocketUrl"), json.webSocketUrl); + + equal(caps.get("moz:accessibilityChecks"), json["moz:accessibilityChecks"]); + equal(caps.get("moz:buildID"), json["moz:buildID"]); + equal(caps.get("moz:debuggerAddress"), json["moz:debuggerAddress"]); + equal(caps.get("moz:platformVersion"), json["moz:platformVersion"]); + equal(caps.get("moz:processID"), json["moz:processID"]); + equal(caps.get("moz:profile"), json["moz:profile"]); + equal(caps.get("moz:webdriverClick"), json["moz:webdriverClick"]); +}); + +add_task(function test_Capabilities_fromJSON() { + const { fromJSON } = Capabilities; + + // plain + for (let typ of [{}, null, undefined]) { + ok(fromJSON(typ).has("browserName")); + } + + // matching + let caps = new Capabilities(); + + caps = fromJSON({ acceptInsecureCerts: true }); + equal(true, caps.get("acceptInsecureCerts")); + caps = fromJSON({ acceptInsecureCerts: false }); + equal(false, caps.get("acceptInsecureCerts")); + + for (let strategy of Object.values(PageLoadStrategy)) { + caps = fromJSON({ pageLoadStrategy: strategy }); + equal(strategy, caps.get("pageLoadStrategy")); + } + + let proxyConfig = { proxyType: "manual" }; + caps = fromJSON({ proxy: proxyConfig }); + equal("manual", caps.get("proxy").proxyType); + + let timeoutsConfig = { implicit: 123 }; + caps = fromJSON({ timeouts: timeoutsConfig }); + equal(123, caps.get("timeouts").implicit); + + caps = fromJSON({ strictFileInteractability: false }); + equal(false, caps.get("strictFileInteractability")); + caps = fromJSON({ strictFileInteractability: true }); + equal(true, caps.get("strictFileInteractability")); + + caps = fromJSON({ webSocketUrl: true }); + equal(true, caps.get("webSocketUrl")); + + caps = fromJSON({ "webauthn:virtualAuthenticators": true }); + equal(true, caps.get("webauthn:virtualAuthenticators")); + caps = fromJSON({ "webauthn:virtualAuthenticators": false }); + equal(false, caps.get("webauthn:virtualAuthenticators")); + Assert.throws( + () => fromJSON({ "webauthn:virtualAuthenticators": "foo" }), + /InvalidArgumentError/ + ); + + caps = fromJSON({ "webauthn:extension:uvm": true }); + equal(true, caps.get("webauthn:extension:uvm")); + caps = fromJSON({ "webauthn:extension:uvm": false }); + equal(false, caps.get("webauthn:extension:uvm")); + Assert.throws( + () => fromJSON({ "webauthn:extension:uvm": "foo" }), + /InvalidArgumentError/ + ); + + caps = fromJSON({ "webauthn:extension:prf": true }); + equal(true, caps.get("webauthn:extension:prf")); + caps = fromJSON({ "webauthn:extension:prf": false }); + equal(false, caps.get("webauthn:extension:prf")); + Assert.throws( + () => fromJSON({ "webauthn:extension:prf": "foo" }), + /InvalidArgumentError/ + ); + + caps = fromJSON({ "webauthn:extension:largeBlob": true }); + equal(true, caps.get("webauthn:extension:largeBlob")); + caps = fromJSON({ "webauthn:extension:largeBlob": false }); + equal(false, caps.get("webauthn:extension:largeBlob")); + Assert.throws( + () => fromJSON({ "webauthn:extension:largeBlob": "foo" }), + /InvalidArgumentError/ + ); + + caps = fromJSON({ "webauthn:extension:credBlob": true }); + equal(true, caps.get("webauthn:extension:credBlob")); + caps = fromJSON({ "webauthn:extension:credBlob": false }); + equal(false, caps.get("webauthn:extension:credBlob")); + Assert.throws( + () => fromJSON({ "webauthn:extension:credBlob": "foo" }), + /InvalidArgumentError/ + ); + + caps = fromJSON({ "moz:accessibilityChecks": true }); + equal(true, caps.get("moz:accessibilityChecks")); + caps = fromJSON({ "moz:accessibilityChecks": false }); + equal(false, caps.get("moz:accessibilityChecks")); + + // capability is always populated with null if remote agent is not listening + caps = fromJSON({}); + equal(null, caps.get("moz:debuggerAddress")); + caps = fromJSON({ "moz:debuggerAddress": "foo" }); + equal(null, caps.get("moz:debuggerAddress")); + caps = fromJSON({ "moz:debuggerAddress": true }); + equal(null, caps.get("moz:debuggerAddress")); + + caps = fromJSON({ "moz:webdriverClick": true }); + equal(true, caps.get("moz:webdriverClick")); + caps = fromJSON({ "moz:webdriverClick": false }); + equal(false, caps.get("moz:webdriverClick")); + + // No longer supported capabilities + Assert.throws( + () => fromJSON({ "moz:useNonSpecCompliantPointerOrigin": false }), + /InvalidArgumentError/ + ); + Assert.throws( + () => fromJSON({ "moz:useNonSpecCompliantPointerOrigin": true }), + /InvalidArgumentError/ + ); +}); + +add_task(function test_mergeCapabilities() { + // Shadowed values. + Assert.throws( + () => + mergeCapabilities( + { acceptInsecureCerts: true }, + { acceptInsecureCerts: false } + ), + /InvalidArgumentError/ + ); + + deepEqual( + { acceptInsecureCerts: true }, + mergeCapabilities({ acceptInsecureCerts: true }, undefined) + ); + deepEqual( + { acceptInsecureCerts: true, browserName: "Firefox" }, + mergeCapabilities({ acceptInsecureCerts: true }, { browserName: "Firefox" }) + ); +}); + +add_task(function test_validateCapabilities_invalid() { + const invalidCapabilities = [ + true, + 42, + "foo", + [], + { acceptInsecureCerts: "foo" }, + { browserName: true }, + { browserVersion: true }, + { platformName: true }, + { pageLoadStrategy: "foo" }, + { proxy: false }, + { strictFileInteractability: "foo" }, + { timeouts: false }, + { unhandledPromptBehavior: false }, + { webSocketUrl: false }, + { webSocketUrl: "foo" }, + { "moz:firefoxOptions": "foo" }, + { "moz:accessibilityChecks": "foo" }, + { "moz:webdriverClick": "foo" }, + { "moz:webdriverClick": 1 }, + { "moz:useNonSpecCompliantPointerOrigin": false }, + { "moz:debuggerAddress": "foo" }, + { "moz:someRandomString": {} }, + ]; + for (const capabilities of invalidCapabilities) { + Assert.throws( + () => validateCapabilities(capabilities), + /InvalidArgumentError/ + ); + } +}); + +add_task(function test_validateCapabilities_valid() { + // Ignore null value. + deepEqual({}, validateCapabilities({ test: null })); + + const validCapabilities = [ + { acceptInsecureCerts: true }, + { browserName: "firefox" }, + { browserVersion: "12" }, + { platformName: "linux" }, + { pageLoadStrategy: "eager" }, + { proxy: { proxyType: "manual", httpProxy: "test.com" } }, + { strictFileInteractability: true }, + { timeouts: { pageLoad: 500 } }, + { unhandledPromptBehavior: "accept" }, + { webSocketUrl: true }, + { "moz:firefoxOptions": {} }, + { "moz:accessibilityChecks": true }, + { "moz:webdriverClick": true }, + { "moz:debuggerAddress": true }, + { "test:extension": "foo" }, + ]; + for (const validCapability of validCapabilities) { + deepEqual(validCapability, validateCapabilities(validCapability)); + } +}); + +add_task(function test_processCapabilities() { + for (const invalidValue of [ + { capabilities: null }, + { capabilities: undefined }, + { capabilities: "foo" }, + { capabilities: true }, + { capabilities: [] }, + { capabilities: { alwaysMatch: null } }, + { capabilities: { alwaysMatch: "foo" } }, + { capabilities: { alwaysMatch: true } }, + { capabilities: { alwaysMatch: [] } }, + { capabilities: { firstMatch: null } }, + { capabilities: { firstMatch: "foo" } }, + { capabilities: { firstMatch: true } }, + { capabilities: { firstMatch: {} } }, + { capabilities: { firstMatch: [] } }, + ]) { + Assert.throws( + () => processCapabilities(invalidValue), + /InvalidArgumentError/ + ); + } + + deepEqual( + { acceptInsecureCerts: true }, + processCapabilities({ + capabilities: { alwaysMatch: { acceptInsecureCerts: true } }, + }) + ); + deepEqual( + { browserName: "Firefox" }, + processCapabilities({ + capabilities: { firstMatch: [{ browserName: "Firefox" }] }, + }) + ); + deepEqual( + { acceptInsecureCerts: true, browserName: "Firefox" }, + processCapabilities({ + capabilities: { + alwaysMatch: { acceptInsecureCerts: true }, + firstMatch: [{ browserName: "Firefox" }], + }, + }) + ); +}); + +// use Proxy.toJSON to test marshal +add_task(function test_marshal() { + let proxy = new Proxy(); + + // drop empty fields + deepEqual({}, proxy.toJSON()); + proxy.proxyType = "manual"; + deepEqual({ proxyType: "manual" }, proxy.toJSON()); + proxy.proxyType = null; + deepEqual({}, proxy.toJSON()); + proxy.proxyType = undefined; + deepEqual({}, proxy.toJSON()); + + // iterate over object literals + proxy.proxyType = { foo: "bar" }; + deepEqual({ proxyType: { foo: "bar" } }, proxy.toJSON()); + + // iterate over complex object that implement toJSON + proxy.proxyType = new Proxy(); + deepEqual({}, proxy.toJSON()); + proxy.proxyType.proxyType = "manual"; + deepEqual({ proxyType: { proxyType: "manual" } }, proxy.toJSON()); + + // drop objects with no entries + proxy.proxyType = { foo: {} }; + deepEqual({}, proxy.toJSON()); + proxy.proxyType = { foo: new Proxy() }; + deepEqual({}, proxy.toJSON()); +}); diff --git a/remote/shared/webdriver/test/xpcshell/test_Errors.js b/remote/shared/webdriver/test/xpcshell/test_Errors.js new file mode 100644 index 0000000000..22e3526039 --- /dev/null +++ b/remote/shared/webdriver/test/xpcshell/test_Errors.js @@ -0,0 +1,543 @@ +/* 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 errors = [ + error.WebDriverError, + + error.DetachedShadowRootError, + error.ElementClickInterceptedError, + error.ElementNotAccessibleError, + error.ElementNotInteractableError, + error.InsecureCertificateError, + error.InvalidArgumentError, + error.InvalidCookieDomainError, + error.InvalidElementStateError, + error.InvalidSelectorError, + error.InvalidSessionIDError, + error.JavaScriptError, + error.MoveTargetOutOfBoundsError, + error.NoSuchAlertError, + error.NoSuchElementError, + error.NoSuchFrameError, + error.NoSuchHandleError, + error.NoSuchInterceptError, + error.NoSuchNodeError, + error.NoSuchRequestError, + error.NoSuchScriptError, + error.NoSuchShadowRootError, + error.NoSuchWindowError, + error.ScriptTimeoutError, + error.SessionNotCreatedError, + error.StaleElementReferenceError, + error.TimeoutError, + error.UnableToSetCookieError, + error.UnexpectedAlertOpenError, + error.UnknownCommandError, + error.UnknownError, + error.UnsupportedOperationError, +]; + +function notok(condition) { + ok(!condition); +} + +add_task(function test_isError() { + notok(error.isError(null)); + notok(error.isError([])); + notok(error.isError(new Date())); + + ok(error.isError(new Components.Exception())); + ok(error.isError(new Error())); + ok(error.isError(new EvalError())); + ok(error.isError(new InternalError())); + ok(error.isError(new RangeError())); + ok(error.isError(new ReferenceError())); + ok(error.isError(new SyntaxError())); + ok(error.isError(new TypeError())); + ok(error.isError(new URIError())); + + errors.forEach(err => ok(error.isError(new err()))); +}); + +add_task(function test_isWebDriverError() { + notok(error.isWebDriverError(new Components.Exception())); + notok(error.isWebDriverError(new Error())); + notok(error.isWebDriverError(new EvalError())); + notok(error.isWebDriverError(new InternalError())); + notok(error.isWebDriverError(new RangeError())); + notok(error.isWebDriverError(new ReferenceError())); + notok(error.isWebDriverError(new SyntaxError())); + notok(error.isWebDriverError(new TypeError())); + notok(error.isWebDriverError(new URIError())); + + errors.forEach(err => ok(error.isWebDriverError(new err()))); +}); + +add_task(function test_wrap() { + // webdriver-derived errors should not be wrapped + errors.forEach(err => { + const unwrappedError = new err("foo"); + const wrappedError = error.wrap(unwrappedError); + + ok(wrappedError instanceof error.WebDriverError); + ok(wrappedError instanceof err); + equal(wrappedError.name, unwrappedError.name); + equal(wrappedError.status, unwrappedError.status); + equal(wrappedError.message, "foo"); + }); + + // JS errors should be wrapped in UnknownError and retain their type + // as part of the message field. + const jsErrors = [ + Error, + EvalError, + InternalError, + RangeError, + ReferenceError, + SyntaxError, + TypeError, + URIError, + ]; + + jsErrors.forEach(err => { + const originalError = new err("foo"); + const wrappedError = error.wrap(originalError); + + ok(wrappedError instanceof error.UnknownError); + equal(wrappedError.name, "UnknownError"); + equal(wrappedError.status, "unknown error"); + equal(wrappedError.message, `${originalError.name}: foo`); + }); +}); + +add_task(function test_stringify() { + equal("<unprintable error>", error.stringify()); + equal("<unprintable error>", error.stringify("foo")); + equal("[object Object]", error.stringify({})); + equal("[object Object]\nfoo", error.stringify({ stack: "foo" })); + equal("Error: foo", error.stringify(new Error("foo")).split("\n")[0]); + + errors.forEach(err => { + const e = new err("foo"); + + equal(`${e.name}: foo`, error.stringify(e).split("\n")[0]); + }); +}); + +add_task(function test_constructor_from_error() { + const data = { a: 3, b: "bar" }; + const origError = new error.WebDriverError("foo", data); + + errors.forEach(err => { + const newError = new err(origError); + + ok(newError instanceof err); + equal(newError.message, origError.message); + equal(newError.stack, origError.stack); + equal(newError.data, origError.data); + }); +}); + +add_task(function test_stack() { + equal("string", typeof error.stack()); + ok(error.stack().includes("test_stack")); + ok(!error.stack().includes("add_task")); +}); + +add_task(function test_toJSON() { + errors.forEach(err => { + const e0 = new err(); + const e0_json = e0.toJSON(); + equal(e0_json.error, e0.status); + equal(e0_json.message, ""); + equal(e0_json.stacktrace, e0.stack); + equal(e0_json.data, undefined); + + // message property + const e1 = new err("a"); + const e1_json = e1.toJSON(); + + equal(e1_json.message, e1.message); + equal(e1_json.stacktrace, e1.stack); + equal(e1_json.data, undefined); + + // message and optional data property + const data = { a: 3, b: "bar" }; + const e2 = new err("foo", data); + const e2_json = e2.toJSON(); + + equal(e2.status, e2_json.error); + equal(e2.message, e2_json.message); + equal(e2_json.data, data); + }); +}); + +add_task(function test_fromJSON() { + errors.forEach(err => { + Assert.throws( + () => err.fromJSON({ error: "foo" }), + /Not of WebDriverError descent/ + ); + Assert.throws( + () => err.fromJSON({ error: "Error" }), + /Not of WebDriverError descent/ + ); + Assert.throws(() => err.fromJSON({}), /Undeserialisable error type/); + Assert.throws(() => err.fromJSON(undefined), /TypeError/); + + // message and stack + const e1 = new err("1"); + const e1_json = { error: e1.status, message: "3", stacktrace: "4" }; + const e1_fromJSON = error.WebDriverError.fromJSON(e1_json); + + ok(e1_fromJSON instanceof error.WebDriverError); + ok(e1_fromJSON instanceof err); + equal(e1_fromJSON.name, e1.name); + equal(e1_fromJSON.status, e1_json.error); + equal(e1_fromJSON.message, e1_json.message); + equal(e1_fromJSON.stack, e1_json.stacktrace); + + // message and optional data + const e2_data = { a: 3, b: "bar" }; + const e2 = new err("1", e2_data); + const e2_json = { error: e1.status, message: "3", data: e2_data }; + const e2_fromJSON = error.WebDriverError.fromJSON(e2_json); + + ok(e2_fromJSON instanceof error.WebDriverError); + ok(e2_fromJSON instanceof err); + equal(e2_fromJSON.name, e2.name); + equal(e2_fromJSON.status, e2_json.error); + equal(e2_fromJSON.message, e2_json.message); + equal(e2_fromJSON.data, e2_json.data); + + // parity with toJSON + const e3_data = { a: 3, b: "bar" }; + const e3 = new err("1", e3_data); + const e3_json = e3.toJSON(); + const e3_fromJSON = error.WebDriverError.fromJSON(e3_json); + + equal(e3_json.error, e3_fromJSON.status); + equal(e3_json.message, e3_fromJSON.message); + equal(e3_json.stacktrace, e3_fromJSON.stack); + }); +}); + +add_task(function test_WebDriverError() { + let err = new error.WebDriverError("foo"); + equal("WebDriverError", err.name); + equal("foo", err.message); + equal("webdriver error", err.status); + ok(err instanceof error.WebDriverError); +}); + +add_task(function test_DetachedShadowRootError() { + let err = new error.DetachedShadowRootError("foo"); + equal("DetachedShadowRootError", err.name); + equal("foo", err.message); + equal("detached shadow root", err.status); + ok(err instanceof error.WebDriverError); +}); + +add_task(function test_ElementClickInterceptedError() { + let otherEl = { + hasAttribute: attr => attr in otherEl, + getAttribute: attr => (attr in otherEl ? otherEl[attr] : null), + nodeType: 1, + localName: "a", + }; + let obscuredEl = { + hasAttribute: attr => attr in obscuredEl, + getAttribute: attr => (attr in obscuredEl ? obscuredEl[attr] : null), + nodeType: 1, + localName: "b", + ownerDocument: { + elementFromPoint() { + return otherEl; + }, + }, + style: { + pointerEvents: "auto", + }, + }; + + let err1 = new error.ElementClickInterceptedError( + undefined, + undefined, + obscuredEl, + { x: 1, y: 2 } + ); + equal("ElementClickInterceptedError", err1.name); + equal( + "Element <b> is not clickable at point (1,2) " + + "because another element <a> obscures it", + err1.message + ); + equal("element click intercepted", err1.status); + ok(err1 instanceof error.WebDriverError); + + obscuredEl.style.pointerEvents = "none"; + let err2 = new error.ElementClickInterceptedError( + undefined, + undefined, + obscuredEl, + { x: 1, y: 2 } + ); + equal( + "Element <b> is not clickable at point (1,2) " + + "because it does not have pointer events enabled, " + + "and element <a> would receive the click instead", + err2.message + ); +}); + +add_task(function test_ElementNotAccessibleError() { + let err = new error.ElementNotAccessibleError("foo"); + equal("ElementNotAccessibleError", err.name); + equal("foo", err.message); + equal("element not accessible", err.status); + ok(err instanceof error.WebDriverError); +}); + +add_task(function test_ElementNotInteractableError() { + let err = new error.ElementNotInteractableError("foo"); + equal("ElementNotInteractableError", err.name); + equal("foo", err.message); + equal("element not interactable", err.status); + ok(err instanceof error.WebDriverError); +}); + +add_task(function test_InsecureCertificateError() { + let err = new error.InsecureCertificateError("foo"); + equal("InsecureCertificateError", err.name); + equal("foo", err.message); + equal("insecure certificate", err.status); + ok(err instanceof error.WebDriverError); +}); + +add_task(function test_InvalidArgumentError() { + let err = new error.InvalidArgumentError("foo"); + equal("InvalidArgumentError", err.name); + equal("foo", err.message); + equal("invalid argument", err.status); + ok(err instanceof error.WebDriverError); +}); + +add_task(function test_InvalidCookieDomainError() { + let err = new error.InvalidCookieDomainError("foo"); + equal("InvalidCookieDomainError", err.name); + equal("foo", err.message); + equal("invalid cookie domain", err.status); + ok(err instanceof error.WebDriverError); +}); + +add_task(function test_InvalidElementStateError() { + let err = new error.InvalidElementStateError("foo"); + equal("InvalidElementStateError", err.name); + equal("foo", err.message); + equal("invalid element state", err.status); + ok(err instanceof error.WebDriverError); +}); + +add_task(function test_InvalidSelectorError() { + let err = new error.InvalidSelectorError("foo"); + equal("InvalidSelectorError", err.name); + equal("foo", err.message); + equal("invalid selector", err.status); + ok(err instanceof error.WebDriverError); +}); + +add_task(function test_InvalidSessionIDError() { + let err = new error.InvalidSessionIDError("foo"); + equal("InvalidSessionIDError", err.name); + equal("foo", err.message); + equal("invalid session id", err.status); + ok(err instanceof error.WebDriverError); +}); + +add_task(function test_JavaScriptError() { + let err = new error.JavaScriptError("foo"); + equal("JavaScriptError", err.name); + equal("foo", err.message); + equal("javascript error", err.status); + ok(err instanceof error.WebDriverError); + + equal("", new error.JavaScriptError(undefined).message); + + let superErr = new RangeError("foo"); + let inheritedErr = new error.JavaScriptError(superErr); + equal("RangeError: foo", inheritedErr.message); + equal(superErr.stack, inheritedErr.stack); +}); + +add_task(function test_MoveTargetOutOfBoundsError() { + let err = new error.MoveTargetOutOfBoundsError("foo"); + equal("MoveTargetOutOfBoundsError", err.name); + equal("foo", err.message); + equal("move target out of bounds", err.status); + ok(err instanceof error.WebDriverError); +}); + +add_task(function test_NoSuchAlertError() { + let err = new error.NoSuchAlertError("foo"); + equal("NoSuchAlertError", err.name); + equal("foo", err.message); + equal("no such alert", err.status); + ok(err instanceof error.WebDriverError); +}); + +add_task(function test_NoSuchElementError() { + let err = new error.NoSuchElementError("foo"); + equal("NoSuchElementError", err.name); + equal("foo", err.message); + equal("no such element", err.status); + ok(err instanceof error.WebDriverError); +}); + +add_task(function test_NoSuchFrameError() { + let err = new error.NoSuchFrameError("foo"); + equal("NoSuchFrameError", err.name); + equal("foo", err.message); + equal("no such frame", err.status); + ok(err instanceof error.WebDriverError); +}); + +add_task(function test_NoSuchHandleError() { + let err = new error.NoSuchHandleError("foo"); + equal("NoSuchHandleError", err.name); + equal("foo", err.message); + equal("no such handle", err.status); + ok(err instanceof error.WebDriverError); +}); + +add_task(function test_NoSuchInterceptError() { + let err = new error.NoSuchInterceptError("foo"); + equal("NoSuchInterceptError", err.name); + equal("foo", err.message); + equal("no such intercept", err.status); + ok(err instanceof error.WebDriverError); +}); + +add_task(function test_NoSuchNodeError() { + let err = new error.NoSuchNodeError("foo"); + equal("NoSuchNodeError", err.name); + equal("foo", err.message); + equal("no such node", err.status); + ok(err instanceof error.WebDriverError); +}); + +add_task(function test_NoSuchRequestError() { + let err = new error.NoSuchRequestError("foo"); + equal("NoSuchRequestError", err.name); + equal("foo", err.message); + equal("no such request", err.status); + ok(err instanceof error.WebDriverError); +}); + +add_task(function test_NoSuchScriptError() { + let err = new error.NoSuchScriptError("foo"); + equal("NoSuchScriptError", err.name); + equal("foo", err.message); + equal("no such script", err.status); + ok(err instanceof error.WebDriverError); +}); + +add_task(function test_NoSuchShadowRootError() { + let err = new error.NoSuchShadowRootError("foo"); + equal("NoSuchShadowRootError", err.name); + equal("foo", err.message); + equal("no such shadow root", err.status); + ok(err instanceof error.WebDriverError); +}); + +add_task(function test_NoSuchUserContextError() { + let err = new error.NoSuchUserContextError("foo"); + equal("NoSuchUserContextError", err.name); + equal("foo", err.message); + equal("no such user context", err.status); + ok(err instanceof error.WebDriverError); +}); + +add_task(function test_NoSuchWindowError() { + let err = new error.NoSuchWindowError("foo"); + equal("NoSuchWindowError", err.name); + equal("foo", err.message); + equal("no such window", err.status); + ok(err instanceof error.WebDriverError); +}); + +add_task(function test_ScriptTimeoutError() { + let err = new error.ScriptTimeoutError("foo"); + equal("ScriptTimeoutError", err.name); + equal("foo", err.message); + equal("script timeout", err.status); + ok(err instanceof error.WebDriverError); +}); + +add_task(function test_SessionNotCreatedError() { + let err = new error.SessionNotCreatedError("foo"); + equal("SessionNotCreatedError", err.name); + equal("foo", err.message); + equal("session not created", err.status); + ok(err instanceof error.WebDriverError); +}); + +add_task(function test_StaleElementReferenceError() { + let err = new error.StaleElementReferenceError("foo"); + equal("StaleElementReferenceError", err.name); + equal("foo", err.message); + equal("stale element reference", err.status); + ok(err instanceof error.WebDriverError); +}); + +add_task(function test_TimeoutError() { + let err = new error.TimeoutError("foo"); + equal("TimeoutError", err.name); + equal("foo", err.message); + equal("timeout", err.status); + ok(err instanceof error.WebDriverError); +}); + +add_task(function test_UnableToSetCookieError() { + let err = new error.UnableToSetCookieError("foo"); + equal("UnableToSetCookieError", err.name); + equal("foo", err.message); + equal("unable to set cookie", err.status); + ok(err instanceof error.WebDriverError); +}); + +add_task(function test_UnexpectedAlertOpenError() { + let err = new error.UnexpectedAlertOpenError("foo"); + equal("UnexpectedAlertOpenError", err.name); + equal("foo", err.message); + equal("unexpected alert open", err.status); + ok(err instanceof error.WebDriverError); +}); + +add_task(function test_UnknownCommandError() { + let err = new error.UnknownCommandError("foo"); + equal("UnknownCommandError", err.name); + equal("foo", err.message); + equal("unknown command", err.status); + ok(err instanceof error.WebDriverError); +}); + +add_task(function test_UnknownError() { + let err = new error.UnknownError("foo"); + equal("UnknownError", err.name); + equal("foo", err.message); + equal("unknown error", err.status); + ok(err instanceof error.WebDriverError); +}); + +add_task(function test_UnsupportedOperationError() { + let err = new error.UnsupportedOperationError("foo"); + equal("UnsupportedOperationError", err.name); + equal("foo", err.message); + equal("unsupported operation", err.status); + ok(err instanceof error.WebDriverError); +}); diff --git a/remote/shared/webdriver/test/xpcshell/test_NodeCache.js b/remote/shared/webdriver/test/xpcshell/test_NodeCache.js new file mode 100644 index 0000000000..4efe9fba3a --- /dev/null +++ b/remote/shared/webdriver/test/xpcshell/test_NodeCache.js @@ -0,0 +1,265 @@ +const { NodeCache } = ChromeUtils.importESModule( + "chrome://remote/content/shared/webdriver/NodeCache.sys.mjs" +); + +function setupTest() { + const browser = Services.appShell.createWindowlessBrowser(false); + + browser.document.body.innerHTML = ` + <div id="foo" style="margin: 50px"> + <iframe></iframe> + <video></video> + <svg xmlns="http://www.w3.org/2000/svg"></svg> + <textarea></textarea> + </div> + <div id="with-comment"><!-- Comment --></div> + `; + + const divEl = browser.document.querySelector("div"); + const svgEl = browser.document.querySelector("svg"); + const textareaEl = browser.document.querySelector("textarea"); + const videoEl = browser.document.querySelector("video"); + + const iframeEl = browser.document.querySelector("iframe"); + const childEl = iframeEl.contentDocument.createElement("div"); + iframeEl.contentDocument.body.appendChild(childEl); + + const shadowRoot = videoEl.openOrClosedShadowRoot; + + return { + browser, + nodeCache: new NodeCache(), + childEl, + divEl, + iframeEl, + shadowRoot, + seenNodeIds: new Map(), + svgEl, + textareaEl, + videoEl, + }; +} + +add_task(function getOrCreateNodeReference_invalid() { + const { nodeCache, seenNodeIds } = setupTest(); + + const invalidValues = [null, undefined, "foo", 42, true, [], {}]; + + for (const value of invalidValues) { + info(`Testing value: ${value}`); + Assert.throws( + () => nodeCache.getOrCreateNodeReference(value, seenNodeIds), + /TypeError/ + ); + } +}); + +add_task(function getOrCreateNodeReference_supportedNodeTypes() { + const { browser, divEl, nodeCache, seenNodeIds } = setupTest(); + + // Bug 1820734: No ownerGlobal is available in XPCShell tests + // const xmlDocument = new DOMParser().parseFromString( + // "<xml></xml>", + // "application/xml" + // ); + + const values = [ + { node: divEl, type: Node.ELEMENT_NODE }, + { node: divEl.attributes[0], type: Node.ATTRIBUTE_NODE }, + { node: browser.document.createTextNode("foo"), type: Node.TEXT_NODE }, + // Bug 1820734: No ownerGlobal is available in XPCShell tests + // { + // node: xmlDocument.createCDATASection("foo"), + // type: Node.CDATA_SECTION_NODE, + // }, + { + node: browser.document.createProcessingInstruction( + "xml-stylesheet", + "href='foo.css'" + ), + type: Node.PROCESSING_INSTRUCTION_NODE_NODE, + }, + { node: browser.document.createComment("foo"), type: Node.COMMENT_NODE }, + { node: browser.document, type: Node.Document_NODE }, + { + node: browser.document.implementation.createDocumentType( + "foo", + "bar", + "dtd" + ), + type: Node.DOCUMENT_TYPE_NODE_NODE, + }, + { + node: browser.document.createDocumentFragment(), + type: Node.DOCUMENT_FRAGMENT_NODE, + }, + ]; + + values.forEach((value, index) => { + info(`Testing value: ${value.type}`); + const nodeRef = nodeCache.getOrCreateNodeReference(value.node, seenNodeIds); + equal(nodeCache.size, index + 1); + equal(typeof nodeRef, "string"); + ok(seenNodeIds.get(browser.browsingContext).includes(nodeRef)); + }); +}); + +add_task(function getOrCreateNodeReference_referenceAlreadyCreated() { + const { browser, divEl, nodeCache, seenNodeIds } = setupTest(); + + const divElRef = nodeCache.getOrCreateNodeReference(divEl, seenNodeIds); + const divElRefOther = nodeCache.getOrCreateNodeReference(divEl, seenNodeIds); + + equal(divElRefOther, divElRef); + equal(nodeCache.size, 1); + equal(seenNodeIds.size, 1); + ok(seenNodeIds.get(browser.browsingContext).includes(divElRef)); +}); + +add_task(function getOrCreateNodeReference_differentReference() { + const { browser, divEl, nodeCache, seenNodeIds, shadowRoot } = setupTest(); + + const divElRef = nodeCache.getOrCreateNodeReference(divEl, seenNodeIds); + equal(nodeCache.size, 1); + equal(seenNodeIds.size, 1); + ok(seenNodeIds.get(browser.browsingContext).includes(divElRef)); + + const shadowRootRef = nodeCache.getOrCreateNodeReference( + shadowRoot, + seenNodeIds + ); + equal(nodeCache.size, 2); + equal(seenNodeIds.size, 1); + ok(seenNodeIds.get(browser.browsingContext).includes(divElRef)); + ok(seenNodeIds.get(browser.browsingContext).includes(shadowRootRef)); + + notEqual(divElRef, shadowRootRef); +}); + +add_task(function getOrCreateNodeReference_differentReferencePerNodeCache() { + const { browser, divEl, nodeCache, seenNodeIds } = setupTest(); + const nodeCache2 = new NodeCache(); + + const divElRef1 = nodeCache.getOrCreateNodeReference(divEl, seenNodeIds); + const divElRef2 = nodeCache2.getOrCreateNodeReference(divEl, seenNodeIds); + + notEqual(divElRef1, divElRef2); + equal( + nodeCache.getNode(browser.browsingContext, divElRef1), + nodeCache2.getNode(browser.browsingContext, divElRef2) + ); + + equal(seenNodeIds.size, 1); + ok(seenNodeIds.get(browser.browsingContext).includes(divElRef1)); + ok(seenNodeIds.get(browser.browsingContext).includes(divElRef2)); + + equal(nodeCache.getNode(browser.browsingContext, divElRef2), null); +}); + +add_task(function clear() { + const { browser, divEl, nodeCache, seenNodeIds, svgEl } = setupTest(); + + nodeCache.getOrCreateNodeReference(divEl, seenNodeIds); + nodeCache.getOrCreateNodeReference(svgEl, seenNodeIds); + equal(nodeCache.size, 2); + equal(seenNodeIds.size, 1); + + // Clear requires explicit arguments. + Assert.throws(() => nodeCache.clear(), /Error/); + + // Clear references for a different browsing context + const browser2 = Services.appShell.createWindowlessBrowser(false); + const imgEl = browser2.document.createElement("img"); + const imgElRef = nodeCache.getOrCreateNodeReference(imgEl, seenNodeIds); + equal(nodeCache.size, 3); + equal(seenNodeIds.size, 2); + + nodeCache.clear({ browsingContext: browser.browsingContext }); + equal(nodeCache.size, 1); + equal(nodeCache.getNode(browser2.browsingContext, imgElRef), imgEl); + + // Clear all references + nodeCache.getOrCreateNodeReference(divEl, seenNodeIds); + equal(nodeCache.size, 2); + equal(seenNodeIds.size, 2); + + nodeCache.clear({ all: true }); + equal(nodeCache.size, 0); +}); + +add_task(function getNode_multiple_nodes() { + const { browser, divEl, nodeCache, seenNodeIds, svgEl } = setupTest(); + + const divElRef = nodeCache.getOrCreateNodeReference(divEl, seenNodeIds); + const svgElRef = nodeCache.getOrCreateNodeReference(svgEl, seenNodeIds); + + equal(nodeCache.getNode(browser.browsingContext, svgElRef), svgEl); + equal(nodeCache.getNode(browser.browsingContext, divElRef), divEl); +}); + +add_task(function getNode_differentBrowsingContextInSameGroup() { + const { iframeEl, divEl, nodeCache, seenNodeIds } = setupTest(); + + const divElRef = nodeCache.getOrCreateNodeReference(divEl, seenNodeIds); + equal(nodeCache.size, 1); + + equal( + nodeCache.getNode(iframeEl.contentWindow.browsingContext, divElRef), + divEl + ); +}); + +add_task(function getNode_differentBrowsingContextInOtherGroup() { + const { divEl, nodeCache, seenNodeIds } = setupTest(); + + const divElRef = nodeCache.getOrCreateNodeReference(divEl, seenNodeIds); + equal(nodeCache.size, 1); + + const browser2 = Services.appShell.createWindowlessBrowser(false); + equal(nodeCache.getNode(browser2.browsingContext, divElRef), null); +}); + +add_task(async function getNode_nodeDeleted() { + const { browser, nodeCache, seenNodeIds } = setupTest(); + let el = browser.document.createElement("div"); + + const elRef = nodeCache.getOrCreateNodeReference(el, seenNodeIds); + + // Delete element and force a garbage collection + el = null; + + await doGC(); + + equal(nodeCache.getNode(browser.browsingContext, elRef), null); +}); + +add_task(function getNodeDetails_forTopBrowsingContext() { + const { browser, divEl, nodeCache, seenNodeIds } = setupTest(); + + const divElRef = nodeCache.getOrCreateNodeReference(divEl, seenNodeIds); + + const nodeDetails = nodeCache.getReferenceDetails(divElRef); + equal(nodeDetails.browserId, browser.browsingContext.browserId); + equal(nodeDetails.browsingContextGroupId, browser.browsingContext.group.id); + equal(nodeDetails.browsingContextId, browser.browsingContext.id); + ok(nodeDetails.isTopBrowsingContext); + ok(nodeDetails.nodeWeakRef); + equal(nodeDetails.nodeWeakRef.get(), divEl); +}); + +add_task(async function getNodeDetails_forChildBrowsingContext() { + const { browser, iframeEl, childEl, nodeCache, seenNodeIds } = setupTest(); + + const childElRef = nodeCache.getOrCreateNodeReference(childEl, seenNodeIds); + + const nodeDetails = nodeCache.getReferenceDetails(childElRef); + equal(nodeDetails.browserId, browser.browsingContext.browserId); + equal(nodeDetails.browsingContextGroupId, browser.browsingContext.group.id); + equal( + nodeDetails.browsingContextId, + iframeEl.contentWindow.browsingContext.id + ); + ok(!nodeDetails.isTopBrowsingContext); + ok(nodeDetails.nodeWeakRef); + equal(nodeDetails.nodeWeakRef.get(), childEl); +}); diff --git a/remote/shared/webdriver/test/xpcshell/test_Session.js b/remote/shared/webdriver/test/xpcshell/test_Session.js new file mode 100644 index 0000000000..3b3d893319 --- /dev/null +++ b/remote/shared/webdriver/test/xpcshell/test_Session.js @@ -0,0 +1,72 @@ +/* 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 { Capabilities, Timeouts } = ChromeUtils.importESModule( + "chrome://remote/content/shared/webdriver/Capabilities.sys.mjs" +); +const { getWebDriverSessionById, WebDriverSession } = + ChromeUtils.importESModule( + "chrome://remote/content/shared/webdriver/Session.sys.mjs" + ); + +add_task(function test_WebDriverSession_ctor() { + const session = new WebDriverSession(); + + equal(typeof session.id, "string"); + ok(session.capabilities instanceof Capabilities); +}); + +add_task(function test_WebDriverSession_destroy() { + const session = new WebDriverSession(); + + session.destroy(); +}); + +add_task(function test_WebDriverSession_getters() { + const session = new WebDriverSession(); + + equal( + session.a11yChecks, + session.capabilities.get("moz:accessibilityChecks") + ); + equal(session.pageLoadStrategy, session.capabilities.get("pageLoadStrategy")); + equal(session.proxy, session.capabilities.get("proxy")); + equal( + session.strictFileInteractability, + session.capabilities.get("strictFileInteractability") + ); + equal(session.timeouts, session.capabilities.get("timeouts")); + equal( + session.unhandledPromptBehavior, + session.capabilities.get("unhandledPromptBehavior") + ); +}); + +add_task(function test_WebDriverSession_setters() { + const session = new WebDriverSession(); + + const timeouts = new Timeouts(); + timeouts.pageLoad = 45; + + session.timeouts = timeouts; + equal(session.timeouts, session.capabilities.get("timeouts")); +}); + +add_task(function test_getWebDriverSessionById() { + const session1 = new WebDriverSession(); + const session2 = new WebDriverSession(); + + equal(getWebDriverSessionById(session1.id), session1); + equal(getWebDriverSessionById(session2.id), session2); + + session1.destroy(); + equal(getWebDriverSessionById(session1.id), undefined); + equal(getWebDriverSessionById(session2.id), session2); + + session2.destroy(); + equal(getWebDriverSessionById(session1.id), undefined); + equal(getWebDriverSessionById(session2.id), undefined); +}); diff --git a/remote/shared/webdriver/test/xpcshell/test_URLPattern_invalid.js b/remote/shared/webdriver/test/xpcshell/test_URLPattern_invalid.js new file mode 100644 index 0000000000..0e537a210f --- /dev/null +++ b/remote/shared/webdriver/test/xpcshell/test_URLPattern_invalid.js @@ -0,0 +1,129 @@ +/* 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 { parseURLPattern } = ChromeUtils.importESModule( + "chrome://remote/content/shared/webdriver/URLPattern.sys.mjs" +); + +add_task( + async function test_parseURLPattern_patternPattern_unescapedCharacters() { + const properties = ["protocol", "hostname", "port", "pathname", "search"]; + const values = ["*", "(", ")", "{", "}"]; + for (const property of properties) { + for (const value of values) { + Assert.throws( + () => parseURLPattern({ type: "pattern", [property]: value }), + /InvalidArgumentError/ + ); + } + } + } +); + +add_task(async function test_parseURLPattern_patternPattern_protocol() { + const values = [ + "", + "http/", + "http\\*", + "http\\(", + "http\\)", + "http\\{", + "http\\}", + "http#", + "http@", + "http%", + ]; + for (const value of values) { + Assert.throws( + () => parseURLPattern({ type: "pattern", protocol: value }), + /InvalidArgumentError/ + ); + } +}); + +add_task( + async function test_parseURLPattern_patternPattern_unsupported_protocol() { + const values = ["ftp", "abc", "webpack"]; + for (const value of values) { + Assert.throws( + () => parseURLPattern({ type: "pattern", protocol: value }), + /UnsupportedOperationError/ + ); + } + } +); + +add_task(async function test_parseURLPattern_patternPattern_hostname() { + const values = ["", "abc/com/", "abc?com", "abc#com", "abc:com"]; + for (const value of values) { + Assert.throws( + () => parseURLPattern({ type: "pattern", hostname: value }), + /InvalidArgumentError/ + ); + } +}); + +add_task(async function test_parseURLPattern_patternPattern_port() { + const values = ["", "abcd", "-1", "80 ", "1.3", ":80", "65536"]; + for (const value of values) { + Assert.throws( + () => parseURLPattern({ type: "pattern", port: value }), + /InvalidArgumentError/ + ); + } +}); + +add_task(async function test_parseURLPattern_patternPattern_pathname() { + const values = ["path?", "path#"]; + for (const value of values) { + Assert.throws( + () => parseURLPattern({ type: "pattern", pathname: value }), + /InvalidArgumentError/ + ); + } +}); + +add_task(async function test_parseURLPattern_patternPattern_search() { + const values = ["search#"]; + for (const value of values) { + Assert.throws( + () => parseURLPattern({ type: "pattern", search: value }), + /InvalidArgumentError/ + ); + } +}); + +add_task(async function test_parseURLPattern_stringPattern_invalid_url() { + const values = ["", "invalid", "http:invalid:url", "[1::", "127.0..1"]; + for (const value of values) { + Assert.throws( + () => parseURLPattern({ type: "string", pattern: value }), + /InvalidArgumentError/ + ); + } +}); + +add_task( + async function test_parseURLPattern_stringPattern_unescaped_characters() { + const values = ["*", "(", ")", "{", "}"]; + for (const value of values) { + Assert.throws( + () => parseURLPattern({ type: "string", pattern: value }), + /InvalidArgumentError/ + ); + } + } +); + +add_task( + async function test_parseURLPattern_stringPattern_unsupported_protocol() { + const values = ["ftp://some/path", "abc:pathplaceholder", "webpack://test"]; + for (const value of values) { + Assert.throws( + () => parseURLPattern({ type: "string", pattern: value }), + /UnsupportedOperationError/ + ); + } + } +); diff --git a/remote/shared/webdriver/test/xpcshell/test_URLPattern_matchURLPattern.js b/remote/shared/webdriver/test/xpcshell/test_URLPattern_matchURLPattern.js new file mode 100644 index 0000000000..f4831d583f --- /dev/null +++ b/remote/shared/webdriver/test/xpcshell/test_URLPattern_matchURLPattern.js @@ -0,0 +1,607 @@ +/* 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 { matchURLPattern, parseURLPattern } = ChromeUtils.importESModule( + "chrome://remote/content/shared/webdriver/URLPattern.sys.mjs" +); + +// Test several variations which should match a string based http://example.com +// pattern. +add_task(async function test_matchURLPattern_url_variations() { + const pattern = parseURLPattern({ + type: "string", + pattern: "http://example.com", + }); + + const urls = [ + "http://example.com", + "http://EXAMPLE.com", + "http://user:password@example.com", + "http://example.com:80", + "http://example.com/", + "http://example.com/#some-hash", + "http:example.com", + "http:/example.com", + "http://example.com?", + "http://example.com/?", + ]; + for (const url of urls) { + ok( + matchURLPattern(pattern, url), + `url "${url}" should match pattern "http://example.com"` + ); + } + + // Test URLs close to http://example.com but which should not match. + const failingUrls = [ + "https://example.com", + "http://example.com:88", + "http://example.com/a", + "http://example.com/?abc", + ]; + for (const url of failingUrls) { + ok( + !matchURLPattern(pattern, url), + `url "${url}" should not match pattern "http://example.com"` + ); + } +}); + +add_task(async function test_matchURLPattern_stringPatterns() { + const tests = [ + { + pattern: "http://example.com", + url: "http://example.com", + match: true, + }, + { + pattern: "HTTP://example.com:80", + url: "http://example.com", + match: true, + }, + { + pattern: "http://example.com:80", + url: "http://example.com", + match: true, + }, + { + pattern: "http://example.com/path", + url: "http://example.com/path", + match: true, + }, + { + pattern: "http://example.com/PATH_CASE", + url: "http://example.com/path_case", + match: false, + }, + { + pattern: "http://example.com/path_single_segment", + url: "http://example.com/path_single_segment/", + match: false, + }, + { + pattern: "http://example.com/path", + url: "http://example.com/path_continued", + match: false, + }, + { + pattern: "http://example.com/path_two_segments/", + url: "http://example.com/path_two_segments/", + match: true, + }, + { + pattern: "http://example.com/path_two_segments/", + url: "http://example.com/path_two_segments", + match: false, + }, + { + pattern: "http://example.com/emptysearch?", + url: "http://example.com/emptysearch?", + match: true, + }, + { + pattern: "http://example.com/emptysearch?", + url: "http://example.com/emptysearch", + match: true, + }, + { + pattern: "http://example.com/emptysearch?", + url: "http://example.com/emptysearch??", + match: false, + }, + { + pattern: "http://example.com/emptysearch?", + url: "http://example.com/emptysearch?a", + match: false, + }, + { + pattern: "http://example.com/search?param", + url: "http://example.com/search?param", + match: true, + }, + { + pattern: "http://example.com/search?param", + url: "http://example.com/search?param=value", + match: false, + }, + { + pattern: "http://example.com/search?param=value", + url: "http://example.com/search?param=value", + match: true, + }, + { + pattern: "http://example.com/search?a=b&c=d", + url: "http://example.com/search?a=b&c=d", + match: true, + }, + { + pattern: "http://example.com/search?a=b&c=d", + url: "http://example.com/search?c=d&a=b", + match: false, + }, + { + pattern: "http://example.com/search?param", + url: "http://example.com/search?param#ref", + match: true, + }, + { + pattern: "http://example.com/search?param#ref", + url: "http://example.com/search?param#ref", + match: true, + }, + { + pattern: "http://example.com/search?param#ref", + url: "http://example.com/search?param", + match: true, + }, + { + pattern: "http://example.com/search?param", + url: "http://example.com/search?parameter", + match: false, + }, + { + pattern: "http://example.com/search?parameter", + url: "http://example.com/search?param", + match: false, + }, + { + pattern: "https://example.com:80", + url: "https://example.com", + match: false, + }, + { + pattern: "https://example.com:443", + url: "https://example.com", + match: true, + }, + { + pattern: "ws://example.com", + url: "ws://example.com:80", + match: true, + }, + ]; + + runMatchPatternTests(tests, "string"); +}); + +add_task(async function test_patternPatterns_no_property() { + const tests = [ + // Test protocol + { + pattern: {}, + url: "https://example.com", + match: true, + }, + { + pattern: {}, + url: "https://example.com", + match: true, + }, + { + pattern: {}, + url: "https://example.com:1234", + match: true, + }, + { + pattern: {}, + url: "https://example.com/a", + match: true, + }, + { + pattern: {}, + url: "https://example.com/a?test", + match: true, + }, + ]; + + runMatchPatternTests(tests, "pattern"); +}); + +add_task(async function test_patternPatterns_protocol() { + const tests = [ + // Test protocol + { + pattern: { + protocol: "http", + }, + url: "http://example.com", + match: true, + }, + { + pattern: { + protocol: "HTTP", + }, + url: "http://example.com", + match: true, + }, + { + pattern: { + protocol: "http", + }, + url: "http://example.com:80", + match: true, + }, + { + pattern: { + protocol: "http", + }, + url: "http://example.com:1234", + match: true, + }, + { + pattern: { + protocol: "http", + port: "80", + }, + url: "http://example.com:80", + match: true, + }, + { + pattern: { + protocol: "http", + port: "1234", + }, + url: "http://example.com:1234", + match: true, + }, + { + pattern: { + protocol: "http", + port: "1234", + }, + url: "http://example.com", + match: false, + }, + { + pattern: { + protocol: "http", + }, + url: "https://wrong-scheme.com", + match: false, + }, + { + pattern: { + protocol: "http", + }, + url: "http://whatever.com/?search#ref", + match: true, + }, + { + pattern: { + protocol: "http", + }, + url: "http://example.com/a", + match: true, + }, + { + pattern: { + protocol: "http", + }, + url: "http://whatever.com/path?search#ref", + match: true, + }, + ]; + + runMatchPatternTests(tests, "pattern"); +}); + +add_task(async function test_patternPatterns_port() { + const tests = [ + { + pattern: { + protocol: "http", + port: "80", + }, + url: "http://abc.com/", + match: true, + }, + { + pattern: { + port: "1234", + }, + url: "http://a.com:1234", + match: true, + }, + { + pattern: { + port: "1234", + }, + url: "https://a.com:1234", + match: true, + }, + ]; + + runMatchPatternTests(tests, "pattern"); +}); + +add_task(async function test_patternPatterns_hostname() { + const tests = [ + { + pattern: { + hostname: "example.com", + }, + url: "http://example.com", + match: true, + }, + { + pattern: { + hostname: "example.com", + }, + url: "http://example.com:80", + match: true, + }, + { + pattern: { + hostname: "example.com", + }, + url: "https://example.com", + match: true, + }, + { + pattern: { + hostname: "example.com", + }, + url: "https://example.com:443", + match: true, + }, + { + pattern: { + hostname: "example.com", + }, + url: "ws://example.com", + match: true, + }, + { + pattern: { + hostname: "example.com", + }, + url: "ws://example.com:80", + match: true, + }, + { + pattern: { + hostname: "example.com", + }, + url: "http://example.com/path", + match: true, + }, + { + pattern: { + hostname: "example.com", + }, + url: "http://example.com/?search", + match: true, + }, + { + pattern: { + hostname: "example\\{.com", + }, + url: "http://example{.com/", + match: true, + }, + { + pattern: { + hostname: "example\\{.com", + }, + url: "http://example\\{.com/", + match: false, + }, + { + pattern: { + hostname: "127.0.0.1", + }, + url: "http://127.0.0.1/", + match: true, + }, + { + pattern: { + hostname: "127.0.0.1", + }, + url: "http://127.0.0.2/", + match: false, + }, + { + pattern: { + hostname: "[2001:db8::1]", + }, + url: "http://[2001:db8::1]/", + match: true, + }, + { + pattern: { + hostname: "[::AB:1]", + }, + url: "http://[::ab:1]/", + match: true, + }, + ]; + + runMatchPatternTests(tests, "pattern"); +}); + +add_task(async function test_patternPatterns_pathname() { + const tests = [ + { + pattern: { + pathname: "/", + }, + url: "http://example.com", + match: true, + }, + { + pattern: { + pathname: "/", + }, + url: "http://example.com/", + match: true, + }, + { + pattern: { + pathname: "/", + }, + url: "http://example.com/?", + match: true, + }, + { + pattern: { + pathname: "path", + }, + url: "http://example.com/path", + match: true, + }, + { + pattern: { + pathname: "/path", + }, + url: "http://example.com/path", + match: true, + }, + { + pattern: { + pathname: "path", + }, + url: "http://example.com/path/", + match: false, + }, + { + pattern: { + pathname: "path", + }, + url: "http://example.com/path_continued", + match: false, + }, + { + pattern: { + pathname: "/", + }, + url: "http://example.com/path", + match: false, + }, + ]; + + runMatchPatternTests(tests, "pattern"); +}); + +add_task(async function test_patternPatterns_search() { + const tests = [ + { + pattern: { + search: "", + }, + url: "http://example.com/?", + match: true, + }, + { + pattern: { + search: "", + }, + url: "http://example.com/", + match: true, + }, + { + pattern: { + search: "", + }, + url: "http://example.com/?#", + match: true, + }, + { + pattern: { + search: "?", + }, + url: "http://example.com/?", + match: true, + }, + { + pattern: { + search: "?a", + }, + url: "http://example.com/?a", + match: true, + }, + { + pattern: { + search: "?", + }, + url: "http://example.com/??", + match: false, + }, + { + pattern: { + search: "query", + }, + url: "http://example.com/?query", + match: true, + }, + { + pattern: { + search: "?query", + }, + url: "http://example.com/?query", + match: true, + }, + { + pattern: { + search: "query=value", + }, + url: "http://example.com/?query=value", + match: true, + }, + { + pattern: { + search: "query", + }, + url: "http://example.com/?query=value", + match: false, + }, + { + pattern: { + search: "query", + }, + url: "http://example.com/?query#value", + match: true, + }, + ]; + + runMatchPatternTests(tests, "pattern"); +}); + +function runMatchPatternTests(tests, type) { + for (const test of tests) { + let pattern; + if (type == "pattern") { + pattern = parseURLPattern({ type: "pattern", ...test.pattern }); + } else { + pattern = parseURLPattern({ type: "string", pattern: test.pattern }); + } + + equal( + matchURLPattern(pattern, test.url), + test.match, + `url "${test.url}" ${ + test.match ? "should" : "should not" + } match pattern ${JSON.stringify(test.pattern)}` + ); + } +} diff --git a/remote/shared/webdriver/test/xpcshell/test_URLPattern_parseURLPattern.js b/remote/shared/webdriver/test/xpcshell/test_URLPattern_parseURLPattern.js new file mode 100644 index 0000000000..d4bf3c5fdf --- /dev/null +++ b/remote/shared/webdriver/test/xpcshell/test_URLPattern_parseURLPattern.js @@ -0,0 +1,369 @@ +/* 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 { parseURLPattern } = ChromeUtils.importESModule( + "chrome://remote/content/shared/webdriver/URLPattern.sys.mjs" +); + +add_task(async function test_parseURLPattern_stringPatterns() { + const STRING_PATTERN_TESTS = [ + { + input: "http://example.com", + protocol: "http", + hostname: "example.com", + port: "", + pathname: "/", + search: "", + }, + { + input: "http://example.com/", + protocol: "http", + hostname: "example.com", + port: "", + pathname: "/", + search: "", + }, + { + input: "http://EXAMPLE.com", + protocol: "http", + hostname: "example.com", + port: "", + pathname: "/", + search: "", + }, + { + input: "http://example%2Ecom", + protocol: "http", + hostname: "example.com", + port: "", + pathname: "/", + search: "", + }, + + { + input: "http://example.com:80", + protocol: "http", + hostname: "example.com", + port: "", + pathname: "/", + search: "", + }, + { + input: "http://example.com:8888", + protocol: "http", + hostname: "example.com", + port: "8888", + pathname: "/", + search: "", + }, + { + input: "http://example.com/a////b", + protocol: "http", + hostname: "example.com", + port: "", + pathname: "/a////b", + search: "", + }, + { + input: "http://example.com/?", + protocol: "http", + hostname: "example.com", + port: "", + pathname: "/", + search: "", + }, + { + input: "http://example.com/??", + protocol: "http", + hostname: "example.com", + port: "", + pathname: "/", + search: "?", + }, + { + input: "http://example.com/?/", + protocol: "http", + hostname: "example.com", + port: "", + pathname: "/", + search: "/", + }, + { + input: "file:///testfolder/test.zip", + protocol: "file", + hostname: "", + port: null, + pathname: "/testfolder/test.zip", + search: "", + }, + { + input: "http://example\\{.com/", + protocol: "http", + hostname: "example{.com", + port: "", + pathname: "/", + search: "", + }, + { + input: "http://[2001:db8::1]/", + protocol: "http", + hostname: "[2001:db8::1]", + port: "", + pathname: "/", + search: "", + }, + { + input: "http://127.0.0.1/", + protocol: "http", + hostname: "127.0.0.1", + port: "", + pathname: "/", + search: "", + }, + ]; + + for (const test of STRING_PATTERN_TESTS) { + const pattern = parseURLPattern({ + type: "string", + pattern: test.input, + }); + + equal(pattern.protocol, "protocol" in test ? test.protocol : null); + equal(pattern.hostname, "hostname" in test ? test.hostname : null); + equal(pattern.port, "port" in test ? test.port : null); + equal(pattern.pathname, "pathname" in test ? test.pathname : null); + equal(pattern.search, "search" in test ? test.search : null); + } +}); + +add_task(async function test_parseURLPattern_patternPatterns() { + const PATTERN_PATTERN_TESTS = [ + { + pattern: { + protocol: "http", + }, + protocol: "http", + hostname: null, + port: null, + pathname: null, + search: null, + }, + { + pattern: { + protocol: "HTTP", + }, + protocol: "http", + hostname: null, + port: null, + pathname: null, + search: null, + }, + { + pattern: { + hostname: "example.com", + }, + protocol: null, + hostname: "example.com", + port: null, + pathname: null, + search: null, + }, + { + pattern: { + hostname: "EXAMPLE.com", + }, + protocol: null, + hostname: "example.com", + port: null, + pathname: null, + search: null, + }, + { + pattern: { + hostname: "127.0.0.1", + }, + protocol: null, + hostname: "127.0.0.1", + port: null, + pathname: null, + search: null, + }, + { + pattern: { + hostname: "[2001:db8::1]", + }, + protocol: null, + hostname: "[2001:db8::1]", + port: null, + pathname: null, + search: null, + }, + { + pattern: { + port: "80", + }, + protocol: null, + hostname: null, + port: "", + pathname: null, + search: null, + }, + { + pattern: { + port: "1234", + }, + protocol: null, + hostname: null, + port: "1234", + pathname: null, + search: null, + }, + { + pattern: { + pathname: "path/to", + }, + protocol: null, + hostname: null, + port: null, + pathname: "/path/to", + search: null, + }, + { + pattern: { + pathname: "/path/to", + }, + protocol: null, + hostname: null, + port: null, + pathname: "/path/to", + search: null, + }, + { + pattern: { + pathname: "/path/to/", + }, + protocol: null, + hostname: null, + port: null, + pathname: "/path/to/", + search: null, + }, + { + pattern: { + search: "?search", + }, + protocol: null, + hostname: null, + port: null, + pathname: null, + search: "search", + }, + { + pattern: { + search: "search", + }, + protocol: null, + hostname: null, + port: null, + pathname: null, + search: "search", + }, + { + pattern: { + search: "?search=something", + }, + protocol: null, + hostname: null, + port: null, + pathname: null, + search: "search=something", + }, + { + pattern: { + search: "search=something", + }, + protocol: null, + hostname: null, + port: null, + pathname: null, + search: "search=something", + }, + ]; + + for (const test of PATTERN_PATTERN_TESTS) { + const pattern = parseURLPattern({ + type: "pattern", + ...test.pattern, + }); + + equal(pattern.protocol, "protocol" in test ? test.protocol : null); + equal(pattern.hostname, "hostname" in test ? test.hostname : null); + equal(pattern.port, "port" in test ? test.port : null); + equal(pattern.pathname, "pathname" in test ? test.pathname : null); + equal(pattern.search, "search" in test ? test.search : null); + } +}); + +add_task(async function test_parseURLPattern_invalid_type() { + const values = [null, undefined, 1, [], "string"]; + for (const value of values) { + Assert.throws(() => parseURLPattern(value), /InvalidArgumentError/); + } +}); + +add_task(async function test_parseURLPattern_invalid_type_type() { + const values = [null, undefined, 1, {}, []]; + for (const type of values) { + Assert.throws(() => parseURLPattern({ type }), /InvalidArgumentError/); + } +}); + +add_task(async function test_parseURLPattern_invalid_type_value() { + const values = ["", "unknownType"]; + for (const type of values) { + Assert.throws(() => parseURLPattern({ type }), /InvalidArgumentError/); + } +}); + +add_task(async function test_parseURLPattern_invalid_stringPatternType() { + const values = [null, undefined, 1, {}, []]; + for (const pattern of values) { + Assert.throws( + () => parseURLPattern({ type: "string", pattern }), + /InvalidArgumentError/ + ); + } +}); + +add_task(async function test_parseURLPattern_invalid_stringPattern() { + const values = [ + "foo", + "*", + "(", + ")", + "{", + "}", + "http\\{s\\}://example.com", + "https://example.com:port/", + ]; + for (const pattern of values) { + Assert.throws( + () => parseURLPattern({ type: "string", pattern }), + /InvalidArgumentError/ + ); + } +}); + +add_task(async function test_parseURLPattern_invalid_patternPattern_type() { + const properties = ["protocol", "hostname", "port", "pathname", "search"]; + const values = [false, 42, [], {}]; + for (const property of properties) { + for (const value of values) { + Assert.throws( + () => parseURLPattern({ type: "pattern", [property]: value }), + /InvalidArgumentError/ + ); + } + } +}); diff --git a/remote/shared/webdriver/test/xpcshell/xpcshell.toml b/remote/shared/webdriver/test/xpcshell/xpcshell.toml new file mode 100644 index 0000000000..1cdd1eb47c --- /dev/null +++ b/remote/shared/webdriver/test/xpcshell/xpcshell.toml @@ -0,0 +1,20 @@ +[DEFAULT] +head = "head.js" + +["test_Actions.js"] + +["test_Assert.js"] + +["test_Capabilities.js"] + +["test_Errors.js"] + +["test_NodeCache.js"] + +["test_Session.js"] + +["test_URLPattern_invalid.js"] + +["test_URLPattern_matchURLPattern.js"] + +["test_URLPattern_parseURLPattern.js"] |