From a90a5cba08fdf6c0ceb95101c275108a152a3aed Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 12 Jun 2024 07:35:37 +0200 Subject: Merging upstream version 127.0. Signed-off-by: Daniel Baumann --- remote/shared/Capture.sys.mjs | 2 +- remote/shared/DOM.sys.mjs | 34 +- remote/shared/Navigate.sys.mjs | 7 +- remote/shared/NetworkRequest.sys.mjs | 254 ++++++++++ remote/shared/NetworkResponse.sys.mjs | 131 ++++++ remote/shared/Permissions.sys.mjs | 90 ++++ remote/shared/listeners/NetworkEventRecord.sys.mjs | 290 ++---------- remote/shared/listeners/NetworkListener.sys.mjs | 11 +- .../test/browser/browser_NetworkListener.js | 31 +- remote/shared/test/xpcshell/test_DOM.js | 114 +++-- remote/shared/webdriver/Accessibility.sys.mjs | 519 +++++++++++++++++++++ remote/shared/webdriver/Actions.sys.mjs | 27 +- remote/shared/webdriver/Assert.sys.mjs | 30 ++ remote/shared/webdriver/Session.sys.mjs | 3 +- .../shared/webdriver/test/xpcshell/test_Assert.js | 14 + 15 files changed, 1240 insertions(+), 317 deletions(-) create mode 100644 remote/shared/NetworkRequest.sys.mjs create mode 100644 remote/shared/NetworkResponse.sys.mjs create mode 100644 remote/shared/Permissions.sys.mjs create mode 100644 remote/shared/webdriver/Accessibility.sys.mjs (limited to 'remote/shared') diff --git a/remote/shared/Capture.sys.mjs b/remote/shared/Capture.sys.mjs index ec34d09aba..a9a20c0b81 100644 --- a/remote/shared/Capture.sys.mjs +++ b/remote/shared/Capture.sys.mjs @@ -74,7 +74,7 @@ capture.canvas = async function ( ) { // FIXME(bug 1761032): This looks a bit sketchy, overrideDPPX doesn't // influence rendering... - const scale = win.browsingContext.overrideDPPX || win.devicePixelRatio; + const scale = browsingContext.overrideDPPX || win.devicePixelRatio; let canvasHeight = height * scale; let canvasWidth = width * scale; diff --git a/remote/shared/DOM.sys.mjs b/remote/shared/DOM.sys.mjs index 664f02328c..51c9298183 100644 --- a/remote/shared/DOM.sys.mjs +++ b/remote/shared/DOM.sys.mjs @@ -622,24 +622,14 @@ dom.isDisabled = function (el) { return false; } - switch (el.localName) { - case "option": - case "optgroup": - if (el.disabled) { - return true; - } - let parent = dom.findClosest(el, "optgroup,select"); - return dom.isDisabled(parent); - - case "button": - case "input": - case "select": - case "textarea": - return el.disabled; - - default: - return false; + // Selenium expects that even an enabled "option" element that is a child + // of a disabled "optgroup" or "select" element to be disabled. + if (["optgroup", "option"].includes(el.localName) && !el.disabled) { + const parent = dom.findClosest(el, "optgroup,select"); + return dom.isDisabled(parent); } + + return el.matches(":disabled"); }; /** @@ -1064,6 +1054,16 @@ dom.isElement = function (obj) { return dom.isDOMElement(obj) || dom.isXULElement(obj); }; +dom.isEnabled = function (el) { + let enabled = false; + + if (el.ownerDocument.contentType !== "text/xml") { + enabled = !dom.isDisabled(el); + } + + return enabled; +}; + /** * Returns the shadow root of an element. * diff --git a/remote/shared/Navigate.sys.mjs b/remote/shared/Navigate.sys.mjs index 9b72c0dfbf..cdb23b54c7 100644 --- a/remote/shared/Navigate.sys.mjs +++ b/remote/shared/Navigate.sys.mjs @@ -91,9 +91,14 @@ export async function waitForInitialNavigationCompleted( isInitial = browsingContext.currentWindowGlobal.isInitialDocument; } + const isLoadingDocument = listener.isLoadingDocument; + lazy.logger.trace( + lazy.truncate`[${browsingContext.id}] Wait for initial navigation: isInitial=${isInitial}, isLoadingDocument=${isLoadingDocument}` + ); + // If the current document is not the initial "about:blank" and is also // no longer loading, assume the navigation is done and return. - if (!isInitial && !listener.isLoadingDocument) { + if (!isInitial && !isLoadingDocument) { lazy.logger.trace( lazy.truncate`[${browsingContext.id}] Document already finished loading: ${browsingContext.currentURI?.spec}` ); diff --git a/remote/shared/NetworkRequest.sys.mjs b/remote/shared/NetworkRequest.sys.mjs new file mode 100644 index 0000000000..6524132752 --- /dev/null +++ b/remote/shared/NetworkRequest.sys.mjs @@ -0,0 +1,254 @@ +/* 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/. */ + +const lazy = {}; +ChromeUtils.defineESModuleGetters(lazy, { + NetworkUtils: + "resource://devtools/shared/network-observer/NetworkUtils.sys.mjs", + + notifyNavigationStarted: + "chrome://remote/content/shared/NavigationManager.sys.mjs", + TabManager: "chrome://remote/content/shared/TabManager.sys.mjs", +}); + +/** + * The NetworkRequest class is a wrapper around the internal channel which + * provides getters and methods closer to fetch's response concept + * (https://fetch.spec.whatwg.org/#concept-response). + */ +export class NetworkRequest { + #channel; + #contextId; + #navigationId; + #navigationManager; + #postData; + #rawHeaders; + #redirectCount; + #requestId; + #timedChannel; + #wrappedChannel; + + /** + * + * @param {nsIChannel} channel + * The channel for the request. + * @param {object} params + * @param {NavigationManager} params.navigationManager + * The NavigationManager where navigations for the current session are + * monitored. + * @param {string=} params.rawHeaders + * The request's raw (ie potentially compressed) headers + */ + constructor(channel, params) { + const { navigationManager, rawHeaders = "" } = params; + + this.#channel = channel; + this.#navigationManager = navigationManager; + this.#rawHeaders = rawHeaders; + + this.#timedChannel = this.#channel.QueryInterface(Ci.nsITimedChannel); + this.#wrappedChannel = ChannelWrapper.get(channel); + + this.#redirectCount = this.#timedChannel.redirectCount; + // The wrappedChannel id remains identical across redirects, whereas + // nsIChannel.channelId is different for each and every request. + this.#requestId = this.#wrappedChannel.id.toString(); + + this.#contextId = this.#getContextId(); + this.#navigationId = this.#getNavigationId(); + } + + get contextId() { + return this.#contextId; + } + + get errorText() { + // TODO: Update with a proper error text. Bug 1873037. + return ChromeUtils.getXPCOMErrorName(this.#channel.status); + } + + get headersSize() { + // TODO: rawHeaders will not be updated after modifying the headers via + // request interception. Need to find another way to retrieve the + // information dynamically. + return this.#rawHeaders.length; + } + + get method() { + return this.#channel.requestMethod; + } + + get navigationId() { + return this.#navigationId; + } + + get postDataSize() { + return this.#postData ? this.#postData.size : 0; + } + + get redirectCount() { + return this.#redirectCount; + } + + get requestId() { + return this.#requestId; + } + + get serializedURL() { + return this.#channel.URI.spec; + } + + get wrappedChannel() { + return this.#wrappedChannel; + } + + /** + * Retrieve the Fetch timings for the NetworkRequest. + * + * @returns {object} + * Object with keys corresponding to fetch timing names, and their + * corresponding values. + */ + getFetchTimings() { + const { + channelCreationTime, + redirectStartTime, + redirectEndTime, + dispatchFetchEventStartTime, + cacheReadStartTime, + domainLookupStartTime, + domainLookupEndTime, + connectStartTime, + connectEndTime, + secureConnectionStartTime, + requestStartTime, + responseStartTime, + responseEndTime, + } = this.#timedChannel; + + // fetchStart should be the post-redirect start time, which should be the + // first non-zero timing from: dispatchFetchEventStart, cacheReadStart and + // domainLookupStart. See https://www.w3.org/TR/navigation-timing-2/#processing-model + const fetchStartTime = + dispatchFetchEventStartTime || + cacheReadStartTime || + domainLookupStartTime; + + // Bug 1805478: Per spec, the origin time should match Performance API's + // timeOrigin for the global which initiated the request. This is not + // available in the parent process, so for now we will use 0. + const timeOrigin = 0; + + return { + timeOrigin, + requestTime: this.#convertTimestamp(channelCreationTime, timeOrigin), + redirectStart: this.#convertTimestamp(redirectStartTime, timeOrigin), + redirectEnd: this.#convertTimestamp(redirectEndTime, timeOrigin), + fetchStart: this.#convertTimestamp(fetchStartTime, timeOrigin), + dnsStart: this.#convertTimestamp(domainLookupStartTime, timeOrigin), + dnsEnd: this.#convertTimestamp(domainLookupEndTime, timeOrigin), + connectStart: this.#convertTimestamp(connectStartTime, timeOrigin), + connectEnd: this.#convertTimestamp(connectEndTime, timeOrigin), + tlsStart: this.#convertTimestamp(secureConnectionStartTime, timeOrigin), + tlsEnd: this.#convertTimestamp(connectEndTime, timeOrigin), + requestStart: this.#convertTimestamp(requestStartTime, timeOrigin), + responseStart: this.#convertTimestamp(responseStartTime, timeOrigin), + responseEnd: this.#convertTimestamp(responseEndTime, timeOrigin), + }; + } + + /** + * Retrieve the list of headers for the NetworkRequest. + * + * @returns {Array.Array} + * Array of (name, value) tuples. + */ + getHeadersList() { + const headers = []; + + this.#channel.visitRequestHeaders({ + visitHeader(name, value) { + // The `Proxy-Authorization` header even though it appears on the channel is not + // actually sent to the server for non CONNECT requests after the HTTP/HTTPS tunnel + // is setup by the proxy. + if (name == "Proxy-Authorization") { + return; + } + headers.push([name, value]); + }, + }); + + return headers; + } + + /** + * Update the postData for this NetworkRequest. This is currently forwarded + * by the DevTools' NetworkObserver. + * + * TODO: We should read this information dynamically from the channel so that + * we can get updated information in case it was modified via network + * interception. + * + * @param {object} postData + * The request POST data. + */ + setPostData(postData) { + this.#postData = postData; + } + + /** + * Convert the provided request timing to a timing relative to the beginning + * of the request. All timings are numbers representing high definition + * timestamps. + * + * @param {number} timing + * High definition timestamp for a request timing relative from the time + * origin. + * @param {number} requestTime + * High definition timestamp for the request start time relative from the + * time origin. + * + * @returns {number} + * High definition timestamp for the request timing relative to the start + * time of the request, or 0 if the provided timing was 0. + */ + #convertTimestamp(timing, requestTime) { + if (timing == 0) { + return 0; + } + + return timing - requestTime; + } + + #getContextId() { + const id = lazy.NetworkUtils.getChannelBrowsingContextID(this.#channel); + const browsingContext = BrowsingContext.get(id); + return lazy.TabManager.getIdForBrowsingContext(browsingContext); + } + + #getNavigationId() { + if (!this.#channel.isMainDocumentChannel) { + return null; + } + + const browsingContext = lazy.TabManager.getBrowsingContextById( + this.#contextId + ); + + let navigation = + this.#navigationManager.getNavigationForBrowsingContext(browsingContext); + + // `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 (!navigation || navigation.finished) { + navigation = lazy.notifyNavigationStarted({ + contextDetails: { context: browsingContext }, + url: this.serializedURL, + }); + } + + return navigation ? navigation.navigationId : null; + } +} diff --git a/remote/shared/NetworkResponse.sys.mjs b/remote/shared/NetworkResponse.sys.mjs new file mode 100644 index 0000000000..45a03fb445 --- /dev/null +++ b/remote/shared/NetworkResponse.sys.mjs @@ -0,0 +1,131 @@ +/* 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/. */ + +const lazy = {}; +ChromeUtils.defineESModuleGetters(lazy, { + NetworkUtils: + "resource://devtools/shared/network-observer/NetworkUtils.sys.mjs", +}); + +/** + * The NetworkResponse class is a wrapper around the internal channel which + * provides getters and methods closer to fetch's response concept + * (https://fetch.spec.whatwg.org/#concept-response). + */ +export class NetworkResponse { + #channel; + #decodedBodySize; + #encodedBodySize; + #fromCache; + #headersTransmittedSize; + #status; + #statusMessage; + #totalTransmittedSize; + #wrappedChannel; + + /** + * + * @param {nsIChannel} channel + * The channel for the response. + * @param {object} params + * @param {boolean} params.fromCache + * Whether the response was read from the cache or not. + * @param {string=} params.rawHeaders + * The response's raw (ie potentially compressed) headers + */ + constructor(channel, params) { + this.#channel = channel; + const { fromCache, rawHeaders = "" } = params; + this.#fromCache = fromCache; + this.#wrappedChannel = ChannelWrapper.get(channel); + + this.#decodedBodySize = 0; + this.#encodedBodySize = 0; + this.#headersTransmittedSize = rawHeaders.length; + this.#totalTransmittedSize = rawHeaders.length; + + // TODO: responseStatus and responseStatusText are sometimes inconsistent. + // For instance, they might be (304, Not Modified) when retrieved during the + // responseStarted event, and then (200, OK) during the responseCompleted + // event. + // For now consider them as immutable and store them on startup. + this.#status = this.#channel.responseStatus; + this.#statusMessage = this.#channel.responseStatusText; + } + + get decodedBodySize() { + return this.#decodedBodySize; + } + + get encodedBodySize() { + return this.#encodedBodySize; + } + + get headersTransmittedSize() { + return this.#headersTransmittedSize; + } + + get fromCache() { + return this.#fromCache; + } + + get protocol() { + return lazy.NetworkUtils.getProtocol(this.#channel); + } + + get serializedURL() { + return this.#channel.URI.spec; + } + + get status() { + return this.#status; + } + + get statusMessage() { + return this.#statusMessage; + } + + get totalTransmittedSize() { + return this.#totalTransmittedSize; + } + + addResponseContent(responseContent) { + this.#decodedBodySize = responseContent.decodedBodySize; + this.#encodedBodySize = responseContent.bodySize; + this.#totalTransmittedSize = responseContent.transferredSize; + } + + getComputedMimeType() { + // TODO: DevTools NetworkObserver is computing a similar value in + // addResponseContent, but uses an inconsistent implementation in + // addResponseStart. This approach can only be used as early as in + // addResponseHeaders. We should move this logic to the NetworkObserver and + // expose mimeType in addResponseStart. Bug 1809670. + let mimeType = ""; + + try { + mimeType = this.#wrappedChannel.contentType; + const contentCharset = this.#channel.contentCharset; + if (contentCharset) { + mimeType += `;charset=${contentCharset}`; + } + } catch (e) { + // Ignore exceptions when reading contentType/contentCharset + } + + return mimeType; + } + + getHeadersList() { + const headers = []; + + this.#channel.visitOriginalResponseHeaders({ + visitHeader(name, value) { + headers.push([name, value]); + }, + }); + + return headers; + } +} diff --git a/remote/shared/Permissions.sys.mjs b/remote/shared/Permissions.sys.mjs new file mode 100644 index 0000000000..50996bf701 --- /dev/null +++ b/remote/shared/Permissions.sys.mjs @@ -0,0 +1,90 @@ +/* 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/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs", +}); + +/** @namespace */ +export const permissions = {}; + +/** + * Get a permission type for the "storage-access" permission. + * + * @param {nsIURI} uri + * The URI to use for building the permission type. + * + * @returns {string} permissionType + * The permission type for the "storage-access" permission. + */ +permissions.getStorageAccessPermissionsType = function (uri) { + const thirdPartyPrincipalSite = Services.eTLD.getSite(uri); + return "3rdPartyFrameStorage^" + thirdPartyPrincipalSite; +}; + +/** + * Set a permission given a permission descriptor, a permission state, + * an origin. + * + * @param {PermissionDescriptor} descriptor + * The descriptor of the permission which will be updated. + * @param {string} state + * State of the permission. It can be `granted`, `denied` or `prompt`. + * @param {string} origin + * The origin which is used as a target for permission update. + * + * @throws {UnsupportedOperationError} + * If state has unsupported value. + */ +permissions.set = function (descriptor, state, origin) { + const principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin); + + switch (state) { + case "granted": { + Services.perms.addFromPrincipal( + principal, + descriptor.type, + Services.perms.ALLOW_ACTION + ); + return; + } + case "denied": { + Services.perms.addFromPrincipal( + principal, + descriptor.type, + Services.perms.DENY_ACTION + ); + return; + } + case "prompt": { + Services.perms.removeFromPrincipal(principal, descriptor.type); + return; + } + default: + throw new lazy.error.UnsupportedOperationError( + "Unrecognized permission keyword for 'Set Permission' operation" + ); + } +}; + +/** + * Validate the permission. + * + * @param {string} permissionName + * The name of the permission which will be validated. + * + * @throws {UnsupportedOperationError} + * If permissionName is not supported. + */ +permissions.validatePermission = function (permissionName) { + // Bug 1609427: PermissionDescriptor for "camera" and "microphone" are not yet implemented. + if (["camera", "microphone"].includes(permissionName)) { + throw new lazy.error.UnsupportedOperationError( + `"descriptor.name" "${permissionName}" is currently unsupported` + ); + } +}; diff --git a/remote/shared/listeners/NetworkEventRecord.sys.mjs b/remote/shared/listeners/NetworkEventRecord.sys.mjs index 72b43e3de1..0f592d62b0 100644 --- a/remote/shared/listeners/NetworkEventRecord.sys.mjs +++ b/remote/shared/listeners/NetworkEventRecord.sys.mjs @@ -4,10 +4,8 @@ const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { - NetworkUtils: - "resource://devtools/shared/network-observer/NetworkUtils.sys.mjs", - - TabManager: "chrome://remote/content/shared/TabManager.sys.mjs", + NetworkRequest: "chrome://remote/content/shared/NetworkRequest.sys.mjs", + NetworkResponse: "chrome://remote/content/shared/NetworkResponse.sys.mjs", }); /** @@ -18,17 +16,10 @@ ChromeUtils.defineESModuleGetters(lazy, { * NetworkListener instance which created it. */ export class NetworkEventRecord { - #contextId; #fromCache; - #isMainDocumentChannel; #networkListener; - #redirectCount; - #requestChannel; - #requestData; - #requestId; - #responseChannel; - #responseData; - #wrappedChannel; + #request; + #response; /** * @@ -39,56 +30,21 @@ export class NetworkEventRecord { * The nsIChannel behind this network event. * @param {NetworkListener} networkListener * The NetworkListener which created this NetworkEventRecord. + * @param {NavigationManager} navigationManager + * The NavigationManager which belongs to the same session as this + * NetworkEventRecord. */ - constructor(networkEvent, channel, networkListener) { - this.#requestChannel = channel; - this.#responseChannel = null; + constructor(networkEvent, channel, networkListener, navigationManager) { + this.#request = new lazy.NetworkRequest(channel, { + navigationManager, + rawHeaders: networkEvent.rawHeaders, + }); + this.#response = null; this.#fromCache = networkEvent.fromCache; - this.#isMainDocumentChannel = channel.isMainDocumentChannel; - - this.#wrappedChannel = ChannelWrapper.get(channel); this.#networkListener = networkListener; - // The context ids computed by TabManager have the lifecycle of a navigable - // and can be reused for all the events emitted from this record. - this.#contextId = this.#getContextId(); - - // The wrappedChannel id remains identical across redirects, whereas - // nsIChannel.channelId is different for each and every request. - this.#requestId = this.#wrappedChannel.id.toString(); - - const { cookies, headers } = - lazy.NetworkUtils.fetchRequestHeadersAndCookies(channel); - - // See the RequestData type definition for the full list of properties that - // should be set on this object. - this.#requestData = { - bodySize: null, - cookies, - headers, - headersSize: networkEvent.rawHeaders ? networkEvent.rawHeaders.length : 0, - method: channel.requestMethod, - request: this.#requestId, - timings: {}, - url: channel.URI.spec, - }; - - // See the ResponseData type definition for the full list of properties that - // should be set on this object. - this.#responseData = { - // encoded size (body) - bodySize: null, - content: { - // decoded size - size: null, - }, - // encoded size (headers) - headersSize: null, - url: channel.URI.spec, - }; - // NetworkObserver creates a network event when request headers have been // parsed. // According to the BiDi spec, we should emit beforeRequestSent when adding @@ -113,8 +69,7 @@ export class NetworkEventRecord { * The request POST data. */ addRequestPostData(postData) { - // Only the postData size is needed for RemoteAgent consumers. - this.#requestData.bodySize = postData.size; + this.#request.setPostData(postData); } /** @@ -130,25 +85,11 @@ export class NetworkEventRecord { * @param {string} options.rawHeaders */ addResponseStart(options) { - const { channel, fromCache, rawHeaders = "" } = options; - this.#responseChannel = channel; - - const { headers } = - lazy.NetworkUtils.fetchResponseHeadersAndCookies(channel); - - const headersSize = rawHeaders.length; - this.#responseData = { - ...this.#responseData, - bodySize: 0, - bytesReceived: headersSize, + const { channel, fromCache, rawHeaders } = options; + this.#response = new lazy.NetworkResponse(channel, { + rawHeaders, fromCache: this.#fromCache || !!fromCache, - headers, - headersSize, - mimeType: this.#getMimeType(), - protocol: lazy.NetworkUtils.getProtocol(channel), - status: channel.responseStatus, - statusText: channel.responseStatusText, - }; + }); // This should be triggered when all headers have been received, matching // the WebDriverBiDi response started trigger in `4.6. HTTP-network fetch` @@ -189,25 +130,16 @@ export class NetworkEventRecord { * * Required API for a NetworkObserver event owner. * - * @param {object} response + * @param {object} responseContent * An object which represents the response content. * @param {object} responseInfo * Additional meta data about the response. */ - addResponseContent(response, responseInfo) { - // Update content-related sizes with the latest data from addResponseContent. - this.#responseData = { - ...this.#responseData, - bodySize: response.bodySize, - bytesReceived: response.transferredSize, - content: { - size: response.decodedBodySize, - }, - }; - + addResponseContent(responseContent, responseInfo) { if (responseInfo.blockedReason) { this.#emitFetchError(); } else { + this.#response.addResponseContent(responseContent); this.#emitResponseCompleted(); } } @@ -234,201 +166,37 @@ export class NetworkEventRecord { this.#emitAuthRequired(authCallbacks); } - /** - * Convert the provided request timing to a timing relative to the beginning - * of the request. All timings are numbers representing high definition - * timestamps. - * - * @param {number} timing - * High definition timestamp for a request timing relative from the time - * origin. - * @param {number} requestTime - * High definition timestamp for the request start time relative from the - * time origin. - * @returns {number} - * High definition timestamp for the request timing relative to the start - * time of the request, or 0 if the provided timing was 0. - */ - #convertTimestamp(timing, requestTime) { - if (timing == 0) { - return 0; - } - - return timing - requestTime; - } - #emitAuthRequired(authCallbacks) { - this.#updateDataFromTimedChannel(); - this.#networkListener.emit("auth-required", { authCallbacks, - contextId: this.#contextId, - isNavigationRequest: this.#isMainDocumentChannel, - redirectCount: this.#redirectCount, - requestChannel: this.#requestChannel, - requestData: this.#requestData, - responseChannel: this.#responseChannel, - responseData: this.#responseData, - timestamp: Date.now(), + request: this.#request, + response: this.#response, }); } #emitBeforeRequestSent() { - this.#updateDataFromTimedChannel(); - this.#networkListener.emit("before-request-sent", { - contextId: this.#contextId, - isNavigationRequest: this.#isMainDocumentChannel, - redirectCount: this.#redirectCount, - requestChannel: this.#requestChannel, - requestData: this.#requestData, - timestamp: Date.now(), + request: this.#request, }); } #emitFetchError() { - this.#updateDataFromTimedChannel(); - this.#networkListener.emit("fetch-error", { - contextId: this.#contextId, - // TODO: Update with a proper error text. Bug 1873037. - errorText: ChromeUtils.getXPCOMErrorName(this.#requestChannel.status), - isNavigationRequest: this.#isMainDocumentChannel, - redirectCount: this.#redirectCount, - requestChannel: this.#requestChannel, - requestData: this.#requestData, - timestamp: Date.now(), + request: this.#request, }); } #emitResponseCompleted() { - this.#updateDataFromTimedChannel(); - this.#networkListener.emit("response-completed", { - contextId: this.#contextId, - isNavigationRequest: this.#isMainDocumentChannel, - redirectCount: this.#redirectCount, - requestChannel: this.#requestChannel, - requestData: this.#requestData, - responseChannel: this.#responseChannel, - responseData: this.#responseData, - timestamp: Date.now(), + request: this.#request, + response: this.#response, }); } #emitResponseStarted() { - this.#updateDataFromTimedChannel(); - this.#networkListener.emit("response-started", { - contextId: this.#contextId, - isNavigationRequest: this.#isMainDocumentChannel, - redirectCount: this.#redirectCount, - requestChannel: this.#requestChannel, - requestData: this.#requestData, - responseChannel: this.#responseChannel, - responseData: this.#responseData, - timestamp: Date.now(), + request: this.#request, + response: this.#response, }); } - - #getBrowsingContext() { - const id = lazy.NetworkUtils.getChannelBrowsingContextID( - this.#requestChannel - ); - return BrowsingContext.get(id); - } - - /** - * Retrieve the navigable id for the current browsing context associated to - * the requests' channel. Network events are recorded in the parent process - * so we always expect to be able to use TabManager.getIdForBrowsingContext. - * - * @returns {string} - * The navigable id corresponding to the given browsing context. - */ - #getContextId() { - return lazy.TabManager.getIdForBrowsingContext(this.#getBrowsingContext()); - } - - #getMimeType() { - // TODO: DevTools NetworkObserver is computing a similar value in - // addResponseContent, but uses an inconsistent implementation in - // addResponseStart. This approach can only be used as early as in - // addResponseHeaders. We should move this logic to the NetworkObserver and - // expose mimeType in addResponseStart. Bug 1809670. - let mimeType = ""; - - try { - mimeType = this.#wrappedChannel.contentType; - const contentCharset = this.#requestChannel.contentCharset; - if (contentCharset) { - mimeType += `;charset=${contentCharset}`; - } - } catch (e) { - // Ignore exceptions when reading contentType/contentCharset - } - - return mimeType; - } - - #getTimingsFromTimedChannel(timedChannel) { - const { - channelCreationTime, - redirectStartTime, - redirectEndTime, - dispatchFetchEventStartTime, - cacheReadStartTime, - domainLookupStartTime, - domainLookupEndTime, - connectStartTime, - connectEndTime, - secureConnectionStartTime, - requestStartTime, - responseStartTime, - responseEndTime, - } = timedChannel; - - // fetchStart should be the post-redirect start time, which should be the - // first non-zero timing from: dispatchFetchEventStart, cacheReadStart and - // domainLookupStart. See https://www.w3.org/TR/navigation-timing-2/#processing-model - const fetchStartTime = - dispatchFetchEventStartTime || - cacheReadStartTime || - domainLookupStartTime; - - // Bug 1805478: Per spec, the origin time should match Performance API's - // timeOrigin for the global which initiated the request. This is not - // available in the parent process, so for now we will use 0. - const timeOrigin = 0; - - return { - timeOrigin, - requestTime: this.#convertTimestamp(channelCreationTime, timeOrigin), - redirectStart: this.#convertTimestamp(redirectStartTime, timeOrigin), - redirectEnd: this.#convertTimestamp(redirectEndTime, timeOrigin), - fetchStart: this.#convertTimestamp(fetchStartTime, timeOrigin), - dnsStart: this.#convertTimestamp(domainLookupStartTime, timeOrigin), - dnsEnd: this.#convertTimestamp(domainLookupEndTime, timeOrigin), - connectStart: this.#convertTimestamp(connectStartTime, timeOrigin), - connectEnd: this.#convertTimestamp(connectEndTime, timeOrigin), - tlsStart: this.#convertTimestamp(secureConnectionStartTime, timeOrigin), - tlsEnd: this.#convertTimestamp(connectEndTime, timeOrigin), - requestStart: this.#convertTimestamp(requestStartTime, timeOrigin), - responseStart: this.#convertTimestamp(responseStartTime, timeOrigin), - responseEnd: this.#convertTimestamp(responseEndTime, timeOrigin), - }; - } - - /** - * Update the timings and the redirect count from the nsITimedChannel - * corresponding to the current channel. This should be called before emitting - * any event from this class. - */ - #updateDataFromTimedChannel() { - const timedChannel = this.#requestChannel.QueryInterface( - Ci.nsITimedChannel - ); - this.#redirectCount = timedChannel.redirectCount; - this.#requestData.timings = this.#getTimingsFromTimedChannel(timedChannel); - } } diff --git a/remote/shared/listeners/NetworkListener.sys.mjs b/remote/shared/listeners/NetworkListener.sys.mjs index 500d2005dc..d0d6d0e44f 100644 --- a/remote/shared/listeners/NetworkListener.sys.mjs +++ b/remote/shared/listeners/NetworkListener.sys.mjs @@ -44,11 +44,13 @@ ChromeUtils.defineESModuleGetters(lazy, { export class NetworkListener { #devtoolsNetworkObserver; #listening; + #navigationManager; - constructor() { + constructor(navigationManager) { lazy.EventEmitter.decorate(this); this.#listening = false; + this.#navigationManager = navigationManager; } destroy() { @@ -104,6 +106,11 @@ export class NetworkListener { }; #onNetworkEvent = (networkEvent, channel) => { - return new lazy.NetworkEventRecord(networkEvent, channel, this); + return new lazy.NetworkEventRecord( + networkEvent, + channel, + this, + this.#navigationManager + ); }; } diff --git a/remote/shared/listeners/test/browser/browser_NetworkListener.js b/remote/shared/listeners/test/browser/browser_NetworkListener.js index cc1b42f2fc..211ccef49c 100644 --- a/remote/shared/listeners/test/browser/browser_NetworkListener.js +++ b/remote/shared/listeners/test/browser/browser_NetworkListener.js @@ -2,6 +2,9 @@ * 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/. */ +const { NavigationManager } = ChromeUtils.importESModule( + "chrome://remote/content/shared/NavigationManager.sys.mjs" +); const { NetworkListener } = ChromeUtils.importESModule( "chrome://remote/content/shared/listeners/NetworkListener.sys.mjs" ); @@ -10,7 +13,10 @@ const { TabManager } = ChromeUtils.importESModule( ); add_task(async function test_beforeRequestSent() { - const listener = new NetworkListener(); + const navigationManager = new NavigationManager(); + navigationManager.startMonitoring(); + + const listener = new NetworkListener(navigationManager); const events = []; const onEvent = (name, data) => events.push(data); listener.on("before-request-sent", onEvent); @@ -54,10 +60,14 @@ add_task(async function test_beforeRequestSent() { gBrowser.removeTab(tab2); listener.off("before-request-sent", onEvent); listener.destroy(); + navigationManager.destroy(); }); add_task(async function test_beforeRequestSent_newTab() { - const listener = new NetworkListener(); + const navigationManager = new NavigationManager(); + navigationManager.startMonitoring(); + + const listener = new NetworkListener(navigationManager); const onBeforeRequestSent = listener.once("before-request-sent"); listener.startListening(); @@ -76,10 +86,14 @@ add_task(async function test_beforeRequestSent_newTab() { "https://example.com/document-builder.sjs?html=tab" ); gBrowser.removeTab(tab); + navigationManager.destroy(); }); add_task(async function test_fetchError() { - const listener = new NetworkListener(); + const navigationManager = new NavigationManager(); + navigationManager.startMonitoring(); + + const listener = new NetworkListener(navigationManager); const onFetchError = listener.once("fetch-error"); listener.startListening(); @@ -90,11 +104,16 @@ add_task(async function test_fetchError() { const event = await onFetchError; assertNetworkEvent(event, contextId, "https://not_a_valid_url/"); - is(event.errorText, "NS_ERROR_UNKNOWN_HOST"); + is(event.request.errorText, "NS_ERROR_UNKNOWN_HOST"); gBrowser.removeTab(tab); + navigationManager.destroy(); }); function assertNetworkEvent(event, expectedContextId, expectedUrl) { - is(event.contextId, expectedContextId, "Event has the expected context id"); - is(event.requestData.url, expectedUrl, "Event has the expected url"); + is( + event.request.contextId, + expectedContextId, + "Event has the expected context id" + ); + is(event.request.serializedURL, expectedUrl, "Event has the expected url"); } diff --git a/remote/shared/test/xpcshell/test_DOM.js b/remote/shared/test/xpcshell/test_DOM.js index 19844659b9..03ac27ed45 100644 --- a/remote/shared/test/xpcshell/test_DOM.js +++ b/remote/shared/test/xpcshell/test_DOM.js @@ -75,27 +75,55 @@ function setupTest() { - +
+