Repository: GoogleChrome/lighthouse
Pull request #10253 (2020-01-23 18:59:31)

docs: recipe for using puppeteer in a gatherer
@paulirish requested this on behalf of some people making UX audits. Turns out, doing input over the CDP is annoying.

Having some trouble with `yarn`, let's see how CI handles it.


Patch:
diff --git a/docs/recipes/custom-gatherer-puppeteer/custom-audit.js b/docs/recipes/custom-gatherer-puppeteer/custom-audit.js
new file mode 100644
index 000000000..1a640cbcf
--- /dev/null
+++ b/docs/recipes/custom-gatherer-puppeteer/custom-audit.js
@@ -0,0 +1,34 @@
+/**
+ * @license Copyright 2020 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';
+
+const Audit = require('lighthouse').Audit;
+
+class CustomAudit extends Audit {
+  static get meta() {
+    return {
+      id: 'custom-audit',
+      title: 'First text input field accepts `123` as input',
+      failureTitle: 'First text input field doesn\'t accept `123` as input',
+      description: 'Example custom audit which relies on a fancy gatherer.',
+
+      // The name of the custom gatherer class that provides input to this audit.
+      requiredArtifacts: ['CustomGatherer'],
+    };
+  }
+
+  static audit(artifacts) {
+    const value = artifacts.CustomGatherer.value;
+    const success = value === '123';
+
+    return {
+      // Cast true/false to 1/0
+      score: Number(success),
+    };
+  }
+}
+
+module.exports = CustomAudit;
diff --git a/docs/recipes/custom-gatherer-puppeteer/custom-config.js b/docs/recipes/custom-gatherer-puppeteer/custom-config.js
new file mode 100644
index 000000000..47fbc0527
--- /dev/null
+++ b/docs/recipes/custom-gatherer-puppeteer/custom-config.js
@@ -0,0 +1,29 @@
+/**
+ * @license Copyright 2020 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';
+
+module.exports = {
+  passes: [{
+    passName: 'defaultPass',
+    gatherers: [
+      'custom-gatherer',
+    ],
+  }],
+
+  audits: [
+    'custom-audit',
+  ],
+
+  categories: {
+    mysite: {
+      title: 'My custom audit',
+      description: 'Super awesome, strings included',
+      auditRefs: [
+        {id: 'custom-audit', weight: 1},
+      ],
+    },
+  },
+};
diff --git a/docs/recipes/custom-gatherer-puppeteer/custom-gatherer.js b/docs/recipes/custom-gatherer-puppeteer/custom-gatherer.js
new file mode 100644
index 000000000..41373da8b
--- /dev/null
+++ b/docs/recipes/custom-gatherer-puppeteer/custom-gatherer.js
@@ -0,0 +1,51 @@
+/**
+ * @license Copyright 2020 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';
+
+/* global document */
+
+const Gatherer = require('lighthouse').Gatherer;
+const Puppeteer = require('puppeteer-core');
+
+async function connect(driver) {
+  const browser = await Puppeteer.connect({
+    browserWSEndpoint: await driver.wsEndpoint(),
+    defaultViewport: null,
+  });
+  const {targetInfo} = await driver.sendCommand('Target.getTargetInfo');
+  const puppeteerTarget = (await browser.targets())
+    .find(target => target._targetId === targetInfo.targetId);
+  const page = await puppeteerTarget.page();
+  return {browser, page};
+}
+
+class CustomGatherer extends Gatherer {
+  async afterPass(options) {
+    const {driver} = options;
+    const {page} = await connect(driver);
+
+    // Inject an input field for our debugging pleasure.
+    function makeInput() {
+      const el = document.createElement('input');
+      el.type = 'number';
+      document.body.append(el);
+    }
+    await driver.evaluateAsync(`(${makeInput})()`);
+    await new Promise(resolve => setTimeout(resolve, 100));
+
+    // Prove that `driver` (Lighthouse) and `page` (Puppeteer) are talking to the same page.
+    await driver.evaluateAsync(`document.querySelector('input').value = '1'`);
+    await page.type('input', '23', {delay: 300});
+    const value = await driver.evaluateAsync(`document.querySelector('input').value`);
+    if (value !== '123') throw new Error('huh?');
+
+    // No need to close the browser or page. Puppeteer doesn't own either of them.
+
+    return {value};
+  }
+}
+
+module.exports = CustomGatherer;
diff --git a/docs/recipes/custom-gatherer-puppeteer/package.json b/docs/recipes/custom-gatherer-puppeteer/package.json
new file mode 100644
index 000000000..f708a69fb
--- /dev/null
+++ b/docs/recipes/custom-gatherer-puppeteer/package.json
@@ -0,0 +1,13 @@
+{
+  "name": "custom-lighthouse-recipe",
+  "private": true,
+  "scripts": {
+    "test": "sh test.sh"
+  },
+  "devDependencies": {
+    "lighthouse": "file:../../../dist/lighthouse.tgz"
+  },
+  "dependencies": {
+    "puppeteer-core": "^2.0.0"
+  }
+}
diff --git a/docs/recipes/custom-gatherer-puppeteer/readme.md b/docs/recipes/custom-gatherer-puppeteer/readme.md
new file mode 100644
index 000000000..337953939
--- /dev/null
+++ b/docs/recipes/custom-gatherer-puppeteer/readme.md
@@ -0,0 +1,7 @@
+# Using Puppeteer in a Gatherer
+
+> **Tip**: see [Basic Custom Audit Recipe](../custom-audit) for basic information about custom audits.
+
+```sh
+lighthouse --config-path=custom-config.js https://www.example.com
+```
diff --git a/package.json b/package.json
index cf242b704..e04d4906d 100644
--- a/package.json
+++ b/package.json
@@ -29,7 +29,7 @@
     "test-clients": "jest \"clients/\"",
     "test-viewer": "yarn unit-viewer && jest lighthouse-viewer/test/viewer-test-pptr.js",
     "test-lantern": "bash lighthouse-core/scripts/test-lantern.sh",
-    "test-docs": "yarn --cwd docs/recipes/auth && jest docs/recipes/integration-test",
+    "test-docs": "yarn --cwd docs/recipes/auth && jest docs/recipes/integration-test && yarn --cwd docs/recipes/custom-gatherer-puppeteer test",
     "unit-core": "jest \"lighthouse-core/\"",
     "unit-cli": "jest \"lighthouse-cli/\"",
     "unit-viewer": "jest \"lighthouse-viewer/.*-test.js\"",


Test patch:
diff --git a/docs/recipes/custom-gatherer-puppeteer/test.sh b/docs/recipes/custom-gatherer-puppeteer/test.sh
new file mode 100644
index 000000000..674135bb5
--- /dev/null
+++ b/docs/recipes/custom-gatherer-puppeteer/test.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+
+# Make sure we're in this `docs/recipes/customer-gatherer-puppeteer` directory
+cd "$(dirname "$0")"
+
+yarn
+node node_modules/.bin/lighthouse --config-path=custom-config.js https://www.example.com --output=json |
+  jq '.audits["custom-audit"].score' |
+  grep -q 1