report(psi): retire prepareLabData, reuse standard report rendering (#13229)

This commit is contained in:
Paul Irish 2021-10-19 13:48:16 -07:00 коммит произвёл GitHub
Родитель b62ab13309
Коммит f4eb93e747
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
10 изменённых файлов: 114 добавлений и 351 удалений

Просмотреть файл

@ -12,7 +12,6 @@ const fs = require('fs');
const path = require('path');
const bundleBuilder = require('./build-bundle.js');
const {minifyFileTransform} = require('./build-utils.js');
const {buildPsiReport} = require('./build-report.js');
const {LH_ROOT} = require('../root.js');
const distDir = path.join(LH_ROOT, 'dist', 'lightrider');
@ -77,7 +76,6 @@ async function run() {
await Promise.all([
buildEntryPoint(),
buildReportGenerator(),
buildPsiReport(),
buildStaticServerBundle(),
]);
}

Просмотреть файл

@ -88,20 +88,6 @@ async function buildFlowReport() {
});
}
async function buildPsiReport() {
const bundle = await rollup.rollup({
input: 'report/clients/psi.js',
plugins: [
rollupPlugins.commonjs(),
],
});
await bundle.write({
file: 'dist/report/psi.js',
format: 'esm',
});
}
async function buildEsModulesBundle() {
const bundle = await rollup.rollup({
input: 'report/clients/bundle.js',
@ -121,6 +107,11 @@ async function buildUmdBundle() {
input: 'report/clients/bundle.js',
plugins: [
rollupPlugins.commonjs(),
rollupPlugins.terser({
format: {
beautify: true,
},
}),
],
});
@ -136,12 +127,12 @@ if (require.main === module) {
buildStandaloneReport();
buildFlowReport();
buildEsModulesBundle();
buildPsiReport();
buildUmdBundle();
}
if (process.argv.includes('--psi')) {
buildPsiReport();
console.error('--psi build removed. use --umd instead.');
process.exit(1);
}
if (process.argv.includes('--standalone')) {
buildStandaloneReport();
@ -160,6 +151,5 @@ if (require.main === module) {
module.exports = {
buildStandaloneReport,
buildFlowReport,
buildPsiReport,
buildUmdBundle,
};

Просмотреть файл

@ -85,7 +85,7 @@ function generatePsiReportHtml(sampleLhr) {
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}/dist/report/bundle.umd.js`, 'utf8')};
${fs.readFileSync(`${LH_ROOT}/report/test-assets/faux-psi.js`, 'utf8')};
`;

Просмотреть файл

@ -1,158 +0,0 @@
/**
* @license
* Copyright 2018 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';
import {DetailsRenderer} from '../renderer/details-renderer.js';
import {DOM} from '../renderer/dom.js';
import {ElementScreenshotRenderer} from '../renderer/element-screenshot-renderer.js';
import {I18n} from '../renderer/i18n.js';
import {openTreemap} from '../renderer/open-tab.js';
import {PerformanceCategoryRenderer} from '../renderer/performance-category-renderer.js';
import {ReportUIFeatures} from '../renderer/report-ui-features.js';
import {Util} from '../renderer/util.js';
/** @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
* and allow us to refactor without two-sided patches.
*
* const {scoreGaugeEl, perfCategoryEl, finalScreenshotDataUri} = prepareLabData(
* LHResultJsonString,
* document
* );
*
* @param {LH.Result | string} LHResult The stringified version of {LH.Result}
* @param {Document} document The host page's window.document
* @return {PrepareLabDataResult}
*/
export function prepareLabData(LHResult, document) {
const lhResult = (typeof LHResult === 'string') ?
/** @type {LH.Result} */ (JSON.parse(LHResult)) : LHResult;
const dom = new DOM(document);
const reportLHR = Util.prepareReportResult(lhResult);
const i18n = new I18n(reportLHR.configSettings.locale, {
// Set missing renderer strings to default (english) values.
...Util.UIStrings,
...reportLHR.i18n.rendererFormattedStrings,
});
Util.i18n = i18n;
Util.reportJson = reportLHR;
const perfCategory = reportLHR.categories.performance;
if (!perfCategory) throw new Error(`No performance category. Can't make lab data section`);
if (!reportLHR.categoryGroups) throw new Error(`No category groups found.`);
// Use custom title and description.
reportLHR.categoryGroups.metrics.title = Util.i18n.strings.labDataTitle;
reportLHR.categoryGroups.metrics.description =
Util.i18n.strings.lsPerformanceCategoryDescription;
const fullPageScreenshot =
reportLHR.audits['full-page-screenshot'] && reportLHR.audits['full-page-screenshot'].details &&
reportLHR.audits['full-page-screenshot'].details.type === 'full-page-screenshot' ?
reportLHR.audits['full-page-screenshot'].details : undefined;
const detailsRenderer = new DetailsRenderer(dom, {fullPageScreenshot});
const perfRenderer = new PerformanceCategoryRenderer(dom, detailsRenderer);
// PSI environment string will ensure the categoryHeader and permalink elements are excluded
const perfCategoryEl = perfRenderer.render(
perfCategory,
reportLHR.categoryGroups,
{environment: 'PSI', gatherMode: lhResult.gatherMode}
);
perfCategoryEl.append(dom.createComponent('styles'));
const scoreGaugeEl = dom.find('.lh-score__gauge', perfCategoryEl);
scoreGaugeEl.remove();
const scoreGaugeWrapperEl = dom.find('.lh-gauge__wrapper', scoreGaugeEl);
scoreGaugeWrapperEl.classList.add('lh-gauge__wrapper--huge');
// Remove navigation link on gauge
scoreGaugeWrapperEl.removeAttribute('href');
const finalScreenshotDataUri = _getFinalScreenshot(perfCategory);
const clonedScoreTemplate = dom.createComponent('scorescale');
const scoreScaleEl = dom.find('.lh-scorescale', clonedScoreTemplate);
const reportUIFeatures = new ReportUIFeatures(dom);
reportUIFeatures.json = lhResult;
/** @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);
// 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.
const screenshotsContainer = document.querySelector('.element-screenshots-container');
if (!screenshotsContainer) {
throw new Error('missing .element-screenshots-container');
}
const screenshotEl = document.createElement('div');
screenshotsContainer.append(screenshotEl);
ElementScreenshotRenderer.installOverlayFeature({
dom,
reportEl,
overlayContainerEl: screenshotEl,
fullPageScreenshot,
});
// Not part of the reportEl, so have to install the feature here too.
ElementScreenshotRenderer.installFullPageScreenshot(
screenshotEl, fullPageScreenshot.screenshot);
}
const showTreemapApp =
lhResult.audits['script-treemap-data'] && lhResult.audits['script-treemap-data'].details;
const buttonContainer = reportEl.querySelector('.lh-audit-group--metrics');
if (showTreemapApp && buttonContainer) {
reportUIFeatures.addButton({
container: buttonContainer,
text: Util.i18n.strings.viewTreemapLabel,
icon: 'treemap',
onClick: () => openTreemap(lhResult),
});
}
};
return {scoreGaugeEl, perfCategoryEl, finalScreenshotDataUri, scoreScaleEl, installFeatures};
}
/**
* @param {LH.ReportResult.Category} perfCategory
* @return {null|string}
*/
function _getFinalScreenshot(perfCategory) {
const auditRef = perfCategory.auditRefs.find(audit => audit.id === 'final-screenshot');
if (!auditRef || !auditRef.result || auditRef.result.scoreDisplayMode === 'error') return null;
const details = auditRef.result.details;
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;
}

Просмотреть файл

@ -478,7 +478,7 @@ export class CategoryRenderer {
*
* @param {LH.ReportResult.Category} category
* @param {Object<string, LH.Result.ReportGroup>=} groupDefinitions
* @param {{environment?: 'PSI', gatherMode: LH.Result.GatherMode}=} options
* @param {{gatherMode: LH.Result.GatherMode}=} options
* @return {Element}
*/
render(category, groupDefinitions = {}, options) {

Просмотреть файл

@ -155,21 +155,15 @@ export class PerformanceCategoryRenderer extends CategoryRenderer {
/**
* @param {LH.ReportResult.Category} category
* @param {Object<string, LH.Result.ReportGroup>} groups
* @param {{gatherMode: LH.Result.GatherMode, environment?: 'PSI'}=} options
* @param {{gatherMode: LH.Result.GatherMode}=} options
* @return {Element}
* @override
*/
render(category, groups, options) {
const strings = Util.i18n.strings;
const element = this.dom.createElement('div', 'lh-category');
if (options && options.environment === 'PSI') {
const gaugeEl = this.dom.createElement('div', 'lh-score__gauge');
gaugeEl.appendChild(this.renderCategoryScore(category, groups, options));
element.appendChild(gaugeEl);
} else {
element.id = category.id;
element.appendChild(this.renderCategoryHeader(category, groups, options));
}
element.id = category.id;
element.appendChild(this.renderCategoryHeader(category, groups, options));
// Metrics.
const metricAudits = category.auditRefs.filter(audit => audit.group === 'metrics');

Просмотреть файл

@ -92,10 +92,7 @@ body {
<body >
<noscript>PSI requires JavaScript. Please enable.</noscript>
<div class="element-screenshots-container"></div>
<div class="tabset lh-vars lh-root">
<div class="tabset">
<input type="radio" name="tabset" id="tab1" aria-controls="mobile" checked>
<label for="tab1">Mobile</label>

Просмотреть файл

@ -5,9 +5,13 @@
*/
'use strict';
/** @fileoverview This file is a glorified call of prepareLabData. */
/** @fileoverview This file exercises two LH reports within the same DOM. */
/** @typedef {import('../clients/psi.js').PrepareLabDataResult} PrepareLabDataResult */
/** @typedef {import('../clients/bundle.js')} lighthouseRenderer */
/** @type {lighthouseRenderer} */
// @ts-expect-error
const lighthouseRenderer = window['report'];
(async function __initPsiReports__() {
// @ts-expect-error
@ -22,25 +26,44 @@
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);
renderLHReport(lhr, container);
}
})();
/**
* @param {LH.Result} lhrData
* @param {HTMLElement} reportContainer
*/
function renderLHReport(lhrData, reportContainer) {
/**
* @param {Document} doc
*/
function getRenderer(doc) {
const dom = new lighthouseRenderer.DOM(doc);
return new lighthouseRenderer.ReportRenderer(dom);
}
const renderer = getRenderer(reportContainer.ownerDocument);
reportContainer.classList.add('lh-root', 'lh-vars');
try {
renderer.renderReport(lhrData, reportContainer);
// TODO: handle topbar removal better
// TODO: display warnings if appropriate.
for (const el of reportContainer.querySelectorAll('.lh-topbar, .lh-warnings')) {
el.setAttribute('hidden', 'true');
}
const features = new lighthouseRenderer.ReportUIFeatures(renderer._dom);
features.initFeatures(lhrData);
} catch (e) {
console.error(e);
reportContainer.textContent = 'Error: LHR failed to render.';
}
}
/**
* Tweak the LHR to make the desktop and mobile reports easier to identify.

Просмотреть файл

@ -0,0 +1,62 @@
/**
* @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';
import fs from 'fs';
import jsdom from 'jsdom';
import * as lighthouseRenderer from '../../clients/bundle.js';
import {LH_ROOT} from '../../../root.js';
const sampleResultsStr =
fs.readFileSync(LH_ROOT + '/lighthouse-core/test/results/sample_v2.json', 'utf-8');
/* eslint-env jest */
describe('lighthouseRenderer bundle', () => {
let document;
beforeAll(() => {
const {window} = new jsdom.JSDOM();
document = window.document;
global.window = global.self = window;
// Stub out matchMedia for Node.
global.self.matchMedia = function() {
return {
addListener: function() {},
};
};
});
afterAll(() => {
global.window = global.self = undefined;
});
it('renders an LHR to DOM', () => {
const lhr = /** @type {LH.Result} */ JSON.parse(sampleResultsStr);
const reportContainer = document.body;
reportContainer.classList.add('lh-vars', 'lh-root');
const dom = new lighthouseRenderer.DOM(reportContainer.ownerDocument);
const renderer = new lighthouseRenderer.ReportRenderer(dom);
renderer.renderReport(lhr, reportContainer);
const features = new lighthouseRenderer.ReportUIFeatures(renderer._dom);
features.initFeatures(lhr);
// Check that the report exists and has some content.
expect(reportContainer instanceof document.defaultView.Element).toBeTruthy();
expect(reportContainer.outerHTML.length).toBeGreaterThan(50000);
const title = reportContainer.querySelector('.lh-audit-group--metrics')
.querySelector('.lh-audit-group__title');
expect(title.textContent).toEqual('Metrics');
});
});

Просмотреть файл

@ -1,143 +0,0 @@
/**
* @license Copyright 2017 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';
import {strict as assert} from 'assert';
import fs from 'fs';
import jsdom from 'jsdom';
import testUtils from '../../../lighthouse-core/test/test-utils.js';
import {prepareLabData} from '../../clients/psi.js';
import {Util} from '../../renderer/util.js';
import {I18n} from '../../renderer/i18n.js';
import {DOM} from '../../renderer/dom.js';
import {CategoryRenderer} from '../../renderer/category-renderer.js';
import {PerformanceCategoryRenderer} from '../../renderer/performance-category-renderer.js';
import {DetailsRenderer} from '../../renderer/details-renderer.js';
import {CriticalRequestChainRenderer} from '../../renderer/crc-details-renderer.js';
import {ElementScreenshotRenderer} from '../../renderer/element-screenshot-renderer.js';
import {ReportUIFeatures} from '../../renderer/report-ui-features.js';
import {LH_ROOT} from '../../../root.js';
const {itIfProtoExists, sampleResultsRoundtripStr} = testUtils.getProtoRoundTrip();
const sampleResultsStr =
fs.readFileSync(LH_ROOT + '/lighthouse-core/test/results/sample_v2.json', 'utf-8');
/* eslint-env jest */
describe('PSI', () => {
let document;
beforeAll(() => {
global.Util = Util;
global.I18n = I18n;
global.DOM = DOM;
global.CategoryRenderer = CategoryRenderer;
global.DetailsRenderer = DetailsRenderer;
global.PerformanceCategoryRenderer = PerformanceCategoryRenderer;
global.CriticalRequestChainRenderer = CriticalRequestChainRenderer;
global.ElementScreenshotRenderer = ElementScreenshotRenderer;
global.ReportUIFeatures = ReportUIFeatures;
const {window} = new jsdom.JSDOM();
document = window.document;
});
afterAll(() => {
global.I18n = undefined;
global.Util = undefined;
global.DOM = undefined;
global.CategoryRenderer = undefined;
global.DetailsRenderer = undefined;
global.PerformanceCategoryRenderer = undefined;
global.CriticalRequestChainRenderer = undefined;
global.ElementScreenshotRenderer = undefined;
global.ReportUIFeatures = undefined;
});
describe('psi prepareLabData helpers', () => {
describe('prepareLabData', () => {
itIfProtoExists('succeeds with LHResult object (roundtrip) input', () => {
const roundTripLHResult = /** @type {LH.Result} */ JSON.parse(sampleResultsRoundtripStr);
const result = prepareLabData(roundTripLHResult, document);
// Check that the report exists and has some content.
assert.ok(result.perfCategoryEl instanceof document.defaultView.Element);
assert.ok(result.perfCategoryEl.outerHTML.length > 50000, 'perfCategory HTML is populated');
// Assume using default locale.
const title = result.perfCategoryEl.querySelector('.lh-audit-group--metrics')
.querySelector('.lh-audit-group__title').textContent;
assert.equal(title, Util.UIStrings.labDataTitle);
});
it('succeeds with stringified LHResult input', () => {
const result = prepareLabData(sampleResultsStr, document);
assert.ok(result.scoreGaugeEl instanceof document.defaultView.Element);
assert.equal(result.scoreGaugeEl.querySelector('.lh-gauge__wrapper').href, '');
assert.ok(result.scoreGaugeEl.outerHTML.includes('<svg'), 'score gauge comes with SVG');
assert.ok(result.perfCategoryEl instanceof document.defaultView.Element);
assert.ok(result.perfCategoryEl.outerHTML.length > 50000, 'perfCategory HTML is populated');
assert.equal(typeof result.finalScreenshotDataUri, 'string');
assert.ok(result.finalScreenshotDataUri.startsWith('data:image/jpeg;base64,'));
});
it('throws if there is no perf category', () => {
const lhrWithoutPerf = JSON.parse(sampleResultsStr);
delete lhrWithoutPerf.categories.performance;
const lhrWithoutPerfStr = JSON.stringify(lhrWithoutPerf);
assert.throws(() => {
prepareLabData(lhrWithoutPerfStr, document);
}, /no performance category/i);
});
it('throws if there is no category groups', () => {
const lhrWithoutGroups = JSON.parse(sampleResultsStr);
delete lhrWithoutGroups.categoryGroups;
const lhrWithoutGroupsStr = JSON.stringify(lhrWithoutGroups);
assert.throws(() => {
prepareLabData(lhrWithoutGroupsStr, document);
}, /no category groups/i);
});
it('includes custom title and description', () => {
const {perfCategoryEl} = prepareLabData(sampleResultsStr, document);
const metricsGroupEl = perfCategoryEl.querySelector('.lh-audit-group--metrics');
// Assume using default locale.
// Replacing markdown because ".textContent" will be post-markdown.
const expectedDescription = Util.UIStrings.lsPerformanceCategoryDescription
.replace('[Lighthouse](https://developers.google.com/web/tools/lighthouse/)', 'Lighthouse');
// Assume using default locale.
const title = metricsGroupEl.querySelector('.lh-audit-group__title').textContent;
const description =
metricsGroupEl.querySelector('.lh-audit-group__description').textContent;
assert.equal(title, Util.UIStrings.labDataTitle);
assert.equal(description, expectedDescription);
});
});
});
describe('_getFinalScreenshot', () => {
it('gets a datauri as a string', () => {
const datauri = prepareLabData(sampleResultsStr, document).finalScreenshotDataUri;
assert.equal(typeof datauri, 'string');
assert.ok(datauri.startsWith('data:image/jpeg;base64,'));
});
it('returns null if there is no final-screenshot audit', () => {
const clonedResults = JSON.parse(sampleResultsStr);
delete clonedResults.audits['final-screenshot'];
const LHResultJsonString = JSON.stringify(clonedResults);
const datauri = prepareLabData(LHResultJsonString, document).finalScreenshotDataUri;
assert.equal(datauri, null);
});
});
});