Bug 1552324 - Implement subset of Network.responseReceived r=remote-protocol-reviewers,whimboo
authorMaja Frydrychowicz <mjzffr@gmail.com>
Fri, 15 May 2020 20:29:00 +0000
changeset 530420 2518e35059833c860c5f60f34c06998343bea6bc
parent 530419 59b9b768bc8c0e006bc79c687fd8226c66e7a68e
child 530421 d4f9dcd527b9f001142fef721d004de14d5e04ad
push id37421
push usercbrindusan@mozilla.com
push dateSat, 16 May 2020 09:34:57 +0000
treeherdermozilla-central@882de07e4cbe [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersremote-protocol-reviewers, whimboo
bugs1552324
milestone78.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1552324 - Implement subset of Network.responseReceived r=remote-protocol-reviewers,whimboo Differential Revision: https://phabricator.services.mozilla.com/D74579
remote/domains/parent/Network.jsm
remote/observers/NetworkObserver.jsm
remote/puppeteer-expected.json
remote/test/browser/head.js
remote/test/browser/network/browser.ini
remote/test/browser/network/browser_navigationEvents.js
remote/test/browser/network/browser_requestWillBeSent.js
remote/test/browser/network/browser_responseReceived.js
remote/test/browser/network/doc_networkEvents.html
remote/test/browser/network/doc_requestWillBeSent.html
remote/test/browser/network/file_networkEvents.js
remote/test/browser/network/file_requestWillBeSent.js
--- a/remote/domains/parent/Network.jsm
+++ b/remote/domains/parent/Network.jsm
@@ -46,16 +46,17 @@ const LOAD_CAUSE_STRINGS = {
 };
 
 class Network extends Domain {
   constructor(session) {
     super(session);
     this.enabled = false;
 
     this._onRequest = this._onRequest.bind(this);
+    this._onResponse = this._onResponse.bind(this);
   }
 
   destructor() {
     this.disable();
 
     super.destructor();
   }
 
@@ -63,26 +64,28 @@ class Network extends Domain {
     if (this.enabled) {
       return;
     }
     this.enabled = true;
     this.session.networkObserver.startTrackingBrowserNetwork(
       this.session.target.browser
     );
     this.session.networkObserver.on("request", this._onRequest);
+    this.session.networkObserver.on("response", this._onResponse);
   }
 
   disable() {
     if (!this.enabled) {
       return;
     }
     this.session.networkObserver.stopTrackingBrowserNetwork(
       this.session.target.browser
     );
     this.session.networkObserver.off("request", this._onRequest);
+    this.session.networkObserver.off("response", this._onResponse);
     this.enabled = false;
   }
 
   /**
    * Deletes browser cookies with matching name and url or domain/path pair.
    *
    * @param {Object} options
    * @param {string} name
@@ -401,16 +404,48 @@ class Network extends Domain {
       initiator: undefined,
       redirectResponse: undefined,
       type: LOAD_CAUSE_STRINGS[data.cause] || "unknown",
       // Bug 1637363 - Add subframe support
       frameId: topFrame.browsingContext?.id.toString(),
       hasUserGesture: undefined,
     });
   }
+
+  _onResponse(eventName, httpChannel, data) {
+    const wrappedChannel = ChannelWrapper.get(httpChannel);
+    const topFrame = getLoadContext(httpChannel).topFrameElement;
+    const headers = headersAsObject(data.headers);
+    this.emit("Network.responseReceived", {
+      requestId: data.requestId,
+      loaderId: data.loaderId,
+      timestamp: Date.now() / 1000,
+      type: LOAD_CAUSE_STRINGS[data.cause] || "unknown",
+      response: {
+        url: httpChannel.URI.spec,
+        status: data.status,
+        statusText: data.statusText,
+        headers,
+        mimeType: wrappedChannel.contentType,
+        requestHeaders: headersAsObject(data.requestHeaders),
+        connectionReused: undefined,
+        connectionId: undefined,
+        remoteIPAddress: data.remoteIPAddress,
+        remotePort: data.remotePort,
+        fromDiskCache: data.fromCache,
+        encodedDataLength: undefined,
+        protocol: httpChannel.protocolVersion,
+        securityDetails: data.securityDetails,
+        // unknown, neutral, insecure, secure, info, insecure-broken
+        securityState: "unknown",
+      },
+      // Bug 1637363 - Add subframe support
+      frameId: topFrame.browsingContext?.id.toString(),
+    });
+  }
 }
 
 function getLoadContext(httpChannel) {
   let loadContext = null;
   try {
     if (httpChannel.notificationCallbacks) {
       loadContext = httpChannel.notificationCallbacks.getInterface(
         Ci.nsILoadContext
--- a/remote/observers/NetworkObserver.jsm
+++ b/remote/observers/NetworkObserver.jsm
@@ -251,38 +251,44 @@ class NetworkObserver {
     const loadContext = getLoadContext(httpChannel);
     if (
       !loadContext ||
       !this._browserSessionCount.has(loadContext.topFrameElement)
     ) {
       return;
     }
     httpChannel.QueryInterface(Ci.nsIHttpChannelInternal);
-    const headers = [];
-    httpChannel.visitResponseHeaders({
-      visitHeader: (name, value) => headers.push({ name, value }),
-    });
-
+    const causeType = httpChannel.loadInfo
+      ? httpChannel.loadInfo.externalContentPolicyType
+      : Ci.nsIContentPolicy.TYPE_OTHER;
     let remoteIPAddress = undefined;
     let remotePort = undefined;
     try {
       remoteIPAddress = httpChannel.remoteAddress;
       remotePort = httpChannel.remotePort;
     } catch (e) {
       // remoteAddress is not defined for cached requests.
     }
     this.emit("response", httpChannel, {
       requestId: requestId(httpChannel),
       securityDetails: getSecurityDetails(httpChannel),
       fromCache,
-      headers,
+      headers: responseHeaders(httpChannel),
+      requestHeaders: requestHeaders(httpChannel),
       remoteIPAddress,
       remotePort,
       status: httpChannel.responseStatus,
       statusText: httpChannel.responseStatusText,
+      cause: causeType,
+      causeString: causeTypeToString(causeType),
+      // clients expect loaderId == requestId for document navigation
+      loaderId:
+        causeType == Ci.nsIContentPolicy.TYPE_DOCUMENT
+          ? requestId(httpChannel)
+          : undefined,
     });
   }
 
   _onResponseFinished(browser, httpChannel, body) {
     const responseStorage = this._browserResponseStorages.get(browser);
     if (!responseStorage) {
       return;
     }
@@ -427,16 +433,24 @@ function requestId(httpChannel) {
 function requestHeaders(httpChannel) {
   const headers = [];
   httpChannel.visitRequestHeaders({
     visitHeader: (name, value) => headers.push({ name, value }),
   });
   return headers;
 }
 
+function responseHeaders(httpChannel) {
+  const headers = [];
+  httpChannel.visitResponseHeaders({
+    visitHeader: (name, value) => headers.push({ name, value }),
+  });
+  return headers;
+}
+
 function causeTypeToString(causeType) {
   for (let key in Ci.nsIContentPolicy) {
     if (Ci.nsIContentPolicy[key] === causeType) {
       return key;
     }
   }
   return "TYPE_OTHER";
 }
--- a/remote/puppeteer-expected.json
+++ b/remote/puppeteer-expected.json
@@ -699,29 +699,29 @@
   ],
   "Firefox Browser Page Page.goto should work with redirects": [
     "PASS"
   ],
   "Firefox Browser Page Page.goto should navigate to about:blank": [
     "PASS"
   ],
   "Firefox Browser Page Page.goto should return response when page changes its URL after load": [
-    "FAIL"
+    "PASS"
   ],
   "Firefox Browser Page Page.goto should work with subframes return 204": [
     "PASS"
   ],
   "Firefox Browser Page Page.goto should fail when server returns 204": [
     "TIMEOUT"
   ],
   "Firefox Browser Page Page.goto should navigate to empty page with domcontentloaded": [
-    "FAIL"
+    "PASS"
   ],
   "Firefox Browser Page Page.goto should work when page calls history API in beforeunload": [
-    "FAIL"
+    "PASS"
   ],
   "Firefox Browser Page Page.goto should navigate to empty page with networkidle0": [
     "SKIP"
   ],
   "Firefox Browser Page Page.goto should navigate to empty page with networkidle2": [
     "SKIP"
   ],
   "Firefox Browser Page Page.goto should fail when navigating to bad url": [
@@ -750,26 +750,26 @@
   ],
   "Firefox Browser Page Page.goto should prioritize default navigation timeout over default timeout": [
     "PASS"
   ],
   "Firefox Browser Page Page.goto should disable timeout when its set to 0": [
     "PASS"
   ],
   "Firefox Browser Page Page.goto should work when navigating to valid url": [
-    "FAIL"
+    "PASS"
   ],
   "Firefox Browser Page Page.goto should work when navigating to data url": [
     "FAIL"
   ],
   "Firefox Browser Page Page.goto should work when navigating to 404": [
-    "FAIL"
+    "PASS"
   ],
   "Firefox Browser Page Page.goto should return last response in redirect chain": [
-    "FAIL"
+    "PASS"
   ],
   "Firefox Browser Page Page.goto should wait for network idle to succeed navigation": [
     "SKIP"
   ],
   "Firefox Browser Page Page.goto should not leak listeners during navigation": [
     "PASS"
   ],
   "Firefox Browser Page Page.goto should not leak listeners during bad navigation": [
@@ -780,26 +780,26 @@
   ],
   "Firefox Browser Page Page.goto should navigate to dataURL and fire dataURL requests": [
     "FAIL"
   ],
   "Firefox Browser Page Page.goto should navigate to URL with hash and fire requests without hash": [
     "FAIL"
   ],
   "Firefox Browser Page Page.goto should work with self requesting page": [
-    "FAIL"
+    "PASS"
   ],
   "Firefox Browser Page Page.goto should fail when navigating and show the url at the error message": [
     "PASS"
   ],
   "Firefox Browser Page Page.goto should send referer": [
     "FAIL"
   ],
   "Firefox Browser Page Page.waitForNavigation should work": [
-    "FAIL"
+    "FAIL", "PASS"
   ],
   "Firefox Browser Page Page.waitForNavigation should work with both domcontentloaded and load": [
     "PASS"
   ],
   "Firefox Browser Page Page.waitForNavigation should work with clicking on anchor links": [
     "TIMEOUT",
     "FAIL"
   ],
@@ -814,17 +814,17 @@
   "Firefox Browser Page Page.waitForNavigation should work with DOM history.back()/history.forward()": [
     "FAIL",
     "TIMEOUT"
   ],
   "Firefox Browser Page Page.waitForNavigation should work when subframe issues window.stop()": [
     "SKIP"
   ],
   "Firefox Browser Page Page.goBack should work": [
-    "FAIL"
+    "PASS"
   ],
   "Firefox Browser Page Page.goBack should work with HistoryAPI": [
     "FAIL"
   ],
   "Firefox Browser Page Frame.goto should navigate subframes": [
     "FAIL"
   ],
   "Firefox Browser Page Frame.goto should reject when frame detaches": [
@@ -856,68 +856,68 @@
   ],
   "Firefox Browser Page Request.frame should work for subframe navigation request": [
     "FAIL"
   ],
   "Firefox Browser Page Request.frame should work for fetch requests": [
     "PASS"
   ],
   "Firefox Browser Page Request.headers should work": [
-    "FAIL"
+    "PASS"
   ],
   "Firefox Browser Page Response.headers should work": [
-    "FAIL"
+    "PASS"
   ],
   "Firefox Browser Page Response.fromCache should return |false| for non-cached content": [
-    "SKIP"
+    "PASS"
   ],
   "Firefox Browser Page Response.fromCache should work": [
-    "SKIP"
+    "FAIL"
   ],
   "Firefox Browser Page Response.fromServiceWorker should return |false| for non-service-worker content": [
     "SKIP"
   ],
   "Firefox Browser Page Response.fromServiceWorker Response.fromServiceWorker": [
     "SKIP"
   ],
   "Firefox Browser Page Request.postData should work": [
     "FAIL"
   ],
   "Firefox Browser Page Request.postData should be |undefined| when there is no post data": [
-    "FAIL"
+    "PASS"
   ],
   "Firefox Browser Page Response.text should work": [
-    "FAIL"
+    "TIMEOUT"
   ],
   "Firefox Browser Page Response.text should return uncompressed text": [
-    "FAIL"
+    "TIMEOUT"
   ],
   "Firefox Browser Page Response.text should throw when requesting body of redirected response": [
     "FAIL"
   ],
   "Firefox Browser Page Response.text should wait until response completes": [
     "TIMEOUT"
   ],
   "Firefox Browser Page Response.json should work": [
-    "FAIL"
+    "TIMEOUT"
   ],
   "Firefox Browser Page Response.buffer should work": [
-    "FAIL"
+    "TIMEOUT"
   ],
   "Firefox Browser Page Response.buffer should work with compression": [
-    "FAIL"
+    "TIMEOUT"
   ],
   "Firefox Browser Page Response.statusText should work": [
-    "FAIL"
+    "PASS"
   ],
   "Firefox Browser Page Network Events Page.Events.Request": [
-    "FAIL"
+    "PASS"
   ],
   "Firefox Browser Page Network Events Page.Events.Response": [
-    "FAIL"
+    "PASS"
   ],
   "Firefox Browser Page Network Events Page.Events.RequestFailed": [
     "FAIL"
   ],
   "Firefox Browser Page Network Events Page.Events.RequestFinished": [
     "FAIL"
   ],
   "Firefox Browser Page Network Events should fire events in proper order": [
@@ -1192,29 +1192,29 @@
   ],
   "Firefox Browser Page Page.waitForRequest should respect default timeout": [
     "PASS"
   ],
   "Firefox Browser Page Page.waitForRequest should work with no timeout": [
     "PASS"
   ],
   "Firefox Browser Page Page.waitForResponse should work": [
-    "TIMEOUT"
+    "PASS"
   ],
   "Firefox Browser Page Page.waitForResponse should respect timeout": [
     "PASS"
   ],
   "Firefox Browser Page Page.waitForResponse should respect default timeout": [
     "PASS"
   ],
   "Firefox Browser Page Page.waitForResponse should work with predicate": [
-    "TIMEOUT"
+    "PASS"
   ],
   "Firefox Browser Page Page.waitForResponse should work with no timeout": [
-    "TIMEOUT"
+    "PASS"
   ],
   "Firefox Browser Page Page.exposeFunction should work": [
     "FAIL"
   ],
   "Firefox Browser Page Page.exposeFunction should throw exception in page context": [
     "FAIL"
   ],
   "Firefox Browser Page Page.exposeFunction should support throwing \"null\"": [
@@ -1800,26 +1800,26 @@
   ],
   "Firefox Browser BrowserContext should isolate localStorage and cookies": [
     "FAIL"
   ],
   "Firefox Browser BrowserContext should work across sessions": [
     "FAIL"
   ],
   "Firefox ignoreHTTPSErrors Response.securityDetails should work": [
-    "FAIL"
+    "PASS"
   ],
   "Firefox ignoreHTTPSErrors Response.securityDetails should be |null| for non-secure requests": [
-    "FAIL"
+    "PASS"
   ],
   "Firefox ignoreHTTPSErrors Response.securityDetails Network redirects should report SecurityDetails": [
-    "FAIL"
+    "PASS"
   ],
   "Firefox ignoreHTTPSErrors should work": [
-    "FAIL"
+    "PASS"
   ],
   "Firefox ignoreHTTPSErrors should work with request interception": [
     "FAIL"
   ],
   "Firefox ignoreHTTPSErrors should work with mixed content": [
     "FAIL"
   ],
   "Firefox DefaultBrowserContext page.cookies() should work": [
--- a/remote/test/browser/head.js
+++ b/remote/test/browser/head.js
@@ -439,42 +439,73 @@ class RecordEvents {
    * The recording stops once we accumulate more than the expected
    * total of all configured events.
    *
    * @param {Object} options
    * @param {CDPEvent} options.event
    *     https://github.com/cyrus-and/chrome-remote-interface#clientdomaineventcallback
    * @param {string} options.eventName
    *     Name to use for reporting.
+   * @param {Function=} options.callback
+   *     ({ eventName, payload }) => {} to be called when each event is received
    * @param {function(payload):string=} options.messageFn
    */
   addRecorder(options = {}) {
     const {
       event,
       eventName,
-      messageFn = () => `Received ${eventName}`,
+      messageFn = () => `Recorded ${eventName}`,
+      callback,
     } = options;
 
     const promise = new Promise(resolve => {
       const unsubscribe = event(payload => {
         info(messageFn(payload));
-        this.events.push({ eventName, payload });
+        this.events.push({ eventName, payload, index: this.events.length });
+        callback?.({ eventName, payload, index: this.events.length - 1 });
         if (this.events.length > this.total) {
           this.subscriptions.delete(unsubscribe);
           unsubscribe();
           resolve(this.events);
         }
       });
       this.subscriptions.add(unsubscribe);
     });
 
     this.promises.add(promise);
   }
 
   /**
+   * Register a promise to await while recording the timeline. The returned
+   * callback resolves the registered promise and adds `step`
+   * to the timeline, along with an associated payload, if provided.
+   *
+   * @param {string} step
+   * @return {Function} callback
+   */
+  addPromise(step) {
+    let callback;
+    const promise = new Promise(resolve => {
+      callback = value => {
+        resolve();
+        info(`Recorded ${step}`);
+        this.events.push({
+          eventName: step,
+          payload: value,
+          index: this.events.length,
+        });
+        return value;
+      };
+    });
+
+    this.promises.add(promise);
+    return callback;
+  }
+
+  /**
    * Record events until we hit the timeout or the expected total is exceeded.
    *
    * @param {number=} timeout
    *     Timeout in milliseconds. Defaults to 1000.
    *
    * @return {Array<{ eventName, payload }>} Recorded events
    */
   async record(timeout = 1000) {
@@ -508,9 +539,24 @@ class RecordEvents {
    * @return {Array<object>}
    *     The events payload, if any.
    */
   findEvents(eventName) {
     return this.events
       .filter(event => event.eventName == eventName)
       .map(event => event.payload);
   }
+
+  /**
+   * Find index of first occurrence of the given event.
+   *
+   * @param {string} eventName
+   *
+   * @return {number} The event index, -1 if not found.
+   */
+  indexOf(eventName) {
+    const event = this.events.find(el => el.eventName == eventName);
+    if (event) {
+      return event.index;
+    }
+    return -1;
+  }
 }
--- a/remote/test/browser/network/browser.ini
+++ b/remote/test/browser/network/browser.ini
@@ -4,22 +4,24 @@ subsuite = remote
 prefs =
   remote.enabled=true
   remote.frames.enabled=true
 support-files =
   !/remote/test/browser/chrome-remote-interface.js
   !/remote/test/browser/head.js
   head.js
   doc_empty.html
-  doc_requestWillBeSent.html
-  file_requestWillBeSent.js
+  doc_networkEvents.html
+  file_networkEvents.js
   sjs-cookies.sjs
 
 [browser_deleteCookies.js]
 [browser_emulateNetworkConditions.js]
 [browser_getCookies.js]
 skip-if = (os == 'win' && os_version == '10.0' && ccov) # Bug 1605650
+[browser_navigationEvents.js]
+[browser_responseReceived.js]
 [browser_requestWillBeSent.js]
 [browser_setCookie.js]
 [browser_setCookies.js]
 [browser_setCacheDisabled.js]
 skip-if = true # Bug 1610382
 [browser_setUserAgentOverride.js]
copy from remote/test/browser/network/browser_requestWillBeSent.js
copy to remote/test/browser/network/browser_navigationEvents.js
--- a/remote/test/browser/network/browser_requestWillBeSent.js
+++ b/remote/test/browser/network/browser_navigationEvents.js
@@ -1,48 +1,145 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
-// Test the Network.requestWillBeSent event
+// Test order and consistency of Network events as a whole.
+// Details of specific events are checked in event-specific test files.
 
 const PAGE_URL =
-  "http://example.com/browser/remote/test/browser/network/doc_requestWillBeSent.html";
+  "http://example.com/browser/remote/test/browser/network/doc_networkEvents.html";
 const JS_URL =
-  "http://example.com/browser/remote/test/browser/network/file_requestWillBeSent.js";
+  "http://example.com/browser/remote/test/browser/network/file_networkEvents.js";
+
+add_task(async function documentNavigationWithScriptResource({ client }) {
+  const { Page, Network } = client;
+  await Network.enable();
+  const { history, urlToEvents } = configureHistory(client);
+  const navigateDone = history.addPromise("Page.navigate");
+
+  const { frameId } = await Page.navigate({ url: PAGE_URL }).then(navigateDone);
+  ok(frameId, "Page.navigate returned a frameId");
 
-add_task(async function({ client }) {
-  const { Page, Network } = client;
+  info("Wait for Network events");
+  const events = await history.record();
+  is(events.length, 5, "Expected number of events");
+  const eventNames = events.map(
+    item => `${item.eventName}(${item.payload.type || ""})`
+  );
+  info(`Received events: ${eventNames}`);
+  const documentEvents = urlToEvents.get(PAGE_URL);
+  const resourceEvents = urlToEvents.get(JS_URL);
+  is(
+    2,
+    documentEvents.length,
+    "Expected number of Network events for document"
+  );
+  is(
+    2,
+    resourceEvents.length,
+    "Expected number of Network events for resource"
+  );
 
-  await Network.enable();
-  info("Network domain has been enabled");
+  const docRequest = documentEvents[0].event;
+  is(docRequest.request.url, PAGE_URL, "Got the doc request");
+  is(docRequest.documentURL, PAGE_URL, "documenURL matches request url");
+
+  const resourceRequest = resourceEvents[0].event;
+  is(resourceRequest.documentURL, PAGE_URL, "documentURL is trigger document");
+  is(resourceRequest.request.url, JS_URL, "Got the JS request");
+  ok(
+    documentEvents[0].index < resourceEvents[0].index,
+    "Document request received before resource request"
+  );
+  const navigateStep = history.indexOf("Page.navigate");
+  ok(
+    documentEvents[1].index < navigateStep,
+    "Page.navigate returns after document response"
+  );
+  ok(
+    navigateStep < resourceEvents[0].index,
+    "Page.navigate returns before resource request"
+  );
 
-  let requests = 0;
-  const onRequests = new Promise(resolve => {
-    Network.requestWillBeSent(event => {
-      info("Received a request");
-      switch (++requests) {
-        case 1:
-          is(event.request.url, PAGE_URL, "Got the page request");
-          is(event.type, "Document", "The page request has 'Document' type");
-          is(
-            event.requestId,
-            event.loaderId,
-            "The page request has requestId = loaderId (puppeteer assumes that to detect the page start request)"
-          );
-          break;
-        case 2:
-          is(event.request.url, JS_URL, "Got the JS request");
-          resolve();
-          break;
-        case 3:
-          ok(false, "Expect only two requests");
+  const docResponse = documentEvents[1].event;
+  is(docResponse.response.url, PAGE_URL, "Got the doc response");
+  is(
+    docRequest.frameId,
+    docResponse.frameId,
+    "Doc response frame id matches that of doc request"
+  );
+  ok(!!docResponse.response.headers.server, "Doc response has headers");
+  // TODO? response reports extra request header "upgrade-insecure-requests":"1"
+  // Assert.deepEqual(
+  //   docResponse.response.requestHeaders,
+  //   docRequest.request.headers,
+  //   "Response event reports same request headers as request event"
+  // );
+
+  ok(
+    docRequest.timestamp <= docResponse.timestamp,
+    "Document request happens before document response"
+  );
+  const resourceResponse = resourceEvents[1].event;
+  is(resourceResponse.response.url, JS_URL, "Got the resource response");
+  todo(
+    resourceResponse.loaderId === docRequest.loaderId,
+    "The same loaderId is used for dependent responses (Bug 1637838)"
+  );
+  ok(!!resourceResponse.frameId, "Resource response has a frame id");
+  is(
+    docRequest.frameId,
+    resourceResponse.frameId,
+    "Resource response frame id matches that of doc request"
+  );
+  Assert.deepEqual(
+    resourceResponse.response.requestHeaders,
+    resourceRequest.request.headers,
+    "Response event reports same request headers as request event"
+  );
+  ok(
+    resourceRequest.timestamp <= resourceResponse.timestamp,
+    "Document request happens before document response"
+  );
+});
+
+function configureHistory(client) {
+  const REQUEST = "Network.requestWillBeSent";
+  const RESPONSE = "Network.responseReceived";
+
+  const { Network } = client;
+  const history = new RecordEvents(4);
+  const urlToEvents = new Map();
+  function updateUrlToEvents(kind) {
+    return ({ payload, index, eventName }) => {
+      const url = payload[kind]?.url;
+      if (!url) {
+        return;
       }
-    });
+      if (!urlToEvents.get(url)) {
+        urlToEvents.set(url, [{ index, event: payload, eventName }]);
+      } else {
+        urlToEvents.get(url).push({ index, event: payload, eventName });
+      }
+    };
+  }
+
+  history.addRecorder({
+    event: Network.requestWillBeSent,
+    eventName: REQUEST,
+    messageFn: payload => {
+      return `Received ${REQUEST} for ${payload.request?.url}`;
+    },
+    callback: updateUrlToEvents("request"),
   });
 
-  const { frameId } = await Page.navigate({ url: PAGE_URL });
-  ok(frameId, "Page.navigate returned a frameId");
-
-  info("Wait for Network.requestWillBeSent events");
-  await onRequests;
-});
+  history.addRecorder({
+    event: Network.responseReceived,
+    eventName: RESPONSE,
+    messageFn: payload => {
+      return `Received ${RESPONSE} for ${payload.response?.url}`;
+    },
+    callback: updateUrlToEvents("response"),
+  });
+  return { history, urlToEvents };
+}
--- a/remote/test/browser/network/browser_requestWillBeSent.js
+++ b/remote/test/browser/network/browser_requestWillBeSent.js
@@ -1,48 +1,100 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
-// Test the Network.requestWillBeSent event
-
 const PAGE_URL =
-  "http://example.com/browser/remote/test/browser/network/doc_requestWillBeSent.html";
+  "http://example.com/browser/remote/test/browser/network/doc_networkEvents.html";
 const JS_URL =
-  "http://example.com/browser/remote/test/browser/network/file_requestWillBeSent.js";
+  "http://example.com/browser/remote/test/browser/network/file_networkEvents.js";
+
+add_task(async function noEventsWhenNetworkDomainDisabled({ client }) {
+  const history = configureHistory(client, 0);
+  await loadURL(PAGE_URL);
+
+  const events = await history.record();
+  is(events.length, 0, "Expected no Network.responseReceived events");
+});
 
-add_task(async function({ client }) {
+add_task(async function noEventsAfterNetworkDomainDisabled({ client }) {
+  const { Network } = client;
+
+  const history = configureHistory(client, 0);
+  await Network.enable();
+  await Network.disable();
+  await loadURL(PAGE_URL);
+
+  const events = await history.record();
+  is(events.length, 0, "Expected no Network.responseReceived events");
+});
+
+add_task(async function documentNavigationWithResource({ client }) {
   const { Page, Network } = client;
+  await Network.enable();
+  const history = configureHistory(client);
 
-  await Network.enable();
-  info("Network domain has been enabled");
+  const { frameId: frameIdNav } = await Page.navigate({ url: PAGE_URL });
+  ok(frameIdNav, "Page.navigate returned a frameId");
+
+  info("Wait for Network events");
+  const events = await history.record();
+  is(events.length, 2, "Expected number of Network.requestWillBeSent events");
 
-  let requests = 0;
-  const onRequests = new Promise(resolve => {
-    Network.requestWillBeSent(event => {
-      info("Received a request");
-      switch (++requests) {
-        case 1:
-          is(event.request.url, PAGE_URL, "Got the page request");
-          is(event.type, "Document", "The page request has 'Document' type");
-          is(
-            event.requestId,
-            event.loaderId,
-            "The page request has requestId = loaderId (puppeteer assumes that to detect the page start request)"
-          );
-          break;
-        case 2:
-          is(event.request.url, JS_URL, "Got the JS request");
-          resolve();
-          break;
-        case 3:
-          ok(false, "Expect only two requests");
-      }
-    });
+  const docRequest = events[0].payload;
+  is(docRequest.request.url, PAGE_URL, "Got the doc request");
+  is(docRequest.documentURL, PAGE_URL, "documenURL matches request url");
+  is(docRequest.type, "Document", "The doc request has 'Document' type");
+  is(docRequest.request.method, "GET", "The doc request has 'GET' method");
+  is(
+    docRequest.requestId,
+    docRequest.loaderId,
+    "The doc request has requestId = loaderId"
+  );
+  is(
+    docRequest.frameId,
+    frameIdNav,
+    "Doc request returns same frameId as Page.navigate"
+  );
+  is(docRequest.request.headers.host, "example.com", "Doc request has headers");
+
+  const resourceRequest = events[1].payload;
+  is(resourceRequest.documentURL, PAGE_URL, "documentURL is trigger document");
+  is(resourceRequest.request.url, JS_URL, "Got the JS request");
+  is(
+    resourceRequest.request.headers.host,
+    "example.com",
+    "Doc request has headers"
+  );
+  is(resourceRequest.type, "Script", "The page request has 'Script' type");
+  is(resourceRequest.request.method, "GET", "The doc request has 'GET' method");
+  is(
+    docRequest.frameId,
+    frameIdNav,
+    "Resource request returns same frameId as Page.navigate"
+  );
+  todo(
+    resourceRequest.loaderId === docRequest.loaderId,
+    "The same loaderId is used for dependent requests (Bug 1637838)"
+  );
+  ok(
+    docRequest.timestamp <= resourceRequest.timestamp,
+    "Document request happens before resource request"
+  );
+});
+
+function configureHistory(client) {
+  const REQUEST = "Network.requestWillBeSent";
+
+  const { Network } = client;
+  const history = new RecordEvents(4);
+
+  history.addRecorder({
+    event: Network.requestWillBeSent,
+    eventName: REQUEST,
+    messageFn: payload => {
+      return `Received ${REQUEST} for ${payload.request?.url}`;
+    },
   });
 
-  const { frameId } = await Page.navigate({ url: PAGE_URL });
-  ok(frameId, "Page.navigate returned a frameId");
-
-  info("Wait for Network.requestWillBeSent events");
-  await onRequests;
-});
+  return history;
+}
new file mode 100644
--- /dev/null
+++ b/remote/test/browser/network/browser_responseReceived.js
@@ -0,0 +1,132 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const PAGE_URL =
+  "http://example.com/browser/remote/test/browser/network/doc_networkEvents.html";
+const JS_URL =
+  "http://example.com/browser/remote/test/browser/network/file_networkEvents.js";
+
+add_task(async function noEventsWhenNetworkDomainDisabled({ client }) {
+  const history = configureHistory(client, 0);
+  await loadURL(PAGE_URL);
+
+  const events = await history.record();
+  is(events.length, 0, "Expected no Network.responseReceived events");
+});
+
+add_task(async function noEventsAfterNetworkDomainDisabled({ client }) {
+  const { Network } = client;
+
+  const history = configureHistory(client, 0);
+  await Network.enable();
+  await Network.disable();
+  await loadURL(PAGE_URL);
+
+  const events = await history.record();
+  is(events.length, 0, "Expected no Network.responseReceived events");
+});
+
+add_task(async function documentNavigationWithResource({ client }) {
+  const { Page, Network } = client;
+  await Network.enable();
+  const history = configureHistory(client, 2);
+
+  const { frameId: frameIdNav } = await Page.navigate({ url: PAGE_URL });
+  ok(frameIdNav, "Page.navigate returned a frameId");
+
+  info("Wait for Network events");
+  const events = await history.record();
+  is(events.length, 2, "Expected number of Network.responseReceived events");
+
+  const docResponse = events[0].payload;
+  is(docResponse.response.url, PAGE_URL, "Got the doc response");
+  is(
+    docResponse.response.mimeType,
+    "text/html",
+    "Doc response has expected mimeType"
+  );
+  is(docResponse.type, "Document", "The doc response has 'Document' type");
+  is(
+    docResponse.requestId,
+    docResponse.loaderId,
+    "The doc request has requestId = loaderId"
+  );
+  is(
+    docResponse.frameId,
+    frameIdNav,
+    "Doc response returns same frameId as Page.navigate"
+  );
+  ok(!!docResponse.response.headers.server, "Doc response has headers");
+  is(docResponse.response.status, 200, "Doc response status is 200");
+  is(docResponse.response.statusText, "OK", "Doc response status is OK");
+  if (docResponse.response.fromDiskCache === false) {
+    is(
+      docResponse.response.remoteIPAddress,
+      "127.0.0.1",
+      "Doc response has an IP address"
+    );
+    ok(
+      typeof docResponse.response.remotePort == "number",
+      "Doc response has a remotePort"
+    );
+  }
+  is(
+    docResponse.response.protocol,
+    "http/1.1",
+    "Doc response has expected protocol"
+  );
+
+  const resourceResponse = events[1].payload;
+  is(resourceResponse.response.url, JS_URL, "Got the resource response");
+  is(
+    resourceResponse.response.mimeType,
+    "application/x-javascript",
+    "Resource response has expected mimeType"
+  );
+  is(
+    resourceResponse.type,
+    "Script",
+    "The resource response has 'Script' type"
+  );
+  ok(!!resourceResponse.frameId, "Resource response has a frame id");
+  ok(
+    !!resourceResponse.response.headers.server,
+    "Resource response has headers"
+  );
+  is(resourceResponse.response.status, 200, "Resource response status is 200");
+  is(resourceResponse.response.statusText, "OK", "Response status is OK");
+  if (resourceResponse.response.fromDiskCache === false) {
+    is(
+      resourceResponse.response.remoteIPAddress,
+      docResponse.response.remoteIPAddress,
+      "Resource response has same IP address and doc response"
+    );
+    ok(
+      typeof resourceResponse.response.remotePort == "number",
+      "Resource response has a remotePort"
+    );
+  }
+  is(
+    resourceResponse.response.protocol,
+    "http/1.1",
+    "Resource response has expected protocol"
+  );
+});
+
+function configureHistory(client, total) {
+  const RESPONSE = "Network.responseReceived";
+
+  const { Network } = client;
+  const history = new RecordEvents(total);
+
+  history.addRecorder({
+    event: Network.responseReceived,
+    eventName: RESPONSE,
+    messageFn: payload => {
+      return `Received ${RESPONSE} for ${payload.response?.url}`;
+    },
+  });
+  return history;
+}
rename from remote/test/browser/network/doc_requestWillBeSent.html
rename to remote/test/browser/network/doc_networkEvents.html
--- a/remote/test/browser/network/doc_requestWillBeSent.html
+++ b/remote/test/browser/network/doc_networkEvents.html
@@ -1,9 +1,9 @@
 <!DOCTYPE html>
 <html>
 <head>
-  <title>Test page for requestWillBeSent</title>
+  <title>Test page for Network events</title>
 </head>
 <body>
-  <script type="text/javascript" src="file_requestWillBeSent.js"></script>
+  <script type="text/javascript" src="file_networkEvents.js"></script>
 </body>
 </html>
rename from remote/test/browser/network/file_requestWillBeSent.js
rename to remote/test/browser/network/file_networkEvents.js
--- a/remote/test/browser/network/file_requestWillBeSent.js
+++ b/remote/test/browser/network/file_networkEvents.js
@@ -1,2 +1,2 @@
-// Test file to emit Network.requestWillBeSent events.
+// Test file to emit Network events.
 var foo = true;