Bug 1438979 - Re-enable browser_webconsole_network_messages_expand.js. r=Honza.
☠☠ backed out by 5be17ff9af7a ☠ ☠
authorNicolas Chevobbe <nchevobbe@mozilla.com>
Mon, 16 Dec 2019 10:13:39 +0000
changeset 507059 d09d8fa3332f9076f523f716657ffd4bf2a24064
parent 507058 37297e07c67b8cb6dc9c4c9929bc29dfde753e4c
child 507060 feb0e7470c707b14852f75e4cb4b362aa6030ce7
push id36922
push userncsoregi@mozilla.com
push dateMon, 16 Dec 2019 17:21:47 +0000
treeherdermozilla-central@27d0d6cc2131 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersHonza
bugs1438979
milestone73.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 1438979 - Re-enable browser_webconsole_network_messages_expand.js. r=Honza. The test is split in two to make it easier to read. Differential Revision: https://phabricator.services.mozilla.com/D56884
devtools/client/netmonitor/src/utils/request-utils.js
devtools/client/webconsole/reducers/messages.js
devtools/client/webconsole/test/browser/browser.ini
devtools/client/webconsole/test/browser/browser_webconsole_network_messages_expand.js
devtools/client/webconsole/test/browser/browser_webconsole_network_messages_expand_before_updates.js
devtools/client/webconsole/test/browser/browser_webconsole_stubs_network_event.js
devtools/client/webconsole/test/browser/sjs_slow-response-test-server.sjs
devtools/client/webconsole/webconsole-wrapper.js
--- a/devtools/client/netmonitor/src/utils/request-utils.js
+++ b/devtools/client/netmonitor/src/utils/request-utils.js
@@ -46,17 +46,17 @@ async function getFormDataSections(
   });
 
   const contentTypeLongString = contentTypeHeader
     ? contentTypeHeader.value
     : "";
 
   const contentType = await getLongString(contentTypeLongString);
 
-  if (contentType.includes("x-www-form-urlencoded")) {
+  if (contentType && contentType.includes("x-www-form-urlencoded")) {
     const postDataLongString = postData.postData.text;
     const text = await getLongString(postDataLongString);
 
     for (const section of text.split(/\r\n|\r|\n/)) {
       // Before displaying it, make sure this section of the POST data
       // isn't a line containing upload stream headers.
       if (payloadHeaders.every(header => !section.startsWith(header.name))) {
         formDataSections.push(section);
--- a/devtools/client/webconsole/reducers/messages.js
+++ b/devtools/client/webconsole/reducers/messages.js
@@ -588,17 +588,20 @@ function messages(
         ),
       };
 
     case constants.NETWORK_MESSAGE_UPDATE:
       return {
         ...state,
         networkMessagesUpdateById: {
           ...networkMessagesUpdateById,
-          [action.message.id]: action.message,
+          [action.message.id]: {
+            ...(networkMessagesUpdateById[action.message.id] || {}),
+            ...action.message,
+          },
         },
       };
 
     case UPDATE_REQUEST:
     case constants.NETWORK_UPDATE_REQUEST: {
       const request = networkMessagesUpdateById[action.id];
       if (!request) {
         return state;
--- a/devtools/client/webconsole/test/browser/browser.ini
+++ b/devtools/client/webconsole/test/browser/browser.ini
@@ -406,18 +406,18 @@ skip-if = fission
 [browser_webconsole_multiple_windows_and_tabs.js]
 [browser_webconsole_navigate_to_parse_error.js]
 fail-if = fission
 [browser_webconsole_network_attach.js]
 [browser_webconsole_network_exceptions.js]
 skip-if = fission
 [browser_webconsole_network_message_close_on_escape.js]
 [browser_webconsole_network_message_ctrl_click.js]
+[browser_webconsole_network_messages_expand_before_updates.js]
 [browser_webconsole_network_messages_expand.js]
-skip-if = true # Bug XXX  # Bug 1438979
 [browser_webconsole_network_messages_openinnet.js]
 fail-if = fission
 [browser_webconsole_network_messages_resend_request.js]
 fail-if = fission
 [browser_webconsole_network_messages_stacktrace_console_initiated_request.js]
 [browser_webconsole_network_messages_status_code.js]
 [browser_webconsole_network_requests_from_chrome.js]
 [browser_webconsole_network_reset_filter.js]
--- a/devtools/client/webconsole/test/browser/browser_webconsole_network_messages_expand.js
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_network_messages_expand.js
@@ -1,88 +1,35 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 const TEST_FILE = "test-network-request.html";
 const TEST_PATH =
-  "https://example.com/browser/devtools/client/webconsole/" + "test/browser/";
+  "https://example.com/browser/devtools/client/webconsole/test/browser/";
 const TEST_URI = TEST_PATH + TEST_FILE;
 
 requestLongerTimeout(2);
 
 pushPref("devtools.webconsole.filter.net", false);
 pushPref("devtools.webconsole.filter.netxhr", true);
 
-const tabs = [
-  {
-    id: "headers",
-    testEmpty: testEmptyHeaders,
-    testContent: testHeaders,
-  },
-  {
-    id: "cookies",
-    testEmpty: testEmptyCookies,
-    testContent: testCookies,
-  },
-  {
-    id: "params",
-    testEmpty: testEmptyParams,
-    testContent: testParams,
-  },
-  {
-    id: "response",
-    testEmpty: testEmptyResponse,
-    testContent: testResponse,
-  },
-  {
-    id: "timings",
-    testEmpty: testEmptyTimings,
-    testContent: testTimings,
-  },
-  {
-    id: "stack-trace",
-    testEmpty: testEmptyStackTrace,
-    testContent: testStackTrace,
-  },
-  {
-    id: "security",
-    testEmpty: testEmptySecurity,
-    testContent: testSecurity,
-  },
-];
-
 /**
  * Main test for checking HTTP logs in the Console panel.
  */
 add_task(async function task() {
   const hud = await openNewTabAndConsole(TEST_URI);
 
   const currentTab = gBrowser.selectedTab;
   const target = await TargetFactory.forTab(currentTab);
 
   // Execute XHR and expand it after all network
   // update events are received. Consequently,
   // check out content of all (HTTP details) tabs.
-  await openRequestAfterUpdates(target, hud);
-
-  // Test proper UI update when request is opened.
-  // For every tab (with HTTP details):
-  // 1. Execute long-time request
-  // 2. Expand the net log before the request finishes (set default tab)
-  // 3. Check the default tab empty content
-  // 4. Wait till the request finishes
-  // 5. Check content of all tabs
-  for (const tab of tabs) {
-    await openRequestBeforeUpdates(target, hud, tab);
-  }
-});
-
-async function openRequestAfterUpdates(target, hud) {
   const toolbox = gDevTools.getToolbox(target);
 
   const xhrUrl = TEST_PATH + "sjs_slow-response-test-server.sjs";
   const onMessage = waitForMessage(hud, xhrUrl);
   const onRequestUpdates = waitForRequestUpdates(hud);
   const onPayloadReady = waitForPayloadReady(hud);
 
   // Fire an XHR POST request.
@@ -98,68 +45,17 @@ async function openRequestAfterUpdates(t
   // Expand network log
   await expandXhrMessage(messageNode);
 
   const toggleButtonNode = messageNode.querySelector(".sidebar-toggle");
   ok(!toggleButtonNode, "Sidebar toggle button shouldn't be shown");
 
   await onPayloadReady;
   await testNetworkMessage(toolbox, messageNode);
-}
-
-async function openRequestBeforeUpdates(target, hud, tab) {
-  const toolbox = gDevTools.getToolbox(target);
-
-  await clearOutput(hud);
-
-  const xhrUrl = TEST_PATH + "sjs_slow-response-test-server.sjs";
-  const onMessage = waitForMessage(hud, xhrUrl);
-  const onRequestUpdates = waitForRequestUpdates(hud);
-  const onPayloadReady = waitForPayloadReady(hud);
-
-  // Fire an XHR POST request.
-  SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() {
-    content.wrappedJSObject.testXhrPostSlowResponse();
-  });
-  const { node: messageNode } = await onMessage;
-  ok(messageNode, "Network message found.");
-
-  // Set the default panel.
-  const state = hud.ui.wrapper.getStore().getState();
-  state.ui.networkMessageActiveTabId = tab.id;
-
-  // Expand network log
-  await expandXhrMessage(messageNode);
-
-  // Except the security tab. It isn't available till the
-  // "securityInfo" packet type is received, so doesn't
-  // fit this part of the test.
-  if (tab.id != "security") {
-    // Make sure the current tab is the expected one.
-    const currentTab = messageNode.querySelector(`#${tab.id}-tab`);
-    is(
-      currentTab.getAttribute("aria-selected"),
-      "true",
-      "The correct tab is selected"
-    );
-
-    // The tab should be empty now.
-    tab.testEmpty(messageNode);
-  }
-
-  // Wait till all updates and payload are received.
-  await onRequestUpdates;
-  await onPayloadReady;
-
-  // Test content of the default tab.
-  await tab.testContent(messageNode);
-
-  // Test all tabs in the network log.
-  await testNetworkMessage(toolbox, messageNode);
-}
+});
 
 // Panel testing helpers
 
 async function testNetworkMessage(toolbox, messageNode) {
   await testStatusInfo(messageNode);
   await testHeaders(messageNode);
   await testCookies(messageNode);
   await testParams(messageNode);
@@ -173,58 +69,42 @@ async function testNetworkMessage(toolbo
 // Status Info
 
 function testStatusInfo(messageNode) {
   const statusInfo = messageNode.querySelector(".status-info");
   ok(statusInfo, "Status info is not empty");
 }
 
 // Headers
-
-function testEmptyHeaders(messageNode) {
-  const emptyNotice = messageNode.querySelector("#headers-panel .empty-notice");
-  ok(emptyNotice, "Headers tab is empty");
-}
-
 async function testHeaders(messageNode) {
   const headersTab = messageNode.querySelector("#headers-tab");
   ok(headersTab, "Headers tab is available");
 
   // Select Headers tab and check the content.
   headersTab.click();
-  await waitFor(() =>
-    messageNode.querySelector("#headers-panel .headers-overview")
+  await waitFor(
+    () => messageNode.querySelector("#headers-panel .headers-overview"),
+    "Wait for .header-overview to be rendered"
   );
 }
 
 // Cookies
-
-function testEmptyCookies(messageNode) {
-  const emptyNotice = messageNode.querySelector("#cookies-panel .empty-notice");
-  ok(emptyNotice, "Cookies tab is empty");
-}
-
 async function testCookies(messageNode) {
   const cookiesTab = messageNode.querySelector("#cookies-tab");
   ok(cookiesTab, "Cookies tab is available");
 
   // Select tab and check the content.
   cookiesTab.click();
-  await waitFor(() =>
-    messageNode.querySelector("#cookies-panel .treeValueCell")
+  await waitFor(
+    () => messageNode.querySelector("#cookies-panel .treeValueCell"),
+    "Wait for .treeValueCell to be rendered"
   );
 }
 
 // Params
-
-function testEmptyParams(messageNode) {
-  const emptyNotice = messageNode.querySelector("#params-panel .empty-notice");
-  ok(emptyNotice, "Params tab is empty");
-}
-
 async function testParams(messageNode) {
   const paramsTab = messageNode.querySelector("#params-tab");
   ok(paramsTab, "Params tab is available");
 
   // Select Params tab and check the content. CodeMirror initialization
   // is delayed to prevent UI freeze, so wait for a little while.
   paramsTab.click();
   const paramsPanel = messageNode.querySelector("#params-panel");
@@ -235,22 +115,16 @@ async function testParams(messageNode) {
   ok(paramsContent, "Params content is available");
   ok(
     paramsContent.textContent.includes("Hello world!"),
     "Post body is correct"
   );
 }
 
 // Response
-
-function testEmptyResponse(messageNode) {
-  const panel = messageNode.querySelector("#response-panel .tab-panel");
-  is(panel.textContent, "", "Cookies tab is empty");
-}
-
 async function testResponse(messageNode) {
   const responseTab = messageNode.querySelector("#response-tab");
   ok(responseTab, "Response tab is available");
 
   // Select Response tab and check the content. CodeMirror initialization
   // is delayed, so again wait for a little while.
   responseTab.click();
   const responsePanel = messageNode.querySelector("#response-panel");
@@ -258,73 +132,55 @@ async function testResponse(messageNode)
   const responseContent = messageNode.querySelector(
     "#response-panel .editor-row-container .CodeMirror"
   );
   ok(responseContent, "Response content is available");
   ok(responseContent.textContent, "Response text is available");
 }
 
 // Timings
-
-function testEmptyTimings(messageNode) {
-  const panel = messageNode.querySelector("#timings-panel .tab-panel");
-  is(panel.textContent, "", "Timings tab is empty");
-}
-
 async function testTimings(messageNode) {
   const timingsTab = messageNode.querySelector("#timings-tab");
   ok(timingsTab, "Timings tab is available");
 
   // Select Timings tab and check the content.
   timingsTab.click();
-  await waitFor(() =>
+  const timingsContent = await waitFor(() =>
     messageNode.querySelector(
-      "#timings-panel .timings-container .timings-label"
+      "#timings-panel .timings-container .timings-label",
+      "Wait for .timings-label to be rendered"
     )
   );
-  const timingsContent = messageNode.querySelector(
-    "#timings-panel .timings-container .timings-label"
-  );
   ok(timingsContent, "Timings content is available");
   ok(timingsContent.textContent, "Timings text is available");
 }
 
 // Stack Trace
-
-function testEmptyStackTrace(messageNode) {
-  const panel = messageNode.querySelector("#stack-trace-panel .stack-trace");
-  is(panel.textContent, "", "StackTrace tab is empty");
-}
-
 async function testStackTrace(messageNode) {
   const stackTraceTab = messageNode.querySelector("#stack-trace-tab");
   ok(stackTraceTab, "StackTrace tab is available");
 
-  // Select Timings tab and check the content.
+  // Select Stack Trace tab and check the content.
   stackTraceTab.click();
-  await waitFor(() =>
-    messageNode.querySelector("#stack-trace-panel .frame-link")
+  await waitFor(
+    () => messageNode.querySelector("#stack-trace-panel .frame-link"),
+    "Wait for .frame-link to be rendered"
   );
 }
 
 // Security
-
-function testEmptySecurity(messageNode) {
-  const panel = messageNode.querySelector("#security-panel .tab-panel");
-  is(panel.textContent, "", "Security tab is empty");
-}
-
 async function testSecurity(messageNode) {
   const securityTab = messageNode.querySelector("#security-tab");
   ok(securityTab, "Security tab is available");
 
-  // Select Timings tab and check the content.
+  // Select Security tab and check the content.
   securityTab.click();
-  await waitFor(() =>
-    messageNode.querySelector("#security-panel .treeTable .treeRow")
+  await waitFor(
+    () => messageNode.querySelector("#security-panel .treeTable .treeRow"),
+    "Wait for #security-panel .treeTable .treeRow to be rendered"
   );
 }
 
 // Waiting helpers
 
 async function waitForPayloadReady(hud) {
   return hud.ui.once("network-request-payload-ready");
 }
@@ -339,10 +195,13 @@ async function waitForRequestUpdates(hud
   return hud.ui.once("network-message-updated");
 }
 
 function expandXhrMessage(node) {
   info(
     "Click on XHR message and wait for the network detail panel to be displayed"
   );
   node.querySelector(".url").click();
-  return waitFor(() => node.querySelector(".network-info"));
+  return waitFor(
+    () => node.querySelector(".network-info"),
+    "Wait for .network-info to be rendered"
+  );
 }
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_network_messages_expand_before_updates.js
@@ -0,0 +1,309 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_FILE = "test-network-request.html";
+const TEST_PATH =
+  "https://example.com/browser/devtools/client/webconsole/test/browser/";
+const TEST_URI = TEST_PATH + TEST_FILE;
+
+requestLongerTimeout(2);
+
+pushPref("devtools.webconsole.filter.net", false);
+pushPref("devtools.webconsole.filter.netxhr", true);
+
+const tabs = [
+  {
+    id: "headers",
+    testEmpty: testEmptyHeaders,
+    testContent: testHeaders,
+  },
+  {
+    id: "cookies",
+    testEmpty: testEmptyCookies,
+    testContent: testCookies,
+  },
+  {
+    id: "params",
+    testEmpty: testEmptyParams,
+    testContent: testParams,
+  },
+  {
+    id: "response",
+    testEmpty: testEmptyResponse,
+    testContent: testResponse,
+  },
+  {
+    id: "timings",
+    testEmpty: testEmptyTimings,
+    testContent: testTimings,
+  },
+  {
+    id: "stack-trace",
+    testEmpty: testEmptyStackTrace,
+    testContent: testStackTrace,
+  },
+  {
+    id: "security",
+    testEmpty: testEmptySecurity,
+    testContent: testSecurity,
+  },
+];
+
+/**
+ * Main test for checking HTTP logs in the Console panel.
+ */
+add_task(async function task() {
+  const hud = await openNewTabAndConsole(TEST_URI);
+
+  const currentTab = gBrowser.selectedTab;
+  const target = await TargetFactory.forTab(currentTab);
+
+  // Test proper UI update when request is opened.
+  // For every tab (with HTTP details):
+  // 1. Execute long-time request
+  // 2. Expand the net log before the request finishes (set default tab)
+  // 3. Check the default tab empty content
+  // 4. Wait till the request finishes
+  // 5. Check content of all tabs
+  for (const tab of tabs) {
+    info(`Test "${tab.id}" panel`);
+    await openRequestBeforeUpdates(target, hud, tab);
+  }
+});
+
+async function openRequestBeforeUpdates(target, hud, tab) {
+  const toolbox = gDevTools.getToolbox(target);
+
+  await clearOutput(hud);
+
+  const xhrUrl = TEST_PATH + "sjs_slow-response-test-server.sjs";
+  const onMessage = waitForMessage(hud, xhrUrl);
+  const onRequestUpdates = waitForRequestUpdates(hud);
+
+  // Fire an XHR POST request.
+  SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() {
+    content.wrappedJSObject.testXhrPostSlowResponse();
+  });
+  const { node: messageNode } = await onMessage;
+  ok(messageNode, "Network message found.");
+
+  // Set the default panel.
+  const state = hud.ui.wrapper.getStore().getState();
+  state.ui.networkMessageActiveTabId = tab.id;
+
+  // Expand network log
+  await expandXhrMessage(messageNode);
+
+  // Except the security tab. It isn't available till the
+  // "securityInfo" packet type is received, so doesn't
+  // fit this part of the test.
+  if (tab.id != "security") {
+    // Make sure the current tab is the expected one.
+    const currentTab = messageNode.querySelector(`#${tab.id}-tab`);
+    is(
+      currentTab.getAttribute("aria-selected"),
+      "true",
+      "The correct tab is selected"
+    );
+
+    info("Test that the tab is empty");
+    tab.testEmpty(messageNode);
+  }
+
+  info("Wait till all updates are received");
+  await onRequestUpdates;
+
+  info("Test content of the default tab");
+  await tab.testContent(messageNode);
+
+  info("Test all tabs in the network log");
+  await testNetworkMessage(toolbox, messageNode);
+}
+
+// Panel testing helpers
+
+async function testNetworkMessage(toolbox, messageNode) {
+  await testStatusInfo(messageNode);
+  await testHeaders(messageNode);
+  await testCookies(messageNode);
+  await testParams(messageNode);
+  await testResponse(messageNode);
+  await testTimings(messageNode);
+  await testStackTrace(messageNode);
+  await testSecurity(messageNode);
+  await waitForLazyRequests(toolbox);
+}
+
+// Status Info
+function testStatusInfo(messageNode) {
+  const statusInfo = messageNode.querySelector(".status-info");
+  ok(statusInfo, "Status info is not empty");
+}
+
+// Headers
+function testEmptyHeaders(messageNode) {
+  const emptyNotice = messageNode.querySelector("#headers-panel .empty-notice");
+  ok(emptyNotice, "Headers tab is empty");
+}
+
+async function testHeaders(messageNode) {
+  const headersTab = messageNode.querySelector("#headers-tab");
+  ok(headersTab, "Headers tab is available");
+
+  // Select Headers tab and check the content.
+  headersTab.click();
+  await waitFor(
+    () => messageNode.querySelector("#headers-panel .headers-overview"),
+    "Wait for .header-overview to be rendered"
+  );
+}
+
+// Cookies
+function testEmptyCookies(messageNode) {
+  const emptyNotice = messageNode.querySelector("#cookies-panel .empty-notice");
+  ok(emptyNotice, "Cookies tab is empty");
+}
+
+async function testCookies(messageNode) {
+  const cookiesTab = messageNode.querySelector("#cookies-tab");
+  ok(cookiesTab, "Cookies tab is available");
+
+  // Select tab and check the content.
+  cookiesTab.click();
+  await waitFor(
+    () => messageNode.querySelector("#cookies-panel .treeValueCell"),
+    "Wait for .treeValueCell to be rendered"
+  );
+}
+
+// Params
+function testEmptyParams(messageNode) {
+  const emptyNotice = messageNode.querySelector("#params-panel .empty-notice");
+  ok(emptyNotice, "Params tab is empty");
+}
+
+async function testParams(messageNode) {
+  const paramsTab = messageNode.querySelector("#params-tab");
+  ok(paramsTab, "Params tab is available");
+
+  // Select Params tab and check the content. CodeMirror initialization
+  // is delayed to prevent UI freeze, so wait for a little while.
+  paramsTab.click();
+  const paramsPanel = messageNode.querySelector("#params-panel");
+  await waitForSourceEditor(paramsPanel);
+  const paramsContent = messageNode.querySelector(
+    "#params-panel .panel-container .CodeMirror"
+  );
+  ok(paramsContent, "Params content is available");
+  ok(
+    paramsContent.textContent.includes("Hello world!"),
+    "Post body is correct"
+  );
+}
+
+// Response
+function testEmptyResponse(messageNode) {
+  const panel = messageNode.querySelector("#response-panel .tab-panel");
+  is(
+    panel.textContent,
+    "No response data available for this request",
+    "Cookies tab is empty"
+  );
+}
+
+async function testResponse(messageNode) {
+  const responseTab = messageNode.querySelector("#response-tab");
+  ok(responseTab, "Response tab is available");
+
+  // Select Response tab and check the content. CodeMirror initialization
+  // is delayed, so again wait for a little while.
+  responseTab.click();
+  const responsePanel = messageNode.querySelector("#response-panel");
+  await waitForSourceEditor(responsePanel);
+  const responseContent = messageNode.querySelector(
+    "#response-panel .editor-row-container .CodeMirror"
+  );
+  ok(responseContent, "Response content is available");
+  ok(responseContent.textContent, "Response text is available");
+}
+
+// Timings
+function testEmptyTimings(messageNode) {
+  const panel = messageNode.querySelector("#timings-panel .tab-panel");
+  is(panel.textContent, "", "Timings tab is empty");
+}
+
+async function testTimings(messageNode) {
+  const timingsTab = messageNode.querySelector("#timings-tab");
+  ok(timingsTab, "Timings tab is available");
+
+  // Select Timings tab and check the content.
+  timingsTab.click();
+  const timingsContent = await waitFor(() =>
+    messageNode.querySelector(
+      "#timings-panel .timings-container .timings-label",
+      "Wait for .timings-label to be rendered"
+    )
+  );
+  ok(timingsContent, "Timings content is available");
+  ok(timingsContent.textContent, "Timings text is available");
+}
+
+// Stack Trace
+function testEmptyStackTrace(messageNode) {
+  const panel = messageNode.querySelector("#stack-trace-panel .stack-trace");
+  is(panel.textContent, "", "StackTrace tab is empty");
+}
+
+async function testStackTrace(messageNode) {
+  const stackTraceTab = messageNode.querySelector("#stack-trace-tab");
+  ok(stackTraceTab, "StackTrace tab is available");
+
+  // Select Stack Trace tab and check the content.
+  stackTraceTab.click();
+  await waitFor(
+    () => messageNode.querySelector("#stack-trace-panel .frame-link"),
+    "Wait for .frame-link to be rendered"
+  );
+}
+
+// Security
+function testEmptySecurity(messageNode) {
+  const panel = messageNode.querySelector("#security-panel .tab-panel");
+  is(panel.textContent, "", "Security tab is empty");
+}
+
+async function testSecurity(messageNode) {
+  const securityTab = messageNode.querySelector("#security-tab");
+  ok(securityTab, "Security tab is available");
+
+  // Select Security tab and check the content.
+  securityTab.click();
+  await waitFor(
+    () => messageNode.querySelector("#security-panel .treeTable .treeRow"),
+    "Wait for #security-panel .treeTable .treeRow to be rendered"
+  );
+}
+
+async function waitForSourceEditor(panel) {
+  return waitUntil(() => {
+    return !!panel.querySelector(".CodeMirror");
+  });
+}
+
+async function waitForRequestUpdates(hud) {
+  return hud.ui.once("network-message-updated");
+}
+
+function expandXhrMessage(node) {
+  info(
+    "Click on XHR message and wait for the network detail panel to be displayed"
+  );
+  node.querySelector(".url").click();
+  return waitFor(
+    () => node.querySelector(".network-info"),
+    "Wait for .network-info to be rendered"
+  );
+}
--- a/devtools/client/webconsole/test/browser/browser_webconsole_stubs_network_event.js
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_stubs_network_event.js
@@ -72,17 +72,17 @@ async function generateNetworkEventStubs
   const { ui } = toolbox.getCurrentPanel().hud;
 
   for (const [key, code] of getCommands()) {
     const consoleFront = await toolbox.target.getFront("console");
     const onNetwork = consoleFront.once("networkEvent", packet => {
       packets.set(key, getCleanedPacket(key, packet));
     });
 
-    const onNetworkUpdate = ui.once("network-message-updated", res => {
+    const onNetworkUpdate = ui.once("network-message-updated", ([res]) => {
       const updateKey = `${key} update`;
       // We cannot ensure the form of the network update packet, some properties
       // might be in another order than in the original packet.
       // Hand-picking only what we need should prevent this.
       const packet = {
         networkInfo: {
           _type: res.networkInfo._type,
           actor: res.networkInfo.actor,
--- a/devtools/client/webconsole/test/browser/sjs_slow-response-test-server.sjs
+++ b/devtools/client/webconsole/test/browser/sjs_slow-response-test-server.sjs
@@ -1,17 +1,23 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
+"use strict";
+
 function handleRequest(request, response) {
   response.processAsync();
 
   let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
-  timer.initWithCallback(() => {
-    // to avoid garbage collection
-    timer = null;
-    response.setStatusLine(request.httpVersion, 200, "OK");
-    response.setHeader("Content-Type", "text/plain", false);
-    response.setHeader("Set-Cookie", "foo=bar; Max-Age=10; HttpOnly", true);
-    response.write("Some response data");
-    response.finish();
-  }, 300, Ci.nsITimer.TYPE_ONE_SHOT); // Make sure this request takes a few hundred ms.
+  timer.initWithCallback(
+    () => {
+      // to avoid garbage collection
+      timer = null;
+      response.setStatusLine(request.httpVersion, 200, "OK");
+      response.setHeader("Content-Type", "text/plain", false);
+      response.setHeader("Set-Cookie", "foo=bar; Max-Age=10; HttpOnly", true);
+      response.write("Some response data");
+      response.finish();
+    },
+    1000,
+    Ci.nsITimer.TYPE_ONE_SHOT
+  ); // Make sure this request takes a few hundred ms.
 }
--- a/devtools/client/webconsole/webconsole-wrapper.js
+++ b/devtools/client/webconsole/webconsole-wrapper.js
@@ -379,18 +379,21 @@ class WebConsoleWrapper {
 
         this.queuedMessageAdds = [];
 
         if (this.queuedMessageUpdates.length > 0) {
           for (const { message, res } of this.queuedMessageUpdates) {
             await store.dispatch(
               actions.networkMessageUpdate(message, null, res)
             );
-            this.webConsoleUI.emit("network-message-updated", res);
           }
+          this.webConsoleUI.emit(
+            "network-message-updated",
+            this.queuedMessageUpdates.map(({ res }) => res)
+          );
           this.queuedMessageUpdates = [];
         }
         if (this.queuedRequestUpdates.length > 0) {
           for (const { id, data } of this.queuedRequestUpdates) {
             await store.dispatch(actions.networkUpdateRequest(id, data));
           }
           this.queuedRequestUpdates = [];