/** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import {reportAssets} from './report-assets.js'; /** @typedef {import('../../types/lhr/lhr').default} LHResult */ /** @typedef {import('../../types/lhr/flow-result').default} FlowResult */ class ReportGenerator { /** * Replaces all the specified strings in source without serial replacements. * @param {string} source * @param {!Array<{search: string, replacement: string}>} replacements * @return {string} */ static replaceStrings(source, replacements) { if (replacements.length === 0) { return source; } const firstReplacement = replacements[0]; const nextReplacements = replacements.slice(1); return source .split(firstReplacement.search) .map(part => ReportGenerator.replaceStrings(part, nextReplacements)) .join(firstReplacement.replacement); } /** * @param {unknown} object * @return {string} */ static sanitizeJson(object) { return JSON.stringify(object) .replace(/ `"${value.replace(/"/g, '""')}"`; /** @param {ReadonlyArray} row @return {string[]} */ const rowFormatter = row => row.map(value => { if (value === null) return 'null'; return value.toString(); }).map(escape); const rows = []; const topLevelKeys = /** @type {const} */( ['requestedUrl', 'finalDisplayedUrl', 'fetchTime', 'gatherMode']); // First we have metadata about the LHR. rows.push(rowFormatter(topLevelKeys)); rows.push(rowFormatter(topLevelKeys.map(key => lhr[key] ?? null))); // Some spacing. rows.push([]); // Categories. rows.push(['category', 'score']); for (const category of Object.values(lhr.categories)) { rows.push(rowFormatter([ category.id, category.score, ])); } rows.push([]); // Audits. rows.push(['category', 'audit', 'score', 'displayValue', 'description']); for (const category of Object.values(lhr.categories)) { for (const auditRef of category.auditRefs) { const audit = lhr.audits[auditRef.id]; if (!audit) continue; rows.push(rowFormatter([ category.id, auditRef.id, audit.score, audit.displayValue || '', audit.description, ])); } } return rows .map(row => row.join(separator)) .join(CRLF); } /** * @param {LHResult|FlowResult} result * @return {result is FlowResult} */ static isFlowResult(result) { return 'steps' in result; } /** * Creates the results output in a format based on the `mode`. * @param {LHResult|FlowResult} result * @param {LHResult['configSettings']['output']} outputModes * @return {string|string[]} */ static generateReport(result, outputModes) { const outputAsArray = Array.isArray(outputModes); if (typeof outputModes === 'string') outputModes = [outputModes]; const output = outputModes.map(outputMode => { // HTML report. if (outputMode === 'html') { if (ReportGenerator.isFlowResult(result)) { return ReportGenerator.generateFlowReportHtml(result); } return ReportGenerator.generateReportHtml(result); } // CSV report. if (outputMode === 'csv') { if (ReportGenerator.isFlowResult(result)) { throw new Error('CSV output is not support for user flows'); } return ReportGenerator.generateReportCSV(result); } // JSON report. if (outputMode === 'json') { return JSON.stringify(result, null, 2); } throw new Error('Invalid output mode: ' + outputMode); }); return outputAsArray ? output : output[0]; } } export {ReportGenerator};