Bug 1904665 - [remote] Prepare the Remote Agent code base for parent process event dispatching. r=webdriver-reviewers,jdescottes

Differential Revision: https://phabricator.services.mozilla.com/D220543
This commit is contained in:
Henrik Skupin 2024-09-10 13:12:33 +00:00
Родитель 861fc2a11e
Коммит dee4668b17
11 изменённых файлов: 1118 добавлений и 527 удалений

Просмотреть файл

@ -9,11 +9,14 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
accessibility:
"chrome://remote/content/shared/webdriver/Accessibility.sys.mjs",
action: "chrome://remote/content/shared/webdriver/Actions.sys.mjs",
AnimationFramePromise: "chrome://remote/content/shared/Sync.sys.mjs",
assertInViewPort: "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",
executeSoon: "chrome://remote/content/shared/Sync.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",
@ -37,8 +40,6 @@ 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() {
@ -59,6 +60,65 @@ 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`
);
}
}
async #finalizeAction() {
// Terminate the current wheel transaction if there is one. Wheel
// transactions should not live longer than a single action chain.
ChromeUtils.endWheelTransaction();
// Wait for the next animation frame to make sure the page's content
// was updated.
await lazy.AnimationFramePromise(this.contentWindow);
}
#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");
@ -77,6 +137,19 @@ 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:_getInViewCentrePoint":
result = this.#getInViewCentrePoint(data);
break;
case "MarionetteCommandsParent:_finalizeAction":
this.#finalizeAction();
break;
case "MarionetteCommandsParent:clearElement":
this.clearElement(data);
waitForNextTick = true;
@ -140,13 +213,6 @@ 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;
@ -164,7 +230,7 @@ export class MarionetteCommandsChild extends JSWindowActorChild {
// Inform the content process that the command has completed. It allows
// it to process async follow-up tasks before the reply is sent.
if (waitForNextTick) {
await new Promise(resolve => Services.tm.dispatchToMainThread(resolve));
await new Promise(resolve => lazy.executeSoon(resolve));
}
const { seenNodeIds, serializedValue, hasSerializedWindows } =
@ -472,42 +538,6 @@ 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.
*/

Просмотреть файл

@ -5,12 +5,14 @@
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", () =>
@ -22,12 +24,146 @@ 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,
});
}
/**
* Finalize an action command.
*
* @returns {Promise}
* Promise that resolves when the finalization is done.
*/
#finalizeAction() {
return this.sendQuery("MarionetteCommandsParent:_finalizeAction");
}
/**
* 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,
@ -243,13 +379,39 @@ export class MarionetteCommandsParent extends JSWindowActorParent {
}
async performActions(actions) {
return this.sendQuery("MarionetteCommandsParent:performActions", {
// 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,
actions,
});
this.#actionsOptions
);
await actionChain.dispatch(this.#actionState, this.#actionsOptions);
// Process async follow-up tasks in content before the reply is sent.
await this.#finalizeAction();
}
/**
* 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() {
return this.sendQuery("MarionetteCommandsParent:releaseActions");
// Bug 1821460: Use top-level browsing context.
if (this.#actionState === null) {
return;
}
await this.#actionState.release(this.#actionsOptions);
this.#actionState = null;
// Process async follow-up tasks in content before the reply is sent.
await this.#finalizeAction();
}
async switchToFrame(id) {

Просмотреть файл

@ -8,6 +8,7 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
executeSoon: "chrome://remote/content/shared/Sync.sys.mjs",
Log: "chrome://remote/content/shared/Log.sys.mjs",
});
@ -19,20 +20,6 @@ const { TYPE_ONE_SHOT, TYPE_REPEATING_SLACK } = Ci.nsITimer;
const PROMISE_TIMEOUT = AppConstants.DEBUG ? 4500 : 1500;
/**
* Dispatch a function to be executed on the main thread.
*
* @param {Function} func
* Function to be executed.
*/
export function executeSoon(func) {
if (typeof func != "function") {
throw new TypeError();
}
Services.tm.dispatchToMainThread(func);
}
/**
* Runs a Promise-like function off the main thread until it is resolved
* through ``resolve`` or ``rejected`` callbacks. The function is
@ -323,7 +310,7 @@ export function MessageManagerDestroyedPromise(messageManager) {
*/
export function IdlePromise(win) {
const animationFramePromise = new Promise(resolve => {
executeSoon(() => {
lazy.executeSoon(() => {
win.requestAnimationFrame(resolve);
});
});

Просмотреть файл

@ -67,24 +67,6 @@ class MockTimer {
}
}
add_task(function test_executeSoon_callback() {
// executeSoon() is already defined for xpcshell in head.js. As such import
// our implementation into a custom namespace.
let sync = ChromeUtils.importESModule(
"chrome://remote/content/marionette/sync.sys.mjs"
);
for (let func of ["foo", null, true, [], {}]) {
Assert.throws(() => sync.executeSoon(func), /TypeError/);
}
let a;
sync.executeSoon(() => {
a = 1;
});
executeSoon(() => equal(1, a));
});
add_task(function test_PollPromise_funcTypes() {
for (let type of ["foo", 42, null, undefined, true, [], {}]) {
Assert.throws(() => new PollPromise(type), /TypeError/);

Просмотреть файл

@ -8,7 +8,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
EventEmitter: "resource://gre/modules/EventEmitter.sys.mjs",
BulkPacket: "chrome://remote/content/marionette/packets.sys.mjs",
executeSoon: "chrome://remote/content/marionette/sync.sys.mjs",
executeSoon: "chrome://remote/content/shared/Sync.sys.mjs",
JSONPacket: "chrome://remote/content/marionette/packets.sys.mjs",
Packet: "chrome://remote/content/marionette/packets.sys.mjs",
StreamUtils: "chrome://remote/content/marionette/stream-utils.sys.mjs",

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

@ -453,6 +453,13 @@
"expectations": ["FAIL"],
"comment": "TODO: add a comment explaining why this expectation is required (include links to issues)"
},
{
"testIdPattern": "[mouse.spec] Mouse should not throw if clicking in parallel",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["firefox", "webDriverBiDi"],
"expectations": ["FAIL"],
"comment": "Needs support for action queue: https://bugzilla.mozilla.org/show_bug.cgi?id=1915798"
},
{
"testIdPattern": "[mouse.spec] Mouse should reset properly",
"platforms": ["darwin", "linux", "win32"],

Просмотреть файл

@ -7,6 +7,7 @@ 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",
@ -16,8 +17,189 @@ 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 },
});
}
/**
* Finalize an action command.
*
* @param {BrowsingContext} context
* The browsing context to forward the command to.
*
* @returns {Promise}
* Promise that resolves when the finalization is done.
*/
#finalizeAction(context) {
return this.messageHandler.forwardCommand({
moduleName: "input",
commandName: "_finalizeAction",
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 },
});
}
/**
* 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"
);
}
async performActions(options = {}) {
const { actions, context: contextId } = options;
@ -34,20 +216,22 @@ class InputModule extends RootBiDiModule {
}
// 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);
}
await this.messageHandler.forwardCommand({
moduleName: "input",
commandName: "performActions",
destination: {
type: lazy.WindowGlobalMessageHandler.type,
id: context.id,
},
params: {
actions,
},
});
const actionsOptions = { ...this.#actionsOptions, context };
const actionChain = await lazy.action.Chain.fromJSON(
inputState,
actions,
actionsOptions
);
await actionChain.dispatch(inputState, actionsOptions);
return {};
// Process async follow-up tasks in content before the reply is sent.
await this.#finalizeAction(context);
}
/**
@ -78,18 +262,17 @@ class InputModule extends RootBiDiModule {
}
// Bug 1821460: Fetch top-level browsing context.
let inputState = this.#inputStates.get(context);
if (inputState === undefined) {
return;
}
await this.messageHandler.forwardCommand({
moduleName: "input",
commandName: "releaseActions",
destination: {
type: lazy.WindowGlobalMessageHandler.type,
id: context.id,
},
params: {},
});
const actionsOptions = { ...this.#actionsOptions, context };
await inputState.release(actionsOptions);
this.#inputStates.delete(context);
return {};
// Process async follow-up tasks in content before the reply is sent.
this.#finalizeAction(context);
}
/**

Просмотреть файл

@ -7,45 +7,96 @@ import { WindowGlobalBiDiModule } from "chrome://remote/content/webdriver-bidi/m
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
action: "chrome://remote/content/shared/webdriver/Actions.sys.mjs",
AnimationFramePromise: "chrome://remote/content/shared/Sync.sys.mjs",
assertInViewPort: "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() {}
async performActions(options) {
const { actions } = options;
if (this.#actionState === null) {
this.#actionState = new lazy.action.State();
_assertInViewPort(options = {}) {
const { target } = options;
return lazy.assertInViewPort(target, this.messageHandler.window);
}
async _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`);
}
}
await this.#deserializeActionOrigins(actions);
const actionChain = lazy.action.Chain.fromJSON(this.#actionState, actions);
await actionChain.dispatch(this.#actionState, this.messageHandler.window);
_finalizeAction() {
// Terminate the current wheel transaction if there is one. Wheel
// transactions should not live longer than a single action chain.
ChromeUtils.endWheelTransaction();
// Wait for the next animation frame to make sure the page's content
// was updated.
return lazy.AnimationFramePromise(this.messageHandler.window);
}
async releaseActions() {
if (this.#actionState === null) {
return;
}
await this.#actionState.release(this.messageHandler.window);
this.#actionState = null;
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 setFiles(options) {
@ -109,50 +160,6 @@ 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(