Backed out 5 changesets (bug 1915798, bug 1904665) for causing failures pointer_mouse_drag.py

Backed out changeset c163aae0b922 (bug 1915798)
Backed out changeset 27efecb55f94 (bug 1915798)
Backed out changeset 7670532062ec (bug 1904665)
Backed out changeset 669d9db4d064 (bug 1904665)
Backed out changeset a7ed7e623ec9 (bug 1904665)
This commit is contained in:
Noemi Erli 2024-09-06 20:58:14 +03:00
parent cf9be26274
commit a4273350cc
23 changed files with 716 additions and 2502 deletions

View File

@ -14,7 +14,6 @@ remote.jar:
# shared modules (all protocols)
content/shared/AppInfo.sys.mjs (shared/AppInfo.sys.mjs)
content/shared/AsyncQueue.sys.mjs (shared/AsyncQueue.sys.mjs)
content/shared/Browser.sys.mjs (shared/Browser.sys.mjs)
content/shared/Capture.sys.mjs (shared/Capture.sys.mjs)
content/shared/ChallengeHeaderParser.sys.mjs (shared/ChallengeHeaderParser.sys.mjs)

View File

@ -9,12 +9,11 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
accessibility:
"chrome://remote/content/shared/webdriver/Accessibility.sys.mjs",
assertInViewPort: "chrome://remote/content/shared/webdriver/Actions.sys.mjs",
action: "chrome://remote/content/shared/webdriver/Actions.sys.mjs",
atom: "chrome://remote/content/marionette/atom.sys.mjs",
dom: "chrome://remote/content/shared/DOM.sys.mjs",
error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
evaluate: "chrome://remote/content/marionette/evaluate.sys.mjs",
event: "chrome://remote/content/shared/webdriver/Event.sys.mjs",
interaction: "chrome://remote/content/marionette/interaction.sys.mjs",
json: "chrome://remote/content/marionette/json.sys.mjs",
Log: "chrome://remote/content/shared/Log.sys.mjs",
@ -38,6 +37,8 @@ export class MarionetteCommandsChild extends JSWindowActorChild {
// sandbox storage and name of the current sandbox
this.sandboxes = new lazy.Sandboxes(() => this.document.defaultView);
// State of the input actions. This is specific to contexts and sessions
this.actionState = null;
}
get innerWindowId() {
@ -58,61 +59,6 @@ export class MarionetteCommandsChild extends JSWindowActorChild {
);
}
#assertInViewPort(options = {}) {
const { target } = options;
return lazy.assertInViewPort(target, this.contentWindow);
}
#dispatchEvent(options = {}) {
const { eventName, details } = options;
const win = this.contentWindow;
switch (eventName) {
case "synthesizeKeyDown":
lazy.event.sendKeyDown(details.eventData, win);
break;
case "synthesizeKeyUp":
lazy.event.sendKeyUp(details.eventData, win);
break;
case "synthesizeMouseAtPoint":
lazy.event.synthesizeMouseAtPoint(
details.x,
details.y,
details.eventData,
win
);
break;
case "synthesizeMultiTouch":
lazy.event.synthesizeMultiTouch(details.eventData, win);
break;
case "synthesizeWheelAtPoint":
lazy.event.synthesizeWheelAtPoint(
details.x,
details.y,
details.eventData,
win
);
break;
default:
throw new Error(
`${eventName} is not a supported event dispatch method`
);
}
}
#endWheelTransaction() {
// Terminate the current wheel transaction if there is one. Wheel
// transactions should not live longer than a single action chain.
ChromeUtils.endWheelTransaction();
}
#getInViewCentrePoint(options) {
const { rect } = options;
return lazy.dom.getInViewCentrePoint(rect, this.contentWindow);
}
async receiveMessage(msg) {
if (!this.contentWindow) {
throw new DOMException("Actor is no longer active", "InactiveActor");
@ -131,19 +77,6 @@ export class MarionetteCommandsChild extends JSWindowActorChild {
);
switch (name) {
case "MarionetteCommandsParent:_assertInViewPort":
result = this.#assertInViewPort(data);
break;
case "MarionetteCommandsParent:_dispatchEvent":
this.#dispatchEvent(data);
waitForNextTick = true;
break;
case "MarionetteCommandsParent:_endWheelTransaction":
this.#endWheelTransaction();
break;
case "MarionetteCommandsParent:_getInViewCentrePoint":
result = this.#getInViewCentrePoint(data);
break;
case "MarionetteCommandsParent:clearElement":
this.clearElement(data);
waitForNextTick = true;
@ -207,6 +140,13 @@ export class MarionetteCommandsChild extends JSWindowActorChild {
case "MarionetteCommandsParent:isElementSelected":
result = await this.isElementSelected(data);
break;
case "MarionetteCommandsParent:performActions":
result = await this.performActions(data);
waitForNextTick = true;
break;
case "MarionetteCommandsParent:releaseActions":
result = await this.releaseActions();
break;
case "MarionetteCommandsParent:sendKeysToElement":
result = await this.sendKeysToElement(data);
waitForNextTick = true;
@ -532,6 +472,42 @@ export class MarionetteCommandsChild extends JSWindowActorChild {
);
}
/**
* Perform a series of grouped actions at the specified points in time.
*
* @param {object} options
* @param {object} options.actions
* Array of objects with each representing an action sequence.
* @param {object} options.capabilities
* Object with a list of WebDriver session capabilities.
*/
async performActions(options = {}) {
const { actions } = options;
if (this.actionState === null) {
this.actionState = new lazy.action.State();
}
let actionChain = lazy.action.Chain.fromJSON(this.actionState, actions);
await actionChain.dispatch(this.actionState, this.document.defaultView);
// Terminate the current wheel transaction if there is one. Wheel
// transactions should not live longer than a single action chain.
ChromeUtils.endWheelTransaction();
}
/**
* The release actions command is used to release all the keys and pointer
* buttons that are currently depressed. This causes events to be fired
* as if the state was released by an explicit series of actions. It also
* clears all the internal state of the virtual devices.
*/
async releaseActions() {
if (this.actionState === null) {
return;
}
await this.actionState.release(this.document.defaultView);
this.actionState = null;
}
/*
* Send key presses to element after focusing on it.
*/

View File

@ -5,14 +5,12 @@
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
action: "chrome://remote/content/shared/webdriver/Actions.sys.mjs",
capture: "chrome://remote/content/shared/Capture.sys.mjs",
error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
getSeenNodesForBrowsingContext:
"chrome://remote/content/shared/webdriver/Session.sys.mjs",
json: "chrome://remote/content/marionette/json.sys.mjs",
Log: "chrome://remote/content/shared/Log.sys.mjs",
WebElement: "chrome://remote/content/marionette/web-reference.sys.mjs",
});
ChromeUtils.defineLazyGetter(lazy, "logger", () =>
@ -24,146 +22,12 @@ ChromeUtils.defineLazyGetter(lazy, "logger", () =>
let webDriverSessionId = null;
export class MarionetteCommandsParent extends JSWindowActorParent {
#actionsOptions;
#actionState;
#deferredDialogOpened;
actorCreated() {
// The {@link Actions.State} of the input actions.
this.#actionState = null;
// Options for actions to pass through performActions and releaseActions.
this.#actionsOptions = {
// Callbacks as defined in the WebDriver specification.
getElementOrigin: this.#getElementOrigin.bind(this),
isElementOrigin: this.#isElementOrigin.bind(this),
// Custom properties and callbacks
context: this.browsingContext,
assertInViewPort: this.#assertInViewPort.bind(this),
dispatchEvent: this.#dispatchEvent.bind(this),
getClientRects: this.#getClientRects.bind(this),
getInViewCentrePoint: this.#getInViewCentrePoint.bind(this),
};
this.#deferredDialogOpened = null;
}
/**
* Assert that the target coordinates are within the visible viewport.
*
* @param {Array.<number>} target
* Coordinates [x, y] of the target relative to the viewport.
* @param {BrowsingContext} _context
* Unused in Marionette.
*
* @returns {Promise<undefined>}
* Promise that rejects, if the coordinates are not within
* the visible viewport.
*
* @throws {MoveTargetOutOfBoundsError}
* If target is outside the viewport.
*/
#assertInViewPort(target, _context) {
return this.sendQuery("MarionetteCommandsParent:_assertInViewPort", {
target,
});
}
/**
* Dispatch an event.
*
* @param {string} eventName
* Name of the event to be dispatched.
* @param {BrowsingContext} _context
* Unused in Marionette.
* @param {object} details
* Details of the event to be dispatched.
*
* @returns {Promise}
* Promise that resolves once the event is dispatched.
*/
#dispatchEvent(eventName, _context, details) {
return this.sendQuery("MarionetteCommandsParent:_dispatchEvent", {
eventName,
details,
});
}
/**
* Terminates the current wheel transaction.
*
* @returns {Promise}
* Promise that resolves when the transaction was terminated.
*/
#endWheelTransaction() {
return this.sendQuery("MarionetteCommandsParent:_endWheelTransaction");
}
/**
* Retrieves the WebElement reference of the origin.
*
* @param {ElementOrigin} origin
* Reference to the element origin of the action.
* @param {BrowsingContext} _context
* Unused in Marionette.
*
* @returns {WebElement}
* The WebElement reference.
*/
#getElementOrigin(origin, _context) {
return origin;
}
/**
* Retrieve the list of client rects for the element.
*
* @param {WebElement} element
* The web element reference to retrieve the rects from.
* @param {BrowsingContext} _context
* Unused in Marionette.
*
* @returns {Promise<Array<Map.<string, number>>>}
* Promise that resolves to a list of DOMRect-like objects.
*/
#getClientRects(element, _context) {
return this.executeScript("return arguments[0].getClientRects()", [
element,
]);
}
/**
* Retrieve the in-view center point for the rect and visible viewport.
*
* @param {DOMRect} rect
* Size and position of the rectangle to check.
* @param { BrowsingContext } _context
* Unused in Marionette.
*
* @returns {Promise<Map.<string, number>>}
* X and Y coordinates that denotes the in-view centre point of
* `rect`.
*/
#getInViewCentrePoint(rect, _context) {
return this.sendQuery("MarionetteCommandsParent:_getInViewCentrePoint", {
rect,
});
}
/**
* Checks if the given object is a valid element origin.
*
* @param {object} origin
* The object to check.
*
* @returns {boolean}
* True, if it's a WebElement.
*/
#isElementOrigin(origin) {
return lazy.WebElement.Identifier in origin;
}
async sendQuery(name, serializedValue) {
const seenNodes = lazy.getSeenNodesForBrowsingContext(
webDriverSessionId,
@ -379,44 +243,13 @@ export class MarionetteCommandsParent extends JSWindowActorParent {
}
async performActions(actions) {
// Bug 1821460: Use top-level browsing context.
if (this.#actionState === null) {
this.#actionState = new lazy.action.State();
}
const actionChain = await lazy.action.Chain.fromJSON(
this.#actionState,
return this.sendQuery("MarionetteCommandsParent:performActions", {
actions,
this.#actionsOptions
);
// Enqueue to serialize access to input state.
await this.#actionState.enqueueAction(() =>
actionChain.dispatch(this.#actionState, this.#actionsOptions)
);
await this.#endWheelTransaction();
});
}
/**
* The release actions command is used to release all the keys and pointer
* buttons that are currently depressed. This causes events to be fired
* as if the state was released by an explicit series of actions. It also
* clears all the internal state of the virtual devices.
*/
async releaseActions() {
// Bug 1821460: Use top-level browsing context.
if (this.#actionState === null) {
return;
}
// Enqueue to serialize access to input state.
await this.#actionState.enqueueAction(() => {
const undoActions = this.#actionState.inputCancelList.reverse();
undoActions.dispatch(this.#actionState, this.#actionsOptions);
});
this.#actionState = null;
return this.sendQuery("MarionetteCommandsParent:releaseActions");
}
async switchToFrame(id) {

View File

@ -1,78 +0,0 @@
/* 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/. */
/**
* Manages a queue of asynchronous tasks, ensuring they are processed sequentially.
*/
export class AsyncQueue {
#processing;
#queue;
constructor() {
this.#queue = [];
this.#processing = false;
}
/**
* Dequeue a task.
*
* @returns {Promise}
* The wrapped task appearing as first item in the queue.
*/
#dequeue() {
return this.#queue.shift();
}
/**
* Dequeue and try to process all the queued tasks.
*
* @returns {Promise<undefined>}
* Promise that resolves when processing the queue is done.
*/
async #processQueue() {
// The queue is already processed or no tasks queued up.
if (this.#processing || this.#queue.length === 0) {
return;
}
this.#processing = true;
while (this.#queue.length) {
const wrappedTask = this.#dequeue();
await wrappedTask();
}
this.#processing = false;
}
/**
* Enqueue a task.
*
* @param {Function} task
* The task to queue.
*
* @returns {Promise<object>}
* Promise that resolves when the task is completed, with the resolved
* value being the result of the task.
*/
enqueue(task) {
const onTaskExecuted = new Promise((resolve, reject) => {
// Wrap the task in a function that will resolve or reject the Promise.
const wrappedTask = async () => {
try {
const result = await task();
resolve(result);
} catch (error) {
reject(error);
}
};
// Add the wrapped task to the queue
this.#queue.push(wrappedTask);
this.#processQueue();
});
return onTaskExecuted;
}
}

View File

@ -1,102 +0,0 @@
/* 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 { setTimeout } = ChromeUtils.importESModule(
"resource://gre/modules/Timer.sys.mjs"
);
const { AsyncQueue } = ChromeUtils.importESModule(
"chrome://remote/content/shared/AsyncQueue.sys.mjs"
);
function sleep(delay = 100) {
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
return new Promise(resolve => setTimeout(resolve, delay));
}
add_task(async function test_enqueueSyncTask() {
let value = "";
const queue = new AsyncQueue();
await Promise.all([
queue.enqueue(() => (value += "foo")),
queue.enqueue(() => (value += "bar")),
]);
equal(value, "foobar", "Tasks run in the correct order");
});
add_task(async function test_enqueueAsyncTask() {
let value = "";
const queue = new AsyncQueue();
await Promise.all([
queue.enqueue(async () => {
await sleep(100);
value += "foo";
}),
queue.enqueue(async () => {
await sleep(10);
value += "bar";
}),
]);
equal(value, "foobar", "Tasks run in the correct order");
});
add_task(async function test_enqueueAsyncTask() {
let value = "";
const queue = new AsyncQueue();
const promises = Promise.all([
queue.enqueue(async () => {
await sleep(100);
value += "foo";
}),
queue.enqueue(async () => {
await sleep(10);
value += "bar";
}),
]);
const promise = queue.enqueue(async () => (value += "42"));
await promise;
await promises;
equal(value, "foobar42", "Tasks run in the correct order");
});
add_task(async function test_returnValue() {
const queue = new AsyncQueue();
const results = await Promise.all([
queue.enqueue(() => "foo"),
queue.enqueue(() => 42),
]);
equal(results[0], "foo", "First task returned correct value");
equal(results[1], 42, "Second task returned correct value");
});
add_task(async function test_enqueueErroneousTasks() {
const queue = new AsyncQueue();
await Assert.rejects(
queue.enqueue(() => {
throw new Error("invalid");
}),
/Error: invalid/,
"Expected error was returned"
);
await Assert.rejects(
queue.enqueue(async () => {
throw new Error("invalid");
}),
/Error: invalid/,
"Expected error was returned"
);
});

View File

@ -3,8 +3,6 @@ head = "head.js"
["test_AppInfo.js"]
["test_AsyncQueue.js"]
["test_ChallengeHeaderParser.js"]
["test_DOM.js"]

File diff suppressed because it is too large Load Diff

View File

@ -84,7 +84,7 @@ event.synthesizeMouseAtPoint = function (left, top, opts, win) {
};
/**
* Synthesize a touch event at a point.
* 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.

View File

@ -35,15 +35,12 @@ add_task(function test_createInputState() {
}
});
add_task(async function test_defaultPointerParameters() {
add_task(function test_defaultPointerParameters() {
let state = new action.State();
const inputTickActions = [
{ type: "pointer", subtype: "pointerDown", button: 0 },
];
const chain = await action.Chain.fromJSON(
state,
chainForTick(inputTickActions)
);
const chain = action.Chain.fromJSON(state, chainForTick(inputTickActions));
const pointerAction = chain[0][0];
equal(
state.getInputSource(pointerAction.id).pointer.constructor.type,
@ -51,7 +48,7 @@ add_task(async function test_defaultPointerParameters() {
);
});
add_task(async function test_processPointerParameters() {
add_task(function test_processPointerParameters() {
for (let subtype of ["pointerDown", "pointerUp"]) {
for (let pointerType of [2, true, {}, []]) {
const inputTickActions = [
@ -63,7 +60,7 @@ add_task(async function test_processPointerParameters() {
},
];
let message = `Action sequence with parameters: {pointerType: ${pointerType} subtype: ${subtype}}`;
await checkFromJSONErrors(
checkFromJSONErrors(
inputTickActions,
/Expected "pointerType" to be a string/,
message
@ -80,7 +77,7 @@ add_task(async function test_processPointerParameters() {
},
];
let message = `Action sequence with parameters: {pointerType: ${pointerType} subtype: ${subtype}}`;
await checkFromJSONErrors(
checkFromJSONErrors(
inputTickActions,
/Expected "pointerType" to be one of/,
message
@ -98,10 +95,7 @@ add_task(async function test_processPointerParameters() {
button: 0,
},
];
const chain = await action.Chain.fromJSON(
state,
chainForTick(inputTickActions)
);
const chain = action.Chain.fromJSON(state, chainForTick(inputTickActions));
const pointerAction = chain[0][0];
equal(
state.getInputSource(pointerAction.id).pointer.constructor.type,
@ -110,12 +104,12 @@ add_task(async function test_processPointerParameters() {
}
});
add_task(async function test_processPointerDownAction() {
add_task(function test_processPointerDownAction() {
for (let button of [-1, "a"]) {
const inputTickActions = [
{ type: "pointer", subtype: "pointerDown", button },
];
await checkFromJSONErrors(
checkFromJSONErrors(
inputTickActions,
/Expected "button" to be a positive integer/,
`pointerDown with {button: ${button}}`
@ -125,21 +119,18 @@ add_task(async function test_processPointerDownAction() {
const inputTickActions = [
{ type: "pointer", subtype: "pointerDown", button: 5 },
];
const chain = await action.Chain.fromJSON(
state,
chainForTick(inputTickActions)
);
const chain = action.Chain.fromJSON(state, chainForTick(inputTickActions));
equal(chain[0][0].button, 5);
});
add_task(async function test_validateActionDurationAndCoordinates() {
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 }];
await checkFromJSONErrors(
checkFromJSONErrors(
inputTickActions,
/Expected "duration" to be a positive integer/,
`{subtype} with {duration: ${duration}}`
@ -153,7 +144,7 @@ add_task(async function test_validateActionDurationAndCoordinates() {
duration: 5000,
};
actionItem[name] = "a";
await checkFromJSONErrors(
checkFromJSONErrors(
[actionItem],
/Expected ".*" to be an integer/,
`${name}: "a", subtype: pointerMove`
@ -161,73 +152,54 @@ add_task(async function test_validateActionDurationAndCoordinates() {
}
});
add_task(async function test_processPointerMoveActionOriginStringValidation() {
for (let origin of ["", "viewports", "pointers"]) {
add_task(function test_processPointerMoveActionOriginValidation() {
for (let origin of [-1, { a: "blah" }, []]) {
const inputTickActions = [
{
type: "pointer",
x: 0,
y: 0,
duration: 5000,
subtype: "pointerMove",
origin,
},
{ type: "pointer", duration: 5000, subtype: "pointerMove", origin },
];
await checkFromJSONErrors(
checkFromJSONErrors(
inputTickActions,
/Expected "origin" to be undefined, "viewport", "pointer", or an element/,
`actionItem.origin: ${origin}`,
{ isElementOrigin: () => false }
`actionItem.origin: (${getTypeString(origin)})`
);
}
});
add_task(async function test_processPointerMoveActionOriginElementValidation() {
const element = { foo: "bar" };
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",
x: 0,
y: 0,
duration: 5000,
subtype: "pointerMove",
origin: element,
origin: domEl,
x: 0,
y: 0,
},
];
// invalid element origin
await checkFromJSONErrors(
inputTickActions,
/Expected "origin" to be undefined, "viewport", "pointer", or an element/,
`actionItem.origin: (${getTypeString(element)})`,
{ isElementOrigin: elem => "foo1" in elem }
);
let state = new action.State();
const actionsOptions = {
isElementOrigin: elem => "foo" in elem,
getElementOrigin: elem => elem,
};
// valid element origin
const chain = await action.Chain.fromJSON(
state,
chainForTick(inputTickActions),
actionsOptions
);
deepEqual(chain[0][0].origin, { element });
const chain = action.Chain.fromJSON(state, chainForTick(inputTickActions));
deepEqual(chain[0][0].origin.element, domEl);
});
add_task(async function test_processPointerMoveActionDefaultOrigin() {
add_task(function test_processPointerMoveActionDefaultOrigin() {
let state = new action.State();
const inputTickActions = [
{ type: "pointer", x: 0, y: 0, duration: 5000, subtype: "pointerMove" },
{ type: "pointer", duration: 5000, subtype: "pointerMove", x: 0, y: 0 },
];
const chain = await action.Chain.fromJSON(
state,
chainForTick(inputTickActions),
{}
);
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,
@ -235,7 +207,7 @@ add_task(async function test_processPointerMoveActionDefaultOrigin() {
});
});
add_task(async function test_processPointerMoveAction() {
add_task(function test_processPointerMoveAction() {
let state = new action.State();
const actionItems = [
{
@ -265,16 +237,7 @@ add_task(async function test_processPointerMoveAction() {
type: "pointer",
actions: actionItems,
};
let actionsOptions = {
isElementOrigin: elem => elem == domEl,
getElementOrigin: elem => elem,
};
let chain = await action.Chain.fromJSON(
state,
[actionSequence],
actionsOptions
);
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];
@ -295,7 +258,7 @@ add_task(async function test_processPointerMoveAction() {
}
});
add_task(async function test_computePointerDestinationViewport() {
add_task(function test_computePointerDestinationViewport() {
const state = new action.State();
const inputTickActions = [
{
@ -306,17 +269,13 @@ add_task(async function test_computePointerDestinationViewport() {
origin: "viewport",
},
];
const chain = await action.Chain.fromJSON(
state,
chainForTick(inputTickActions),
{}
);
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 = await actionItem.origin.getTargetCoordinates(
const target = actionItem.origin.getTargetCoordinates(
inputSource,
[actionItem.x, actionItem.y],
null
@ -325,7 +284,7 @@ add_task(async function test_computePointerDestinationViewport() {
equal(actionItem.y, target[1]);
});
add_task(async function test_computePointerDestinationPointer() {
add_task(function test_computePointerDestinationPointer() {
const state = new action.State();
const inputTickActions = [
{
@ -336,16 +295,12 @@ add_task(async function test_computePointerDestinationPointer() {
origin: "pointer",
},
];
const chain = await action.Chain.fromJSON(
state,
chainForTick(inputTickActions),
{}
);
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 = await actionItem.origin.getTargetCoordinates(
const target = actionItem.origin.getTargetCoordinates(
inputSource,
[actionItem.x, actionItem.y],
null
@ -354,7 +309,7 @@ add_task(async function test_computePointerDestinationPointer() {
equal(actionItem.y + inputSource.y, target[1]);
});
add_task(async function test_processPointerAction() {
add_task(function test_processPointerAction() {
for (let pointerType of ["mouse", "touch"]) {
const actionItems = [
{
@ -381,7 +336,7 @@ add_task(async function test_processPointerAction() {
actions: actionItems,
};
const state = new action.State();
const chain = await action.Chain.fromJSON(state, [actionSequence], {});
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];
@ -404,7 +359,7 @@ add_task(async function test_processPointerAction() {
}
});
add_task(async function test_processPauseAction() {
add_task(function test_processPauseAction() {
for (let type of ["none", "key", "pointer"]) {
const state = new action.State();
const actionSequence = {
@ -412,8 +367,7 @@ add_task(async function test_processPauseAction() {
id: "some_id",
actions: [{ type: "pause", duration: 5000 }],
};
const actions = await action.Chain.fromJSON(state, [actionSequence], {});
const actionItem = actions[0][0];
const actionItem = action.Chain.fromJSON(state, [actionSequence])[0][0];
equal(actionItem.type, "none");
equal(actionItem.subtype, "pause");
equal(actionItem.id, "some_id");
@ -425,16 +379,15 @@ add_task(async function test_processPauseAction() {
id: "some_id",
actions: [{ type: "pause" }],
};
const actions = await action.Chain.fromJSON(state, [actionSequence], {});
const actionItem = actions[0][0];
const actionItem = action.Chain.fromJSON(state, [actionSequence])[0][0];
equal(actionItem.duration, undefined);
});
add_task(async function test_processActionSubtypeValidation() {
add_task(function test_processActionSubtypeValidation() {
for (let type of ["none", "key", "pointer"]) {
const message = `type: ${type}, subtype: dancing`;
const inputTickActions = [{ type, subtype: "dancing" }];
await checkFromJSONErrors(
checkFromJSONErrors(
inputTickActions,
new RegExp(`Expected known subtype for type`),
message
@ -442,11 +395,11 @@ add_task(async function test_processActionSubtypeValidation() {
}
});
add_task(async function test_processKeyActionDown() {
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)})`;
await checkFromJSONErrors(
checkFromJSONErrors(
inputTickActions,
/Expected "value" to be a string that represents single code point/,
message
@ -459,8 +412,7 @@ add_task(async function test_processKeyActionDown() {
id: "keyboard",
actions: [{ type: "keyDown", value: "\uE004" }],
};
const actions = await action.Chain.fromJSON(state, [actionSequence], {});
const actionItem = actions[0][0];
const actionItem = action.Chain.fromJSON(state, [actionSequence])[0][0];
equal(actionItem.type, "key");
equal(actionItem.id, "keyboard");
@ -468,20 +420,20 @@ add_task(async function test_processKeyActionDown() {
equal(actionItem.value, "\ue004");
});
add_task(async function test_processInputSourceActionSequenceValidation() {
await checkFromJSONErrors(
add_task(function test_processInputSourceActionSequenceValidation() {
checkFromJSONErrors(
[{ type: "swim", subtype: "pause", id: "some id" }],
/Expected known action type/,
"actionSequence type: swim"
);
await checkFromJSONErrors(
checkFromJSONErrors(
[{ type: "none", subtype: "pause", id: -1 }],
/Expected "id" to be a string/,
"actionSequence id: -1"
);
await checkFromJSONErrors(
checkFromJSONErrors(
[{ type: "none", subtype: "pause", id: undefined }],
/Expected "id" to be a string/,
"actionSequence id: undefined"
@ -494,19 +446,19 @@ add_task(async function test_processInputSourceActionSequenceValidation() {
const errorRegex = /Expected "actionSequence.actions" to be an array/;
const message = "actionSequence actions: -1";
await Assert.rejects(
action.Chain.fromJSON(state, actionSequence, {}),
Assert.throws(
() => action.Chain.fromJSON(state, actionSequence),
/InvalidArgumentError/,
message
);
await Assert.rejects(
action.Chain.fromJSON(state, actionSequence, {}),
Assert.throws(
() => action.Chain.fromJSON(state, actionSequence),
errorRegex,
message
);
});
add_task(async function test_processInputSourceActionSequence() {
add_task(function test_processInputSourceActionSequence() {
const state = new action.State();
const actionItem = { type: "pause", duration: 5 };
const actionSequence = {
@ -514,7 +466,7 @@ add_task(async function test_processInputSourceActionSequence() {
id: "some id",
actions: [actionItem],
};
const chain = await action.Chain.fromJSON(state, [actionSequence], {});
const chain = action.Chain.fromJSON(state, [actionSequence]);
equal(chain.length, 1);
const tickActions = chain[0];
equal(tickActions.length, 1);
@ -524,7 +476,7 @@ add_task(async function test_processInputSourceActionSequence() {
equal(tickActions[0].id, "some id");
});
add_task(async function test_processInputSourceActionSequencePointer() {
add_task(function test_processInputSourceActionSequencePointer() {
const state = new action.State();
const actionItem = { type: "pointerDown", button: 1 };
const actionSequence = {
@ -535,7 +487,7 @@ add_task(async function test_processInputSourceActionSequencePointer() {
pointerType: "mouse", // TODO "pen"
},
};
const chain = await action.Chain.fromJSON(state, [actionSequence], {});
const chain = action.Chain.fromJSON(state, [actionSequence]);
equal(chain.length, 1);
const tickActions = chain[0];
equal(tickActions.length, 1);
@ -548,7 +500,7 @@ add_task(async function test_processInputSourceActionSequencePointer() {
equal(inputSource.pointer.constructor.type, "mouse");
});
add_task(async function test_processInputSourceActionSequenceKey() {
add_task(function test_processInputSourceActionSequenceKey() {
const state = new action.State();
const actionItem = { type: "keyUp", value: "a" };
const actionSequence = {
@ -556,7 +508,7 @@ add_task(async function test_processInputSourceActionSequenceKey() {
id: "9",
actions: [actionItem],
};
const chain = await action.Chain.fromJSON(state, [actionSequence], {});
const chain = action.Chain.fromJSON(state, [actionSequence]);
equal(chain.length, 1);
const tickActions = chain[0];
equal(tickActions.length, 1);
@ -566,7 +518,7 @@ add_task(async function test_processInputSourceActionSequenceKey() {
equal(tickActions[0].id, "9");
});
add_task(async function test_processInputSourceActionSequenceInputStateMap() {
add_task(function test_processInputSourceActionSequenceInputStateMap() {
const state = new action.State();
const id = "1";
const actionItem = { type: "pause", duration: 5000 };
@ -575,7 +527,7 @@ add_task(async function test_processInputSourceActionSequenceInputStateMap() {
id,
actions: [actionItem],
};
await action.Chain.fromJSON(state, [actionSequence], {});
action.Chain.fromJSON(state, [actionSequence]);
equal(state.inputStateMap.size, 1);
equal(state.inputStateMap.get(id).constructor.type, "key");
@ -587,7 +539,7 @@ add_task(async function test_processInputSourceActionSequenceInputStateMap() {
id,
actions: [actionItem1],
};
await action.Chain.fromJSON(state1, [actionSequence1], {});
action.Chain.fromJSON(state1, [actionSequence1]);
equal(state1.inputStateMap.size, 1);
// Overwrite the state in the initial map with one of a different type
@ -595,41 +547,41 @@ add_task(async function test_processInputSourceActionSequenceInputStateMap() {
equal(state.inputStateMap.get(id).constructor.type, "pointer");
const message = "Wrong state for input id type";
await Assert.rejects(
action.Chain.fromJSON(state, [actionSequence]),
Assert.throws(
() => action.Chain.fromJSON(state, [actionSequence]),
/InvalidArgumentError/,
message
);
await Assert.rejects(
action.Chain.fromJSON(state, [actionSequence]),
Assert.throws(
() => action.Chain.fromJSON(state, [actionSequence]),
/Expected input source \[object String\] "1" to be type pointer/,
message
);
});
add_task(async function test_extractActionChainValidation() {
add_task(function test_extractActionChainValidation() {
for (let actions of [-1, "a", undefined, null]) {
const state = new action.State();
let message = `actions: ${getTypeString(actions)}`;
await Assert.rejects(
action.Chain.fromJSON(state, actions),
Assert.throws(
() => action.Chain.fromJSON(state, actions),
/InvalidArgumentError/,
message
);
await Assert.rejects(
action.Chain.fromJSON(state, actions),
Assert.throws(
() => action.Chain.fromJSON(state, actions),
/Expected "actions" to be an array/,
message
);
}
});
add_task(async function test_extractActionChainEmpty() {
add_task(function test_extractActionChainEmpty() {
const state = new action.State();
deepEqual(await action.Chain.fromJSON(state, [], {}), []);
deepEqual(action.Chain.fromJSON(state, []), []);
});
add_task(async function test_extractActionChain_oneTickOneInput() {
add_task(function test_extractActionChain_oneTickOneInput() {
const state = new action.State();
const actionItem = { type: "pause", duration: 5000 };
const actionSequence = {
@ -637,11 +589,7 @@ add_task(async function test_extractActionChain_oneTickOneInput() {
id: "some id",
actions: [actionItem],
};
const actionsByTick = await action.Chain.fromJSON(
state,
[actionSequence],
{}
);
const actionsByTick = action.Chain.fromJSON(state, [actionSequence]);
equal(1, actionsByTick.length);
equal(1, actionsByTick[0].length);
equal(actionsByTick[0][0].id, actionSequence.id);
@ -650,7 +598,7 @@ add_task(async function test_extractActionChain_oneTickOneInput() {
equal(actionsByTick[0][0].duration, actionItem.duration);
});
add_task(async function test_extractActionChain_twoAndThreeTicks() {
add_task(function test_extractActionChain_twoAndThreeTicks() {
const state = new action.State();
const mouseActionItems = [
{
@ -689,11 +637,10 @@ add_task(async function test_extractActionChain_twoAndThreeTicks() {
id: "1",
actions: keyActionItems,
};
let actionsByTick = await action.Chain.fromJSON(
state,
[keyActionSequence, mouseActionSequence],
{}
);
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);
@ -705,7 +652,7 @@ add_task(async function test_extractActionChain_twoAndThreeTicks() {
equal(actionsByTick[2][0].subtype, "keyUp");
});
add_task(async function test_computeTickDuration() {
add_task(function test_computeTickDuration() {
const state = new action.State();
const expected = 8000;
const inputTickActions = [
@ -717,17 +664,13 @@ add_task(async function test_computeTickDuration() {
{ type: "pointer", subtype: "pause", duration: expected },
{ type: "pointer", subtype: "pointerUp", button: 0 },
];
const chain = await action.Chain.fromJSON(
state,
chainForTick(inputTickActions),
{}
);
const chain = action.Chain.fromJSON(state, chainForTick(inputTickActions));
equal(1, chain.length);
const tickActions = chain[0];
equal(expected, tickActions.getDuration());
});
add_task(async function test_computeTickDuration_noDurations() {
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.
@ -738,11 +681,7 @@ add_task(async function test_computeTickDuration_noDurations() {
{ type: "pointer", subtype: "pointerDown", button: 0 },
{ type: "key", subtype: "keyUp", value: "a" },
];
const chain = await action.Chain.fromJSON(
state,
chainForTick(inputTickActions),
{}
);
const chain = action.Chain.fromJSON(state, chainForTick(inputTickActions));
equal(0, chain[0].getDuration());
});
@ -780,37 +719,19 @@ function getTypeString(obj) {
return Object.prototype.toString.call(obj);
}
async function checkFromJSONErrors(
inputTickActions,
regex,
message,
options = {}
) {
const { isElementOrigin = () => true, getElementOrigin = elem => elem } =
options;
function checkFromJSONErrors(inputTickActions, regex, message) {
const state = new action.State();
const actionsOptions = { isElementOrigin, getElementOrigin };
if (typeof message == "undefined") {
message = `fromJSON`;
}
await Assert.rejects(
action.Chain.fromJSON(
state,
chainForTick(inputTickActions),
actionsOptions
),
Assert.throws(
() => action.Chain.fromJSON(state, chainForTick(inputTickActions)),
/InvalidArgumentError/,
message
);
await Assert.rejects(
action.Chain.fromJSON(
state,
chainForTick(inputTickActions),
actionsOptions
),
Assert.throws(
() => action.Chain.fromJSON(state, chainForTick(inputTickActions)),
regex,
message
);

View File

@ -7,7 +7,6 @@ import { RootBiDiModule } from "chrome://remote/content/webdriver-bidi/modules/R
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
action: "chrome://remote/content/shared/webdriver/Actions.sys.mjs",
assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs",
error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
pprint: "chrome://remote/content/shared/Format.sys.mjs",
@ -17,223 +16,8 @@ ChromeUtils.defineESModuleGetters(lazy, {
});
class InputModule extends RootBiDiModule {
#actionsOptions;
#inputStates;
constructor(messageHandler) {
super(messageHandler);
// Browsing context => input state.
// Bug 1821460: Move to WebDriver Session and share with Marionette.
this.#inputStates = new WeakMap();
// Options for actions to pass through performActions and releaseActions.
this.#actionsOptions = {
// Callbacks as defined in the WebDriver specification.
getElementOrigin: this.#getElementOrigin.bind(this),
isElementOrigin: this.#isElementOrigin.bind(this),
// Custom callbacks.
assertInViewPort: this.#assertInViewPort.bind(this),
dispatchEvent: this.#dispatchEvent.bind(this),
getClientRects: this.#getClientRects.bind(this),
getInViewCentrePoint: this.#getInViewCentrePoint.bind(this),
};
}
destroy() {}
/**
* Assert that the target coordinates are within the visible viewport.
*
* @param {Array.<number>} target
* Coordinates [x, y] of the target relative to the viewport.
* @param {BrowsingContext} context
* The browsing context to dispatch the event to.
*
* @returns {Promise<undefined>}
* Promise that rejects, if the coordinates are not within
* the visible viewport.
*
* @throws {MoveTargetOutOfBoundsError}
* If target is outside the viewport.
*/
#assertInViewPort(target, context) {
return this.messageHandler.forwardCommand({
moduleName: "input",
commandName: "_assertInViewPort",
destination: {
type: lazy.WindowGlobalMessageHandler.type,
id: context.id,
},
params: { target },
});
}
/**
* Dispatch an event.
*
* @param {string} eventName
* Name of the event to be dispatched.
* @param {BrowsingContext} context
* The browsing context to dispatch the event to.
* @param {object} details
* Details of the event to be dispatched.
*
* @returns {Promise}
* Promise that resolves once the event is dispatched.
*/
#dispatchEvent(eventName, context, details) {
return this.messageHandler.forwardCommand({
moduleName: "input",
commandName: "_dispatchEvent",
destination: {
type: lazy.WindowGlobalMessageHandler.type,
id: context.id,
},
params: { eventName, details },
});
}
/**
* Terminates the current wheel transaction.
*
* @param {BrowsingContext} context
* The browsing context to terminate the wheel transaction for.
*
* @returns {Promise}
* Promise that resolves when the transaction was terminated.
*/
#endWheelTransaction(context) {
return this.messageHandler.forwardCommand({
moduleName: "input",
commandName: "_endWheelTransaction",
destination: {
type: lazy.WindowGlobalMessageHandler.type,
id: context.id,
},
});
}
/**
* Retrieve the list of client rects for the element.
*
* @param {Node} node
* The web element reference to retrieve the rects from.
* @param {BrowsingContext} context
* The browsing context to dispatch the event to.
*
* @returns {Promise<Array<Map.<string, number>>>}
* Promise that resolves to a list of DOMRect-like objects.
*/
#getClientRects(node, context) {
return this.messageHandler.forwardCommand({
moduleName: "input",
commandName: "_getClientRects",
destination: {
type: lazy.WindowGlobalMessageHandler.type,
id: context.id,
},
params: { element: node },
});
}
/**
* Retrieves the Node reference of the origin.
*
* @param {ElementOrigin} origin
* Reference to the element origin of the action.
* @param {BrowsingContext} context
* The browsing context to dispatch the event to.
*
* @returns {Promise<SharedReference>}
* Promise that resolves to the shared reference.
*/
#getElementOrigin(origin, context) {
return this.messageHandler.forwardCommand({
moduleName: "input",
commandName: "_getElementOrigin",
destination: {
type: lazy.WindowGlobalMessageHandler.type,
id: context.id,
},
params: { origin },
});
}
/**
* Retrieves the action's input state.
*
* @param {BrowsingContext} context
* The Browsing Context to retrieve the input state for.
*
* @returns {Actions.InputState}
* The action's input state.
*/
#getInputState(context) {
// Bug 1821460: Fetch top-level browsing context.
let inputState = this.#inputStates.get(context);
if (inputState === undefined) {
inputState = new lazy.action.State();
this.#inputStates.set(context, inputState);
}
return inputState;
}
/**
* Retrieve the in-view center point for the rect and visible viewport.
*
* @param {DOMRect} rect
* Size and position of the rectangle to check.
* @param {BrowsingContext} context
* The browsing context to dispatch the event to.
*
* @returns {Promise<Map.<string, number>>}
* X and Y coordinates that denotes the in-view centre point of
* `rect`.
*/
#getInViewCentrePoint(rect, context) {
return this.messageHandler.forwardCommand({
moduleName: "input",
commandName: "_getInViewCentrePoint",
destination: {
type: lazy.WindowGlobalMessageHandler.type,
id: context.id,
},
params: { rect },
});
}
/**
* Checks if the given object is a valid element origin.
*
* @param {object} origin
* The object to check.
*
* @returns {boolean}
* True, if the object references a shared reference.
*/
#isElementOrigin(origin) {
return (
origin?.type === "element" && typeof origin.element?.sharedId === "string"
);
}
/**
* Resets the action's input state.
*
* @param {BrowsingContext} context
* The Browsing Context to reset the input state for.
*/
#resetInputState(context) {
// Bug 1821460: Fetch top-level browsing context.
if (this.#inputStates.has(context)) {
this.#inputStates.delete(context);
}
}
async performActions(options = {}) {
const { actions, context: contextId } = options;
@ -249,21 +33,21 @@ class InputModule extends RootBiDiModule {
);
}
const inputState = this.#getInputState(context);
const actionsOptions = { ...this.#actionsOptions, context };
// Bug 1821460: Fetch top-level browsing context.
const actionChain = await lazy.action.Chain.fromJSON(
inputState,
actions,
actionsOptions
);
await this.messageHandler.forwardCommand({
moduleName: "input",
commandName: "performActions",
destination: {
type: lazy.WindowGlobalMessageHandler.type,
id: context.id,
},
params: {
actions,
},
});
// Enqueue to serialize access to input state.
await inputState.enqueueAction(() =>
actionChain.dispatch(inputState, actionsOptions)
);
await this.#endWheelTransaction(context);
return {};
}
/**
@ -293,16 +77,19 @@ class InputModule extends RootBiDiModule {
);
}
const inputState = this.#getInputState(context);
const actionsOptions = { ...this.#actionsOptions, context };
// Bug 1821460: Fetch top-level browsing context.
// Enqueue to serialize access to input state.
await inputState.enqueueAction(() => {
const undoActions = inputState.inputCancelList.reverse();
return undoActions.dispatch(inputState, actionsOptions);
await this.messageHandler.forwardCommand({
moduleName: "input",
commandName: "releaseActions",
destination: {
type: lazy.WindowGlobalMessageHandler.type,
id: context.id,
},
params: {},
});
this.#resetInputState(context);
return {};
}
/**

View File

@ -7,91 +7,45 @@ import { WindowGlobalBiDiModule } from "chrome://remote/content/webdriver-bidi/m
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
assertInViewPort: "chrome://remote/content/shared/webdriver/Actions.sys.mjs",
action: "chrome://remote/content/shared/webdriver/Actions.sys.mjs",
dom: "chrome://remote/content/shared/DOM.sys.mjs",
error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
event: "chrome://remote/content/shared/webdriver/Event.sys.mjs",
});
class InputModule extends WindowGlobalBiDiModule {
#actionState;
constructor(messageHandler) {
super(messageHandler);
this.#actionState = null;
}
destroy() {}
_assertInViewPort(options = {}) {
const { target } = options;
return lazy.assertInViewPort(target, this.messageHandler.window);
}
_dispatchEvent(options = {}) {
const { eventName, details } = options;
switch (eventName) {
case "synthesizeKeyDown":
lazy.event.sendKeyDown(details.eventData, this.messageHandler.window);
break;
case "synthesizeKeyUp":
lazy.event.sendKeyUp(details.eventData, this.messageHandler.window);
break;
case "synthesizeMouseAtPoint":
lazy.event.synthesizeMouseAtPoint(
details.x,
details.y,
details.eventData,
this.messageHandler.window
);
break;
case "synthesizeMultiTouch":
lazy.event.synthesizeMultiTouch(
details.eventData,
this.messageHandler.window
);
break;
case "synthesizeWheelAtPoint":
lazy.event.synthesizeWheelAtPoint(
details.x,
details.y,
details.eventData,
this.messageHandler.window
);
break;
default:
throw new Error(`${eventName} is not a supported type for dispatching`);
async performActions(options) {
const { actions } = options;
if (this.#actionState === null) {
this.#actionState = new lazy.action.State();
}
}
_endWheelTransaction() {
await this.#deserializeActionOrigins(actions);
const actionChain = lazy.action.Chain.fromJSON(this.#actionState, actions);
await actionChain.dispatch(this.#actionState, this.messageHandler.window);
// Terminate the current wheel transaction if there is one. Wheel
// transactions should not live longer than a single action chain.
ChromeUtils.endWheelTransaction();
}
async _getClientRects(options = {}) {
const { element: reference } = options;
const element = await this.#deserializeElementSharedReference(reference);
const rects = element.getClientRects();
// To avoid serialization and deserialization of DOMRect and DOMRectList
// convert to plain object and Array.
return [...rects].map(rect => {
const { x, y, width, height, top, right, bottom, left } = rect;
return { x, y, width, height, top, right, bottom, left };
});
}
async _getElementOrigin(options) {
const { origin } = options;
const reference = origin.element;
this.#deserializeElementSharedReference(reference);
return reference;
}
_getInViewCentrePoint(options = {}) {
const { rect } = options;
return lazy.dom.getInViewCentrePoint(rect, this.messageHandler.window);
async releaseActions() {
if (this.#actionState === null) {
return;
}
await this.#actionState.release(this.messageHandler.window);
this.#actionState = null;
}
async setFiles(options) {
@ -155,6 +109,50 @@ class InputModule extends WindowGlobalBiDiModule {
}
}
/**
* In the provided array of input.SourceActions, replace all origins matching
* the input.ElementOrigin production with the Element corresponding to this
* origin.
*
* Note that this method replaces the content of the `actions` in place, and
* does not return a new array.
*
* @param {Array<input.SourceActions>} actions
* The array of SourceActions to deserialize.
* @returns {Promise}
* A promise which resolves when all ElementOrigin origins have been
* deserialized.
*/
async #deserializeActionOrigins(actions) {
const promises = [];
if (!Array.isArray(actions)) {
// Silently ignore invalid action chains because they are fully parsed later.
return Promise.resolve();
}
for (const actionsByTick of actions) {
if (!Array.isArray(actionsByTick?.actions)) {
// Silently ignore invalid actions because they are fully parsed later.
return Promise.resolve();
}
for (const action of actionsByTick.actions) {
if (action?.origin?.type === "element") {
promises.push(
(async () => {
action.origin = await this.#deserializeElementSharedReference(
action.origin.element
);
})()
);
}
}
}
return Promise.all(promises);
}
async #deserializeElementSharedReference(sharedReference) {
if (typeof sharedReference?.sharedId !== "string") {
throw new lazy.error.InvalidArgumentError(

View File

@ -1,6 +1,7 @@
[pointer_pen.py]
[test_null_response_value]
expected: FAIL
expected:
ERROR
[test_pen_pointer_in_shadow_tree[outer-open\]]
expected: FAIL

View File

@ -2,39 +2,6 @@ import json
from webdriver.bidi.modules.script import ContextTarget
async def add_mouse_listeners(bidi_session, context, include_mousemove=True):
result = await bidi_session.script.call_function(
function_declaration="""(include_mousemove) => {
window.allEvents = { events: []};
const events = ["auxclick", "click", "mousedown", "mouseup"];
if (include_mousemove) {
events.push("mousemove");
}
function handleEvent(event) {
window.allEvents.events.push({
type: event.type,
detail: event.detail,
clientX: event.clientX,
clientY: event.clientY,
isTrusted: event.isTrusted,
button: event.button,
buttons: event.buttons,
});
};
for (const event of events) {
document.addEventListener(event, handleEvent);
}
}""",
arguments=[{"type": "boolean", "value": include_mousemove}],
await_promise=False,
target=ContextTarget(context["context"]),
)
async def get_object_from_context(bidi_session, context, object_path):
"""Return a plain JS object from a given context, accessible at the given object_path"""
events_str = await bidi_session.script.evaluate(
@ -62,7 +29,6 @@ async def get_events(bidi_session, context):
# tests expect ''.
if "code" in e and e["code"] == "Unidentified":
e["code"] = ""
return events

View File

@ -54,26 +54,34 @@ async def test_click_at_coordinates(bidi_session, top_context, load_static_test_
assert expected == filtered_events[1:]
@pytest.mark.parametrize("origin", ["element", "pointer", "viewport"])
async def test_params_actions_origin_outside_viewport(
bidi_session, top_context, get_actions_origin_page, get_element, origin
):
if origin == "element":
url = get_actions_origin_page(
"""width: 100px; height: 50px; background: green;
position: relative; left: -200px; top: -100px;"""
)
await bidi_session.browsing_context.navigate(
context=top_context["context"],
url=url,
wait="complete",
@pytest.mark.parametrize("origin", ["pointer", "viewport"])
async def test_params_actions_origin_outside_viewport(bidi_session, top_context, origin):
actions = Actions()
actions.add_pointer().pointer_move(x=-50, y=-50, origin=origin)
with pytest.raises(MoveTargetOutOfBoundsException):
await bidi_session.input.perform_actions(
actions=actions, context=top_context["context"]
)
element = await get_element("#inner")
origin = get_element_origin(element)
async def test_params_actions_origin_element_outside_viewport(
bidi_session, top_context, get_actions_origin_page, get_element
):
url = get_actions_origin_page(
"""width: 100px; height: 50px; background: green;
position: relative; left: -200px; top: -100px;"""
)
await bidi_session.browsing_context.navigate(
context=top_context["context"],
url=url,
wait="complete",
)
elem = await get_element("#inner")
actions = Actions()
actions.add_pointer().pointer_move(x=-100, y=-100, origin=origin)
actions.add_pointer().pointer_move(x=0, y=0, origin=get_element_origin(elem))
with pytest.raises(MoveTargetOutOfBoundsException):
await bidi_session.input.perform_actions(

View File

@ -1,6 +1,5 @@
import pytest
from webdriver.bidi.error import MoveTargetOutOfBoundsException
from webdriver.bidi.modules.input import Actions, get_element_origin
from .. import get_events
@ -14,36 +13,6 @@ from . import (
pytestmark = pytest.mark.asyncio
@pytest.mark.parametrize("origin", ["element", "pointer", "viewport"])
async def test_params_actions_origin_outside_viewport(
bidi_session, get_actions_origin_page, top_context, get_element, origin
):
if origin == "element":
url = get_actions_origin_page(
"""width: 100px; height: 50px; background: green;
position: relative; left: -200px; top: -100px;"""
)
await bidi_session.browsing_context.navigate(
context=top_context["context"],
url=url,
wait="complete",
)
element = await get_element("#inner")
origin = get_element_origin(element)
actions = Actions()
(
actions.add_pointer(pointer_type="pen")
.pointer_move(x=-100, y=-100, origin=origin)
)
with pytest.raises(MoveTargetOutOfBoundsException):
await bidi_session.input.perform_actions(
actions=actions, context=top_context["context"]
)
@pytest.mark.parametrize("mode", ["open", "closed"])
@pytest.mark.parametrize("nested", [False, True], ids=["outer", "inner"])
async def test_pen_pointer_in_shadow_tree(

View File

@ -1,6 +1,5 @@
import pytest
from webdriver.bidi.error import MoveTargetOutOfBoundsException
from webdriver.bidi.modules.input import Actions, get_element_origin
from .. import get_events
@ -14,36 +13,6 @@ from . import (
pytestmark = pytest.mark.asyncio
@pytest.mark.parametrize("origin", ["element", "pointer", "viewport"])
async def test_params_actions_origin_outside_viewport(
bidi_session, get_actions_origin_page, top_context, get_element, origin
):
if origin == "element":
url = get_actions_origin_page(
"""width: 100px; height: 50px; background: green;
position: relative; left: -200px; top: -100px;"""
)
await bidi_session.browsing_context.navigate(
context=top_context["context"],
url=url,
wait="complete",
)
element = await get_element("#inner")
origin = get_element_origin(element)
actions = Actions()
(
actions.add_pointer(pointer_type="touch")
.pointer_move(x=-100, y=-100, origin=origin)
)
with pytest.raises(MoveTargetOutOfBoundsException):
await bidi_session.input.perform_actions(
actions=actions, context=top_context["context"]
)
@pytest.mark.parametrize("mode", ["open", "closed"])
@pytest.mark.parametrize("nested", [False, True], ids=["outer", "inner"])
async def test_touch_pointer_in_shadow_tree(

View File

@ -1,123 +0,0 @@
import asyncio
import pytest
from webdriver.bidi.modules.input import Actions
from tests.support.helpers import filter_supported_key_events
from tests.support.keys import Keys
from .. import add_mouse_listeners, get_events, get_keys_value
from ... import recursive_compare
pytestmark = pytest.mark.asyncio
async def test_parallel_key(bidi_session, top_context, setup_key_test):
actions_1 = Actions()
actions_1.add_key().send_keys("a").key_down(Keys.SHIFT)
actions_2 = Actions()
actions_2.add_key().send_keys("B").key_up(Keys.SHIFT)
# Run both actions in parallel to check that they are queued for
# sequential execution.
actions_performed = [
bidi_session.input.perform_actions(
actions=actions_1, context=top_context["context"]
),
bidi_session.input.perform_actions(
actions=actions_2, context=top_context["context"]
),
]
await asyncio.gather(*actions_performed)
expected = [
{"code": "KeyA", "key": "a", "type": "keydown"},
{"code": "KeyA", "key": "a", "type": "keypress"},
{"code": "KeyA", "key": "a", "type": "keyup"},
{"code": "ShiftLeft", "key": "Shift", "type": "keydown"},
{"code": "KeyB", "key": "B", "type": "keydown"},
{"code": "KeyB", "key": "B", "type": "keypress"},
{"code": "KeyB", "key": "B", "type": "keyup"},
{"code": "ShiftLeft", "key": "Shift", "type": "keyup"},
]
all_events = await get_events(bidi_session, top_context["context"])
(key_events, expected) = filter_supported_key_events(all_events, expected)
recursive_compare(expected, key_events)
keys_value = await get_keys_value(bidi_session, top_context["context"])
assert keys_value == "aB"
async def test_parallel_pointer(bidi_session, get_test_page, top_context):
url = get_test_page()
await bidi_session.browsing_context.navigate(
context=top_context["context"],
url=url,
wait="complete")
await add_mouse_listeners(bidi_session, top_context)
point_1 = {"x": 5, "y": 10}
point_2 = {"x": 10, "y": 20}
actions_1 = Actions()
(
actions_1.add_pointer()
.pointer_move(x=point_1["x"], y=point_1["y"])
.pointer_down(button=0)
.pointer_up(button=0)
)
actions_2 = Actions()
(
actions_2.add_pointer()
.pointer_move(x=point_2["x"], y=point_2["y"])
.pointer_down(button=0)
.pointer_up(button=0)
)
# Run both actions in parallel to check that they are queued for
# sequential execution.
actions_performed = [
bidi_session.input.perform_actions(
actions=actions_1, context=top_context["context"]
),
bidi_session.input.perform_actions(
actions=actions_2, context=top_context["context"]
),
]
await asyncio.gather(*actions_performed)
common_attributes = {
"button": 0,
"buttons": 0,
"detail": 1,
"isTrusted": True,
"clientX": point_1["x"],
"clientY": point_1["y"],
}
mouse_events = [
{"type": "mousemove"},
{"type": "mousedown", "buttons": 1},
{"type": "mouseup"},
{"type": "click"},
]
# Expected events for the first action.
expected_events_1 = [{**common_attributes, **event}
for event in mouse_events]
# Expected events for the second action.
common_attributes.update(
{"clientX": point_2["x"], "clientY": point_2["y"]})
expected_events_2 = [{**common_attributes, **event}
for event in mouse_events]
events = await get_events(bidi_session, top_context["context"])
assert events[:4] == expected_events_1
assert events[4:] == expected_events_2

View File

@ -1,6 +1,6 @@
import pytest
from webdriver.bidi.error import MoveTargetOutOfBoundsException, NoSuchFrameException
from webdriver.bidi.error import NoSuchFrameException
from webdriver.bidi.modules.input import Actions, get_element_origin
from webdriver.bidi.modules.script import ContextTarget
@ -20,23 +20,6 @@ async def test_invalid_browsing_context(bidi_session):
await bidi_session.input.perform_actions(actions=actions, context="foo")
@pytest.mark.parametrize("origin", ["element", "viewport"])
async def test_params_actions_origin_outside_viewport(
bidi_session, setup_wheel_test, top_context, get_element, origin
):
if origin == "element":
element = await get_element("#scrollable")
origin = get_element_origin(element)
actions = Actions()
actions.add_wheel().scroll(x=-100, y=-100, delta_x=10, delta_y=20, origin=origin)
with pytest.raises(MoveTargetOutOfBoundsException):
await bidi_session.input.perform_actions(
actions=actions, context=top_context["context"]
)
@pytest.mark.parametrize("delta_x, delta_y", [(0, 10), (5, 0), (5, 10)])
async def test_scroll_not_scrollable(
bidi_session, setup_wheel_test, top_context, get_element, delta_x, delta_y

View File

@ -1,126 +0,0 @@
import asyncio
import pytest
from webdriver.bidi.modules.input import Actions
from tests.support.helpers import filter_supported_key_events
from tests.support.keys import Keys
from .. import add_mouse_listeners, get_events, get_keys_value
from ... import recursive_compare
pytestmark = pytest.mark.asyncio
async def test_parallel_key(bidi_session, top_context, setup_key_test):
actions_1 = Actions()
actions_1.add_key().key_down("a").key_down(Keys.SHIFT)
actions_2 = Actions()
actions_2.add_key().key_down("b")
# Run the first release actions in-between to check that it is queued for
# sequential execution, and the state is reset before the 2nd action.
actions_performed = [
bidi_session.input.perform_actions(
actions=actions_1, context=top_context["context"]
),
bidi_session.input.release_actions(context=top_context["context"]),
bidi_session.input.perform_actions(
actions=actions_2, context=top_context["context"]
),
bidi_session.input.release_actions(context=top_context["context"]),
]
await asyncio.gather(*actions_performed)
expected = [
{"code": "KeyA", "key": "a", "type": "keydown"},
{"code": "KeyA", "key": "a", "type": "keypress"},
{"code": "ShiftLeft", "key": "Shift", "type": "keydown"},
{"code": "ShiftLeft", "key": "Shift", "type": "keyup"},
{"code": "KeyA", "key": "a", "type": "keyup"},
{"code": "KeyB", "key": "b", "type": "keydown"},
{"code": "KeyB", "key": "b", "type": "keypress"},
{"code": "KeyB", "key": "b", "type": "keyup"},
]
all_events = await get_events(bidi_session, top_context["context"])
(key_events, expected) = filter_supported_key_events(all_events, expected)
recursive_compare(expected, key_events)
keys_value = await get_keys_value(bidi_session, top_context["context"])
assert keys_value == "ab"
async def test_parallel_pointer(bidi_session, get_test_page, top_context):
url = get_test_page()
await bidi_session.browsing_context.navigate(
context=top_context["context"],
url=url,
wait="complete")
await add_mouse_listeners(bidi_session, top_context)
point_1 = {"x": 5, "y": 10}
point_2 = {"x": 10, "y": 20}
actions_1 = Actions()
(
actions_1.add_pointer()
.pointer_move(x=point_1["x"], y=point_1["y"])
.pointer_down(button=0)
)
actions_2 = Actions()
(
actions_2.add_pointer()
.pointer_move(x=point_2["x"], y=point_2["y"])
.pointer_down(button=0)
)
# Run the first release actions in-between to check that it is queued for
# sequential execution, and the state is reset before the 2nd action.
actions_performed = [
bidi_session.input.perform_actions(
actions=actions_1, context=top_context["context"]
),
bidi_session.input.release_actions(context=top_context["context"]),
bidi_session.input.perform_actions(
actions=actions_2, context=top_context["context"]
),
bidi_session.input.release_actions(context=top_context["context"]),
]
await asyncio.gather(*actions_performed)
common_attributes = {
"button": 0,
"buttons": 0,
"detail": 1,
"isTrusted": True,
"clientX": point_1["x"],
"clientY": point_1["y"],
}
mouse_events = [
{"type": "mousemove"},
{"type": "mousedown", "buttons": 1},
{"type": "mouseup"},
{"type": "click"},
]
# Expected events for the first action.
expected_events_1 = [{**common_attributes, **event}
for event in mouse_events]
# Expected events for the second action.
common_attributes.update(
{"clientX": point_2["x"], "clientY": point_2["y"]})
expected_events_2 = [{**common_attributes, **event}
for event in mouse_events]
events = await get_events(bidi_session, top_context["context"])
assert events[:4] == expected_events_1
assert events[4:] == expected_events_2

View File

@ -1,11 +1,6 @@
import pytest
from webdriver.error import (
InvalidArgumentException,
MoveTargetOutOfBoundsException,
NoSuchWindowException,
StaleElementReferenceException,
)
from webdriver.error import InvalidArgumentException, NoSuchWindowException, StaleElementReferenceException
from tests.classic.perform_actions.support.mouse import (
get_inview_center,
@ -42,15 +37,6 @@ def test_stale_element_reference(session, stale_element, mouse_chain, as_frame):
mouse_chain.click(element=element).perform()
@pytest.mark.parametrize("origin", ["element", "pointer", "viewport"])
def test_params_actions_origin_outside_viewport(session, test_actions_page, mouse_chain, origin):
if origin == "element":
origin = session.find.css("#outer", all=False)
with pytest.raises(MoveTargetOutOfBoundsException):
mouse_chain.pointer_move(-100, -100, origin=origin).perform()
def test_click_at_coordinates(session, test_actions_page, mouse_chain):
div_point = {
"x": 82,

View File

@ -1,10 +1,6 @@
import pytest
from webdriver.error import (
MoveTargetOutOfBoundsException,
NoSuchWindowException,
StaleElementReferenceException,
)
from webdriver.error import NoSuchWindowException, StaleElementReferenceException
from tests.classic.perform_actions.support.mouse import (
get_inview_center,
@ -38,15 +34,6 @@ def test_stale_element_reference(session, stale_element, pen_chain, as_frame):
pen_chain.click(element=element).perform()
@pytest.mark.parametrize("origin", ["element", "pointer", "viewport"])
def test_params_actions_origin_outside_viewport(session, test_actions_page, pen_chain, origin):
if origin == "element":
origin = session.find.css("#outer", all=False)
with pytest.raises(MoveTargetOutOfBoundsException):
pen_chain.pointer_move(-100, -100, origin=origin).perform()
@pytest.mark.parametrize("mode", ["open", "closed"])
@pytest.mark.parametrize("nested", [False, True], ids=["outer", "inner"])
def test_pen_pointer_in_shadow_tree(

View File

@ -1,10 +1,6 @@
import pytest
from webdriver.error import (
MoveTargetOutOfBoundsException,
NoSuchWindowException,
StaleElementReferenceException
)
from webdriver.error import NoSuchWindowException, StaleElementReferenceException
from tests.classic.perform_actions.support.mouse import (
get_inview_center,
get_viewport_rect,
@ -13,7 +9,6 @@ from tests.classic.perform_actions.support.refine import get_events
from . import assert_pointer_events, record_pointer_events
def test_null_response_value(session, touch_chain):
value = touch_chain.click().perform()
assert value is None
@ -37,15 +32,6 @@ def test_stale_element_reference(session, stale_element, touch_chain, as_frame):
touch_chain.click(element=element).perform()
@pytest.mark.parametrize("origin", ["element", "pointer", "viewport"])
def test_params_actions_origin_outside_viewport(session, test_actions_page, touch_chain, origin):
if origin == "element":
origin = session.find.css("#outer", all=False)
with pytest.raises(MoveTargetOutOfBoundsException):
touch_chain.pointer_move(-100, -100, origin=origin).perform()
@pytest.mark.parametrize("mode", ["open", "closed"])
@pytest.mark.parametrize("nested", [False, True], ids=["outer", "inner"])
def test_touch_pointer_in_shadow_tree(

View File

@ -1,6 +1,6 @@
import pytest
from webdriver.error import MoveTargetOutOfBoundsException, NoSuchWindowException
from webdriver.error import NoSuchWindowException
import time
from tests.classic.perform_actions.support.refine import get_events
@ -23,17 +23,6 @@ def test_no_browsing_context(session, closed_window, wheel_chain):
wheel_chain.scroll(0, 0, 0, 10).perform()
@pytest.mark.parametrize("origin", ["element", "viewport"])
def test_params_actions_origin_outside_viewport(
session, test_actions_scroll_page, wheel_chain, origin
):
if origin == "element":
origin = session.find.css("#scrollable", all=False)
with pytest.raises(MoveTargetOutOfBoundsException):
wheel_chain.scroll(-100, -100, 10, 20, origin="viewport").perform()
def test_scroll_not_scrollable(session, test_actions_scroll_page, wheel_chain):
target = session.find.css("#not-scrollable", all=False)