Repository: GoogleChrome/lighthouse
Pull request #5858 (2018-08-18 00:23:41)

core(tsc): type check config files
This was flagged testing the typescript nightly against the current code base. `LH.Config.Category.description` and `LH.Config.Group.description` should both be optional [as they are in the LHR](https://github.com/GoogleChrome/lighthouse/blob/d1031575f3f79d7bedbe0f8d5e45a51e16189853/typings/lhr.d.ts#L60), and `LH.Config.Category.manualDescription` should also exist.

Also adds type annotations to the files themselves so that they're checked directly, rather than indirectly through their use in `config.js`.


Patch:
diff --git a/lighthouse-core/audits/dobetterweb/external-anchors-use-rel-noopener.js b/lighthouse-core/audits/dobetterweb/external-anchors-use-rel-noopener.js
index 131789cea..d518d8a7e 100644
--- a/lighthouse-core/audits/dobetterweb/external-anchors-use-rel-noopener.js
+++ b/lighthouse-core/audits/dobetterweb/external-anchors-use-rel-noopener.js
@@ -33,16 +33,12 @@ class ExternalAnchorsUseRelNoopenerAudit extends Audit {
     const warnings = [];
     const pageHost = new URL(artifacts.URL.finalUrl).host;
     // Filter usages to exclude anchors that are same origin
-    // TODO: better extendedInfo for anchors with no href attribute:
-    // https://github.com/GoogleChrome/lighthouse/issues/1233
-    // https://github.com/GoogleChrome/lighthouse/issues/1345
     const failingAnchors = artifacts.AnchorsWithNoRelNoopener
       .filter(anchor => {
         try {
           return new URL(anchor.href).host !== pageHost;
         } catch (err) {
-          // TODO(phulce): make this message better with anchor.outerHTML
-          warnings.push('Unable to determine the destination for anchor tag. ' +
+          warnings.push(`Unable to determine the destination for anchor (${anchor.outerHTML}). ` +
             'If not used as a hyperlink, consider removing target=_blank.');
           return true;
         }
@@ -55,10 +51,7 @@ class ExternalAnchorsUseRelNoopenerAudit extends Audit {
           href: anchor.href || 'Unknown',
           target: anchor.target || '',
           rel: anchor.rel || '',
-          url: '<a' +
-              (anchor.href ? ` href="${anchor.href}"` : '') +
-              (anchor.target ? ` target="${anchor.target}"` : '') +
-              (anchor.rel ? ` rel="${anchor.rel}"` : '') + '>',
+          outerHTML: anchor.outerHTML || '',
         };
       });
 
diff --git a/lighthouse-core/config/default-config.js b/lighthouse-core/config/default-config.js
index f582044cf..251d62cd0 100644
--- a/lighthouse-core/config/default-config.js
+++ b/lighthouse-core/config/default-config.js
@@ -35,7 +35,8 @@ const UIStrings = {
 
 const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings);
 
-module.exports = {
+/** @type {LH.Config.Json} */
+const defaultConfig = {
   settings: constants.defaultSettings,
   passes: [{
     passName: 'defaultPass',
@@ -445,3 +446,5 @@ Object.defineProperty(module.exports, 'UIStrings', {
   enumerable: false,
   get: () => UIStrings,
 });
+
+module.exports = defaultConfig;
diff --git a/lighthouse-core/config/full-config.js b/lighthouse-core/config/full-config.js
index 5d430473d..89ba2f57e 100644
--- a/lighthouse-core/config/full-config.js
+++ b/lighthouse-core/config/full-config.js
@@ -5,7 +5,8 @@
  */
 'use strict';
 
-module.exports = {
+/** @type {LH.Config.Json} */
+const fullConfig = {
   extends: 'lighthouse:default',
   settings: {},
   passes: [
@@ -19,6 +20,7 @@ module.exports = {
   audits: [
     'byte-efficiency/unused-javascript',
   ],
+  // @ts-ignore TODO(bckenny): type extended Config where e.g. category.title isn't required
   categories: {
     'performance': {
       auditRefs: [
@@ -27,3 +29,5 @@ module.exports = {
     },
   },
 };
+
+module.exports = fullConfig;
diff --git a/lighthouse-core/config/mixed-content-config.js b/lighthouse-core/config/mixed-content-config.js
index 86341e4e5..89526804b 100644
--- a/lighthouse-core/config/mixed-content-config.js
+++ b/lighthouse-core/config/mixed-content-config.js
@@ -5,7 +5,8 @@
  */
 'use strict';
 
-module.exports = {
+/** @type {LH.Config.Json} */
+const mixedContentConfig = {
   // This performs two passes:
   // (1) Gather the default resources requested by the page, and
   // (2) Re-load page but attempt to upgrade each request to HTTPS.
@@ -35,3 +36,5 @@ module.exports = {
     },
   },
 };
+
+module.exports = mixedContentConfig;
diff --git a/lighthouse-core/config/perf-config.js b/lighthouse-core/config/perf-config.js
index a00e14459..a28d383d9 100644
--- a/lighthouse-core/config/perf-config.js
+++ b/lighthouse-core/config/perf-config.js
@@ -5,10 +5,13 @@
  */
 'use strict';
 
-module.exports = {
+/** @type {LH.Config.Json} */
+const perfConfig = {
   extends: 'lighthouse:default',
   settings: {
     throttlingMethod: 'devtools',
     onlyCategories: ['performance'],
   },
 };
+
+module.exports = perfConfig;
diff --git a/lighthouse-core/config/plots-config.js b/lighthouse-core/config/plots-config.js
index 29eb53534..6f8598bda 100644
--- a/lighthouse-core/config/plots-config.js
+++ b/lighthouse-core/config/plots-config.js
@@ -5,7 +5,8 @@
  */
 'use strict';
 
-module.exports = {
+/** @type {LH.Config.Json} */
+const plotsConfig = {
   extends: 'lighthouse:default',
   settings: {
     onlyAudits: [
@@ -17,3 +18,5 @@ module.exports = {
     ],
   },
 };
+
+module.exports = plotsConfig;
diff --git a/lighthouse-core/gather/gatherers/accessibility.js b/lighthouse-core/gather/gatherers/accessibility.js
index 800a3d873..8ec011298 100644
--- a/lighthouse-core/gather/gatherers/accessibility.js
+++ b/lighthouse-core/gather/gatherers/accessibility.js
@@ -5,11 +5,12 @@
  */
 'use strict';
 
-/* global window, document, Node */
+/* global window, document, Node, getOuterHTMLSnippet */
 
 const Gatherer = require('./gatherer');
 const fs = require('fs');
 const axeLibSource = fs.readFileSync(require.resolve('axe-core/axe.min.js'), 'utf8');
+const pageFunctions = require('../../lib/page-functions');
 
 /**
  * This is run in the page, not Lighthouse itself.
@@ -44,6 +45,7 @@ function runA11yChecks() {
     // @ts-ignore
     axeResult.violations.forEach(v => v.nodes.forEach(node => {
       node.path = getNodePath(node.element);
+      // @ts-ignore - getOuterHTMLSnippet put into scope via stringification
       node.snippet = getOuterHTMLSnippet(node.element);
       // avoid circular JSON concerns
       node.element = node.any = node.all = node.none = undefined;
@@ -84,18 +86,6 @@ function runA11yChecks() {
     path.reverse();
     return path.join(',');
   }
-
-  /**
-   * Gets the opening tag text of the given node.
-   * @param {Element} el
-   * @return {string}
-   */
-  function getOuterHTMLSnippet(el) {
-    const reOpeningTag = /^.*?>/;
-    const match = el.outerHTML.match(reOpeningTag);
-    // Should always be a snippet, so just fall back to '' to keep a string.
-    return match && match[0] || '';
-  }
 }
 
 class Accessibility extends Gatherer {
@@ -106,6 +96,7 @@ class Accessibility extends Gatherer {
   afterPass(passContext) {
     const driver = passContext.driver;
     const expression = `(function () {
+      ${pageFunctions.getOuterHTMLSnippet.toString()};
       ${axeLibSource};
       return (${runA11yChecks.toString()}());
     })()`;
diff --git a/lighthouse-core/gather/gatherers/dobetterweb/anchors-with-no-rel-noopener.js b/lighthouse-core/gather/gatherers/dobetterweb/anchors-with-no-rel-noopener.js
index 1a2cd0ce3..d2beb9473 100644
--- a/lighthouse-core/gather/gatherers/dobetterweb/anchors-with-no-rel-noopener.js
+++ b/lighthouse-core/gather/gatherers/dobetterweb/anchors-with-no-rel-noopener.js
@@ -6,7 +6,7 @@
 'use strict';
 
 const Gatherer = require('../gatherer');
-const DOMHelpers = require('../../../lib/dom-helpers.js');
+const pageFunctions = require('../../../lib/page-functions.js');
 
 class AnchorsWithNoRelNoopener extends Gatherer {
   /**
@@ -15,13 +15,15 @@ class AnchorsWithNoRelNoopener extends Gatherer {
    */
   afterPass(passContext) {
     const expression = `(function() {
-      ${DOMHelpers.getElementsInDocumentFnString}; // define function on page
+      ${pageFunctions.getOuterHTMLSnippet.toString()};
+      ${pageFunctions.getElementsInDocument.toString()}; // define function on page
       const selector = 'a[target="_blank"]:not([rel~="noopener"]):not([rel~="noreferrer"])';
       const elements = getElementsInDocument(selector);
       return elements.map(node => ({
         href: node.href,
         rel: node.getAttribute('rel'),
-        target: node.getAttribute('target')
+        target: node.getAttribute('target'),
+        outerHTML: getOuterHTMLSnippet(node),
       }));
     })()`;
 
diff --git a/lighthouse-core/gather/gatherers/dobetterweb/domstats.js b/lighthouse-core/gather/gatherers/dobetterweb/domstats.js
index a524339ff..866e7b633 100644
--- a/lighthouse-core/gather/gatherers/dobetterweb/domstats.js
+++ b/lighthouse-core/gather/gatherers/dobetterweb/domstats.js
@@ -9,23 +9,12 @@
  * and total number of nodes used on the page.
  */
 
-/* global ShadowRoot */
+/* global ShadowRoot, getOuterHTMLSnippet */
 
 'use strict';
 
 const Gatherer = require('../gatherer');
-
-/**
- * Gets the opening tag text of the given node.
- * @param {Element} element
- * @return {?string}
- */
-/* istanbul ignore next */
-function getOuterHTMLSnippet(element) {
-  const reOpeningTag = /^.*?>/;
-  const match = element.outerHTML.match(reOpeningTag);
-  return match && match[0];
-}
+const pageFunctions = require('../../../lib/page-functions');
 
 /**
  * Constructs a pretty label from element's selectors. For example, given
@@ -151,7 +140,7 @@ class DOMStats extends Gatherer {
    */
   afterPass(passContext) {
     const expression = `(function() {
-      ${getOuterHTMLSnippet.toString()};
+      ${pageFunctions.getOuterHTMLSnippet.toString()};
       ${createSelectorsLabel.toString()};
       ${elementPathInDOM.toString()};
       return (${getDOMStats.toString()}(document.documentElement));
diff --git a/lighthouse-core/gather/gatherers/dobetterweb/password-inputs-with-prevented-paste.js b/lighthouse-core/gather/gatherers/dobetterweb/password-inputs-with-prevented-paste.js
index 20adfdca4..6b50a0e7b 100644
--- a/lighthouse-core/gather/gatherers/dobetterweb/password-inputs-with-prevented-paste.js
+++ b/lighthouse-core/gather/gatherers/dobetterweb/password-inputs-with-prevented-paste.js
@@ -5,9 +5,10 @@
  */
 'use strict';
 
-/* global document ClipboardEvent */
+/* global document ClipboardEvent getOuterHTMLSnippet */
 
 const Gatherer = require('../gatherer');
+const pageFunctions = require('../../../lib/page-functions');
 
 // This is run in the page, not Lighthouse itself.
 /**
@@ -15,18 +16,6 @@ const Gatherer = require('../gatherer');
  */
 /* istanbul ignore next */
 function findPasswordInputsWithPreventedPaste() {
-  /**
-   * Gets the opening tag text of the given element.
-   * @param {Element} element
-   * @return {string}
-   */
-  function getOuterHTMLSnippet(element) {
-    const reOpeningTag = /^.*?>/;
-    const match = element.outerHTML.match(reOpeningTag);
-    // @ts-ignore We are confident match was found.
-    return match && match[0];
-  }
-
   return Array.from(document.querySelectorAll('input[type="password"]'))
     .filter(passwordInput =>
       !passwordInput.dispatchEvent(
@@ -34,6 +23,7 @@ function findPasswordInputsWithPreventedPaste() {
       )
     )
     .map(passwordInput => ({
+      // @ts-ignore - getOuterHTMLSnippet put into scope via stringification
       snippet: getOuterHTMLSnippet(passwordInput),
     }));
 }
@@ -44,9 +34,10 @@ class PasswordInputsWithPreventedPaste extends Gatherer {
    * @return {Promise<LH.Artifacts['PasswordInputsWithPreventedPaste']>}
    */
   afterPass(passContext) {
-    return passContext.driver.evaluateAsync(
-      `(${findPasswordInputsWithPreventedPaste.toString()}())`
-    );
+    return passContext.driver.evaluateAsync(`(() => {
+      ${pageFunctions.getOuterHTMLSnippet.toString()};
+      return (${findPasswordInputsWithPreventedPaste.toString()}());
+    })()`);
   }
 }
 
diff --git a/lighthouse-core/gather/gatherers/image-usage.js b/lighthouse-core/gather/gatherers/image-usage.js
index e0debb4d2..c3f89ccbf 100644
--- a/lighthouse-core/gather/gatherers/image-usage.js
+++ b/lighthouse-core/gather/gatherers/image-usage.js
@@ -10,7 +10,7 @@
 'use strict';
 
 const Gatherer = require('./gatherer');
-const DOMHelpers = require('../../lib/dom-helpers.js');
+const pageFunctions = require('../../lib/page-functions.js');
 const Driver = require('../driver.js'); // eslint-disable-line no-unused-vars
 
 /* global window, getElementsInDocument, Image */
@@ -161,7 +161,7 @@ class ImageUsage extends Gatherer {
     }, /** @type {Object<string, LH.Artifacts.SingleImageUsage['networkRecord']>} */ ({}));
 
     const expression = `(function() {
-      ${DOMHelpers.getElementsInDocumentFnString}; // define function on page
+      ${pageFunctions.getElementsInDocument.toString()}; // define function on page
       return (${collectImageElementInfo.toString()})();
     })()`;
 
diff --git a/lighthouse-core/gather/gatherers/seo/crawlable-links.js b/lighthouse-core/gather/gatherers/seo/crawlable-links.js
index ed495113e..12354fa4d 100644
--- a/lighthouse-core/gather/gatherers/seo/crawlable-links.js
+++ b/lighthouse-core/gather/gatherers/seo/crawlable-links.js
@@ -6,7 +6,7 @@
 'use strict';
 
 const Gatherer = require('../gatherer');
-const DOMHelpers = require('../../../lib/dom-helpers.js');
+const pageFunctions = require('../../../lib/page-functions.js');
 
 class CrawlableLinks extends Gatherer {
   /**
@@ -15,7 +15,7 @@ class CrawlableLinks extends Gatherer {
    */
   afterPass(passContext) {
     const expression = `(function() {
-      ${DOMHelpers.getElementsInDocumentFnString}; // define function on page
+      ${pageFunctions.getElementsInDocument.toString()}; // define function on page
       const selector = 'a[href]:not([rel~="nofollow"])';
       const elements = getElementsInDocument(selector);
       return elements
diff --git a/lighthouse-core/gather/gatherers/seo/embedded-content.js b/lighthouse-core/gather/gatherers/seo/embedded-content.js
index 3df7258fa..da8717126 100644
--- a/lighthouse-core/gather/gatherers/seo/embedded-content.js
+++ b/lighthouse-core/gather/gatherers/seo/embedded-content.js
@@ -6,7 +6,7 @@
 'use strict';
 
 const Gatherer = require('../gatherer');
-const DOMHelpers = require('../../../lib/dom-helpers.js');
+const pageFunctions = require('../../../lib/page-functions.js');
 
 class EmbeddedContent extends Gatherer {
   /**
@@ -15,7 +15,7 @@ class EmbeddedContent extends Gatherer {
    */
   afterPass(passContext) {
     const expression = `(function() {
-      ${DOMHelpers.getElementsInDocumentFnString}; // define function on page
+      ${pageFunctions.getElementsInDocument.toString()}; // define function on page
       const selector = 'object, embed, applet';
       const elements = getElementsInDocument(selector);
       return elements
diff --git a/lighthouse-core/lib/dom-helpers.js b/lighthouse-core/lib/dom-helpers.js
deleted file mode 100644
index ef7c59c74..000000000
--- a/lighthouse-core/lib/dom-helpers.js
+++ /dev/null
@@ -1,46 +0,0 @@
-/**
- * @license Copyright 2017 Google Inc. All Rights Reserved.
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
- * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- */
-'use strict';
-
-/**
- * @fileoverview Helpers to be used with driver.evaluateAsync(). That is, these
- * methods are intended to be injected into and run in the context of the page.
- */
-
-/* global document */
-
-// TODO: move to page-functions.js
-
-/**
- * @param {string=} selector Optional simple CSS selector to filter nodes on.
- *     Combinators are not supported.
- * @return {Array<Element>}
- */
-/* istanbul ignore next */
-function getElementsInDocument(selector) {
-  /** @type {Array<Element>} */
-  const results = [];
-
-  /** @param {NodeListOf<Element>} nodes */
-  const _findAllElements = nodes => {
-    for (let i = 0, el; el = nodes[i]; ++i) {
-      if (!selector || el.matches(selector)) {
-        results.push(el);
-      }
-      // If the element has a shadow root, dig deeper.
-      if (el.shadowRoot) {
-        _findAllElements(el.shadowRoot.querySelectorAll('*'));
-      }
-    }
-  };
-  _findAllElements(document.querySelectorAll('*'));
-
-  return results;
-}
-
-module.exports = {
-  getElementsInDocumentFnString: getElementsInDocument.toString(),
-};
diff --git a/lighthouse-core/lib/page-functions.js b/lighthouse-core/lib/page-functions.js
index 8480ee295..ee828a05e 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 */
+/* global window document */
 
 /**
  * Helper functions that are passed by `toString()` by Driver to be evaluated in target page.
@@ -78,8 +78,49 @@ function checkTimeSinceLastLongTask() {
   });
 }
 
+/**
+ * @param {string=} selector Optional simple CSS selector to filter nodes on.
+ *     Combinators are not supported.
+ * @return {Array<Element>}
+ */
+/* istanbul ignore next */
+function getElementsInDocument(selector) {
+  /** @type {Array<Element>} */
+  const results = [];
+
+  /** @param {NodeListOf<Element>} nodes */
+  const _findAllElements = nodes => {
+    for (let i = 0, el; el = nodes[i]; ++i) {
+      if (!selector || el.matches(selector)) {
+        results.push(el);
+      }
+      // If the element has a shadow root, dig deeper.
+      if (el.shadowRoot) {
+        _findAllElements(el.shadowRoot.querySelectorAll('*'));
+      }
+    }
+  };
+  _findAllElements(document.querySelectorAll('*'));
+
+  return results;
+}
+
+/**
+ * Gets the opening tag text of the given node.
+ * @param {Element} element
+ * @return {string}
+ */
+/* istanbul ignore next */
+function getOuterHTMLSnippet(element) {
+  const reOpeningTag = /^.*?>/;
+  const match = element.outerHTML.match(reOpeningTag);
+  return match && match[0] || '';
+}
+
 module.exports = {
   wrapRuntimeEvalErrorInBrowser,
   registerPerformanceObserverInPage,
   checkTimeSinceLastLongTask,
+  getElementsInDocument,
+  getOuterHTMLSnippet,
 };
diff --git a/typings/artifacts.d.ts b/typings/artifacts.d.ts
index 7238139c9..9f3103892 100644
--- a/typings/artifacts.d.ts
+++ b/typings/artifacts.d.ts
@@ -43,7 +43,7 @@ declare global {
       /** The results of running the aXe accessibility tests on the page. */
       Accessibility: Artifacts.Accessibility;
       /** Information on all anchors in the page that aren't nofollow or noreferrer. */
-      AnchorsWithNoRelNoopener: {href: string; rel: string; target: string}[];
+      AnchorsWithNoRelNoopener: {href: string; rel: string; target: string, outerHTML: string}[];
       /** The value of the page's <html> manifest attribute, or null if not defined */
       AppCacheManifest: string | null;
       /** Array of all URLs cached in CacheStorage. */
diff --git a/typings/config.d.ts b/typings/config.d.ts
index 9cb6b692d..1866a0955 100644
--- a/typings/config.d.ts
+++ b/typings/config.d.ts
@@ -52,13 +52,14 @@ declare global {
 
       export interface CategoryJson {
         title: string;
-        description: string;
         auditRefs: AuditRefJson[];
+        description?: string;
+        manualDescription?: string;
       }
 
       export interface GroupJson {
         title: string;
-        description: string;
+        description?: string;
       }
 
       export type AuditJson = {


Test patch:
diff --git a/lighthouse-cli/test/smokehouse/dobetterweb/dbw-expectations.js b/lighthouse-cli/test/smokehouse/dobetterweb/dbw-expectations.js
index 3bdd03ce3..0cd47040d 100644
--- a/lighthouse-cli/test/smokehouse/dobetterweb/dbw-expectations.js
+++ b/lighthouse-cli/test/smokehouse/dobetterweb/dbw-expectations.js
@@ -39,7 +39,7 @@ module.exports = [
       },
       'external-anchors-use-rel-noopener': {
         score: 0,
-        warnings: [/Unable to determine/],
+        warnings: [/Unable to determine.*<a target="_blank">/],
         details: {
           items: {
             length: 3,
diff --git a/lighthouse-core/test/results/artifacts/artifacts.json b/lighthouse-core/test/results/artifacts/artifacts.json
index ee1761c5e..38f785e80 100644
--- a/lighthouse-core/test/results/artifacts/artifacts.json
+++ b/lighthouse-core/test/results/artifacts/artifacts.json
@@ -2227,16 +2227,19 @@
   "AnchorsWithNoRelNoopener": [
     {
       "href": "https://www.google.com/",
+      "outerHTML": "<a href=\"https://www.google.com/\" target=\"blank\">Hello</a>",
       "rel": null,
       "target": "_blank"
     },
     {
       "href": "",
+      "outerHTML": "<a target=\"blank\">Hello</a>",
       "rel": null,
       "target": "_blank"
     },
     {
       "href": "https://www.google.com/",
+      "outerHTML": "<a rel=\"nofollow\" href=\"https://www.google.com/\" target=\"blank\">Hello</a>",
       "rel": "nofollow",
       "target": "_blank"
     },
diff --git a/lighthouse-core/test/results/sample_v2.json b/lighthouse-core/test/results/sample_v2.json
index b31e7baa2..f3378f94d 100644
--- a/lighthouse-core/test/results/sample_v2.json
+++ b/lighthouse-core/test/results/sample_v2.json
@@ -2226,7 +2226,7 @@
       "scoreDisplayMode": "binary",
       "rawValue": false,
       "warnings": [
-        "Unable to determine the destination for anchor tag. If not used as a hyperlink, consider removing target=_blank."
+        "Unable to determine the destination for anchor (<a target=\"blank\">Hello</a>). If not used as a hyperlink, consider removing target=_blank."
       ],
       "details": {
         "type": "table",
@@ -2252,19 +2252,19 @@
             "href": "https://www.google.com/",
             "target": "_blank",
             "rel": "",
-            "url": "<a href=\"https://www.google.com/\" target=\"_blank\">"
+            "outerHTML": "<a href=\"https://www.google.com/\" target=\"blank\">Hello</a>"
           },
           {
             "href": "Unknown",
             "target": "_blank",
             "rel": "",
-            "url": "<a target=\"_blank\">"
+            "outerHTML": "<a target=\"blank\">Hello</a>"
           },
           {
             "href": "https://www.google.com/",
             "target": "_blank",
             "rel": "nofollow",
-            "url": "<a href=\"https://www.google.com/\" target=\"_blank\" rel=\"nofollow\">"
+            "outerHTML": "<a rel=\"nofollow\" href=\"https://www.google.com/\" target=\"blank\">Hello</a>"
           }
         ]
       }