Bug 1421213 - Clicking on the request status code should open the corresponding MDN page. r=nchevobbe
authorabhinav <abhinav.koppula@gmail.com>
Tue, 16 Jan 2018 07:14:43 +0530
changeset 453981 76f2c6767866e40710cc8472cb0484e69e154529
parent 453980 1a202fc6df1d2d581385197104fe16048a8383a8
child 453982 64c447e345774572eaf311bc2a29ae83a73d129a
push id1648
push usermtabara@mozilla.com
push dateThu, 01 Mar 2018 12:45:47 +0000
treeherdermozilla-release@cbb9688c2eeb [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnchevobbe
bugs1421213
milestone59.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 1421213 - Clicking on the request status code should open the corresponding MDN page. r=nchevobbe MozReview-Commit-ID: JlU7pJiZ689
devtools/client/netmonitor/src/components/MdnLink.js
devtools/client/netmonitor/src/utils/mdn-utils.js
devtools/client/themes/webconsole.css
devtools/client/webconsole/hudservice.js
devtools/client/webconsole/new-console-output/components/Message.js
devtools/client/webconsole/new-console-output/components/message-types/NetworkEventMessage.js
devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
devtools/client/webconsole/new-console-output/test/mochitest/browser.ini
devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_allow_mixedcontent_securityerrors.js
devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_block_mixedcontent_securityerrors.js
devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_network_messages_status_code.js
devtools/client/webconsole/new-console-output/test/mochitest/head.js
--- a/devtools/client/netmonitor/src/components/MdnLink.js
+++ b/devtools/client/netmonitor/src/components/MdnLink.js
@@ -30,16 +30,18 @@ MDNLink.propTypes = {
   url: PropTypes.string.isRequired,
 };
 
 function onLearnMoreClick(e, url) {
   e.stopPropagation();
   e.preventDefault();
 
   let win = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
-  if (e.button === 1) {
+  let { button, ctrlKey, metaKey } = e;
+  let isOSX = Services.appinfo.OS == "Darwin";
+  if (button === 1 || (button === 0 && (isOSX ? metaKey : ctrlKey))) {
     win.openUILinkIn(url, "tabshifted");
   } else {
     win.openUILinkIn(url, "tab");
   }
 }
 
 module.exports = MDNLink;
--- a/devtools/client/netmonitor/src/utils/mdn-utils.js
+++ b/devtools/client/netmonitor/src/utils/mdn-utils.js
@@ -136,63 +136,65 @@ const SUPPORTED_HTTP_CODES = [
     "502",
     "503",
     "504",
     "505",
     "511"
 ];
 
 const MDN_URL = "https://developer.mozilla.org/docs/";
-const GA_PARAMS =
-  "?utm_source=mozilla&utm_medium=devtools-netmonitor&utm_campaign=default";
+const getGAParams = (panelId = "netmonitor") => {
+  return `?utm_source=mozilla&utm_medium=devtools-${panelId}&utm_campaign=default`;
+};
 
 /**
  * Get the MDN URL for the specified header.
  *
  * @param {string} header Name of the header for the baseURL to use.
  *
  * @return {string} The MDN URL for the header, or null if not available.
  */
 function getHeadersURL(header) {
   const lowerCaseHeader = header.toLowerCase();
   let idx = SUPPORTED_HEADERS.findIndex(item =>
     item.toLowerCase() === lowerCaseHeader);
   return idx > -1 ?
-    `${MDN_URL}Web/HTTP/Headers/${SUPPORTED_HEADERS[idx] + GA_PARAMS}` : null;
+    `${MDN_URL}Web/HTTP/Headers/${SUPPORTED_HEADERS[idx] + getGAParams()}` : null;
 }
 
 /**
  * Get the MDN URL for the specified HTTP status code.
  *
  * @param {string} HTTP status code for the baseURL to use.
  *
  * @return {string} The MDN URL for the HTTP status code, or null if not available.
  */
-function getHTTPStatusCodeURL(statusCode) {
+function getHTTPStatusCodeURL(statusCode, panelId) {
   let idx = SUPPORTED_HTTP_CODES.indexOf(statusCode);
   return idx > -1 ?
-    `${MDN_URL}Web/HTTP/Status/${SUPPORTED_HTTP_CODES[idx] + GA_PARAMS}` : null;
+    `${MDN_URL}Web/HTTP/Status/${SUPPORTED_HTTP_CODES[idx] + getGAParams(panelId)}`
+      : null;
 }
 
 /**
  * Get the MDN URL of the Timings tag for Network Monitor.
  *
  * @return {string} the MDN URL of the Timings tag for Network Monitor.
  */
 function getNetMonitorTimingsURL() {
-  return `${MDN_URL}Tools/Network_Monitor${GA_PARAMS}#Timings`;
+  return `${MDN_URL}Tools/Network_Monitor${getGAParams()}#Timings`;
 }
 
 /**
  * Get the MDN URL for Performance Analysis
  *
  * @return {string} The MDN URL for the documentation of Performance Analysis.
  */
 function getPerformanceAnalysisURL() {
-  return `${MDN_URL}Tools/Network_Monitor${GA_PARAMS}#Performance_analysis`;
+  return `${MDN_URL}Tools/Network_Monitor${getGAParams()}#Performance_analysis`;
 }
 
 module.exports = {
   getHeadersURL,
   getHTTPStatusCodeURL,
   getNetMonitorTimingsURL,
   getPerformanceAnalysisURL,
 };
--- a/devtools/client/themes/webconsole.css
+++ b/devtools/client/themes/webconsole.css
@@ -1007,16 +1007,18 @@ a.learn-more-link.webconsole-learn-more-
 .webconsole-output-wrapper .message.network .status {
   color: var(--theme-highlight-blue);
   font-style: inherit;
 }
 
 .webconsole-output-wrapper .message.network .status .status-info .status-code {
   padding: 0 2px;
   border-radius: 3px;
+  text-decoration: none;
+  font-style: normal;
 }
 
 .webconsole-output-wrapper .message.network .status .status-info .status-code:not([data-code^="3"]) {
   color: var(--theme-body-background);
 }
 
 
 .network.message .network-info {
--- a/devtools/client/webconsole/hudservice.js
+++ b/devtools/client/webconsole/hudservice.js
@@ -415,19 +415,24 @@ WebConsole.prototype = {
   },
 
   /**
    * Open a link in a new tab.
    *
    * @param string aLink
    *        The URL you want to open in a new tab.
    */
-  openLink: function WC_openLink(aLink)
+  openLink: function WC_openLink(aLink, e)
   {
-    this.chromeUtilsWindow.openUILinkIn(aLink, "tab");
+    let isOSX = Services.appinfo.OS == "Darwin";
+    if (e != null && (e.button === 1 || (e.button === 0 && (isOSX ? e.metaKey : e.ctrlKey)))) {
+      this.chromeUtilsWindow.openUILinkIn(aLink, "tabshifted");
+    } else {
+      this.chromeUtilsWindow.openUILinkIn(aLink, "tab");
+    }
   },
 
   /**
    * Open a link in Firefox's view source.
    *
    * @param string aSourceURL
    *        The URL of the file.
    * @param integer aSourceLine
--- a/devtools/client/webconsole/new-console-output/components/Message.js
+++ b/devtools/client/webconsole/new-console-output/components/Message.js
@@ -88,19 +88,19 @@ class Message extends Component {
       // did not emit for them.
       if (this.props.serviceContainer) {
         this.props.serviceContainer.emitNewMessage(
           this.messageNode, this.props.messageId, this.props.timeStamp);
       }
     }
   }
 
-  onLearnMoreClick() {
+  onLearnMoreClick(e) {
     let {exceptionDocURL} = this.props;
-    this.props.serviceContainer.openLink(exceptionDocURL);
+    this.props.serviceContainer.openLink(exceptionDocURL, e);
   }
 
   onContextMenu(e) {
     let { serviceContainer, source, request, messageId } = this.props;
     let messageInfo = {
       source,
       request,
       messageId,
--- a/devtools/client/webconsole/new-console-output/components/message-types/NetworkEventMessage.js
+++ b/devtools/client/webconsole/new-console-output/components/message-types/NetworkEventMessage.js
@@ -9,16 +9,18 @@
 // React & Redux
 const { createFactory } = require("devtools/client/shared/vendor/react");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const Message = createFactory(require("devtools/client/webconsole/new-console-output/components/Message"));
 const actions = require("devtools/client/webconsole/new-console-output/actions/index");
 const { l10n } = require("devtools/client/webconsole/new-console-output/utils/messages");
 const TabboxPanel = createFactory(require("devtools/client/netmonitor/src/components/TabboxPanel"));
+const { getHTTPStatusCodeURL } = require("devtools/client/netmonitor/src/utils/mdn-utils");
+const LEARN_MORE = l10n.getStr("webConsoleMoreInfoLabel");
 
 NetworkEventMessage.displayName = "NetworkEventMessage";
 
 NetworkEventMessage.propTypes = {
   message: PropTypes.object.isRequired,
   serviceContainer: PropTypes.shape({
     openNetworkPanel: PropTypes.func.isRequired,
   }),
@@ -69,17 +71,27 @@ function NetworkEventMessage({
     status,
     statusText,
   } = response;
 
   const topLevelClasses = [ "cm-s-mozilla" ];
   let statusCode, statusInfo;
 
   if (httpVersion && status && statusText !== undefined && totalTime !== undefined) {
-    statusCode = dom.span({className: "status-code", "data-code": status}, status);
+    let statusCodeDocURL = getHTTPStatusCodeURL(status.toString(), "webconsole");
+    statusCode = dom.a({
+      className: "status-code",
+      "data-code": status,
+      title: LEARN_MORE,
+      onClick: (e) => {
+        e.stopPropagation();
+        e.preventDefault();
+        serviceContainer.openLink(statusCodeDocURL, e);
+      }
+    }, status);
     statusInfo = dom.span(
       {className: "status-info"},
       `[${httpVersion} `, statusCode, ` ${statusText} ${totalTime}ms]`
     );
   }
 
   const toggle = () => {
     if (open) {
--- a/devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
+++ b/devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
@@ -83,18 +83,18 @@ NewConsoleOutputWrapper.prototype = {
         emitNewMessage: (node, messageId, timeStamp) => {
           this.jsterm.hud.emit("new-messages", new Set([{
             node,
             messageId,
             timeStamp,
           }]));
         },
         hudProxy: hud.proxy,
-        openLink: url => {
-          hud.owner.openLink(url);
+        openLink: (url, e) => {
+          hud.owner.openLink(url, e);
         },
         createElement: nodename => {
           return this.document.createElement(nodename);
         },
         getLongString: (grip) => {
           return hud.proxy.webConsoleClient.getString(grip);
         },
         requestData(id, type) {
--- a/devtools/client/webconsole/new-console-output/test/mochitest/browser.ini
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser.ini
@@ -321,16 +321,17 @@ skip-if = true #	Bug 1404384
 [browser_webconsole_mixedcontent.js]
 tags = mcb
 skip-if = true #	Bug 1404886
 [browser_webconsole_multiple_windows_and_tabs.js]
 [browser_webconsole_network_attach.js]
 [browser_webconsole_network_exceptions.js]
 [browser_webconsole_network_messages_expand.js]
 [browser_webconsole_network_messages_openinnet.js]
+[browser_webconsole_network_messages_status_code.js]
 [browser_webconsole_network_requests_from_chrome.js]
 [browser_webconsole_network_reset_filter.js]
 [browser_webconsole_nodes_highlight.js]
 [browser_webconsole_nodes_select.js]
 [browser_webconsole_object_in_sidebar.js]
 [browser_webconsole_object_inspector.js]
 [browser_webconsole_object_inspector_entries.js]
 [browser_webconsole_observer_notifications.js]
--- a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_allow_mixedcontent_securityerrors.js
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_allow_mixedcontent_securityerrors.js
@@ -38,13 +38,35 @@ add_task(async function () {
   const onMixedDisplayContent = waitUntilWarningMessage(displayContentText);
 
   await onMixedDisplayContent;
   ok(true, "Mixed display content warning message is visible");
 
   const mixedActiveContentMessage = await onMixedActiveContent;
   ok(true, "Mixed active content warning message is visible");
 
+  const checkLink = ({ link, where, expectedLink, expectedTab }) => {
+    is(link, expectedLink, `Clicking the provided link opens ${link}`);
+    is(where, expectedTab, `Clicking the provided link opens in expected tab`);
+  }
+
   info("Clicking on the Learn More link");
   const learnMoreLink = mixedActiveContentMessage.querySelector(".learn-more-link");
-  const url = await simulateLinkClick(learnMoreLink);
-  is(url, LEARN_MORE_URI, `Clicking the provided link opens ${url}`);
+  let linkSimulation = await simulateLinkClick(learnMoreLink);
+  checkLink({
+    ...linkSimulation,
+    expectedLink: LEARN_MORE_URI,
+    expectedTab: "tab"
+  });
+
+  let isOSX = Services.appinfo.OS == "Darwin";
+  let ctrlOrCmdKeyMouseEvent = new MouseEvent("click", {
+    bubbles: true,
+    [isOSX ? "metaKey" : "ctrlKey"]: true,
+    view: window
+  });
+  linkSimulation = await simulateLinkClick(learnMoreLink, ctrlOrCmdKeyMouseEvent);
+  checkLink({
+    ...linkSimulation,
+    expectedLink: LEARN_MORE_URI,
+    expectedTab: "tabshifted"
+  });
 });
--- a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_block_mixedcontent_securityerrors.js
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_block_mixedcontent_securityerrors.js
@@ -44,18 +44,18 @@ add_task(async function() {
   await onBlockedImage;
   ok(true, "Blocked mixed display content error message is visible");
 
   const blockedMixedActiveContentMessage = await onBlockedIframe;
   ok(true, "Blocked mixed active content error message is visible");
 
   info("Clicking on the Learn More link");
   let learnMoreLink = blockedMixedActiveContentMessage.querySelector(".learn-more-link");
-  let url = await simulateLinkClick(learnMoreLink);
-  is(url, LEARN_MORE_URI, `Clicking the provided link opens ${url}`);
+  let response = await simulateLinkClick(learnMoreLink);
+  is(response.link, LEARN_MORE_URI, `Clicking the provided link opens ${response.link}`);
 
   info("Test disabling mixed content protection");
 
   let {gIdentityHandler} = gBrowser.ownerGlobal;
   ok(gIdentityHandler._identityBox.classList.contains("mixedActiveBlocked"),
     "Mixed Active Content state appeared on identity box");
   // Disabe mixed content protection.
   gIdentityHandler.disableMixedContentProtection();
@@ -69,18 +69,18 @@ add_task(async function() {
   await onMixedDisplayContent;
   ok(true, "Mixed display content warning message is visible");
 
   const mixedActiveContentMessage = await onMixedActiveContent;
   ok(true, "Mixed active content warning message is visible");
 
   info("Clicking on the Learn More link");
   learnMoreLink = mixedActiveContentMessage.querySelector(".learn-more-link");
-  url = await simulateLinkClick(learnMoreLink);
-  is(url, LEARN_MORE_URI, `Clicking the provided link opens ${url}`);
+  response = await simulateLinkClick(learnMoreLink);
+  is(response.link, LEARN_MORE_URI, `Clicking the provided link opens ${response.link}`);
 });
 
 function pushPrefEnv() {
   const prefs = [
     ["security.mixed_content.block_active_content", true],
     ["security.mixed_content.block_display_content", true],
   ];
 
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_network_messages_status_code.js
@@ -0,0 +1,104 @@
+/* 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 = "http://example.com/browser/devtools/client/webconsole/new-console-output/test/mochitest/";
+const TEST_URI = TEST_PATH + TEST_FILE;
+
+const NET_PREF = "devtools.webconsole.filter.net";
+const XHR_PREF = "devtools.webconsole.filter.netxhr";
+let { l10n } = require("devtools/client/webconsole/new-console-output/utils/messages");
+const LEARN_MORE_URI = "https://developer.mozilla.org/docs/Web/HTTP/Status/200" + STATUS_CODES_GA_PARAMS;
+
+pushPref(NET_PREF, true);
+pushPref(XHR_PREF, true);
+
+add_task(async function task() {
+  const hud = await openNewTabAndConsole(TEST_URI);
+
+  const currentTab = gBrowser.selectedTab;
+  let target = TargetFactory.forTab(currentTab);
+  let toolbox = gDevTools.getToolbox(target);
+  let {ui} = toolbox.getCurrentPanel().hud;
+  const onNetworkMessageUpdate = ui.jsterm.hud.once("network-message-updated");
+
+  // Fire an XHR POST request.
+  await ContentTask.spawn(gBrowser.selectedBrowser, null, function () {
+    content.wrappedJSObject.testXhrPost();
+  });
+
+  info("XHR executed");
+  await onNetworkMessageUpdate;
+
+  let xhrUrl = TEST_PATH + "test-data.json";
+  let messageNode = await waitFor(() => findMessage(hud, xhrUrl));
+  let urlNode = messageNode.querySelector(".url");
+  let statusCodeNode = messageNode.querySelector(".status-code");
+  info("Network message found.");
+
+  ok(statusCodeNode.title, l10n.getStr("webConsoleMoreInfoLabel"));
+  let {
+    middleMouseEvent,
+    ctrlOrCmdKeyMouseEvent,
+    rightClickMouseEvent,
+    rightClickCtrlOrCmdKeyMouseEvent,
+  } = getMouseEvents();
+
+  let testCases = [
+    { clickEvent: middleMouseEvent, link: LEARN_MORE_URI, where: "tabshifted" },
+    { clickEvent: null, link: LEARN_MORE_URI, where: "tab" },
+    { clickEvent: ctrlOrCmdKeyMouseEvent, link: LEARN_MORE_URI, where: "tabshifted" },
+    { clickEvent: rightClickMouseEvent, link: null, where: null },
+    { clickEvent: rightClickCtrlOrCmdKeyMouseEvent, link: null, where: null }
+  ];
+
+  for (let testCase of testCases) {
+    const { clickEvent } = testCase;
+    let onConsoleMenuOpened = [rightClickMouseEvent, rightClickCtrlOrCmdKeyMouseEvent].includes(clickEvent) ?
+      hud.ui.newConsoleOutput.once("menu-open") : null;
+
+    let { link, where } = await simulateLinkClick(statusCodeNode, testCase.clickEvent);
+    is(link, testCase.link, `Clicking the provided link opens ${link}`);
+    is(where, testCase.where, `Link opened in correct tab`);
+
+    if (onConsoleMenuOpened) {
+      info("Check if context menu is opened on right clicking the status-code");
+      await onConsoleMenuOpened;
+    }
+  }
+});
+
+function getMouseEvents() {
+  let isOSX = Services.appinfo.OS == "Darwin";
+
+  let middleMouseEvent = new MouseEvent("click", {
+    bubbles: true,
+    button: 1,
+    view: window
+  });
+  let ctrlOrCmdKeyMouseEvent = new MouseEvent("click", {
+    bubbles: true,
+    [isOSX ? "metaKey" : "ctrlKey"]: true,
+    view: window
+  });
+  let rightClickMouseEvent = new MouseEvent("contextmenu", {
+    bubbles: true,
+    button: 2,
+    view: window
+  });
+  let rightClickCtrlOrCmdKeyMouseEvent = new MouseEvent("contextmenu", {
+    bubbles: true,
+    button: 2,
+    [isOSX ? "metaKey" : "ctrlKey"]: true,
+    view: window
+  });
+
+  return {
+    middleMouseEvent,
+    ctrlOrCmdKeyMouseEvent,
+    rightClickMouseEvent,
+    rightClickCtrlOrCmdKeyMouseEvent,
+  };
+}
--- a/devtools/client/webconsole/new-console-output/test/mochitest/head.js
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/head.js
@@ -15,19 +15,26 @@ Services.scriptloader.loadSubScript(
 // shared-head.js handles imports, constants, and utility functions
 // Load the shared-head file first.
 Services.scriptloader.loadSubScript(
   "chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js",
   this);
 
 var {HUDService} = require("devtools/client/webconsole/hudservice");
 var WCUL10n = require("devtools/client/webconsole/webconsole-l10n");
-const DOCS_GA_PARAMS = "?utm_source=mozilla" +
-                       "&utm_medium=firefox-console-errors" +
-                       "&utm_campaign=default";
+const DOCS_GA_PARAMS = `?${new URLSearchParams({
+  "utm_source": "mozilla",
+  "utm_medium": "firefox-console-errors",
+  "utm_campaign": "default"
+})}`;
+const STATUS_CODES_GA_PARAMS = `?${new URLSearchParams({
+  "utm_source": "mozilla",
+  "utm_medium": "devtools-webconsole",
+  "utm_campaign": "default"
+})}`;
 
 Services.prefs.setBoolPref("devtools.webconsole.new-frontend-enabled", true);
 registerCleanupFunction(function* () {
   Services.prefs.clearUserPref("devtools.webconsole.new-frontend-enabled");
   Services.prefs.clearUserPref("devtools.webconsole.ui.filterbar");
 
   // Reset all filter prefs between tests. First flushPrefEnv in case one of the
   // filter prefs has been pushed for the test
@@ -408,34 +415,63 @@ async function closeConsole(tab = gBrows
     await toolbox.destroy();
   }
 }
 
 /**
  * Fake clicking a link and return the URL we would have navigated to.
  * This function should be used to check external links since we can't access
  * network in tests.
+ * This can also be used to test that a click will not be fired.
  *
  * @param ElementNode element
  *        The <a> element we want to simulate click on.
+ * @param Object clickEventProps
+ *        The custom properties which would be used to dispatch a click event
  * @returns Promise
- *          A Promise that resolved when the link clik simulation occured.
+ *          A Promise that is resolved when the link click simulation occured or when the click is not dispatched.
+ *          The promise resolves with an object that holds the following properties
+ *          - link: url of the link or null(if event not fired)
+ *          - where: "tab" if tab is active or "tabshifted" if tab is inactive or null(if event not fired)
  */
-function simulateLinkClick(element) {
-  return new Promise((resolve) => {
-    // Override openUILinkIn to prevent navigating.
-    let oldOpenUILinkIn = window.openUILinkIn;
-    window.openUILinkIn = function (link) {
+function simulateLinkClick(element, clickEventProps) {
+  // Override openUILinkIn to prevent navigating.
+  let oldOpenUILinkIn = window.openUILinkIn;
+
+  const onOpenLink = new Promise((resolve) => {
+    window.openUILinkIn = function (link, where) {
       window.openUILinkIn = oldOpenUILinkIn;
-      resolve(link);
+      resolve({link: link, where});
     };
 
-    // Click on the link.
-    element.click();
+    if (clickEventProps) {
+      // Click on the link using the event properties.
+      element.dispatchEvent(clickEventProps);
+    } else {
+      // Click on the link.
+      element.click();
+    }
   });
+
+  // Declare a timeout Promise that we can use to make sure openUILinkIn was not called.
+  let timeoutId;
+  const onTimeout = new Promise(function(resolve, reject) {
+    timeoutId = setTimeout(() => {
+      window.openUILinkIn = oldOpenUILinkIn;
+      timeoutId = null;
+      resolve({link: null, where: null});
+    }, 1000);
+  });
+
+  onOpenLink.then(() => {
+    if (timeoutId) {
+      clearTimeout(timeoutId);
+    }
+  });
+  return Promise.race([onOpenLink, onTimeout]);
 }
 
 /**
  * Open a new browser window and return a promise that resolves when the new window has
  * fired the "browser-delayed-startup-finished" event.
  *
  * @returns Promise
  *          A Promise that resolves when the window is ready.