зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
861fc2a11e
Коммит
dee4668b17
|
@ -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", {
|
||||
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,
|
||||
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 },
|
||||
];
|
||||
checkFromJSONErrors(
|
||||
inputTickActions,
|
||||
/Expected "origin" to be undefined, "viewport", "pointer", or an element/,
|
||||
`actionItem.origin: ${origin}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
add_task(function test_processPointerMoveActionElementOrigin() {
|
||||
let state = new action.State();
|
||||
const inputTickActions = [
|
||||
{
|
||||
type: "pointer",
|
||||
duration: 5000,
|
||||
subtype: "pointerMove",
|
||||
origin: domEl,
|
||||
x: 0,
|
||||
y: 0,
|
||||
duration: 5000,
|
||||
subtype: "pointerMove",
|
||||
origin,
|
||||
},
|
||||
];
|
||||
const chain = action.Chain.fromJSON(state, chainForTick(inputTickActions));
|
||||
deepEqual(chain[0][0].origin.element, domEl);
|
||||
await checkFromJSONErrors(
|
||||
inputTickActions,
|
||||
/Expected "origin" to be undefined, "viewport", "pointer", or an element/,
|
||||
`actionItem.origin: ${origin}`,
|
||||
{ isElementOrigin: () => false }
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
add_task(function test_processPointerMoveActionDefaultOrigin() {
|
||||
add_task(async function test_processPointerMoveActionOriginElementValidation() {
|
||||
const element = { foo: "bar" };
|
||||
const inputTickActions = [
|
||||
{
|
||||
type: "pointer",
|
||||
x: 0,
|
||||
y: 0,
|
||||
duration: 5000,
|
||||
subtype: "pointerMove",
|
||||
origin: element,
|
||||
},
|
||||
];
|
||||
|
||||
// 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(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: {
|
||||
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);
|
||||
}
|
||||
|
||||
await this.#deserializeActionOrigins(actions);
|
||||
const actionChain = lazy.action.Chain.fromJSON(this.#actionState, actions);
|
||||
async _dispatchEvent(options = {}) {
|
||||
const { eventName, details } = options;
|
||||
|
||||
await actionChain.dispatch(this.#actionState, this.messageHandler.window);
|
||||
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`);
|
||||
}
|
||||
}
|
||||
|
||||
_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;
|
||||
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 };
|
||||
});
|
||||
}
|
||||
await this.#actionState.release(this.messageHandler.window);
|
||||
this.#actionState = null;
|
||||
|
||||
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(
|
||||
|
|
Загрузка…
Ссылка в новой задаче