diff options
Diffstat (limited to 'remote/shared/webdriver')
-rw-r--r-- | remote/shared/webdriver/Actions.sys.mjs | 57 | ||||
-rw-r--r-- | remote/shared/webdriver/Capabilities.sys.mjs | 6 | ||||
-rw-r--r-- | remote/shared/webdriver/Errors.sys.mjs | 17 | ||||
-rw-r--r-- | remote/shared/webdriver/Event.sys.mjs | 295 | ||||
-rw-r--r-- | remote/shared/webdriver/process-actors/WebDriverProcessDataChild.sys.mjs | 2 | ||||
-rw-r--r-- | remote/shared/webdriver/test/xpcshell/test_Errors.js | 9 |
6 files changed, 340 insertions, 46 deletions
diff --git a/remote/shared/webdriver/Actions.sys.mjs b/remote/shared/webdriver/Actions.sys.mjs index 4f5a41a421..2639c4dc9f 100644 --- a/remote/shared/webdriver/Actions.sys.mjs +++ b/remote/shared/webdriver/Actions.sys.mjs @@ -13,7 +13,7 @@ ChromeUtils.defineESModuleGetters(lazy, { clearTimeout: "resource://gre/modules/Timer.sys.mjs", dom: "chrome://remote/content/shared/DOM.sys.mjs", error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs", - event: "chrome://remote/content/marionette/event.sys.mjs", + event: "chrome://remote/content/shared/webdriver/Event.sys.mjs", keyData: "chrome://remote/content/shared/webdriver/KeyData.sys.mjs", Log: "chrome://remote/content/shared/Log.sys.mjs", pprint: "chrome://remote/content/shared/Format.sys.mjs", @@ -508,11 +508,8 @@ class Origin { * Viewport coordinates of the origin of this coordinate system. * * This is overridden in subclasses to provide a class-specific origin. - * - * @param {InputSource} inputSource - State of current input device. - * @param {WindowProxy} win - Current window global */ - getOriginCoordinates(inputSource, win) { + getOriginCoordinates() { throw new Error( `originCoordinates not defined for ${this.constructor.name}` ); @@ -559,13 +556,13 @@ class Origin { } class ViewportOrigin extends Origin { - getOriginCoordinates(inputSource, win) { + getOriginCoordinates() { return { x: 0, y: 0 }; } } class PointerOrigin extends Origin { - getOriginCoordinates(inputSource, win) { + getOriginCoordinates(inputSource) { return { x: inputSource.x, y: inputSource.y }; } } @@ -624,13 +621,9 @@ class Action { * This is overridden by subclasses to implement the type-specific * dispatch of the action. * - * @param {State} state - Actions state. - * @param {InputSource} inputSource - State of the current input device. - * @param {number} tickDuration - Length of the current tick, in ms. - * @param {WindowProxy} win - Current window global. * @returns {Promise} - Promise that is resolved once the action is complete. */ - dispatch(state, inputSource, tickDuration, win) { + dispatch() { throw new Error( `Action subclass ${this.constructor.name} must override dispatch()` ); @@ -708,10 +701,9 @@ class PauseAction extends NullAction { * @param {State} state - Actions state. * @param {InputSource} inputSource - State of the current input device. * @param {number} tickDuration - Length of the current tick, in ms. - * @param {WindowProxy} win - Current window global. * @returns {Promise} - Promise that is resolved once the action is complete. */ - dispatch(state, inputSource, tickDuration, win) { + dispatch(state, inputSource, tickDuration) { const ms = this.duration ?? tickDuration; lazy.logger.trace( @@ -1416,15 +1408,9 @@ class TouchActionGroup { * This is overridden by subclasses to implement the type-specific * dispatch of the action. * - * @param {State} state - Actions state. - * @param {null} inputSource - * This is always null; the argument only exists for compatibility - * with {@link Action.dispatch}. - * @param {number} tickDuration - Length of the current tick, in ms. - * @param {WindowProxy} win - Current window global. * @returns {Promise} - Promise that is resolved once the action is complete. */ - dispatch(state, inputSource, tickDuration, win) { + dispatch() { throw new Error( "TouchActionGroup subclass missing dispatch implementation" ); @@ -1622,7 +1608,7 @@ class PointerMoveTouchActionGroup extends TouchActionGroup { } ); const reachedTarget = perPointerData.every( - ([inputSource, action, target]) => + ([inputSource, , target]) => target[0] === inputSource.x && target[1] === inputSource.y ); @@ -1778,38 +1764,22 @@ class Pointer { /** * Implementation of depressing the pointer. - * - * @param {State} state - Actions state. - * @param {InputSource} inputSource - State of the current input device. - * @param {Action} action - The Action object invoking the pointer - * @param {WindowProxy} win - Current window global. */ - pointerDown(state, inputSource, action, win) { + pointerDown() { throw new Error(`Unimplemented pointerDown for pointerType ${this.type}`); } /** * Implementation of releasing the pointer. - * - * @param {State} state - Actions state. - * @param {InputSource} inputSource - State of the current input device. - * @param {Action} action - The Action object invoking the pointer - * @param {WindowProxy} win - Current window global. */ - pointerUp(state, inputSource, action, win) { + pointerUp() { throw new Error(`Unimplemented pointerUp for pointerType ${this.type}`); } /** * Implementation of moving the pointer. - * - * @param {State} state - Actions state. - * @param {InputSource} inputSource - State of the current input device. - * @param {number} targetX - Target X coordinate of the pointer move - * @param {number} targetY - Target Y coordinate of the pointer move - * @param {WindowProxy} win - Current window global. */ - pointerMove(state, inputSource, targetX, targetY, win) { + pointerMove() { throw new Error(`Unimplemented pointerMove for pointerType ${this.type}`); } @@ -2138,11 +2108,8 @@ class InputEventData { /** * Update the input data based on global and input state - * - * @param {State} state - Actions state. - * @param {InputSource} inputSource - State of the current input device. */ - update(state, inputSource) {} + update() {} toString() { return `${this.constructor.name} ${JSON.stringify(this)}`; diff --git a/remote/shared/webdriver/Capabilities.sys.mjs b/remote/shared/webdriver/Capabilities.sys.mjs index e3761315f2..3c30ea0789 100644 --- a/remote/shared/webdriver/Capabilities.sys.mjs +++ b/remote/shared/webdriver/Capabilities.sys.mjs @@ -445,6 +445,12 @@ export class Capabilities extends Map { ["timeouts", new Timeouts()], ["strictFileInteractability", false], ["unhandledPromptBehavior", UnhandledPromptBehavior.DismissAndNotify], + [ + "userAgent", + Cc["@mozilla.org/network/protocol;1?name=http"].getService( + Ci.nsIHttpProtocolHandler + ).userAgent, + ], ["webSocketUrl", null], // proprietary diff --git a/remote/shared/webdriver/Errors.sys.mjs b/remote/shared/webdriver/Errors.sys.mjs index 53b9d4426b..7060131075 100644 --- a/remote/shared/webdriver/Errors.sys.mjs +++ b/remote/shared/webdriver/Errors.sys.mjs @@ -41,6 +41,7 @@ const ERRORS = new Set([ "TimeoutError", "UnableToCaptureScreen", "UnableToSetCookieError", + "UnableToSetFileInputError", "UnexpectedAlertOpenError", "UnknownCommandError", "UnknownError", @@ -757,6 +758,21 @@ class UnableToSetCookieError extends WebDriverError { } /** + * A command to set a file could not be satisfied. + * + * @param {string=} message + * Optional string describing error situation. + * @param {object=} data + * Additional error data helpful in diagnosing the error. + */ +class UnableToSetFileInputError extends WebDriverError { + constructor(message, data = {}) { + super(message, data); + this.status = "unable to set file input"; + } +} + +/** * A command to capture a screenshot could not be satisfied. * * @param {string=} message @@ -865,6 +881,7 @@ const STATUSES = new Map([ ["timeout", TimeoutError], ["unable to capture screen", UnableToCaptureScreen], ["unable to set cookie", UnableToSetCookieError], + ["unable to set file input", UnableToSetFileInputError], ["unexpected alert open", UnexpectedAlertOpenError], ["unknown command", UnknownCommandError], ["unknown error", UnknownError], diff --git a/remote/shared/webdriver/Event.sys.mjs b/remote/shared/webdriver/Event.sys.mjs new file mode 100644 index 0000000000..2240f1f2b2 --- /dev/null +++ b/remote/shared/webdriver/Event.sys.mjs @@ -0,0 +1,295 @@ +/* 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/. */ + +/* eslint-disable no-restricted-globals */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + keyData: "chrome://remote/content/shared/webdriver/KeyData.sys.mjs", +}); + +/** Provides functionality for creating and sending DOM events. */ +export const event = {}; + +const _eventUtils = new WeakMap(); + +function _getEventUtils(win) { + if (!_eventUtils.has(win)) { + const eventUtilsObject = { + window: win, + parent: win, + _EU_Ci: Ci, + _EU_Cc: Cc, + }; + Services.scriptloader.loadSubScript( + "chrome://remote/content/external/EventUtils.js", + eventUtilsObject + ); + _eventUtils.set(win, eventUtilsObject); + } + return _eventUtils.get(win); +} + +event.MouseEvents = { + click: 0, + dblclick: 1, + mousedown: 2, + mouseup: 3, + mouseover: 4, + mouseout: 5, +}; + +event.Modifiers = { + shiftKey: 0, + ctrlKey: 1, + altKey: 2, + metaKey: 3, +}; + +event.MouseButton = { + isPrimary(button) { + return button === 0; + }, + isAuxiliary(button) { + return button === 1; + }, + isSecondary(button) { + return button === 2; + }, +}; + +/** + * Synthesise a mouse event at a point. + * + * If the type is specified in opts, an mouse event of that type is + * fired. Otherwise, a mousedown followed by a mouseup is performed. + * + * @param {number} left + * Offset from viewport left, in CSS pixels + * @param {number} top + * Offset from viewport top, in CSS pixels + * @param {object} opts + * Object which may contain the properties "shiftKey", "ctrlKey", + * "altKey", "metaKey", "accessKey", "clickCount", "button", and + * "type". + * @param {Window} win + * Window object. + * + * @returns {boolean} defaultPrevented + */ +event.synthesizeMouseAtPoint = function (left, top, opts, win) { + return _getEventUtils(win).synthesizeMouseAtPoint(left, top, opts, win); +}; + +/** + * Synthesise a touch event at a point. + * + * If the type is specified in opts, a touch event of that type is + * fired. Otherwise, a touchstart followed by a touchend is performed. + * + * @param {number} left + * Offset from viewport left, in CSS pixels + * @param {number} top + * Offset from viewport top, in CSS pixels + * @param {object} opts + * Object which may contain the properties "id", "rx", "ry", "angle", + * "force", "shiftKey", "ctrlKey", "altKey", "metaKey", "accessKey", + * "type". + * @param {Window} win + * Window object. + * + * @returns {boolean} defaultPrevented + */ +event.synthesizeTouchAtPoint = function (left, top, opts, win) { + return _getEventUtils(win).synthesizeTouchAtPoint(left, top, opts, win); +}; + +/** + * Synthesise a wheel scroll event at a point. + * + * @param {number} left + * Offset from viewport left, in CSS pixels + * @param {number} top + * Offset from viewport top, in CSS pixels + * @param {object} opts + * Object which may contain the properties "shiftKey", "ctrlKey", + * "altKey", "metaKey", "accessKey", "deltaX", "deltaY", "deltaZ", + * "deltaMode", "lineOrPageDeltaX", "lineOrPageDeltaY", "isMomentum", + * "isNoLineOrPageDelta", "isCustomizedByPrefs", "expectedOverflowDeltaX", + * "expectedOverflowDeltaY" + * @param {Window} win + * Window object. + */ +event.synthesizeWheelAtPoint = function (left, top, opts, win) { + const dpr = win.devicePixelRatio; + + // All delta properties expect the value in device pixels while the + // WebDriver specification uses CSS pixels. + if (typeof opts.deltaX !== "undefined") { + opts.deltaX *= dpr; + } + if (typeof opts.deltaY !== "undefined") { + opts.deltaY *= dpr; + } + if (typeof opts.deltaZ !== "undefined") { + opts.deltaZ *= dpr; + } + + return _getEventUtils(win).synthesizeWheelAtPoint(left, top, opts, win); +}; + +event.synthesizeMultiTouch = function (opts, win) { + const modifiers = _getEventUtils(win)._parseModifiers(opts); + win.windowUtils.sendTouchEvent( + opts.type, + opts.id, + opts.x, + opts.y, + opts.rx, + opts.ry, + opts.angle, + opts.force, + opts.tiltx, + opts.tilty, + opts.twist, + modifiers + ); +}; + +/** + * Synthesize a keydown event for a single key. + * + * @param {object} key + * Key data as returned by keyData.getData + * @param {Window} win + * Window object. + */ +event.sendKeyDown = function (key, win) { + event.sendSingleKey(key, win, "keydown"); +}; + +/** + * Synthesize a keyup event for a single key. + * + * @param {object} key + * Key data as returned by keyData.getData + * @param {Window} win + * Window object. + */ +event.sendKeyUp = function (key, win) { + event.sendSingleKey(key, win, "keyup"); +}; + +/** + * Synthesize a key event for a single key. + * + * @param {object} key + * Key data as returned by keyData.getData + * @param {Window} win + * Window object. + * @param {string=} type + * Event to emit. By default the full keydown/keypressed/keyup event + * sequence is emitted. + */ +event.sendSingleKey = function (key, win, type = null) { + let keyValue = key.key; + if (!key.printable) { + keyValue = `KEY_${keyValue}`; + } + const event = { + code: key.code, + location: key.location, + altKey: key.altKey ?? false, + shiftKey: key.shiftKey ?? false, + ctrlKey: key.ctrlKey ?? false, + metaKey: key.metaKey ?? false, + repeat: key.repeat ?? false, + }; + if (type) { + event.type = type; + } + _getEventUtils(win).synthesizeKey(keyValue, event, win); +}; + +/** + * Send a string as a series of keypresses. + * + * @param {string} keyString + * Sequence of characters to send as key presses + * @param {Window} win + * Window object + */ +event.sendKeys = function (keyString, win) { + const modifiers = {}; + for (let modifier in event.Modifiers) { + modifiers[modifier] = false; + } + + for (let keyValue of keyString) { + // keyValue will contain enough to represent the UTF-16 encoding of a single abstract character + // i.e. either a single scalar value, or a surrogate pair + if (modifiers.shiftKey) { + keyValue = lazy.keyData.getShiftedKey(keyValue); + } + const data = lazy.keyData.getData(keyValue); + const key = { ...data, ...modifiers }; + if (data.modifier) { + // Negating the state of the modifier here is not spec compliant but + // makes us compatible to Chrome's behavior for now. That's fine unless + // we know the correct behavior. + // + // @see: https://github.com/w3c/webdriver/issues/1734 + modifiers[data.modifier] = !modifiers[data.modifier]; + } + event.sendSingleKey(key, win); + } +}; + +event.sendEvent = function (eventType, el, modifiers = {}, opts = {}) { + opts.canBubble = opts.canBubble || true; + + let doc = el.ownerDocument || el.document; + let ev = doc.createEvent("Event"); + + ev.shiftKey = modifiers.shift; + ev.metaKey = modifiers.meta; + ev.altKey = modifiers.alt; + ev.ctrlKey = modifiers.ctrl; + + ev.initEvent(eventType, opts.canBubble, true); + el.dispatchEvent(ev); +}; + +event.mouseover = function (el, modifiers = {}, opts = {}) { + return event.sendEvent("mouseover", el, modifiers, opts); +}; + +event.mousemove = function (el, modifiers = {}, opts = {}) { + return event.sendEvent("mousemove", el, modifiers, opts); +}; + +event.mousedown = function (el, modifiers = {}, opts = {}) { + return event.sendEvent("mousedown", el, modifiers, opts); +}; + +event.mouseup = function (el, modifiers = {}, opts = {}) { + return event.sendEvent("mouseup", el, modifiers, opts); +}; + +event.cancel = function (el, modifiers = {}, opts = {}) { + return event.sendEvent("cancel", el, modifiers, opts); +}; + +event.click = function (el, modifiers = {}, opts = {}) { + return event.sendEvent("click", el, modifiers, opts); +}; + +event.change = function (el, modifiers = {}, opts = {}) { + return event.sendEvent("change", el, modifiers, opts); +}; + +event.input = function (el, modifiers = {}, opts = {}) { + return event.sendEvent("input", el, modifiers, opts); +}; diff --git a/remote/shared/webdriver/process-actors/WebDriverProcessDataChild.sys.mjs b/remote/shared/webdriver/process-actors/WebDriverProcessDataChild.sys.mjs index 39db9d939e..8101240abe 100644 --- a/remote/shared/webdriver/process-actors/WebDriverProcessDataChild.sys.mjs +++ b/remote/shared/webdriver/process-actors/WebDriverProcessDataChild.sys.mjs @@ -17,7 +17,7 @@ class BrowsingContextObserver { this.actor = actor; } - async observe(subject, topic, data) { + async observe(subject, topic) { if (topic === "browsing-context-discarded") { this.actor.cleanUp({ browsingContext: subject }); } diff --git a/remote/shared/webdriver/test/xpcshell/test_Errors.js b/remote/shared/webdriver/test/xpcshell/test_Errors.js index 22e3526039..d4803dee87 100644 --- a/remote/shared/webdriver/test/xpcshell/test_Errors.js +++ b/remote/shared/webdriver/test/xpcshell/test_Errors.js @@ -36,6 +36,7 @@ const errors = [ error.StaleElementReferenceError, error.TimeoutError, error.UnableToSetCookieError, + error.UnableToSetFileInputError, error.UnexpectedAlertOpenError, error.UnknownCommandError, error.UnknownError, @@ -510,6 +511,14 @@ add_task(function test_UnableToSetCookieError() { ok(err instanceof error.WebDriverError); }); +add_task(function test_UnableToSetFileInputError() { + let err = new error.UnableToSetFileInputError("foo"); + equal("UnableToSetFileInputError", err.name); + equal("foo", err.message); + equal("unable to set file input", err.status); + ok(err instanceof error.WebDriverError); +}); + add_task(function test_UnexpectedAlertOpenError() { let err = new error.UnexpectedAlertOpenError("foo"); equal("UnexpectedAlertOpenError", err.name); |