From 8dd16259287f58f9273002717ec4d27e97127719 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 12 Jun 2024 07:43:14 +0200 Subject: Merging upstream version 127.0. Signed-off-by: Daniel Baumann --- .../webdriver-bidi/modules/ModuleRegistry.sys.mjs | 2 + .../modules/root/browsingContext.sys.mjs | 128 ++++-- remote/webdriver-bidi/modules/root/network.sys.mjs | 457 +++++++++------------ .../modules/root/permissions.sys.mjs | 140 +++++++ .../modules/windowglobal/browsingContext.sys.mjs | 228 +++++++--- .../modules/windowglobal/input.sys.mjs | 4 + 6 files changed, 600 insertions(+), 359 deletions(-) create mode 100644 remote/webdriver-bidi/modules/root/permissions.sys.mjs (limited to 'remote/webdriver-bidi/modules') diff --git a/remote/webdriver-bidi/modules/ModuleRegistry.sys.mjs b/remote/webdriver-bidi/modules/ModuleRegistry.sys.mjs index 63713f1f02..145e227fc7 100644 --- a/remote/webdriver-bidi/modules/ModuleRegistry.sys.mjs +++ b/remote/webdriver-bidi/modules/ModuleRegistry.sys.mjs @@ -18,6 +18,8 @@ ChromeUtils.defineESModuleGetters(modules.root, { log: "chrome://remote/content/webdriver-bidi/modules/root/log.sys.mjs", network: "chrome://remote/content/webdriver-bidi/modules/root/network.sys.mjs", + permissions: + "chrome://remote/content/webdriver-bidi/modules/root/permissions.sys.mjs", script: "chrome://remote/content/webdriver-bidi/modules/root/script.sys.mjs", session: "chrome://remote/content/webdriver-bidi/modules/root/session.sys.mjs", diff --git a/remote/webdriver-bidi/modules/root/browsingContext.sys.mjs b/remote/webdriver-bidi/modules/root/browsingContext.sys.mjs index 8424bebf4a..649e801175 100644 --- a/remote/webdriver-bidi/modules/root/browsingContext.sys.mjs +++ b/remote/webdriver-bidi/modules/root/browsingContext.sys.mjs @@ -85,6 +85,7 @@ const CreateType = { * @enum {LocatorType} */ export const LocatorType = { + accessibility: "accessibility", css: "css", innerText: "innerText", xpath: "xpath", @@ -545,7 +546,7 @@ class BrowsingContextModule extends Module { // On Android there is only a single window allowed. As such fallback to // open a new tab instead. const type = lazy.AppInfo.isAndroid ? "tab" : typeHint; - + let waitForVisibilityChangePromise; switch (type) { case "window": { const newWindow = await lazy.windowManager.openBrowserWindow({ @@ -572,8 +573,6 @@ class BrowsingContextModule extends Module { window = lazy.TabManager.getWindowForTab(referenceTab); } - 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 @@ -581,32 +580,32 @@ class BrowsingContextModule extends Module { // // 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 - ) + + // Create the promise immediately, but await it later in parallel with + // waitForInitialNavigationCompleted. + waitForVisibilityChangePromise = this.#waitForVisibilityChange( + lazy.TabManager.getBrowserForTab(selectedTab).browsingContext ); } - promises.unshift( - lazy.TabManager.addTab({ - focus: !background, - referenceTab, - userContextId: userContext, - }) - ); - - const [tab] = await Promise.all(promises); + const tab = await lazy.TabManager.addTab({ + focus: !background, + referenceTab, + userContextId: userContext, + }); browser = lazy.TabManager.getBrowserForTab(tab); } } - await lazy.waitForInitialNavigationCompleted( - browser.browsingContext.webProgress, - { - unloadTimeout: 5000, - } - ); + await Promise.all([ + lazy.waitForInitialNavigationCompleted( + browser.browsingContext.webProgress, + { + unloadTimeout: 5000, + } + ), + waitForVisibilityChangePromise, + ]); // The tab on Android is always opened in the foreground, // so we need to select the previous tab, @@ -805,12 +804,32 @@ class BrowsingContextModule extends Module { /** * Used as an argument for browsingContext.locateNodes command, as one of the available variants - * {CssLocator}, {InnerTextLocator} or {XPathLocator}, to represent a way of how lookup of nodes + * {AccessibilityLocator}, {CssLocator}, {InnerTextLocator} or {XPathLocator}, to represent a way of how lookup of nodes * is going to be performed. * * @typedef Locator */ + /** + * Used as a value argument for browsingContext.locateNodes command + * in case of a lookup by accessibility attributes. + * + * @typedef AccessibilityLocatorValue + * + * @property {string=} name + * @property {string=} role + */ + + /** + * Used as an argument for browsingContext.locateNodes command + * to represent a lookup by accessibility attributes. + * + * @typedef AccessibilityLocator + * + * @property {LocatorType} [type=LocatorType.accessibility] + * @property {AccessibilityLocatorValue} value + */ + /** * Used as an argument for browsingContext.locateNodes command * to represent a lookup by css selector. @@ -900,7 +919,42 @@ class BrowsingContextModule extends Module { `Expected "locator.type" to be one of ${locatorTypes}, got ${locator.type}` )(locator.type); - if (![LocatorType.css, LocatorType.xpath].includes(locator.type)) { + if ( + [LocatorType.css, LocatorType.innerText, LocatorType.xpath].includes( + locator.type + ) + ) { + lazy.assert.string( + locator.value, + `Expected "locator.value" of "locator.type" "${locator.type}" to be a string, got ${locator.value}` + ); + } + if (locator.type == LocatorType.accessibility) { + lazy.assert.object( + locator.value, + `Expected "locator.value" of "locator.type" "${locator.type}" to be an object, got ${locator.value}` + ); + + const { name = null, role = null } = locator.value; + if (name !== null) { + lazy.assert.string( + locator.value.name, + `Expected "locator.value.name" of "locator.type" "${locator.type}" to be a string, got ${name}` + ); + } + if (role !== null) { + lazy.assert.string( + locator.value.role, + `Expected "locator.value.role" of "locator.type" "${locator.type}" to be a string, got ${role}` + ); + } + } + + if ( + ![LocatorType.accessibility, LocatorType.css, LocatorType.xpath].includes( + locator.type + ) + ) { throw new lazy.error.UnsupportedOperationError( `"locator.type" argument with value: ${locator.type} is not supported yet.` ); @@ -1249,7 +1303,11 @@ class BrowsingContextModule extends Module { * @param {object=} options * @param {string} options.context * Id of the browsing context. - * @param {Viewport|null} options.viewport + * @param {(number|null)=} options.devicePixelRatio + * A value to override device pixel ratio, or `null` to reset it to + * the original value. Different values will not cause the rendering to change, + * only image srcsets and media queries will be applied as if DPR is redefined. + * @param {(Viewport|null)=} options.viewport * Dimensions to set the viewport to, or `null` to reset it * to the original dimensions. * @@ -1259,7 +1317,7 @@ class BrowsingContextModule extends Module { * Raised when the command is called on Android. */ async setViewport(options = {}) { - const { context: contextId, viewport } = options; + const { context: contextId, devicePixelRatio, viewport } = options; if (lazy.AppInfo.isAndroid) { // Bug 1840084: Add Android support for modifying the viewport. @@ -1322,6 +1380,24 @@ class BrowsingContextModule extends Module { browser.style.setProperty("width", targetWidth + "px"); } + if (devicePixelRatio !== undefined) { + if (devicePixelRatio !== null) { + lazy.assert.number( + devicePixelRatio, + `Expected "devicePixelRatio" to be a number or null, got ${devicePixelRatio}` + ); + lazy.assert.that( + devicePixelRatio => devicePixelRatio > 0, + `Expected "devicePixelRatio" to be greater than 0, got ${devicePixelRatio}` + )(devicePixelRatio); + + context.overrideDPPX = devicePixelRatio; + } else { + // Will reset to use the global default scaling factor. + context.overrideDPPX = 0; + } + } + if (targetHeight !== currentHeight || targetWidth !== currentWidth) { // Wait until the viewport has been resized await this.messageHandler.forwardCommand({ diff --git a/remote/webdriver-bidi/modules/root/network.sys.mjs b/remote/webdriver-bidi/modules/root/network.sys.mjs index 6850e3f372..326fa87a02 100644 --- a/remote/webdriver-bidi/modules/root/network.sys.mjs +++ b/remote/webdriver-bidi/modules/root/network.sys.mjs @@ -12,8 +12,6 @@ ChromeUtils.defineESModuleGetters(lazy, { generateUUID: "chrome://remote/content/shared/UUID.sys.mjs", matchURLPattern: "chrome://remote/content/shared/webdriver/URLPattern.sys.mjs", - notifyNavigationStarted: - "chrome://remote/content/shared/NavigationManager.sys.mjs", NetworkListener: "chrome://remote/content/shared/listeners/NetworkListener.sys.mjs", parseChallengeHeader: @@ -309,7 +307,9 @@ class NetworkModule extends Module { // Set of event names which have active subscriptions this.#subscribedEvents = new Set(); - this.#networkListener = new lazy.NetworkListener(); + this.#networkListener = new lazy.NetworkListener( + this.messageHandler.navigationManager + ); this.#networkListener.on("auth-required", this.#onAuthRequired); this.#networkListener.on("before-request-sent", this.#onBeforeRequestSent); this.#networkListener.on("fetch-error", this.#onFetchError); @@ -549,8 +549,7 @@ class NetworkModule extends Module { ); } - const wrapper = ChannelWrapper.get(request); - wrapper.resume(); + request.wrappedChannel.resume(); resolveBlockedEvent(); } @@ -684,8 +683,7 @@ class NetworkModule extends Module { await authCallbacks.provideAuthCredentials(); } } else { - const wrapper = ChannelWrapper.get(request); - wrapper.resume(); + request.wrappedChannel.resume(); } resolveBlockedEvent(); @@ -803,9 +801,8 @@ class NetworkModule extends Module { ); } - const wrapper = ChannelWrapper.get(request); - wrapper.resume(); - wrapper.cancel( + request.wrappedChannel.resume(); + request.wrappedChannel.cancel( Cr.NS_ERROR_ABORT, Ci.nsILoadInfo.BLOCKING_REASON_WEBDRIVER_BIDI ); @@ -933,8 +930,7 @@ class NetworkModule extends Module { if (phase === InterceptPhase.AuthRequired) { await authCallbacks.provideAuthCredentials(); } else { - const wrapper = ChannelWrapper.get(request); - wrapper.resume(); + request.wrappedChannel.resume(); } resolveBlockedEvent(); @@ -987,11 +983,7 @@ class NetworkModule extends Module { * The response channel. */ #addBlockedRequest(requestId, phase, options = {}) { - const { - authCallbacks, - requestChannel: request, - responseChannel: response, - } = options; + const { authCallbacks, request, response } = options; const { promise: blockedEventPromise, resolve: resolveBlockedEvent } = Promise.withResolvers(); @@ -1117,14 +1109,14 @@ class NetworkModule extends Module { } } - #extractChallenges(responseData) { + #extractChallenges(response) { let headerName; // Using case-insensitive match for header names, so we use the lowercase // version of the "WWW-Authenticate" / "Proxy-Authenticate" strings. - if (responseData.status === 401) { + if (response.status === 401) { headerName = "www-authenticate"; - } else if (responseData.status === 407) { + } else if (response.status === 407) { headerName = "proxy-authenticate"; } else { return null; @@ -1132,10 +1124,10 @@ class NetworkModule extends Module { const challenges = []; - for (const header of responseData.headers) { - if (header.name.toLowerCase() === headerName) { + for (const [name, value] of response.getHeadersList()) { + if (name.toLowerCase() === headerName) { // A single header can contain several challenges. - const headerChallenges = lazy.parseChallengeHeader(header.value); + const headerChallenges = lazy.parseChallengeHeader(value); for (const headerChallenge of headerChallenges) { const realmParam = headerChallenge.params.find( param => param.name == "realm" @@ -1177,7 +1169,7 @@ class NetworkModule extends Module { }; } - #getNetworkIntercepts(event, requestData, contextId) { + #getNetworkIntercepts(event, request, topContextId) { const intercepts = []; let phase; @@ -1197,17 +1189,11 @@ class NetworkModule extends Module { return intercepts; } - // Retrieve the top browsing context id for this network event. - const browsingContext = lazy.TabManager.getBrowsingContextById(contextId); - const topLevelContextId = lazy.TabManager.getIdForBrowsingContext( - browsingContext.top - ); - - const url = requestData.url; + const url = request.serializedURL; for (const [interceptId, intercept] of this.#interceptMap) { if ( intercept.contexts !== null && - !intercept.contexts.includes(topLevelContextId) + !intercept.contexts.includes(topContextId) ) { // Skip this intercept if the event's context does not match the list // of contexts for this intercept. @@ -1228,31 +1214,96 @@ class NetworkModule extends Module { return intercepts; } - #getNavigationId(eventName, isNavigationRequest, browsingContext, url) { - if (!isNavigationRequest) { - // Not a navigation request return null. - return null; + #getRequestData(request) { + const requestId = request.requestId; + + // "Let url be the result of running the URL serializer with request’s URL" + // request.serializedURL is already serialized. + const url = request.serializedURL; + const method = request.method; + + const bodySize = request.postDataSize; + const headersSize = request.headersSize; + const headers = []; + const cookies = []; + + for (const [name, value] of request.getHeadersList()) { + headers.push(this.#serializeHeader(name, value)); + if (name.toLowerCase() == "cookie") { + // TODO: Retrieve the actual cookies from the cookie store. + const headerCookies = value.split(";"); + for (const cookie of headerCookies) { + const equal = cookie.indexOf("="); + const cookieName = cookie.substr(0, equal); + const cookieValue = cookie.substr(equal + 1); + const serializedCookie = this.#serializeHeader( + unescape(cookieName.trim()), + unescape(cookieValue.trim()) + ); + cookies.push(serializedCookie); + } + } } - let navigation = - this.messageHandler.navigationManager.getNavigationForBrowsingContext( - browsingContext - ); + const timings = request.getFetchTimings(); - // `onBeforeRequestSent` might be too early for the NavigationManager. - // If there is no ongoing navigation, create one ourselves. - // TODO: Bug 1835704 to detect navigations earlier and avoid this. - if ( - eventName === "network.beforeRequestSent" && - (!navigation || navigation.finished) - ) { - navigation = lazy.notifyNavigationStarted({ - contextDetails: { context: browsingContext }, - url, - }); + return { + request: requestId, + url, + method, + bodySize, + headersSize, + headers, + cookies, + timings, + }; + } + + #getResponseContentInfo(response) { + return { + size: response.decodedBodySize, + }; + } + + #getResponseData(response) { + const url = response.serializedURL; + const protocol = response.protocol; + const status = response.status; + const statusText = response.statusMessage; + // TODO: Ideally we should have a `isCacheStateLocal` getter + // const fromCache = response.isCacheStateLocal(); + const fromCache = response.fromCache; + const mimeType = response.getComputedMimeType(); + const headers = []; + for (const [name, value] of response.getHeadersList()) { + headers.push(this.#serializeHeader(name, value)); + } + + const bytesReceived = response.totalTransmittedSize; + const headersSize = response.headersTransmittedSize; + const bodySize = response.encodedBodySize; + const content = this.#getResponseContentInfo(response); + const authChallenges = this.#extractChallenges(response); + + const params = { + url, + protocol, + status, + statusText, + fromCache, + headers, + mimeType, + bytesReceived, + headersSize, + bodySize, + content, + }; + + if (authChallenges !== null) { + params.authChallenges = authChallenges; } - return navigation ? navigation.navigationId : null; + return params; } #getSuspendMarkerText(requestData, phase) { @@ -1260,21 +1311,13 @@ class NetworkModule extends Module { } #onAuthRequired = (name, data) => { - const { - authCallbacks, - contextId, - isNavigationRequest, - redirectCount, - requestChannel, - requestData, - responseChannel, - responseData, - timestamp, - } = data; + const { authCallbacks, request, response } = data; let isBlocked = false; try { - const browsingContext = lazy.TabManager.getBrowsingContextById(contextId); + const browsingContext = lazy.TabManager.getBrowsingContextById( + request.contextId + ); if (!browsingContext) { // Do not emit events if the context id does not match any existing // browsing context. @@ -1283,18 +1326,9 @@ class NetworkModule extends Module { const protocolEventName = "network.authRequired"; - // Process the navigation to create potentially missing navigation ids - // before the early return below. - const navigation = this.#getNavigationId( - protocolEventName, - isNavigationRequest, - browsingContext, - requestData.url - ); - const isListening = this.messageHandler.eventsDispatcher.hasListener( protocolEventName, - { contextId } + { contextId: request.contextId } ); if (!isListening) { // If there are no listeners subscribed to this event and this context, @@ -1302,23 +1336,16 @@ class NetworkModule extends Module { return; } - const baseParameters = this.#processNetworkEvent(protocolEventName, { - contextId, - navigation, - redirectCount, - requestData, - timestamp, - }); + const baseParameters = this.#processNetworkEvent( + protocolEventName, + request + ); - const authRequiredEvent = this.#serializeNetworkEvent({ + const responseData = this.#getResponseData(response); + const authRequiredEvent = { ...baseParameters, response: responseData, - }); - - const authChallenges = this.#extractChallenges(responseData); - // authChallenges should never be null for a request which triggered an - // authRequired event. - authRequiredEvent.response.authChallenges = authChallenges; + }; this.emitEvent( protocolEventName, @@ -1337,8 +1364,8 @@ class NetworkModule extends Module { InterceptPhase.AuthRequired, { authCallbacks, - requestChannel, - responseChannel, + request, + response, } ); } @@ -1352,16 +1379,11 @@ class NetworkModule extends Module { }; #onBeforeRequestSent = (name, data) => { - const { - contextId, - isNavigationRequest, - redirectCount, - requestChannel, - requestData, - timestamp, - } = data; + const { request } = data; - const browsingContext = lazy.TabManager.getBrowsingContextById(contextId); + const browsingContext = lazy.TabManager.getBrowsingContextById( + request.contextId + ); if (!browsingContext) { // Do not emit events if the context id does not match any existing // browsing context. @@ -1371,15 +1393,6 @@ class NetworkModule extends Module { const internalEventName = "network._beforeRequestSent"; const protocolEventName = "network.beforeRequestSent"; - // Process the navigation to create potentially missing navigation ids - // before the early return below. - const navigation = this.#getNavigationId( - protocolEventName, - isNavigationRequest, - browsingContext, - requestData.url - ); - // Always emit internal events, they are used to support the browsingContext // navigate command. // Bug 1861922: Replace internal events with a Network listener helper @@ -1387,15 +1400,15 @@ class NetworkModule extends Module { this.emitEvent( internalEventName, { - navigation, - url: requestData.url, + navigation: request.navigationId, + url: request.serializedURL, }, this.#getContextInfo(browsingContext) ); const isListening = this.messageHandler.eventsDispatcher.hasListener( protocolEventName, - { contextId } + { contextId: request.contextId } ); if (!isListening) { // If there are no listeners subscribed to this event and this context, @@ -1403,23 +1416,20 @@ class NetworkModule extends Module { return; } - const baseParameters = this.#processNetworkEvent(protocolEventName, { - contextId, - navigation, - redirectCount, - requestData, - timestamp, - }); + const baseParameters = this.#processNetworkEvent( + protocolEventName, + request + ); // Bug 1805479: Handle the initiator, including stacktrace details. const initiator = { type: InitiatorType.Other, }; - const beforeRequestSentEvent = this.#serializeNetworkEvent({ + const beforeRequestSentEvent = { ...baseParameters, initiator, - }); + }; this.emitEvent( protocolEventName, @@ -1430,32 +1440,26 @@ class NetworkModule extends Module { if (beforeRequestSentEvent.isBlocked) { // TODO: Requests suspended in beforeRequestSent still reach the server at // the moment. https://bugzilla.mozilla.org/show_bug.cgi?id=1849686 - const wrapper = ChannelWrapper.get(requestChannel); - wrapper.suspend( - this.#getSuspendMarkerText(requestData, "beforeRequestSent") + request.wrappedChannel.suspend( + this.#getSuspendMarkerText(request, "beforeRequestSent") ); this.#addBlockedRequest( beforeRequestSentEvent.request.request, InterceptPhase.BeforeRequestSent, { - requestChannel, + request, } ); } }; #onFetchError = (name, data) => { - const { - contextId, - errorText, - isNavigationRequest, - redirectCount, - requestData, - timestamp, - } = data; + const { request } = data; - const browsingContext = lazy.TabManager.getBrowsingContextById(contextId); + const browsingContext = lazy.TabManager.getBrowsingContextById( + request.contextId + ); if (!browsingContext) { // Do not emit events if the context id does not match any existing // browsing context. @@ -1465,15 +1469,6 @@ class NetworkModule extends Module { const internalEventName = "network._fetchError"; const protocolEventName = "network.fetchError"; - // Process the navigation to create potentially missing navigation ids - // before the early return below. - const navigation = this.#getNavigationId( - protocolEventName, - isNavigationRequest, - browsingContext, - requestData.url - ); - // Always emit internal events, they are used to support the browsingContext // navigate command. // Bug 1861922: Replace internal events with a Network listener helper @@ -1481,15 +1476,15 @@ class NetworkModule extends Module { this.emitEvent( internalEventName, { - navigation, - url: requestData.url, + navigation: request.navigationId, + url: request.serializedURL, }, this.#getContextInfo(browsingContext) ); const isListening = this.messageHandler.eventsDispatcher.hasListener( protocolEventName, - { contextId } + { contextId: request.contextId } ); if (!isListening) { // If there are no listeners subscribed to this event and this context, @@ -1497,18 +1492,15 @@ class NetworkModule extends Module { return; } - const baseParameters = this.#processNetworkEvent(protocolEventName, { - contextId, - navigation, - redirectCount, - requestData, - timestamp, - }); + const baseParameters = this.#processNetworkEvent( + protocolEventName, + request + ); - const fetchErrorEvent = this.#serializeNetworkEvent({ + const fetchErrorEvent = { ...baseParameters, - errorText, - }); + errorText: request.errorText, + }; this.emitEvent( protocolEventName, @@ -1518,18 +1510,11 @@ class NetworkModule extends Module { }; #onResponseEvent = (name, data) => { - const { - contextId, - isNavigationRequest, - redirectCount, - requestChannel, - requestData, - responseChannel, - responseData, - timestamp, - } = data; + const { request, response } = data; - const browsingContext = lazy.TabManager.getBrowsingContextById(contextId); + const browsingContext = lazy.TabManager.getBrowsingContextById( + request.contextId + ); if (!browsingContext) { // Do not emit events if the context id does not match any existing // browsing context. @@ -1546,15 +1531,6 @@ class NetworkModule extends Module { ? "network._responseStarted" : "network._responseCompleted"; - // Process the navigation to create potentially missing navigation ids - // before the early return below. - const navigation = this.#getNavigationId( - protocolEventName, - isNavigationRequest, - browsingContext, - requestData.url - ); - // Always emit internal events, they are used to support the browsingContext // navigate command. // Bug 1861922: Replace internal events with a Network listener helper @@ -1562,15 +1538,15 @@ class NetworkModule extends Module { this.emitEvent( internalEventName, { - navigation, - url: requestData.url, + navigation: request.navigationId, + url: request.serializedURL, }, this.#getContextInfo(browsingContext) ); const isListening = this.messageHandler.eventsDispatcher.hasListener( protocolEventName, - { contextId } + { contextId: request.contextId } ); if (!isListening) { // If there are no listeners subscribed to this event and this context, @@ -1578,23 +1554,17 @@ class NetworkModule extends Module { return; } - const baseParameters = this.#processNetworkEvent(protocolEventName, { - contextId, - navigation, - redirectCount, - requestData, - timestamp, - }); + const baseParameters = this.#processNetworkEvent( + protocolEventName, + request + ); - const responseEvent = this.#serializeNetworkEvent({ + const responseData = this.#getResponseData(response); + + const responseEvent = { ...baseParameters, response: responseData, - }); - - const authChallenges = this.#extractChallenges(responseData); - if (authChallenges !== null) { - responseEvent.response.authChallenges = authChallenges; - } + }; this.emitEvent( protocolEventName, @@ -1606,51 +1576,40 @@ class NetworkModule extends Module { protocolEventName === "network.responseStarted" && responseEvent.isBlocked ) { - const wrapper = ChannelWrapper.get(requestChannel); - wrapper.suspend( - this.#getSuspendMarkerText(requestData, "responseStarted") + request.wrappedChannel.suspend( + this.#getSuspendMarkerText(request, "responseStarted") ); this.#addBlockedRequest( responseEvent.request.request, InterceptPhase.ResponseStarted, { - requestChannel, - responseChannel, + request, + response, } ); } }; - /** - * Process the network event data for a given network event name and create - * the corresponding base parameters. - * - * @param {string} eventName - * One of the supported network event names. - * @param {object} data - * @param {string} data.contextId - * The browsing context id for the network event. - * @param {string|null} data.navigation - * The navigation id if this is a network event for a navigation request. - * @param {number} data.redirectCount - * The redirect count for the network event. - * @param {RequestData} data.requestData - * The network.RequestData information for the network event. - * @param {number} data.timestamp - * The timestamp when the network event was created. - */ - #processNetworkEvent(eventName, data) { - const { contextId, navigation, redirectCount, requestData, timestamp } = - data; - const intercepts = this.#getNetworkIntercepts( - eventName, - requestData, - contextId - ); - const isBlocked = !!intercepts.length; + #processNetworkEvent(event, request) { + const requestData = this.#getRequestData(request); + const navigation = request.navigationId; + let contextId = null; + let topContextId = null; + if (request.contextId) { + // Retrieve the top browsing context id for this network event. + contextId = request.contextId; + const browsingContext = lazy.TabManager.getBrowsingContextById(contextId); + topContextId = lazy.TabManager.getIdForBrowsingContext( + browsingContext.top + ); + } - const baseParameters = { + const intercepts = this.#getNetworkIntercepts(event, request, topContextId); + const redirectCount = request.redirectCount; + const timestamp = Date.now(); + const isBlocked = !!intercepts.length; + const params = { context: contextId, isBlocked, navigation, @@ -1660,51 +1619,17 @@ class NetworkModule extends Module { }; if (isBlocked) { - baseParameters.intercepts = intercepts; + params.intercepts = intercepts; } - return baseParameters; - } - - #serializeHeadersOrCookies(headersOrCookies) { - return headersOrCookies.map(item => ({ - name: item.name, - value: this.#serializeStringAsBytesValue(item.value), - })); + return params; } - /** - * Serialize in-place all cookies and headers arrays found in a given network - * event payload. - * - * @param {object} networkEvent - * The network event parameters object to serialize. - * @returns {object} - * The serialized network event parameters. - */ - #serializeNetworkEvent(networkEvent) { - // Make a shallow copy of networkEvent before serializing the headers and - // cookies arrays in request/response. - const serialized = { ...networkEvent }; - - // Make a shallow copy of the request data. - serialized.request = { ...networkEvent.request }; - serialized.request.cookies = this.#serializeHeadersOrCookies( - networkEvent.request.cookies - ); - serialized.request.headers = this.#serializeHeadersOrCookies( - networkEvent.request.headers - ); - - if (networkEvent.response?.headers) { - // Make a shallow copy of the response data. - serialized.response = { ...networkEvent.response }; - serialized.response.headers = this.#serializeHeadersOrCookies( - networkEvent.response.headers - ); - } - - return serialized; + #serializeHeader(name, value) { + return { + name, + value: this.#serializeStringAsBytesValue(value), + }; } /** diff --git a/remote/webdriver-bidi/modules/root/permissions.sys.mjs b/remote/webdriver-bidi/modules/root/permissions.sys.mjs new file mode 100644 index 0000000000..7c66b113b0 --- /dev/null +++ b/remote/webdriver-bidi/modules/root/permissions.sys.mjs @@ -0,0 +1,140 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Module } from "chrome://remote/content/shared/messagehandler/Module.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs", + error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs", + permissions: "chrome://remote/content/shared/Permissions.sys.mjs", +}); + +export const PermissionState = { + denied: "denied", + granted: "granted", + prompt: "prompt", +}; + +class PermissionsModule extends Module { + constructor(messageHandler) { + super(messageHandler); + } + + destroy() {} + + /** + * An object that holds the information about permission descriptor + * for Webdriver BiDi permissions.setPermission command. + * + * @typedef PermissionDescriptor + * + * @property {string} name + * The name of the permission. + */ + + /** + * Set to a given permission descriptor a given state on a provided origin. + * + * @param {object=} options + * @param {PermissionDescriptor} options.descriptor + * The descriptor of the permission which will be updated. + * @param {PermissionState} options.state + * The state which will be set to the permission. + * @param {string} options.origin + * The origin which is used as a target for permission update. + * @param {string=} options.userContext [unsupported] + * The id of the user context which should be used as a target + * for permission update. + * + * @throws {InvalidArgumentError} + * Raised if an argument is of an invalid type or value. + * @throws {UnsupportedOperationError} + * Raised when unsupported permissions are set or userContext + * argument is used. + */ + async setPermission(options = {}) { + const { + descriptor, + state, + origin, + userContext: userContextId = null, + } = options; + + lazy.assert.object( + descriptor, + `Expected "descriptor" to be an object, got ${descriptor}` + ); + const permissionName = descriptor.name; + lazy.assert.string( + permissionName, + `Expected "descriptor.name" to be a string, got ${permissionName}` + ); + + lazy.permissions.validatePermission(permissionName); + + // Bug 1878741: Allowing this permission causes timing related Android crash. + if (descriptor.name === "notifications") { + if (Services.prefs.getBoolPref("notification.prompt.testing", false)) { + // Okay, do nothing. The notifications module will work without permission. + return; + } + throw new lazy.error.UnsupportedOperationError( + `Setting "descriptor.name" "notifications" expected "notification.prompt.testing" preference to be set` + ); + } + + if (permissionName === "storage-access") { + // TODO: Bug 1895457. Add support for "storage-access" permission. + throw new lazy.error.UnsupportedOperationError( + `"descriptor.name" "${permissionName}" is currently unsupported` + ); + } + + const permissionStateTypes = Object.keys(PermissionState); + lazy.assert.that( + state => permissionStateTypes.includes(state), + `Expected "state" to be one of ${permissionStateTypes}, got ${state}` + )(state); + + lazy.assert.string( + origin, + `Expected "origin" to be a string, got ${origin}` + ); + lazy.assert.that( + origin => URL.canParse(origin), + `Expected "origin" to be a valid URL, got ${origin}` + )(origin); + + if (userContextId !== null) { + lazy.assert.string( + userContextId, + `Expected "userContext" to be a string, got ${userContextId}` + ); + + // TODO: Bug 1894217. Add support for "userContext" argument. + throw new lazy.error.UnsupportedOperationError( + `"userContext" is not supported yet` + ); + } + + const activeWindow = Services.wm.getMostRecentBrowserWindow(); + let typedDescriptor; + try { + typedDescriptor = activeWindow.navigator.permissions.parseSetParameters({ + descriptor, + state, + }); + } catch (err) { + throw new lazy.error.InvalidArgumentError( + `The conversion of "descriptor" was not successful: ${err.message}` + ); + } + + lazy.permissions.set(typedDescriptor, state, origin); + } +} + +export const permissions = PermissionsModule; diff --git a/remote/webdriver-bidi/modules/windowglobal/browsingContext.sys.mjs b/remote/webdriver-bidi/modules/windowglobal/browsingContext.sys.mjs index ef61954284..6dbd5440e0 100644 --- a/remote/webdriver-bidi/modules/windowglobal/browsingContext.sys.mjs +++ b/remote/webdriver-bidi/modules/windowglobal/browsingContext.sys.mjs @@ -7,6 +7,8 @@ import { WindowGlobalBiDiModule } from "chrome://remote/content/webdriver-bidi/m const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { + accessibility: + "chrome://remote/content/shared/webdriver/Accessibility.sys.mjs", AnimationFramePromise: "chrome://remote/content/shared/Sync.sys.mjs", assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs", ClipRectangleType: @@ -48,6 +50,66 @@ class BrowsingContextModule extends WindowGlobalBiDiModule { this.#subscribedEvents = null; } + /** + * Collect nodes using accessibility attributes. + * + * @see https://w3c.github.io/webdriver-bidi/#collect-nodes-using-accessibility-attributes + */ + async #collectNodesUsingAccessibilityAttributes( + contextNodes, + selector, + maxReturnedNodeCount, + returnedNodes + ) { + if (returnedNodes === null) { + returnedNodes = []; + } + + for (const contextNode of contextNodes) { + let match = true; + + if (contextNode.nodeType === ELEMENT_NODE) { + if ("role" in selector) { + const role = await lazy.accessibility.getComputedRole(contextNode); + + if (selector.role !== role) { + match = false; + } + } + + if ("name" in selector) { + const name = await lazy.accessibility.getAccessibleName(contextNode); + if (selector.name !== name) { + match = false; + } + } + } else { + match = false; + } + + if (match) { + if ( + maxReturnedNodeCount !== null && + returnedNodes.length === maxReturnedNodeCount + ) { + break; + } + returnedNodes.push(contextNode); + } + + const childNodes = [...contextNode.children]; + + await this.#collectNodesUsingAccessibilityAttributes( + childNodes, + selector, + maxReturnedNodeCount, + returnedNodes + ); + } + + return returnedNodes; + } + #getNavigationInfo(data) { // Note: the navigation id is collected in the parent-process and will be // added via event interception by the windowglobal-in-root module. @@ -85,75 +147,29 @@ class BrowsingContextModule extends WindowGlobalBiDiModule { ); } - #startListening() { - if (this.#subscribedEvents.size == 0) { - this.#loadListener.startListening(); - } - } - - #stopListening() { - if (this.#subscribedEvents.size == 0) { - this.#loadListener.stopListening(); - } - } - - #subscribeEvent(event) { - switch (event) { - case "browsingContext._documentInteractive": - this.#startListening(); - this.#subscribedEvents.add("browsingContext._documentInteractive"); - break; - case "browsingContext.domContentLoaded": - this.#startListening(); - this.#subscribedEvents.add("browsingContext.domContentLoaded"); - break; - case "browsingContext.load": - this.#startListening(); - this.#subscribedEvents.add("browsingContext.load"); - break; - } - } - - #unsubscribeEvent(event) { - switch (event) { - case "browsingContext._documentInteractive": - this.#subscribedEvents.delete("browsingContext._documentInteractive"); - break; - case "browsingContext.domContentLoaded": - this.#subscribedEvents.delete("browsingContext.domContentLoaded"); - break; - case "browsingContext.load": - this.#subscribedEvents.delete("browsingContext.load"); - break; - } - - this.#stopListening(); - } - - #onDOMContentLoaded = (eventName, data) => { - if (this.#subscribedEvents.has("browsingContext._documentInteractive")) { - this.messageHandler.emitEvent("browsingContext._documentInteractive", { - baseURL: data.target.baseURI, - contextId: this.messageHandler.contextId, - documentURL: data.target.URL, - innerWindowId: this.messageHandler.innerWindowId, - readyState: data.target.readyState, - }); - } - - if (this.#subscribedEvents.has("browsingContext.domContentLoaded")) { - this.emitEvent( - "browsingContext.domContentLoaded", - this.#getNavigationInfo(data) + /** + * Locate nodes using accessibility attributes. + * + * @see https://w3c.github.io/webdriver-bidi/#locate-nodes-using-accessibility-attributes + */ + async #locateNodesUsingAccessibilityAttributes( + contextNodes, + selector, + maxReturnedNodeCount + ) { + if (!("role" in selector) && !("name" in selector)) { + throw new lazy.error.InvalidSelectorError( + "Locating nodes by accessibility attributes requires `role` or `name` arguments" ); } - }; - #onLoad = (eventName, data) => { - if (this.#subscribedEvents.has("browsingContext.load")) { - this.emitEvent("browsingContext.load", this.#getNavigationInfo(data)); - } - }; + return this.#collectNodesUsingAccessibilityAttributes( + contextNodes, + selector, + maxReturnedNodeCount, + null + ); + } /** * Locate nodes using css selector. @@ -259,6 +275,31 @@ class BrowsingContextModule extends WindowGlobalBiDiModule { return new DOMRect(x, y, width, height); } + #onDOMContentLoaded = (eventName, data) => { + if (this.#subscribedEvents.has("browsingContext._documentInteractive")) { + this.messageHandler.emitEvent("browsingContext._documentInteractive", { + baseURL: data.target.baseURI, + contextId: this.messageHandler.contextId, + documentURL: data.target.URL, + innerWindowId: this.messageHandler.innerWindowId, + readyState: data.target.readyState, + }); + } + + if (this.#subscribedEvents.has("browsingContext.domContentLoaded")) { + this.emitEvent( + "browsingContext.domContentLoaded", + this.#getNavigationInfo(data) + ); + } + }; + + #onLoad = (eventName, data) => { + if (this.#subscribedEvents.has("browsingContext.load")) { + this.emitEvent("browsingContext.load", this.#getNavigationInfo(data)); + } + }; + /** * Create a new rectangle which will be an intersection of * rectangles specified as arguments. @@ -288,6 +329,51 @@ class BrowsingContextModule extends WindowGlobalBiDiModule { return new DOMRect(x_min, y_min, width, height); } + #startListening() { + if (this.#subscribedEvents.size == 0) { + this.#loadListener.startListening(); + } + } + + #stopListening() { + if (this.#subscribedEvents.size == 0) { + this.#loadListener.stopListening(); + } + } + + #subscribeEvent(event) { + switch (event) { + case "browsingContext._documentInteractive": + this.#startListening(); + this.#subscribedEvents.add("browsingContext._documentInteractive"); + break; + case "browsingContext.domContentLoaded": + this.#startListening(); + this.#subscribedEvents.add("browsingContext.domContentLoaded"); + break; + case "browsingContext.load": + this.#startListening(); + this.#subscribedEvents.add("browsingContext.load"); + break; + } + } + + #unsubscribeEvent(event) { + switch (event) { + case "browsingContext._documentInteractive": + this.#subscribedEvents.delete("browsingContext._documentInteractive"); + break; + case "browsingContext.domContentLoaded": + this.#subscribedEvents.delete("browsingContext.domContentLoaded"); + break; + case "browsingContext.load": + this.#subscribedEvents.delete("browsingContext.load"); + break; + } + + this.#stopListening(); + } + /** * Internal commands */ @@ -425,7 +511,7 @@ class BrowsingContextModule extends WindowGlobalBiDiModule { return this.#rectangleIntersection(originRect, clipRect); } - _locateNodes(params = {}) { + async _locateNodes(params = {}) { const { locator, maxNodeCount, serializationOptions, startNodes } = params; const realm = this.messageHandler.getRealm(); @@ -451,6 +537,14 @@ class BrowsingContextModule extends WindowGlobalBiDiModule { let returnedNodes; switch (locator.type) { + case lazy.LocatorType.accessibility: { + returnedNodes = await this.#locateNodesUsingAccessibilityAttributes( + contextNodes, + locator.value, + maxNodeCount + ); + break; + } case lazy.LocatorType.css: { returnedNodes = this.#locateNodesUsingCss( contextNodes, diff --git a/remote/webdriver-bidi/modules/windowglobal/input.sys.mjs b/remote/webdriver-bidi/modules/windowglobal/input.sys.mjs index b91cce2310..8db2cbb30b 100644 --- a/remote/webdriver-bidi/modules/windowglobal/input.sys.mjs +++ b/remote/webdriver-bidi/modules/windowglobal/input.sys.mjs @@ -34,6 +34,10 @@ class InputModule extends WindowGlobalBiDiModule { const actionChain = lazy.action.Chain.fromJSON(this.#actionState, actions); await actionChain.dispatch(this.#actionState, this.messageHandler.window); + + // Terminate the current wheel transaction if there is one. Wheel + // transactions should not live longer than a single action chain. + ChromeUtils.endWheelTransaction(); } async releaseActions() { -- cgit v1.2.3