Repository: GoogleChrome/lighthouse
Pull request #12815 (2021-07-21 17:03:39)

report: create faux psi report
I wanted a test page where there are two LHRs, like in PSI. This helps surface the peculiar behavior we get with tabs and element-screenshot-overlay and how it's gotta live outside the tab body.

here 'tis: https://lighthouse-git-faux-psi-googlechrome.vercel.app/%E2%8C%A3.psi.english

adamraine: I noticed clicking on an audit filter in the desktop tab doesn't do anything on the desktop audits, but does change the filter for the mobile tab.
paulirish: > I noticed clicking on an audit filter in the desktop tab doesn't do anything on the desktop audits, but does change the filter for the mobile tab.
> 
> Edit: ope this is a PSI bug

heh. well. thats exactly what this lil stupid report is for. :)  
kinda perfect, tbh. 
paulirish: love the review. agree with everything. thank you!

Patch:
diff --git a/build/build-report.js b/build/build-report.js
index d4ca39814..d0377a808 100644
--- a/build/build-report.js
+++ b/build/build-report.js
@@ -85,13 +85,25 @@ async function buildUmdBundle() {
 }
 
 if (require.main === module) {
-  if (process.argv[2] === '--only-standalone') {
-    buildStandaloneReport();
-  } else {
+  if (process.argv.length <= 2) {
     buildStandaloneReport();
+    buildEsModulesBundle();
+    buildPsiReport();
     buildUmdBundle();
+  }
+
+  if (process.argv.includes('--psi')) {
+    buildPsiReport();
+  }
+  if (process.argv.includes('--standalone')) {
+    buildStandaloneReport();
+  }
+  if (process.argv.includes('--esm')) {
     buildEsModulesBundle();
   }
+  if (process.argv.includes('--umd')) {
+    buildUmdBundle();
+  }
 }
 
 module.exports = {
diff --git a/lighthouse-core/scripts/build-report-for-autodeployment.js b/lighthouse-core/scripts/build-report-for-autodeployment.js
index 963c03e6d..ce2f1e2b8 100644
--- a/lighthouse-core/scripts/build-report-for-autodeployment.js
+++ b/lighthouse-core/scripts/build-report-for-autodeployment.js
@@ -16,6 +16,8 @@ const {defaultSettings} = require('../config/constants.js');
 const lighthouse = require('../index.js');
 const lhr = /** @type {LH.Result} */ (require('../../lighthouse-core/test/results/sample_v2.json'));
 const {LH_ROOT} = require('../../root.js');
+const htmlReportAssets = require('../../report/report-assets.js');
+
 
 const DIST = path.join(LH_ROOT, `dist/now`);
 
@@ -29,19 +31,23 @@ const DIST = path.join(LH_ROOT, `dist/now`);
     'ɑrabic': swapLocale(lhr, 'ar').lhr,
     'xl-accented': swapLocale(lhr, 'en-XL').lhr,
     'error': errorLhr,
+    'single-category': tweakLhrForPsi(lhr),
   };
 
   // Generate and write reports
   Object.entries(filenameToLhr).forEach(([filename, lhr]) => {
-    let html = ReportGenerator.generateReportHtml(lhr);
-    // TODO: PSI is another variant to consider
-    for (const variant of ['', '⌣.cdt.']) {
+    for (const variant of ['', '⌣.cdt.', '⌣.psi.']) {
+      let html = variant.includes('psi') ?
+        generatePsiReportHtml() :
+        ReportGenerator.generateReportHtml(lhr);
+
       if (variant.includes('cdt')) {
         // TODO: Make the DevTools Audits panel "emulation" more comprehensive
         // - the parent widget/vbox container with overflow
         // - a more constrained/realistic default size
         html = html.replace(`"lh-root lh-vars"`, `"lh-root lh-vars lh-devtools"`);
       }
+
       const filepath = `${DIST}/${variant}${filename}/index.html`;
       fs.mkdirSync(path.dirname(filepath), {recursive: true});
       fs.writeFileSync(filepath, html, {encoding: 'utf-8'});
@@ -50,6 +56,26 @@ const DIST = path.join(LH_ROOT, `dist/now`);
   });
 })();
 
+
+/**
+ * @return {string}
+ */
+function generatePsiReportHtml() {
+  const sanitizedJson = ReportGenerator.sanitizeJson(tweakLhrForPsi(lhr));
+  const PSI_TEMPLATE = fs.readFileSync(
+    `${LH_ROOT}/report/test-assets/faux-psi-template.html`, 'utf8');
+  const PSI_JAVASCRIPT = `
+${fs.readFileSync(`${LH_ROOT}/dist/report/psi.js`, 'utf8')};
+${fs.readFileSync(`${LH_ROOT}/report/test-assets/faux-psi.js`, 'utf8')};
+  `;
+
+  const html = ReportGenerator.replaceStrings(PSI_TEMPLATE, [
+    {search: '%%LIGHTHOUSE_JSON%%', replacement: sanitizedJson},
+    {search: '%%LIGHTHOUSE_JAVASCRIPT%%', replacement: PSI_JAVASCRIPT},
+    {search: '/*%%LIGHTHOUSE_CSS%%*/', replacement: htmlReportAssets.REPORT_CSS},
+  ]);
+  return html;
+}
 /**
  * Add a plugin to demo plugin rendering.
  * @param {LH.Result} lhr
@@ -63,6 +89,24 @@ function addPluginCategory(lhr) {
   };
 }
 
+/**
+ * Drops the LHR to only one, solo category (performance), and removes budgets.
+ * @param {LH.Result} lhr
+ */
+function tweakLhrForPsi(lhr) {
+  /** @type {LH.Result} */
+  const clone = JSON.parse(JSON.stringify(lhr));
+  clone.categories = {
+    'performance': clone.categories.performance,
+  };
+  // no budgets in PSI
+  delete clone.audits['performance-budget'];
+  clone.categories.performance.auditRefs = clone.categories.performance.auditRefs.filter(audit => {
+    return !audit.id.endsWith('-budget');
+  });
+  return clone;
+}
+
 /**
  * Generate an LHR with errors for the renderer to display.
  * We'll write an "empty" artifacts file to disk, only to use it in auditMode.
diff --git a/lighthouse-viewer/app/index.html b/lighthouse-viewer/app/index.html
index 19f6ecbc5..7a2aa11dc 100644
--- a/lighthouse-viewer/app/index.html
+++ b/lighthouse-viewer/app/index.html
@@ -16,7 +16,6 @@ Unless required by applicable law or agreed to in writing, software distributed
   <link rel="stylesheet" href="styles/bundled.css">
 </head>
 <body class="lh-root lh-vars">
-<div hidden>%%LIGHTHOUSE_TEMPLATES%%</div>
 
 <div class="drop_zone"></div>
 
diff --git a/package.json b/package.json
index 1af5db1d7..a7a87d826 100644
--- a/package.json
+++ b/package.json
@@ -24,6 +24,7 @@
     "build-lr": "yarn reset-link && node ./build/build-lightrider-bundles.js",
     "build-pack": "bash build/build-pack.sh",
     "build-report": "node build/build-report-components.js && yarn eslint --fix report/renderer/components.js && node build/build-report.js",
+    "build-dist-reports": "yarn build-report && node lighthouse-core/scripts/build-report-for-autodeployment.js",
     "build-treemap": "node ./build/build-treemap.js",
     "build-viewer": "node ./build/build-viewer.js",
     "reset-link": "(yarn unlink || true) && yarn link && yarn link lighthouse",
@@ -32,7 +33,7 @@
     "lint": "[ \"$CI\" = true ] && eslint --quiet -f codeframe . || eslint .",
     "smoke": "node lighthouse-cli/test/smokehouse/frontends/smokehouse-bin.js",
     "debug": "node --inspect-brk ./lighthouse-cli/index.js",
-    "start": "yarn build-report --only-standalone && node ./lighthouse-cli/index.js",
+    "start": "yarn build-report --standalone && node ./lighthouse-cli/index.js",
     "jest": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js",
     "test": "yarn diff:sample-json && yarn lint --quiet && yarn unit && yarn type-check",
     "test-bundle": "yarn smoke --runner bundle -j=1 --retries=2 --invert-match forms",
@@ -62,7 +63,7 @@
     "fast": "node ./lighthouse-cli/index.js --preset=desktop --throttlingMethod=provided",
     "deploy-treemap": "yarn build-treemap --deploy",
     "deploy-viewer": "yarn build-viewer --deploy",
-    "now-build": "yarn build-report && node lighthouse-core/scripts/build-report-for-autodeployment.js && yarn build-viewer && yarn build-treemap && cp -r dist/gh-pages dist/now/gh-pages",
+    "now-build": "yarn build-dist-reports && yarn build-viewer && yarn build-treemap && cp -r dist/gh-pages dist/now/gh-pages",
     "dogfood-lhci": "./lighthouse-core/scripts/dogfood-lhci.sh",
     "timing-trace": "node lighthouse-core/scripts/generate-timing-trace.js",
     "changelog": "conventional-changelog --config ./build/changelog-generator/index.js --infile changelog.md --same-file",
diff --git a/report/assets/standalone-template.html b/report/assets/standalone-template.html
index cca9e9913..5a64103a4 100644
--- a/report/assets/standalone-template.html
+++ b/report/assets/standalone-template.html
@@ -25,7 +25,6 @@ limitations under the License.
 </head>
 <body class="lh-root lh-vars">
   <noscript>Lighthouse report requires JavaScript. Please enable.</noscript>
-  <div hidden>%%LIGHTHOUSE_TEMPLATES%%</div>
 
   <main><!-- report populated here --></main>
 
diff --git a/report/clients/psi.js b/report/clients/psi.js
index fbeabce90..29d48b5f3 100644
--- a/report/clients/psi.js
+++ b/report/clients/psi.js
@@ -24,6 +24,10 @@ import {PerformanceCategoryRenderer} from '../renderer/performance-category-rend
 import {ReportUIFeatures} from '../renderer/report-ui-features.js';
 import {Util} from '../renderer/util.js';
 
+/* global window */
+
+/** @typedef {{scoreGaugeEl: Element, perfCategoryEl: Element, finalScreenshotDataUri: string|null, scoreScaleEl: Element, installFeatures: Function}} PrepareLabDataResult */
+
 /**
  * Returns all the elements that PSI needs to render the report
  * We expose this helper method to minimize the 'public' API surface of the renderer
@@ -36,7 +40,7 @@ import {Util} from '../renderer/util.js';
  *
  * @param {LH.Result | string} LHResult The stringified version of {LH.Result}
  * @param {Document} document The host page's window.document
- * @return {{scoreGaugeEl: Element, perfCategoryEl: Element, finalScreenshotDataUri: string|null, scoreScaleEl: Element, installFeatures: Function}}
+ * @return {PrepareLabDataResult}
  */
 export function prepareLabData(LHResult, document) {
   const lhResult = (typeof LHResult === 'string') ?
@@ -89,10 +93,11 @@ export function prepareLabData(LHResult, document) {
   /** @param {HTMLElement} reportEl */
   const installFeatures = (reportEl) => {
     if (fullPageScreenshot) {
+      // 1) Add fpss css var to reportEl parent so any thumbnails will work
       ElementScreenshotRenderer.installFullPageScreenshot(
         reportEl, fullPageScreenshot.screenshot);
 
-      // Append the overlay element to a specific part of the DOM so that
+      // 2) Append the overlay element to a specific part of the DOM so that
       // the sticky tab group element renders correctly. If put in the reportEl
       // like normal, then the sticky header would bleed through the overlay
       // element.
@@ -141,3 +146,9 @@ function _getFinalScreenshot(perfCategory) {
   if (!details || details.type !== 'screenshot') return null;
   return details.data;
 }
+
+// TODO: remove with report API refactor.
+if (typeof window !== 'undefined') {
+  // @ts-expect-error
+  window.prepareLabData = prepareLabData;
+}
diff --git a/report/report-generator.js b/report/report-generator.js
index 05031d274..ff84a0c58 100644
--- a/report/report-generator.js
+++ b/report/report-generator.js
@@ -28,15 +28,23 @@ class ReportGenerator {
   }
 
   /**
-   * Returns the report HTML as a string with the report JSON and renderer JS inlined.
+   * @param {unknown} object
+   * @return {string}
+   */
+  static sanitizeJson(object) {
+    return JSON.stringify(object)
+    .replace(/</g, '\\u003c') // replaces opening script tags
+    .replace(/\u2028/g, '\\u2028') // replaces line separators ()
+    .replace(/\u2029/g, '\\u2029'); // replaces paragraph separators
+  }
+
+  /**
+   * Returns the standalone report HTML as a string with the report JSON and renderer JS inlined.
    * @param {LH.Result} lhr
    * @return {string}
    */
   static generateReportHtml(lhr) {
-    const sanitizedJson = JSON.stringify(lhr)
-      .replace(/</g, '\\u003c') // replaces opening script tags
-      .replace(/\u2028/g, '\\u2028') // replaces line separators ()
-      .replace(/\u2029/g, '\\u2029'); // replaces paragraph separators
+    const sanitizedJson = ReportGenerator.sanitizeJson(lhr);
     // terser does its own sanitization, but keep this basic replace for when
     // we want to generate a report without minification.
     const sanitizedJavascript = htmlReportAssets.REPORT_JAVASCRIPT.replace(/<\//g, '\\u003c/');


Test patch:
diff --git a/report/test-assets/faux-psi-template.html b/report/test-assets/faux-psi-template.html
new file mode 100644
index 000000000..85a1aa4a5
--- /dev/null
+++ b/report/test-assets/faux-psi-template.html
@@ -0,0 +1,126 @@
+<!--
+ * Copyright 2021 The Lighthouse Authors. 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.
+-->
+
+<!--
+  This fake psi page is intended to reproduce the experience of rendering two LHRs within the same DOM,
+  like in PageSpeed Insights. This helps surface the peculiar behavior around tabs,
+  expectations of unique IDs, and scoped report containment.
+-->
+
+<!doctype html>
+<html lang="en">
+<head>
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
+  <title>Faux PageSpeed Report</title>
+  <style>/*%%LIGHTHOUSE_CSS%%*/</style>
+
+  <style>
+/* Inspired by https://codepen.io/markcaron/pen/MvGRYV?editors=1100 but improved. s*/
+/*
+ The tab visibility basics
+*/
+.tabset > input[type="radio"] {
+  position: absolute;
+  left: -200vw;
+}
+
+.tabset .tab-panel {
+  display: none;
+}
+
+.tabset > input:nth-of-type(1):checked ~ .tab-panels > .tab-panel:nth-of-type(1),
+.tabset > input:nth-of-type(2):checked ~ .tab-panels > .tab-panel:nth-of-type(2) {
+  display: block;
+}
+
+/*
+ Purdy tabs
+*/
+.tabset > label {
+  position: relative;
+  display: inline-block;
+  padding: 15px 55px 15px;
+  border: 1px solid transparent;
+  border-bottom: 0;
+  cursor: pointer;
+  font-weight: 600;
+}
+
+.tabset > label:hover {
+  background-color: hsl(0deg 0% 100% / 24%);
+}
+
+.tabset > label:hover,
+.tabset > input:focus + label {
+  color: #06c;
+}
+
+.tabset > input:checked + label {
+  border-color: #ccc;
+  border-bottom: 1px solid #fff;
+  margin-bottom: -1px;
+  background: white;
+}
+
+.tab-panel {
+  padding: 30px 0;
+  border-top: 1px solid #ccc;
+}
+
+body {
+  padding: 30px;
+}
+
+.tabset {
+  max-width: 65em;
+  background: #eee;
+}
+
+.tab-panels {
+  background: white;
+}
+
+/* extra styles */
+body {
+  background: hsl(217deg 89% 61%);
+}
+  </style>
+</head>
+<body >
+  <noscript>PSI requires JavaScript. Please enable.</noscript>
+
+  <div class="element-screenshots-container"></div>
+
+
+  <div class="tabset lh-vars lh-root">
+
+    <input type="radio" name="tabset" id="tab1" aria-controls="mobile" checked>
+    <label for="tab1">Mobile</label>
+
+    <input type="radio" name="tabset" id="tab2" aria-controls="desktop">
+    <label for="tab2">Desktop</label>
+
+    <div class="tab-panels">
+      <section id="mobile" class="tab-panel">
+        <main></main>
+      </section>
+
+      <section id="desktop" class="tab-panel">
+        <main></main>
+      </section>
+
+    </div>
+  </div>
+
+  <script>window.__LIGHTHOUSE_JSON__ = %%LIGHTHOUSE_JSON%%;</script>
+  <script type="module">
+%%LIGHTHOUSE_JAVASCRIPT%%
+  //# sourceURL=psi-reportrenderer.js
+  </script>
+  <script>console.log('window.__LIGHTHOUSE_JSON__', __LIGHTHOUSE_JSON__);</script>
+</body>
+</html>
diff --git a/report/test-assets/faux-psi.js b/report/test-assets/faux-psi.js
new file mode 100644
index 000000000..8bf0093eb
--- /dev/null
+++ b/report/test-assets/faux-psi.js
@@ -0,0 +1,94 @@
+/**
+ * @license Copyright 2021 The Lighthouse Authors. 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 This file is a glorified call of prepareLabData. */
+
+/* global document window */
+
+/** @typedef {import('../clients/psi.js').PrepareLabDataResult} PrepareLabDataResult */
+
+(async function __initPsiReports__() {
+  // @ts-expect-error
+  const mobileLHR = window.__LIGHTHOUSE_JSON__;
+  const desktopLHR = JSON.parse(JSON.stringify(mobileLHR));
+
+  const lhrs = {
+    'mobile': mobileLHR,
+    'desktop': desktopLHR,
+  };
+
+  for (const [tabId, lhr] of Object.entries(lhrs)) {
+    await distinguishLHR(lhr, tabId);
+
+    // @ts-expect-error
+    const pldd = /** @type {PrepareLabDataResult} */ (window.prepareLabData(lhr, document));
+    const {scoreGaugeEl, perfCategoryEl,
+      finalScreenshotDataUri, scoreScaleEl, installFeatures} = pldd;
+
+    const container = document.querySelector(`#${tabId} main`);
+    if (!container) throw new Error('Unexpected DOM. Bailing.');
+    container.append(scoreGaugeEl);
+    container.append(scoreScaleEl);
+    if (finalScreenshotDataUri) {
+      const imgEl = document.createElement('img');
+      imgEl.src = finalScreenshotDataUri;
+      container.append(imgEl);
+    }
+    container.append(perfCategoryEl);
+    installFeatures(container);
+  }
+})();
+
+
+/**
+ * Tweak the LHR to make the desktop and mobile reports easier to identify.
+ * Adjusted: Perf category name and score, and emoji placed on top of key screenshots.
+ * @param {LH.Result} lhr
+ * @param {string} tabId
+ */
+async function distinguishLHR(lhr, tabId) {
+  lhr.categories.performance.title += ` ${tabId}`; // for easier identification
+  if (tabId === 'desktop') {
+    lhr.categories.performance.score = 0.81;
+  }
+
+  const finalSSDetails = lhr.audits['final-screenshot'] && lhr.audits['final-screenshot'].details;
+  if (!finalSSDetails || finalSSDetails.type !== 'screenshot') throw new Error();
+  finalSSDetails.data = await decorateScreenshot(finalSSDetails.data, tabId);
+
+  const fpSSDetails = lhr.audits['full-page-screenshot'] &&
+      lhr.audits['full-page-screenshot'].details;
+  if (!fpSSDetails || fpSSDetails.type !== 'full-page-screenshot') throw new Error();
+  fpSSDetails.screenshot.data = await decorateScreenshot(fpSSDetails.screenshot.data, tabId);
+}
+
+/**
+ * Add 📱 and 💻 emoji on top of screenshot
+ * @param {string} datauri
+ * @param {string} tabId
+ */
+async function decorateScreenshot(datauri, tabId) {
+  const img = document.createElement('img');
+
+  await new Promise((resolve, reject) => {
+    img.addEventListener('load', () => resolve(img));
+    img.addEventListener('error', (err) => reject(err));
+    img.src = datauri;
+  });
+  const c = document.createElement('canvas');
+  c.width = img.width;
+  c.height = img.height;
+
+  const ctx = c.getContext('2d');
+  if (!ctx) throw new Error();
+  ctx.drawImage(img, 0, 0);
+  ctx.font = `${img.width / 2}px serif`;
+  ctx.textAlign = 'center';
+  ctx.globalAlpha = 0.7;
+  ctx.fillText(tabId === 'mobile' ? '📱' : '💻', img.width / 2, Math.min(img.height / 2, 700));
+  return c.toDataURL();
+}
diff --git a/report/test/clients/psi-test.js b/report/test/clients/psi-test.js
index 9539f25fc..72ff6c69b 100644
--- a/report/test/clients/psi-test.js
+++ b/report/test/clients/psi-test.js
@@ -27,7 +27,7 @@ const sampleResultsStr =
 
 /* eslint-env jest */
 
-describe('DOM', () => {
+describe('PSI', () => {
   let document;
   beforeAll(() => {
     global.Util = Util;