diff options
Diffstat (limited to 'remote/webdriver-bidi/modules')
6 files changed, 356 insertions, 67 deletions
diff --git a/remote/webdriver-bidi/modules/root/browsingContext.sys.mjs b/remote/webdriver-bidi/modules/root/browsingContext.sys.mjs index bc600e89cd..f2a5d5e645 100644 --- a/remote/webdriver-bidi/modules/root/browsingContext.sys.mjs +++ b/remote/webdriver-bidi/modules/root/browsingContext.sys.mjs @@ -232,11 +232,27 @@ class BrowsingContextModule extends Module { ); } - const tab = lazy.TabManager.getTabForBrowsingContext(context); - const window = lazy.TabManager.getWindowForTab(tab); + const targetTab = lazy.TabManager.getTabForBrowsingContext(context); + const targetWindow = lazy.TabManager.getWindowForTab(targetTab); + const selectedTab = lazy.TabManager.getTabBrowser(targetWindow).selectedTab; + + const activated = [ + lazy.windowManager.focusWindow(targetWindow), + lazy.TabManager.selectTab(targetTab), + ]; - await lazy.windowManager.focusWindow(window); - await lazy.TabManager.selectTab(tab); + if (targetTab !== selectedTab && !lazy.AppInfo.isAndroid) { + // We need to wait until the "document.visibilityState" of the currently + // selected tab in the target window is marked as "hidden". + // + // Bug 1884142: It's not supported on Android for the TestRunner package. + const selectedBrowser = lazy.TabManager.getBrowserForTab(selectedTab); + activated.push( + this.#waitForVisibilityChange(selectedBrowser.browsingContext) + ); + } + + await Promise.all(activated); } /** @@ -532,33 +548,58 @@ class BrowsingContextModule extends Module { const type = lazy.AppInfo.isAndroid ? "tab" : typeHint; switch (type) { - case "window": + case "window": { const newWindow = await lazy.windowManager.openBrowserWindow({ focus: !background, userContextId: userContext, }); browser = lazy.TabManager.getTabBrowser(newWindow).selectedBrowser; break; - - case "tab": + } + case "tab": { if (!lazy.TabManager.supportsTabs()) { throw new lazy.error.UnsupportedOperationError( `browsingContext.create with type "tab" not supported in ${lazy.AppInfo.name}` ); } + // The window to open the new tab in. + let window = Services.wm.getMostRecentWindow(null); + let referenceTab; if (referenceContext !== null) { referenceTab = lazy.TabManager.getTabForBrowsingContext(referenceContext); + window = lazy.TabManager.getWindowForTab(referenceTab); } - const tab = await lazy.TabManager.addTab({ - focus: !background, - referenceTab, - userContextId: userContext, - }); + const promises = []; + + if (!background && !lazy.AppInfo.isAndroid) { + // When opening a new foreground tab we need to wait until the + // "document.visibilityState" of the currently selected tab in this + // window is marked as "hidden". + // + // Bug 1884142: It's not supported on Android for the TestRunner package. + const selectedTab = lazy.TabManager.getTabBrowser(window).selectedTab; + promises.push( + this.#waitForVisibilityChange( + lazy.TabManager.getBrowserForTab(selectedTab).browsingContext + ) + ); + } + + promises.unshift( + lazy.TabManager.addTab({ + focus: !background, + referenceTab, + userContextId: userContext, + }) + ); + + const [tab] = await Promise.all(promises); browser = lazy.TabManager.getBrowserForTab(tab); + } } await lazy.waitForInitialNavigationCompleted( @@ -1918,6 +1959,21 @@ class BrowsingContextModule extends Module { } } + #waitForVisibilityChange(browsingContext) { + return this.messageHandler.forwardCommand({ + moduleName: "browsingContext", + commandName: "_awaitVisibilityState", + destination: { + type: lazy.WindowGlobalMessageHandler.type, + id: browsingContext.id, + }, + params: { + value: "hidden", + }, + retryOnAbort: true, + }); + } + /** * Internal commands */ diff --git a/remote/webdriver-bidi/modules/root/input.sys.mjs b/remote/webdriver-bidi/modules/root/input.sys.mjs index 8edd8299b7..0764ac3f92 100644 --- a/remote/webdriver-bidi/modules/root/input.sys.mjs +++ b/remote/webdriver-bidi/modules/root/input.sys.mjs @@ -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 []; } diff --git a/remote/webdriver-bidi/modules/root/storage.sys.mjs b/remote/webdriver-bidi/modules/root/storage.sys.mjs index 50fbd8ecd6..3eced2da4c 100644 --- a/remote/webdriver-bidi/modules/root/storage.sys.mjs +++ b/remote/webdriver-bidi/modules/root/storage.sys.mjs @@ -12,6 +12,8 @@ ChromeUtils.defineESModuleGetters(lazy, { "chrome://remote/content/webdriver-bidi/modules/root/network.sys.mjs", error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs", TabManager: "chrome://remote/content/shared/TabManager.sys.mjs", + UserContextManager: + "chrome://remote/content/shared/UserContextManager.sys.mjs", }); const CookieFieldsMapping = { @@ -96,14 +98,14 @@ class StorageModule extends Module { * * @property {PartitionType} [type=PartitionType.storageKey] * @property {string=} sourceOrigin - * @property {string=} userContext (not supported) + * @property {string=} userContext */ /** * @typedef PartitionKey * * @property {string=} sourceOrigin - * @property {string=} userContext (not supported) + * @property {string=} userContext */ /** @@ -119,6 +121,48 @@ class StorageModule extends Module { */ /** + * Remove zero or more cookies which match a set of provided parameters. + * + * @param {object=} options + * @param {CookieFilter=} options.filter + * An object which holds field names and values, which + * should be used to filter the output of the command. + * @param {PartitionDescriptor=} options.partition + * An object which holds the information which + * should be used to build a partition key. + * + * @returns {PartitionKey} + * An object with the partition key which was used to + * retrieve cookies which had to be removed. + * @throws {InvalidArgumentError} + * If the provided arguments are not valid. + * @throws {NoSuchFrameError} + * If the provided browsing context cannot be found. + */ + async deleteCookies(options = {}) { + let { filter = {} } = options; + const { partition: partitionSpec = null } = options; + + this.#assertPartition(partitionSpec); + filter = this.#assertCookieFilter(filter); + + const partitionKey = this.#expandStoragePartitionSpec(partitionSpec); + const store = this.#getTheCookieStore(partitionKey); + const cookies = this.#getMatchingCookies(store, filter); + + for (const cookie of cookies) { + Services.cookies.remove( + cookie.host, + cookie.name, + cookie.path, + cookie.originAttributes + ); + } + + return { partitionKey: this.#formatPartitionKey(partitionKey) }; + } + + /** * Retrieve zero or more cookies which match a set of provided parameters. * * @param {object=} options @@ -136,27 +180,27 @@ class StorageModule extends Module { * If the provided arguments are not valid. * @throws {NoSuchFrameError} * If the provided browsing context cannot be found. - * @throws {UnsupportedOperationError} - * Raised when the command is called with `userContext` as - * in `partition` argument. */ async getCookies(options = {}) { let { filter = {} } = options; const { partition: partitionSpec = null } = options; this.#assertPartition(partitionSpec); - filter = this.#assertGetCookieFilter(filter); + filter = this.#assertCookieFilter(filter); const partitionKey = this.#expandStoragePartitionSpec(partitionSpec); const store = this.#getTheCookieStore(partitionKey); const cookies = this.#getMatchingCookies(store, filter); + const serializedCookies = []; - // Bug 1875255. Exchange platform id for Webdriver BiDi id for the user context to return it to the client. - // For now we use platform user context id for returning cookies for a specific browsing context in the platform API, - // but we can not return it directly to the client, so for now we just remove it from the response. - delete partitionKey.userContext; + for (const cookie of cookies) { + serializedCookies.push(this.#serializeCookie(cookie)); + } - return { cookies, partitionKey }; + return { + cookies: serializedCookies, + partitionKey: this.#formatPartitionKey(partitionKey), + }; } /** @@ -195,9 +239,6 @@ class StorageModule extends Module { * If the provided browsing context cannot be found. * @throws {UnableToSetCookieError} * If the cookie was not added. - * @throws {UnsupportedOperationError} - * Raised when the command is called with `userContext` as - * in `partition` argument. */ async setCookie(options = {}) { const { cookie: cookieSpec, partition: partitionSpec = null } = options; @@ -264,12 +305,7 @@ class StorageModule extends Module { throw new lazy.error.UnableToSetCookieError(e); } - // Bug 1875255. Exchange platform id for Webdriver BiDi id for the user context to return it to the client. - // For now we use platform user context id for returning cookies for a specific browsing context in the platform API, - // but we can not return it directly to the client, so for now we just remove it from the response. - delete partitionKey.userContext; - - return { partitionKey }; + return { partitionKey: this.#formatPartitionKey(partitionKey) }; } #assertCookie(cookie) { @@ -318,7 +354,7 @@ class StorageModule extends Module { } } - #assertGetCookieFilter(filter) { + #assertCookieFilter(filter) { lazy.assert.object( filter, `Expected "filter" to be an object, got ${filter}` @@ -454,10 +490,11 @@ class StorageModule extends Module { `Expected "partition.userContext" to be a string, got ${userContext}` ); - // TODO: Bug 1875255. Implement support for "userContext" field. - throw new lazy.error.UnsupportedOperationError( - `"userContext" as a field on "partition" argument is not supported yet for "storage.getCookies" command` - ); + if (!lazy.UserContextManager.hasUserContextId(userContext)) { + throw new lazy.error.NoSuchUserContextError( + `User Context with id ${userContext} was not found` + ); + } } break; } @@ -505,6 +542,40 @@ class StorageModule extends Module { } /** + * Deserialize filter. + * + * @see https://w3c.github.io/webdriver-bidi/#deserialize-filter + */ + #deserializeFilter(filter) { + const deserializedFilter = {}; + for (const [fieldName, value] of Object.entries(filter)) { + if (value === null) { + continue; + } + + const deserializedName = CookieFieldsMapping[fieldName]; + let deserializedValue; + + switch (deserializedName) { + case "sameSite": + deserializedValue = this.#getSameSitePlatformProperty(value); + break; + + case "value": + deserializedValue = this.#deserializeProtocolBytes(value); + break; + + default: + deserializedValue = value; + } + + deserializedFilter[deserializedName] = deserializedValue; + } + + return deserializedFilter; + } + + /** * Deserialize the value to string, since platform API * returns cookie's value as a string. */ @@ -544,7 +615,14 @@ class StorageModule extends Module { const partitionKey = {}; for (const keyName of PartitionKeyAttributes) { if (keyName in partitionSpec) { - partitionKey[keyName] = partitionSpec[keyName]; + // Retrieve a platform user context id. + if (keyName === "userContext") { + partitionKey[keyName] = lazy.UserContextManager.getInternalIdById( + partitionSpec.userContext + ); + } else { + partitionKey[keyName] = partitionSpec[keyName]; + } } } @@ -552,6 +630,20 @@ class StorageModule extends Module { } /** + * Prepare the partition key in the right format for returning to a client. + */ + #formatPartitionKey(partitionKey) { + if ("userContext" in partitionKey) { + // Exchange platform id for Webdriver BiDi id for the user context to return it to the client. + partitionKey.userContext = lazy.UserContextManager.getIdByInternalId( + partitionKey.userContext + ); + } + + return partitionKey; + } + + /** * Retrieves a browsing context based on its id. * * @param {number} contextId @@ -595,11 +687,11 @@ class StorageModule extends Module { */ #getMatchingCookies(cookieStore, filter) { const cookies = []; + const deserializedFilter = this.#deserializeFilter(filter); for (const storedCookie of cookieStore) { - const serializedCookie = this.#serializeCookie(storedCookie); - if (this.#matchCookie(serializedCookie, filter)) { - cookies.push(serializedCookie); + if (this.#matchCookie(storedCookie, deserializedFilter)) { + cookies.push(storedCookie); } } return cookies; @@ -649,18 +741,14 @@ class StorageModule extends Module { // Prepare the data in the format required for the platform API. const originAttributes = this.#getOriginAttributes(storagePartitionKey); - // In case we want to get the cookies for a certain `sourceOrigin`, - // we have to additionally specify `hostname`. When `sourceOrigin` is not present - // `hostname` will stay equal undefined. - let hostname; // In case we want to get the cookies for a certain `sourceOrigin`, // we have to separately retrieve cookies for a hostname built from `sourceOrigin`, // and with `partitionKey` equal an empty string to retrieve the cookies that which were set - // by this hostname but without `partitionKey`, e.g. with `document.cookie` + // by this hostname but without `partitionKey`, e.g. with `document.cookie`. if (storagePartitionKey.sourceOrigin) { const url = new URL(storagePartitionKey.sourceOrigin); - hostname = url.hostname; + const hostname = url.hostname; const principal = Services.scriptSecurityManager.createContentPrincipal( Services.io.newURI(url), @@ -688,8 +776,7 @@ class StorageModule extends Module { // Add the cookies which exactly match a built partition attributes. store = store.concat( Services.cookies.getCookiesWithOriginAttributes( - JSON.stringify(originAttributes), - hostname + JSON.stringify(originAttributes) ) ); @@ -702,19 +789,23 @@ class StorageModule extends Module { * @see https://w3c.github.io/webdriver-bidi/#match-cookie */ #matchCookie(storedCookie, filter) { - for (const [fieldName] of Object.entries(CookieFieldsMapping)) { - let value = filter[fieldName]; - if (value !== null) { - let storedCookieValue = storedCookie[fieldName]; - - if (fieldName === "value") { - value = this.#deserializeProtocolBytes(value); - storedCookieValue = this.#deserializeProtocolBytes(storedCookieValue); - } + for (const [fieldName, value] of Object.entries(filter)) { + // Since we set `null` to not specified values, we have to check for `null` here + // and not match on these values. + if (value === null) { + continue; + } - if (storedCookieValue !== value) { - return false; - } + let storedCookieValue = storedCookie[fieldName]; + + // The platform represantation of cookie doesn't contain a size field, + // so we have to calculate it to match. + if (fieldName === "size") { + storedCookieValue = this.#getCookieSize(storedCookie); + } + + if (storedCookieValue !== value) { + return false; } } diff --git a/remote/webdriver-bidi/modules/windowglobal/browsingContext.sys.mjs b/remote/webdriver-bidi/modules/windowglobal/browsingContext.sys.mjs index 8421445d2c..adf821601d 100644 --- a/remote/webdriver-bidi/modules/windowglobal/browsingContext.sys.mjs +++ b/remote/webdriver-bidi/modules/windowglobal/browsingContext.sys.mjs @@ -17,6 +17,7 @@ ChromeUtils.defineESModuleGetters(lazy, { "chrome://remote/content/webdriver-bidi/modules/root/browsingContext.sys.mjs", OriginType: "chrome://remote/content/webdriver-bidi/modules/root/browsingContext.sys.mjs", + PollPromise: "chrome://remote/content/shared/Sync.sys.mjs", }); const DOCUMENT_FRAGMENT_NODE = 11; @@ -356,6 +357,29 @@ class BrowsingContextModule extends WindowGlobalBiDiModule { }); } + /** + * Waits until the visibility state of the document has the expected value. + * + * @param {object} options + * @param {number} options.value + * Expected value of the visibility state. + * + * @returns {Promise} + * Promise that resolves when the visibility state has the expected value. + */ + async _awaitVisibilityState(options) { + const { value } = options; + const win = this.messageHandler.window; + + await lazy.PollPromise((resolve, reject) => { + if (win.document.visibilityState === value) { + resolve(); + } else { + reject(); + } + }); + } + _getBaseURL() { return this.messageHandler.window.document.baseURI; } diff --git a/remote/webdriver-bidi/modules/windowglobal/input.sys.mjs b/remote/webdriver-bidi/modules/windowglobal/input.sys.mjs index 099cf53d46..b91cce2310 100644 --- a/remote/webdriver-bidi/modules/windowglobal/input.sys.mjs +++ b/remote/webdriver-bidi/modules/windowglobal/input.sys.mjs @@ -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}` ); } diff --git a/remote/webdriver-bidi/modules/windowglobal/script.sys.mjs b/remote/webdriver-bidi/modules/windowglobal/script.sys.mjs index e0f9542bdd..88d58f8064 100644 --- a/remote/webdriver-bidi/modules/windowglobal/script.sys.mjs +++ b/remote/webdriver-bidi/modules/windowglobal/script.sys.mjs @@ -290,7 +290,7 @@ class ScriptModule extends WindowGlobalBiDiModule { const rawObject = maybeDebuggerObject.unsafeDereference(); // TODO: Getters for Maps and Sets iterators return "Opaque" objects and - // are not iterable. RemoteValue.jsm' serializer should handle calling + // are not iterable. RemoteValue.sys.mjs' serializer should handle calling // waiveXrays on Maps/Sets/... and then unwaiveXrays on entries but since // we serialize with maxDepth=1, calling waiveXrays once on the root // object allows to return correctly serialized values. |