Bug 1855040 - Implement "input.setFiles" command. r=webdriver-reviewers,whimboo,jdescottes

Differential Revision: https://phabricator.services.mozilla.com/D203285
This commit is contained in:
Alexandra Borovova 2024-03-12 09:41:24 +00:00
parent 4e99ec1bc9
commit 4873bc8217
6 changed files with 153 additions and 12 deletions

View file

@ -41,6 +41,7 @@ const ERRORS = new Set([
"TimeoutError",
"UnableToCaptureScreen",
"UnableToSetCookieError",
"UnableToSetFileInputError",
"UnexpectedAlertOpenError",
"UnknownCommandError",
"UnknownError",
@ -756,6 +757,21 @@ class UnableToSetCookieError extends WebDriverError {
}
}
/**
* A command to set a file could not be satisfied.
*
* @param {string=} message
* Optional string describing error situation.
* @param {object=} data
* Additional error data helpful in diagnosing the error.
*/
class UnableToSetFileInputError extends WebDriverError {
constructor(message, data = {}) {
super(message, data);
this.status = "unable to set file input";
}
}
/**
* A command to capture a screenshot could not be satisfied.
*
@ -865,6 +881,7 @@ const STATUSES = new Map([
["timeout", TimeoutError],
["unable to capture screen", UnableToCaptureScreen],
["unable to set cookie", UnableToSetCookieError],
["unable to set file input", UnableToSetFileInputError],
["unexpected alert open", UnexpectedAlertOpenError],
["unknown command", UnknownCommandError],
["unknown error", UnknownError],

View file

@ -278,6 +278,10 @@ event.mouseup = function (el, modifiers = {}, opts = {}) {
return event.sendEvent("mouseup", el, modifiers, opts);
};
event.cancel = function (el, modifiers = {}, opts = {}) {
return event.sendEvent("cancel", el, modifiers, opts);
};
event.click = function (el, modifiers = {}, opts = {}) {
return event.sendEvent("click", el, modifiers, opts);
};

View file

@ -36,6 +36,7 @@ const errors = [
error.StaleElementReferenceError,
error.TimeoutError,
error.UnableToSetCookieError,
error.UnableToSetFileInputError,
error.UnexpectedAlertOpenError,
error.UnknownCommandError,
error.UnknownError,
@ -510,6 +511,14 @@ add_task(function test_UnableToSetCookieError() {
ok(err instanceof error.WebDriverError);
});
add_task(function test_UnableToSetFileInputError() {
let err = new error.UnableToSetFileInputError("foo");
equal("UnableToSetFileInputError", err.name);
equal("foo", err.message);
equal("unable to set file input", err.status);
ok(err instanceof error.WebDriverError);
});
add_task(function test_UnexpectedAlertOpenError() {
let err = new error.UnexpectedAlertOpenError("foo");
equal("UnexpectedAlertOpenError", err.name);

View file

@ -467,13 +467,6 @@
"expectations": ["SKIP"],
"comment": "TODO: add a comment explaining why this expectation is required (include links to issues)"
},
{
"testIdPattern": "[input.spec] input tests ElementHandle.uploadFile *",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["firefox", "webDriverBiDi"],
"expectations": ["FAIL"],
"comment": "TODO: add a comment explaining why this expectation is required (include links to issues)"
},
{
"testIdPattern": "[jshandle.spec] JSHandle JSHandle.jsonValue should not throw for circular objects",
"platforms": ["darwin", "linux", "win32"],

View file

@ -91,6 +91,63 @@ class InputModule extends Module {
return {};
}
/**
* Sets the file property of a given input element with type file to a set of file paths.
*
* @param {object=} options
* @param {string} options.context
* Id of the browsing context to set the file property
* of a given input element.
* @param {SharedReference} options.element
* A reference to a node, which is used as
* a target for setting files.
* @param {Array<string>} options.files
* A list of file paths which should be set.
*
* @throws {InvalidArgumentError}
* Raised if an argument is of an invalid type or value.
* @throws {NoSuchElementError}
* If the input element cannot be found.
* @throws {NoSuchFrameError}
* If the browsing context cannot be found.
* @throws {UnableToSetFileInputError}
* If the set of file paths was not set to the input element.
*/
async setFiles(options = {}) {
const { context: contextId, element, files } = options;
lazy.assert.string(
contextId,
`Expected "context" to be a string, got ${contextId}`
);
const context = lazy.TabManager.getBrowsingContextById(contextId);
if (!context) {
throw new lazy.error.NoSuchFrameError(
`Browsing context with id ${contextId} not found`
);
}
lazy.assert.array(files, `Expected "files" to be an array, got ${files}`);
for (const file of files) {
lazy.assert.string(
file,
`Expected an element of "files" to be a string, got ${file}`
);
}
await this.messageHandler.forwardCommand({
moduleName: "input",
commandName: "setFiles",
destination: {
type: lazy.WindowGlobalMessageHandler.type,
id: context.id,
},
params: { element, files },
});
}
static get supportedEvents() {
return [];
}

View file

@ -10,6 +10,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
action: "chrome://remote/content/shared/webdriver/Actions.sys.mjs",
dom: "chrome://remote/content/shared/DOM.sys.mjs",
error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
event: "chrome://remote/content/shared/webdriver/Event.sys.mjs",
});
class InputModule extends WindowGlobalBiDiModule {
@ -43,6 +44,67 @@ class InputModule extends WindowGlobalBiDiModule {
this.#actionState = null;
}
async setFiles(options) {
const { element: sharedReference, files } = options;
const element = await this.#deserializeElementSharedReference(
sharedReference
);
if (
!HTMLInputElement.isInstance(element) ||
element.type !== "file" ||
element.disabled
) {
throw new lazy.error.UnableToSetFileInputError(
`Element needs to be an <input> element with type "file" and not disabled`
);
}
if (files.length > 1 && !element.hasAttribute("multiple")) {
throw new lazy.error.UnableToSetFileInputError(
`Element should have an attribute "multiple" set when trying to set more than 1 file`
);
}
const fileObjects = [];
for (const file of files) {
try {
fileObjects.push(await File.createFromFileName(file));
} catch (e) {
throw new lazy.error.InvalidArgumentError(
`Failed to add file ${file} (${e})`
);
}
}
const selectedFiles = Array.from(element.files);
const intersection = fileObjects.filter(fileObject =>
selectedFiles.some(
selectedFile =>
// Compare file fields to identify if the files are equal.
// TODO: Bug 1883856. Add check for full path or use a different way
// to compare files when it's available.
selectedFile.name === fileObject.name &&
selectedFile.size === fileObject.size &&
selectedFile.type === fileObject.type
)
);
if (
intersection.length === selectedFiles.length &&
selectedFiles.length === fileObjects.length
) {
lazy.event.cancel(element);
} else {
element.mozSetFileArray(fileObjects);
lazy.event.input(element);
lazy.event.change(element);
}
}
/**
* In the provided array of input.SourceActions, replace all origins matching
* the input.ElementOrigin production with the Element corresponding to this
@ -75,8 +137,8 @@ class InputModule extends WindowGlobalBiDiModule {
if (action?.origin?.type === "element") {
promises.push(
(async () => {
action.origin = await this.#getElementFromElementOrigin(
action.origin
action.origin = await this.#deserializeElementSharedReference(
action.origin.element
);
})()
);
@ -87,11 +149,10 @@ class InputModule extends WindowGlobalBiDiModule {
return Promise.all(promises);
}
async #getElementFromElementOrigin(origin) {
const sharedReference = origin.element;
async #deserializeElementSharedReference(sharedReference) {
if (typeof sharedReference?.sharedId !== "string") {
throw new lazy.error.InvalidArgumentError(
`Expected "origin.element" to be a SharedReference, got: ${sharedReference}`
`Expected "element" to be a SharedReference, got: ${sharedReference}`
);
}