forked from mirrors/gecko-dev
Bug 1855040 - Implement "input.setFiles" command. r=webdriver-reviewers,whimboo,jdescottes
Differential Revision: https://phabricator.services.mozilla.com/D203285
This commit is contained in:
parent
4e99ec1bc9
commit
4873bc8217
6 changed files with 153 additions and 12 deletions
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
|
|
|
|||
|
|
@ -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 [];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}`
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue