Repository: GoogleChrome/lighthouse
Pull request #6702 (2018-12-02 09:58:34)

core: page function updates relating to upcoming tap targets audit
**Summary**

Splitting up [this PR](https://github.com/GoogleChrome/lighthouse/pull/5846) into more digestible bits. 

This PR just contains some page function tweaks, and a new function to get an element selector that we can show to the user.

**Related Issues/PRs**

Tap targets ticket: https://github.com/GoogleChrome/lighthouse/issues/4358



Patch:
diff --git a/lighthouse-core/gather/gatherers/accessibility.js b/lighthouse-core/gather/gatherers/accessibility.js
index a0226a7da..0af785460 100644
--- a/lighthouse-core/gather/gatherers/accessibility.js
+++ b/lighthouse-core/gather/gatherers/accessibility.js
@@ -5,7 +5,7 @@
  */
 'use strict';
 
-/* global window, document, Node, getOuterHTMLSnippet */
+/* global window, document, getOuterHTMLSnippet, getNodePath */
 
 const Gatherer = require('./gatherer');
 const fs = require('fs');
@@ -45,6 +45,7 @@ function runA11yChecks() {
     // Augment the node objects with outerHTML snippet & custom path string
     // @ts-ignore
     axeResult.violations.forEach(v => v.nodes.forEach(node => {
+      // @ts-ignore - getNodePath put into scope via stringification
       node.path = getNodePath(node.element);
       // @ts-ignore - getOuterHTMLSnippet put into scope via stringification
       node.snippet = getOuterHTMLSnippet(node.element);
@@ -56,37 +57,6 @@ function runA11yChecks() {
     axeResult = {violations: axeResult.violations, notApplicable: axeResult.inapplicable};
     return axeResult;
   });
-
-  /**
-   * Adapted from DevTools' SDK.DOMNode.prototype.path
-   *   https://github.com/ChromeDevTools/devtools-frontend/blob/7a2e162ddefd/front_end/sdk/DOMModel.js#L530-L552
-   * TODO: Doesn't handle frames or shadow roots...
-   * @param {Node} node
-   */
-  function getNodePath(node) {
-    /** @param {Node} node */
-    function getNodeIndex(node) {
-      let index = 0;
-      let prevNode;
-      while (prevNode = node.previousSibling) {
-        node = prevNode;
-        // skip empty text nodes
-        if (node.nodeType === Node.TEXT_NODE && node.textContent &&
-          node.textContent.trim().length === 0) continue;
-        index++;
-      }
-      return index;
-    }
-
-    const path = [];
-    while (node && node.parentNode) {
-      const index = getNodeIndex(node);
-      path.push([index, node.nodeName]);
-      node = node.parentNode;
-    }
-    path.reverse();
-    return path.join(',');
-  }
 }
 
 class Accessibility extends Gatherer {
@@ -98,6 +68,7 @@ class Accessibility extends Gatherer {
     const driver = passContext.driver;
     const expression = `(function () {
       ${pageFunctions.getOuterHTMLSnippetString};
+      ${pageFunctions.getNodePathString};
       ${axeLibSource};
       return (${runA11yChecks.toString()}());
     })()`;
diff --git a/lighthouse-core/lib/page-functions.js b/lighthouse-core/lib/page-functions.js
index 960476f15..9e9b79d69 100644
--- a/lighthouse-core/lib/page-functions.js
+++ b/lighthouse-core/lib/page-functions.js
@@ -6,7 +6,7 @@
 // @ts-nocheck
 'use strict';
 
-/* global window document */
+/* global window document Node */
 
 /**
  * Helper functions that are passed by `toString()` by Driver to be evaluated in target page.
@@ -119,7 +119,7 @@ function getOuterHTMLSnippet(element, ignoreAttrs = []) {
     clone.removeAttribute(attribute);
   });
 
-  const reOpeningTag = /^.*?>/;
+  const reOpeningTag = /^[\s\S]*?>/;
   const match = clone.outerHTML.match(reOpeningTag);
 
   return (match && match[0]) || '';
@@ -153,6 +153,71 @@ function ultradumbBenchmark() {
   return Math.round(iterations / durationInSeconds);
 }
 
+/**
+ * Adapted from DevTools' SDK.DOMNode.prototype.path
+ *   https://github.com/ChromeDevTools/devtools-frontend/blob/7a2e162ddefd/front_end/sdk/DOMModel.js#L530-L552
+ * TODO: Doesn't handle frames or shadow roots...
+ * @param {Node} node
+ */
+/* istanbul ignore next */
+function getNodePath(node) {
+  /** @param {Node} node */
+  function getNodeIndex(node) {
+    let index = 0;
+    let prevNode;
+    while (prevNode = node.previousSibling) {
+      node = prevNode;
+      // skip empty text nodes
+      if (node.nodeType === Node.TEXT_NODE && node.textContent &&
+        node.textContent.trim().length === 0) continue;
+      index++;
+    }
+    return index;
+  }
+
+  const path = [];
+  while (node && node.parentNode) {
+    const index = getNodeIndex(node);
+    path.push([index, node.nodeName]);
+    node = node.parentNode;
+  }
+  path.reverse();
+  return path.join(',');
+}
+
+/**
+ * @param {Element} node
+ * @returns {string}
+ */
+/* istanbul ignore next */
+function getNodeSelector(node) {
+  /**
+   * @param {Element} node
+   */
+  function getSelectorPart(node) {
+    let part = node.tagName.toLowerCase();
+    if (node.id) {
+      part += '#' + node.id;
+    } else if (node.classList.length > 0) {
+      part += '.' + node.classList[0];
+    }
+    return part;
+  }
+
+  const parts = [];
+  while (parts.length < 4) {
+    parts.unshift(getSelectorPart(node));
+    if (!node.parentElement) {
+      break;
+    }
+    node = node.parentElement;
+    if (node.tagName === 'HTML') {
+      break;
+    }
+  }
+  return parts.join(' > ');
+}
+
 module.exports = {
   wrapRuntimeEvalErrorInBrowserString: wrapRuntimeEvalErrorInBrowser.toString(),
   registerPerformanceObserverInPageString: registerPerformanceObserverInPage.toString(),
@@ -162,4 +227,7 @@ module.exports = {
   getOuterHTMLSnippet: getOuterHTMLSnippet,
   ultradumbBenchmark: ultradumbBenchmark,
   ultradumbBenchmarkString: ultradumbBenchmark.toString(),
+  getNodePathString: getNodePath.toString(),
+  getNodeSelectorString: getNodeSelector.toString(),
+  getNodeSelector: getNodeSelector,
 };
diff --git a/lighthouse-core/lib/url-shim.js b/lighthouse-core/lib/url-shim.js
index 260136058..0bf06d2fa 100644
--- a/lighthouse-core/lib/url-shim.js
+++ b/lighthouse-core/lib/url-shim.js
@@ -209,7 +209,7 @@ URLShim.URL = URL;
 URLShim.URLSearchParams = (typeof self !== 'undefined' && self.URLSearchParams) ||
     require('url').URLSearchParams;
 
-URLShim.NON_NETWORK_PROTOCOLS = ['blob', 'data'];
+URLShim.NON_NETWORK_PROTOCOLS = ['blob', 'data', 'intent'];
 
 URLShim.INVALID_URL_DEBUG_STRING =
     'Lighthouse was unable to determine the URL of some script executions. ' +


Test patch:
diff --git a/lighthouse-core/test/audits/uses-rel-preload-test.js b/lighthouse-core/test/audits/uses-rel-preload-test.js
index 12176f309..6765a7370 100644
--- a/lighthouse-core/test/audits/uses-rel-preload-test.js
+++ b/lighthouse-core/test/audits/uses-rel-preload-test.js
@@ -210,6 +210,18 @@ describe('Performance: uses-rel-preload audit', () => {
     });
   });
 
+  it(`shouldn't suggest preload for protocol intent`, () => {
+    const networkRecords = getMockNetworkRecords();
+    networkRecords[2].protocol = 'intent';
+
+    const artifacts = mockArtifacts(networkRecords, defaultMainResourceUrl);
+    const context = {settings: {}, computedCache: new Map()};
+    return UsesRelPreload.audit(artifacts, context).then(output => {
+      assert.equal(output.rawValue, 0);
+      assert.equal(output.details.items.length, 0);
+    });
+  });
+
   it('does not throw on a real trace/devtools log', async () => {
     const artifacts = {
       URL: {finalUrl: 'https://pwa.rocks/'},
diff --git a/lighthouse-core/test/lib/page-functions-test.js b/lighthouse-core/test/lib/page-functions-test.js
index 8db4648fb..d04114a0c 100644
--- a/lighthouse-core/test/lib/page-functions-test.js
+++ b/lighthouse-core/test/lib/page-functions-test.js
@@ -12,7 +12,7 @@ const pageFunctions = require('../../lib/page-functions');
 
 /* eslint-env jest */
 
-describe('DetailsRenderer', () => {
+describe('Page Functions', () => {
   let dom;
 
   beforeAll(() => {
@@ -44,5 +44,19 @@ describe('DetailsRenderer', () => {
         ['style-missing', 'aria-label-missing']
       ), '<div id="1" style="style" aria-label="label">');
     });
+
+    it('works if attribute values contain line breaks', () => {
+      assert.equal(pageFunctions.getOuterHTMLSnippet(
+        dom.createElement('div', '', {style: 'style1\nstyle2'})), '<div style="style1\nstyle2">');
+    });
+  });
+
+  describe('getNodeSelector', () => {
+    it('Uses IDs where available and otherwise falls back to classes', () => {
+      const parentEl = dom.createElement('div', '', {id: 'wrapper', class: 'dont-use-this'});
+      const childEl = dom.createElement('div', '', {class: 'child'});
+      parentEl.appendChild(childEl);
+      assert.equal(pageFunctions.getNodeSelector(childEl), 'div#wrapper > div.child');
+    });
   });
 });