core(runner): split lhr, artifacts return, respect output type (#4999)

This commit is contained in:
Patrick Hulce 2018-04-23 16:55:40 -07:00 коммит произвёл GitHub
Родитель 16419e7ac8
Коммит 711ca6e50f
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
23 изменённых файлов: 262 добавлений и 279 удалений

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

@ -41,7 +41,7 @@ node generate_report_v2.js > temp.report.html; open temp.report.html
const ReportGeneratorV2 = require('./lighthouse-core/report/v2/report-generator');
const results = require('./temp.report.json');
const html = new ReportGeneratorV2().generateReportHtml(results);
const html = ReportGeneratorV2.generateReportHtml(results);
console.log(html);
```
@ -64,7 +64,7 @@ su - travis
```
```sh
# you may also want to mount a local folder into your docker instance.
# you may also want to mount a local folder into your docker instance.
# This will mount your local machines's ~/temp/trav folder into the container's /home/travis/mountpoint folder
docker run -v $HOME/temp/trav:/home/travis/mountpoint --name travis-debug -dit travisci/ci-garnet:packer-1496954857 /sbin/init

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

@ -84,7 +84,7 @@ if (cliFlags.extraHeaders) {
}
/**
* @return {Promise<LH.Results|void>}
* @return {Promise<LH.RunnerResult|void>}
*/
function run() {
return Promise.resolve()

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

@ -6,7 +6,6 @@
'use strict';
const fs = require('fs');
const ReportGenerator = require('../lighthouse-core/report/v2/report-generator');
const log = require('lighthouse-logger');
/**
@ -34,65 +33,6 @@ function checkOutputPath(path) {
return path;
}
/**
* Converts the results to a CSV formatted string
* Each row describes the result of 1 audit with
* - the name of the category the audit belongs to
* - the name of the audit
* - a description of the audit
* - the score type that is used for the audit
* - the score value of the audit
*
* @param {LH.Results} results
* @returns {string}
*/
function toCSVReport(results) {
// To keep things "official" we follow the CSV specification (RFC4180)
// The document describes how to deal with escaping commas and quotes etc.
const CRLF = '\r\n';
const separator = ',';
/** @param {string} value @returns {string} */
const escape = (value) => `"${value.replace(/"/g, '""')}"`;
// Possible TODO: tightly couple headers and row values
const header = ['category', 'name', 'title', 'type', 'score'];
const table = results.reportCategories.map(category => {
return category.audits.map(catAudit => {
const audit = results.audits[catAudit.id];
return [category.name, audit.name, audit.description, audit.scoreDisplayMode, audit.score]
.map(value => value.toString())
.map(escape);
});
});
// @ts-ignore TS loses track of type Array
const flattedTable = [].concat(...table);
return [header, ...flattedTable].map(row => row.join(separator)).join(CRLF);
}
/**
* Creates the results output in a format based on the `mode`.
* @param {!LH.Results} results
* @param {string} outputMode
* @return {string}
*/
function createOutput(results, outputMode) {
// HTML report.
if (outputMode === OutputMode.html) {
return new ReportGenerator().generateReportHtml(results);
}
// JSON report.
if (outputMode === OutputMode.json) {
return JSON.stringify(results, null, 2);
}
// CSV report.
if (outputMode === OutputMode.csv) {
return toCSVReport(results);
}
throw new Error('Invalid output mode: ' + outputMode);
}
/**
* Writes the output to stdout.
* @param {string} output
@ -130,15 +70,15 @@ function writeFile(filePath, output, outputMode) {
/**
* Writes the results.
* @param {!LH.Results} results
* @param {LH.RunnerResult} results
* @param {string} mode
* @param {string} path
* @return {Promise<LH.Results>}
* @return {Promise<LH.RunnerResult>}
*/
function write(results, mode, path) {
return new Promise((resolve, reject) => {
const outputPath = checkOutputPath(path);
const output = createOutput(results, mode);
const output = results.report;
if (outputPath === 'stdout') {
return writeToStdout(output).then(_ => resolve(results));
@ -161,7 +101,6 @@ function getValidOutputOptions() {
module.exports = {
checkOutputPath,
createOutput,
write,
OutputMode,
getValidOutputOptions,

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

@ -95,12 +95,12 @@ function handleError(err) {
}
/**
* @param {!LH.Results} results
* @param {!Object} artifacts
* @param {!LH.RunnerResult} runnerResult
* @param {!LH.Flags} flags
* @return {Promise<void>}
*/
function saveResults(results, artifacts, flags) {
function saveResults(runnerResult, flags) {
const {lhr, artifacts} = runnerResult;
const cwd = process.cwd();
let promise = Promise.resolve();
@ -110,12 +110,12 @@ function saveResults(results, artifacts, flags) {
// Use the output path as the prefix for all generated files.
// If no output path is set, generate a file prefix using the URL and date.
const configuredPath = !flags.outputPath || flags.outputPath === 'stdout' ?
getFilenamePrefix(results) :
getFilenamePrefix(lhr) :
flags.outputPath.replace(/\.\w{2,4}$/, '');
const resolvedPath = path.resolve(cwd, configuredPath);
if (flags.saveAssets) {
promise = promise.then(_ => assetSaver.saveAssets(artifacts, results.audits, resolvedPath));
promise = promise.then(_ => assetSaver.saveAssets(artifacts, lhr.audits, resolvedPath));
}
return promise.then(_ => {
@ -123,13 +123,13 @@ function saveResults(results, artifacts, flags) {
return flags.output.reduce((innerPromise, outputType) => {
const extension = outputType;
const outputPath = `${resolvedPath}.report.${extension}`;
return innerPromise.then(() => Printer.write(results, outputType, outputPath));
return innerPromise.then(() => Printer.write(runnerResult, outputType, outputPath));
}, Promise.resolve());
} else {
const extension = flags.output;
const outputPath =
flags.outputPath || `${resolvedPath}.report.${extension}`;
return Printer.write(results, flags.output, outputPath).then(_ => {
return Printer.write(runnerResult, flags.output, outputPath).then(_ => {
if (flags.output === Printer.OutputMode[Printer.OutputMode.html]) {
if (flags.view) {
opn(outputPath, {wait: false});
@ -149,7 +149,7 @@ function saveResults(results, artifacts, flags) {
* @param {string} url
* @param {LH.Flags} flags
* @param {LH.Config.Json|undefined} config
* @return {Promise<LH.Results|void>}
* @return {Promise<LH.RunnerResult|void>}
*/
function runLighthouse(url, flags, config) {
/** @type {!LH.LaunchedChrome} */
@ -167,12 +167,10 @@ function runLighthouse(url, flags, config) {
}
const resultsP = chromeP.then(_ => {
return lighthouse(url, flags, config).then(results => {
return potentiallyKillChrome().then(_ => results);
}).then(results => {
const artifacts = results.artifacts;
delete results.artifacts;
return saveResults(results, artifacts, flags).then(_ => results);
return lighthouse(url, flags, config).then(runnerResult => {
return potentiallyKillChrome().then(_ => runnerResult);
}).then(runnerResult => {
return saveResults(runnerResult, flags).then(_ => runnerResult);
});
});

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

@ -10,7 +10,6 @@ const Printer = require('../../printer.js');
const assert = require('assert');
const fs = require('fs');
const sampleResults = require('../../../lighthouse-core/test/results/sample_v2.json');
const csvValidator = require('csv-validator');
describe('Printer', () => {
it('accepts valid output paths', () => {
@ -23,68 +22,24 @@ describe('Printer', () => {
assert.notEqual(Printer.checkOutputPath(path), path);
});
it('creates JSON for results', () => {
const mode = Printer.OutputMode.json;
const jsonOutput = Printer.createOutput(sampleResults, mode);
assert.doesNotThrow(_ => JSON.parse(jsonOutput));
});
it('creates HTML for results', () => {
const mode = Printer.OutputMode.html;
const htmlOutput = Printer.createOutput(sampleResults, mode);
assert.ok(/<!doctype/gim.test(htmlOutput));
assert.ok(/<html lang="en"/gim.test(htmlOutput));
});
it('creates CSV for results', async () => {
const mode = Printer.OutputMode.csv;
const path = './.results-as-csv.csv';
const headers = {
category: '',
name: '',
title: '',
type: '',
score: 42,
};
await Printer.write(sampleResults, mode, path);
try {
await csvValidator(path, headers);
} catch (err) {
assert.fail('CSV parser error:\n' + err.join('\n'));
} finally {
fs.unlinkSync(path);
}
});
it('writes file for results', () => {
const mode = 'html';
const path = './.test-file.html';
// Now do a second pass where the file is written out.
return Printer.write(sampleResults, mode, path).then(_ => {
const path = './.test-file.json';
const report = JSON.stringify(sampleResults);
return Printer.write({report}, 'json', path).then(_ => {
const fileContents = fs.readFileSync(path, 'utf8');
assert.ok(/<!doctype/gim.test(fileContents));
assert.ok(/lighthouseVersion/gim.test(fileContents));
fs.unlinkSync(path);
});
});
it('throws for invalid paths', () => {
const mode = 'html';
const path = '!/#@.html';
return Printer.write(sampleResults, mode, path).catch(err => {
const path = '!/#@.json';
const report = JSON.stringify(sampleResults);
return Printer.write({report}, 'html', path).catch(err => {
assert.ok(err.code === 'ENOENT');
});
});
it('writes extended info', () => {
const mode = Printer.OutputMode.html;
const htmlOutput = Printer.createOutput(sampleResults, mode);
const outputCheck = new RegExp('dobetterweb/dbw_tester.css', 'i');
assert.ok(outputCheck.test(htmlOutput));
});
it('returns output modes', () => {
const modes = Printer.getValidOutputOptions();
assert.ok(Array.isArray(modes));

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

@ -28,18 +28,19 @@ describe('CLI run', function() {
const timeoutFlag = `--max-wait-for-load=${9000}`;
const flags = getFlags(`--output=json --output-path=${filename} ${timeoutFlag} ${url}`);
return run.runLighthouse(url, flags, fastConfig).then(passedResults => {
const {lhr} = passedResults;
assert.ok(fs.existsSync(filename));
const results = JSON.parse(fs.readFileSync(filename, 'utf-8'));
assert.equal(results.audits.viewport.rawValue, false);
// passed results match saved results
assert.strictEqual(results.fetchedAt, passedResults.fetchedAt);
assert.strictEqual(results.url, passedResults.url);
assert.strictEqual(results.audits.viewport.rawValue, passedResults.audits.viewport.rawValue);
assert.strictEqual(results.fetchedAt, lhr.fetchedAt);
assert.strictEqual(results.url, lhr.url);
assert.strictEqual(results.audits.viewport.rawValue, lhr.audits.viewport.rawValue);
assert.strictEqual(
Object.keys(results.audits).length,
Object.keys(passedResults.audits).length);
assert.deepStrictEqual(results.timing, passedResults.timing);
Object.keys(lhr.audits).length);
assert.deepStrictEqual(results.timing, lhr.timing);
fs.unlinkSync(filename);
});

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

@ -20,4 +20,4 @@ function ReportGenerator() {}
* @param {!ReportRenderer.ReportJSON} reportJson
* @return {string}
*/
ReportGenerator.prototype.generateReportHtml = function(reportJson) {};
ReportGenerator.generateReportHtml = function(reportJson) {};

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

@ -153,6 +153,8 @@ function merge(base, extension) {
// If the default value doesn't exist or is explicitly null, defer to the extending value
if (typeof base === 'undefined' || base === null) {
return extension;
} else if (typeof extension === 'undefined') {
return base;
} else if (Array.isArray(extension)) {
if (!Array.isArray(base)) throw new TypeError(`Expected array but got ${typeof base}`);
const merged = base.slice();

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

@ -28,6 +28,7 @@ const throttling = {
/** @type {LH.Config.Settings} */
const defaultSettings = {
output: 'json',
maxWaitForLoad: 45 * 1000,
throttlingMethod: 'devtools',
throttling: throttling.mobile3G,

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

@ -30,10 +30,9 @@ const Config = require('./config/config');
* @param {string} url
* @param {LH.Flags} flags
* @param {LH.Config.Json|undefined} configJSON
* @return {Promise<LH.Results>}
* @return {Promise<LH.RunnerResult>}
*/
function lighthouse(url, flags = {}, configJSON) {
const startTime = Date.now();
return Promise.resolve().then(_ => {
// set logging preferences, assume quiet
flags.logLevel = flags.logLevel || 'error';
@ -44,15 +43,7 @@ function lighthouse(url, flags = {}, configJSON) {
const connection = new ChromeProtocol(flags.port, flags.hostname);
// kick off a lighthouse run
return Runner.run(connection, {url, config})
.then((lighthouseResults = {}) => {
// Annotate with time to run lighthouse.
const endTime = Date.now();
lighthouseResults.timing = lighthouseResults.timing || {};
lighthouseResults.timing.total = endTime - startTime;
return lighthouseResults;
});
return Runner.run(connection, {url, config});
});
}

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

@ -12,12 +12,12 @@
* Generate a filenamePrefix of hostname_YYYY-MM-DD_HH-MM-SS
* Date/time uses the local timezone, however Node has unreliable ICU
* support, so we must construct a YYYY-MM-DD date format manually. :/
* @param {{url: string, fetchedAt: string}} results
* @param {{url: string, fetchedAt: string}} lhr
* @return {string}
*/
function getFilenamePrefix(results) {
const hostname = new (URLConstructor || URL)(results.url).hostname;
const date = (results.fetchedAt && new Date(results.fetchedAt)) || new Date();
function getFilenamePrefix(lhr) {
const hostname = new (URLConstructor || URL)(lhr.url).hostname;
const date = (lhr.fetchedAt && new Date(lhr.fetchedAt)) || new Date();
const timeStr = date.toLocaleTimeString('en-US', {hour12: false});
const dateParts = date.toLocaleDateString('en-US', {

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

@ -0,0 +1,31 @@
/**
* @license Copyright 2018 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 fs = require('fs');
const REPORT_TEMPLATE = fs.readFileSync(__dirname + '/report-template.html', 'utf8');
const REPORT_JAVASCRIPT = [
fs.readFileSync(__dirname + '/renderer/util.js', 'utf8'),
fs.readFileSync(__dirname + '/renderer/dom.js', 'utf8'),
fs.readFileSync(__dirname + '/renderer/details-renderer.js', 'utf8'),
fs.readFileSync(__dirname + '/renderer/crc-details-renderer.js', 'utf8'),
fs.readFileSync(__dirname + '/../../lib/file-namer.js', 'utf8'),
fs.readFileSync(__dirname + '/renderer/logger.js', 'utf8'),
fs.readFileSync(__dirname + '/renderer/report-ui-features.js', 'utf8'),
fs.readFileSync(__dirname + '/renderer/category-renderer.js', 'utf8'),
fs.readFileSync(__dirname + '/renderer/performance-category-renderer.js', 'utf8'),
fs.readFileSync(__dirname + '/renderer/report-renderer.js', 'utf8'),
].join(';\n');
const REPORT_CSS = fs.readFileSync(__dirname + '/report-styles.css', 'utf8');
const REPORT_TEMPLATES = fs.readFileSync(__dirname + '/templates.html', 'utf8');
module.exports = {
REPORT_TEMPLATE,
REPORT_TEMPLATES,
REPORT_JAVASCRIPT,
REPORT_CSS,
};

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

@ -5,47 +5,9 @@
*/
'use strict';
const fs = require('fs');
const REPORT_TEMPLATE = fs.readFileSync(__dirname + '/report-template.html', 'utf8');
const REPORT_JAVASCRIPT = [
fs.readFileSync(__dirname + '/renderer/util.js', 'utf8'),
fs.readFileSync(__dirname + '/renderer/dom.js', 'utf8'),
fs.readFileSync(__dirname + '/renderer/details-renderer.js', 'utf8'),
fs.readFileSync(__dirname + '/renderer/crc-details-renderer.js', 'utf8'),
fs.readFileSync(__dirname + '/../../lib/file-namer.js', 'utf8'),
fs.readFileSync(__dirname + '/renderer/logger.js', 'utf8'),
fs.readFileSync(__dirname + '/renderer/report-ui-features.js', 'utf8'),
fs.readFileSync(__dirname + '/renderer/category-renderer.js', 'utf8'),
fs.readFileSync(__dirname + '/renderer/performance-category-renderer.js', 'utf8'),
fs.readFileSync(__dirname + '/renderer/report-renderer.js', 'utf8'),
].join(';\n');
const REPORT_CSS = fs.readFileSync(__dirname + '/report-styles.css', 'utf8');
const REPORT_TEMPLATES = fs.readFileSync(__dirname + '/templates.html', 'utf8');
const htmlReportAssets = require('./html-report-assets');
class ReportGeneratorV2 {
/**
* @return {string}
*/
static get reportJs() {
return REPORT_JAVASCRIPT;
}
/**
* @return {string}
*/
static get reportCss() {
return REPORT_CSS;
}
/**
* @return {string}
*/
static get reportTemplates() {
return REPORT_TEMPLATES;
}
/**
* Replaces all the specified strings in source without serial replacements.
* @param {string} source
@ -65,26 +27,84 @@ class ReportGeneratorV2 {
.join(firstReplacement.replacement);
}
/**
* Returns the report HTML as a string with the report JSON and renderer JS inlined.
* @param {!Object} reportAsJson
* @param {LH.Result} lhr
* @return {string}
*/
generateReportHtml(reportAsJson) {
const sanitizedJson = JSON.stringify(reportAsJson)
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 sanitizedJavascript = REPORT_JAVASCRIPT.replace(/<\//g, '\\u003c/');
const sanitizedJavascript = htmlReportAssets.REPORT_JAVASCRIPT.replace(/<\//g, '\\u003c/');
return ReportGeneratorV2.replaceStrings(REPORT_TEMPLATE, [
return ReportGeneratorV2.replaceStrings(htmlReportAssets.REPORT_TEMPLATE, [
{search: '%%LIGHTHOUSE_JSON%%', replacement: sanitizedJson},
{search: '%%LIGHTHOUSE_JAVASCRIPT%%', replacement: sanitizedJavascript},
{search: '/*%%LIGHTHOUSE_CSS%%*/', replacement: REPORT_CSS},
{search: '%%LIGHTHOUSE_TEMPLATES%%', replacement: REPORT_TEMPLATES},
{search: '/*%%LIGHTHOUSE_CSS%%*/', replacement: htmlReportAssets.REPORT_CSS},
{search: '%%LIGHTHOUSE_TEMPLATES%%', replacement: htmlReportAssets.REPORT_TEMPLATES},
]);
}
/**
* Converts the results to a CSV formatted string
* Each row describes the result of 1 audit with
* - the name of the category the audit belongs to
* - the name of the audit
* - a description of the audit
* - the score type that is used for the audit
* - the score value of the audit
*
* @param {LH.Result} lhr
* @returns {string}
*/
static generateReportCSV(lhr) {
// To keep things "official" we follow the CSV specification (RFC4180)
// The document describes how to deal with escaping commas and quotes etc.
const CRLF = '\r\n';
const separator = ',';
/** @param {string} value @returns {string} */
const escape = value => `"${value.replace(/"/g, '""')}"`;
// Possible TODO: tightly couple headers and row values
const header = ['category', 'name', 'title', 'type', 'score'];
const table = lhr.reportCategories.map(category => {
return category.audits.map(catAudit => {
const audit = lhr.audits[catAudit.id];
return [category.name, audit.name, audit.description, audit.scoreDisplayMode, audit.score]
.map(value => value.toString())
.map(escape);
});
});
// @ts-ignore TS loses track of type Array
const flattedTable = [].concat(...table);
return [header, ...flattedTable].map(row => row.join(separator)).join(CRLF);
}
/**
* Creates the results output in a format based on the `mode`.
* @param {LH.Result} lhr
* @param {'json'|'html'|'csv'} outputMode
* @return {string}
*/
static generateReport(lhr, outputMode) {
// HTML report.
if (outputMode === 'html') {
return ReportGeneratorV2.generateReportHtml(lhr);
}
// CSV report.
if (outputMode === 'csv') {
return ReportGeneratorV2.generateReportCSV(lhr);
}
// JSON report.
if (outputMode === 'json') {
return JSON.stringify(lhr, null, 2);
}
throw new Error('Invalid output mode: ' + outputMode);
}
}
module.exports = ReportGeneratorV2;

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

@ -17,19 +17,19 @@ const fs = require('fs');
const path = require('path');
const URL = require('./lib/url-shim');
const Sentry = require('./lib/sentry');
const generateReport = require('./report/v2/report-generator').generateReport;
const Connection = require('./gather/connections/connection.js'); // eslint-disable-line no-unused-vars
/** @typedef {LH.Result & {artifacts: LH.Artifacts}} RunnerResult */
class Runner {
/**
* @param {Connection} connection
* @param {{config: LH.Config, url: string, initialUrl?: string, driverMock?: Driver}} opts
* @return {Promise<RunnerResult|undefined>}
* @return {Promise<LH.RunnerResult|undefined>}
*/
static async run(connection, opts) {
try {
const startTime = Date.now();
const settings = opts.config.settings;
/**
@ -118,7 +118,8 @@ class Runner {
if (opts.config.categories) {
reportCategories = ReportScoring.scoreAllCategories(opts.config.categories, resultsById);
}
return {
const lhr = {
userAgent: artifacts.UserAgent,
lighthouseVersion,
fetchedAt: artifacts.fetchedAt,
@ -127,11 +128,14 @@ class Runner {
url: opts.url,
runWarnings: lighthouseRunWarnings,
audits: resultsById,
artifacts,
runtimeConfig: Runner.getRuntimeConfig(settings),
reportCategories,
reportGroups: opts.config.groups,
timing: {total: Date.now() - startTime},
};
const report = generateReport(lhr, settings.output);
return {lhr, artifacts, report};
} catch (err) {
// @ts-ignore TODO(bckenny): Sentry type checking
await Sentry.captureException(err, {level: 'fatal'});

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

@ -472,6 +472,11 @@ describe('Config', () => {
assert.ok(config.settings.disableDeviceEmulation, 'missing setting from flags');
});
it('inherits default settings when undefined', () => {
const config = new Config({settings: undefined});
assert.ok(typeof config.settings.maxWaitForLoad === 'number', 'missing setting from default');
});
describe('#extendConfigJSON', () => {
it('should merge passes', () => {
const configA = {

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

@ -90,7 +90,7 @@ describe('Module Tests', function() {
it('should return formatted LHR when given no categories', function() {
const exampleUrl = 'https://example.com/';
return lighthouse(exampleUrl, {
output: 'json',
output: 'html',
}, {
settings: {
auditMode: __dirname + '/fixtures/artifacts/perflog/',
@ -99,17 +99,19 @@ describe('Module Tests', function() {
'viewport',
],
}).then(results => {
assert.ok(results.lighthouseVersion);
assert.ok(results.fetchedAt);
assert.equal(results.url, exampleUrl);
assert.equal(results.initialUrl, exampleUrl);
assert.ok(Array.isArray(results.reportCategories));
assert.equal(results.reportCategories.length, 0);
assert.ok(results.audits.viewport);
assert.strictEqual(results.audits.viewport.score, 0);
assert.ok(results.audits.viewport.debugString);
assert.ok(results.timing);
assert.equal(typeof results.timing.total, 'number');
assert.ok(/<html/.test(results.report), 'did not create html report');
assert.ok(results.artifacts.ViewportDimensions, 'did not set artifacts');
assert.ok(results.lhr.lighthouseVersion);
assert.ok(results.lhr.fetchedAt);
assert.equal(results.lhr.url, exampleUrl);
assert.equal(results.lhr.initialUrl, exampleUrl);
assert.ok(Array.isArray(results.lhr.reportCategories));
assert.equal(results.lhr.reportCategories.length, 0);
assert.ok(results.lhr.audits.viewport);
assert.strictEqual(results.lhr.audits.viewport.score, 0);
assert.ok(results.lhr.audits.viewport.debugString);
assert.ok(results.lhr.timing);
assert.equal(typeof results.lhr.timing.total, 'number');
});
});

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

@ -10,6 +10,8 @@ const fs = require('fs');
const jsdom = require('jsdom');
const ReportGeneratorV2 = require('../../../report/v2/report-generator.js');
const TEMPLATES_FILE = fs.readFileSync(__dirname + '/../../../report/v2/templates.html', 'utf8');
const sampleResults = require('../../../../lighthouse-core/test/results/sample_v2.json');
const csvValidator = require('csv-validator');
/* eslint-env mocha */
@ -37,38 +39,79 @@ describe('ReportGeneratorV2', () => {
describe('#generateHtmlReport', () => {
it('should return html', () => {
const result = new ReportGeneratorV2().generateReportHtml({});
const result = ReportGeneratorV2.generateReportHtml({});
assert.ok(result.includes('doctype html'), 'includes doctype');
assert.ok(result.trim().match(/<\/html>$/), 'ends with HTML tag');
});
it('should inject the report JSON', () => {
const code = 'hax\u2028hax</script><script>console.log("pwned");%%LIGHTHOUSE_JAVASCRIPT%%';
const result = new ReportGeneratorV2().generateReportHtml({code});
const result = ReportGeneratorV2.generateReportHtml({code});
assert.ok(result.includes('"code":"hax\\u2028'), 'injects the json');
assert.ok(result.includes('hax\\u003c/script'), 'escapes HTML tags');
assert.ok(result.includes('LIGHTHOUSE_JAVASCRIPT'), 'cannot be tricked');
});
it('should inject the report templates', () => {
const page = jsdom.jsdom(new ReportGeneratorV2().generateReportHtml({}));
const page = jsdom.jsdom(ReportGeneratorV2.generateReportHtml({}));
const templates = jsdom.jsdom(TEMPLATES_FILE);
assert.equal(page.querySelectorAll('template[id^="tmpl-"]').length,
templates.querySelectorAll('template[id^="tmpl-"]').length, 'all templates injected');
});
it('should inject the report CSS', () => {
const result = new ReportGeneratorV2().generateReportHtml({});
const result = ReportGeneratorV2.generateReportHtml({});
assert.ok(!result.includes('/*%%LIGHTHOUSE_CSS%%*/'));
assert.ok(result.includes('--pass-color'));
});
it('should inject the report renderer javascript', () => {
const result = new ReportGeneratorV2().generateReportHtml({});
const result = ReportGeneratorV2.generateReportHtml({});
assert.ok(result.includes('ReportRenderer'), 'injects the script');
assert.ok(result.includes('robustness: \\u003c/script'), 'escapes HTML tags in javascript');
assert.ok(result.includes('pre$`post'), 'does not break from String.replace');
assert.ok(result.includes('LIGHTHOUSE_JSON'), 'cannot be tricked');
});
});
describe('#generateReport', () => {
it('creates JSON for results', () => {
const jsonOutput = ReportGeneratorV2.generateReport(sampleResults, 'json');
assert.doesNotThrow(_ => JSON.parse(jsonOutput));
});
it('creates HTML for results', () => {
const htmlOutput = ReportGeneratorV2.generateReport(sampleResults, 'html');
assert.ok(/<!doctype/gim.test(htmlOutput));
assert.ok(/<html lang="en"/gim.test(htmlOutput));
});
it('creates CSV for results', async () => {
const path = './.results-as-csv.csv';
const headers = {
category: '',
name: '',
title: '',
type: '',
score: 42,
};
const csvOutput = ReportGeneratorV2.generateReport(sampleResults, 'csv');
fs.writeFileSync(path, csvOutput);
try {
await csvValidator(path, headers);
} catch (err) {
assert.fail('CSV parser error:\n' + err.join('\n'));
} finally {
fs.unlinkSync(path);
}
});
it('writes extended info', () => {
const htmlOutput = ReportGeneratorV2.generateReport(sampleResults, 'html');
const outputCheck = new RegExp('dobetterweb/dbw_tester.css', 'i');
assert.ok(outputCheck.test(htmlOutput));
});
});
});

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

@ -179,8 +179,8 @@ describe('Runner', () => {
});
return Runner.run({}, {url, config}).then(results => {
assert.equal(results.initialUrl, url);
assert.equal(results.audits['eavesdrop-audit'].rawValue, true);
assert.equal(results.lhr.initialUrl, url);
assert.equal(results.lhr.audits['eavesdrop-audit'].rawValue, true);
// assert that the options we received matched expectations
assert.deepEqual(calls, [{x: 1}, {x: 2}]);
});
@ -199,7 +199,7 @@ describe('Runner', () => {
});
return Runner.run({}, {url, config}).then(results => {
const audits = results.audits;
const audits = results.lhr.audits;
assert.equal(audits['user-timings'].displayValue, 2);
assert.equal(audits['user-timings'].rawValue, false);
});
@ -245,7 +245,7 @@ describe('Runner', () => {
});
return Runner.run({}, {url, config}).then(results => {
const auditResult = results.audits['user-timings'];
const auditResult = results.lhr.audits['user-timings'];
assert.strictEqual(auditResult.rawValue, null);
assert.strictEqual(auditResult.error, true);
assert.ok(auditResult.debugString.includes('traces'));
@ -265,7 +265,7 @@ describe('Runner', () => {
});
return Runner.run({}, {url, config}).then(results => {
const auditResult = results.audits['content-width'];
const auditResult = results.lhr.audits['content-width'];
assert.strictEqual(auditResult.rawValue, null);
assert.strictEqual(auditResult.error, true);
assert.ok(auditResult.debugString.includes('ViewportDimensions'));
@ -293,7 +293,7 @@ describe('Runner', () => {
config.artifacts.ViewportDimensions = artifactError;
return Runner.run({}, {url, config}).then(results => {
const auditResult = results.audits['content-width'];
const auditResult = results.lhr.audits['content-width'];
assert.strictEqual(auditResult.rawValue, null);
assert.strictEqual(auditResult.error, true);
assert.ok(auditResult.debugString.includes(errorMessage));
@ -330,7 +330,7 @@ describe('Runner', () => {
});
return Runner.run({}, {url, config}).then(results => {
const auditResult = results.audits['throwy-audit'];
const auditResult = results.lhr.audits['throwy-audit'];
assert.strictEqual(auditResult.rawValue, null);
assert.strictEqual(auditResult.error, true);
assert.ok(auditResult.debugString.includes(errorMessage));
@ -376,7 +376,7 @@ describe('Runner', () => {
});
return Runner.run({}, {url, config}).then(results => {
const audits = results.audits;
const audits = results.lhr.audits;
assert.equal(audits['critical-request-chains'].displayValue, '5 chains found');
assert.equal(audits['critical-request-chains'].rawValue, false);
});
@ -410,11 +410,11 @@ describe('Runner', () => {
});
return Runner.run(null, {url, config, driverMock}).then(results => {
assert.ok(results.lighthouseVersion);
assert.ok(results.fetchedAt);
assert.equal(results.initialUrl, url);
assert.ok(results.lhr.lighthouseVersion);
assert.ok(results.lhr.fetchedAt);
assert.equal(results.lhr.initialUrl, url);
assert.equal(gatherRunnerRunSpy.called, true, 'GatherRunner.run was not called');
assert.equal(results.audits['content-width'].name, 'content-width');
assert.equal(results.lhr.audits['content-width'].name, 'content-width');
});
});
@ -440,14 +440,14 @@ describe('Runner', () => {
});
return Runner.run(null, {url, config, driverMock}).then(results => {
assert.ok(results.lighthouseVersion);
assert.ok(results.fetchedAt);
assert.equal(results.initialUrl, url);
assert.ok(results.lhr.lighthouseVersion);
assert.ok(results.lhr.fetchedAt);
assert.equal(results.lhr.initialUrl, url);
assert.equal(gatherRunnerRunSpy.called, true, 'GatherRunner.run was not called');
assert.equal(results.audits['content-width'].name, 'content-width');
assert.equal(results.audits['content-width'].score, 1);
assert.equal(results.reportCategories[0].score, 1);
assert.equal(results.reportCategories[0].audits[0].id, 'content-width');
assert.equal(results.lhr.audits['content-width'].name, 'content-width');
assert.equal(results.lhr.audits['content-width'].score, 1);
assert.equal(results.lhr.reportCategories[0].score, 1);
assert.equal(results.lhr.reportCategories[0].audits[0].id, 'content-width');
});
});
@ -537,7 +537,7 @@ describe('Runner', () => {
});
return Runner.run(null, {url, config, driverMock}).then(results => {
assert.deepStrictEqual(results.runWarnings, [
assert.deepStrictEqual(results.lhr.runWarnings, [
'I\'m a warning!',
'Also a warning',
]);

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

@ -11,8 +11,6 @@ const ExtensionProtocol = require('../../../lighthouse-core/gather/connections/e
const log = require('lighthouse-logger');
const assetSaver = require('../../../lighthouse-core/lib/asset-saver.js');
const ReportGeneratorV2 = require('../../../lighthouse-core/report/v2/report-generator');
const STORAGE_KEY = 'lighthouse_audits';
const SETTINGS_KEY = 'lighthouse_settings';
@ -71,15 +69,6 @@ function updateBadgeUI(optUrl) {
}
}
/**
* Removes artifacts from the result object for portability
* @param {!Object} result Lighthouse results object
*/
function filterOutArtifacts(result) {
// strip them out, as the networkRecords artifact has circular structures
result.artifacts = undefined;
}
/**
* @param {!Object} options Lighthouse options.
* @param {!Array<string>} categoryIDs Name values of categories to include.
@ -89,15 +78,16 @@ window.runLighthouseInExtension = function(options, categoryIDs) {
// Default to 'info' logging level.
log.setLevel('info');
const connection = new ExtensionProtocol();
options.flags = Object.assign({}, options.flags, {output: 'html'});
// return enableOtherChromeExtensions(false)
// .then(_ => connection.getCurrentTabURL())
return connection.getCurrentTabURL()
.then(url => window.runLighthouseForConnection(connection, url, options,
categoryIDs, updateBadgeUI))
.then(results => {
filterOutArtifacts(results);
.then(runnerResult => {
// return enableOtherChromeExtensions(true).then(_ => {
const blobURL = window.createReportPageAsBlob(results, 'extension');
const blobURL = window.createReportPageAsBlob(runnerResult, 'extension');
chrome.windows.create({url: blobURL});
// });
}).catch(err => {
@ -112,39 +102,38 @@ window.runLighthouseInExtension = function(options, categoryIDs) {
* @param {!Connection} connection
* @param {string} url
* @param {!Object} options Lighthouse options.
Specify lightriderFormat to change the output format.
Specify outputFormat to change the output format.
* @param {!Array<string>} categoryIDs Name values of categories to include.
* @return {!Promise}
*/
window.runLighthouseAsInCLI = function(connection, url, options, categoryIDs) {
log.setLevel('info');
const startTime = Date.now();
options.flags = Object.assign({}, options.flags, {output: options.outputFormat});
return window.runLighthouseForConnection(connection, url, options, categoryIDs)
.then(results => {
const endTime = Date.now();
results.timing = {total: endTime - startTime};
let promise = Promise.resolve();
if (options && options.logAssets) {
promise = promise.then(_ => assetSaver.logAssets(results.artifacts, results.audits));
promise = promise.then(_ => assetSaver.logAssets(results.artifacts, results.lhr.audits));
}
return promise.then( _ => {
filterOutArtifacts(results);
const json = options && options.outputFormat === 'json';
return json ? JSON.stringify(results) : new ReportGeneratorV2().generateReportHtml(results);
return results.report;
});
});
};
/**
* @param {!Object} results Lighthouse results object
* @param {LH.RunnerResult} runnerResult Lighthouse results object
* @param {!string} reportContext Where the report is going
* @return {!string} Blob URL of the report (or error page) HTML
*/
window.createReportPageAsBlob = function(results) {
window.createReportPageAsBlob = function(runnerResult) {
performance.mark('report-start');
const html = new ReportGeneratorV2().generateReportHtml(results);
const html = runnerResult.report;
const blob = new Blob([html], {type: 'text/html'});
const blobURL = window.URL.createObjectURL(blob);

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

@ -123,6 +123,11 @@ gulp.task('browserify-lighthouse', () => {
.ignore('rimraf')
.ignore('pako/lib/zlib/inflate.js');
// Prevent the DevTools background script from getting the stringified HTML.
if (/lighthouse-background/.test(file.path)) {
bundle.ignore(require.resolve('../lighthouse-core/report/v2/html-report-assets.js'));
}
// Expose the audits, gatherers, and computed artifacts so they can be dynamically loaded.
const corePath = '../lighthouse-core/';
const driverPath = `${corePath}gather/`;

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

@ -43,7 +43,7 @@ class ViewerUIFeatures extends ReportUIFeatures {
* @override
*/
getReportHtml() {
return new ReportGenerator().generateReportHtml(this.json);
return ReportGenerator.generateReportHtml(this.json);
}
/**

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

@ -20,7 +20,7 @@ const uglifyEs = require('uglify-es');
const composer = require('gulp-uglify/composer');
const uglify = composer(uglifyEs, console);
const ReportGenerator = require('../lighthouse-core/report/v2/report-generator.js');
const htmlReportAssets = require('../lighthouse-core/report/v2/html-report-assets');
const lighthousePackage = require('../package.json');
const $ = gulpLoadPlugins();
@ -62,7 +62,7 @@ gulp.task('images', () => {
// Concat Report and Viewer stylesheets into single viewer.css file.
gulp.task('concat-css', () => {
const reportCss = streamFromString(ReportGenerator.reportCss, 'report-styles.css');
const reportCss = streamFromString(htmlReportAssets.REPORT_CSS, 'report-styles.css');
const viewerCss = gulp.src('app/styles/viewer.css');
return streamqueue({objectMode: true}, reportCss, viewerCss)
@ -71,7 +71,7 @@ gulp.task('concat-css', () => {
});
gulp.task('html', () => {
const templatesStr = ReportGenerator.reportTemplates;
const templatesStr = htmlReportAssets.REPORT_TEMPLATES;
return gulp.src('app/index.html')
.pipe($.replace(/%%LIGHTHOUSE_TEMPLATES%%/, _ => templatesStr))
@ -105,7 +105,7 @@ gulp.task('compile-js', () => {
.pipe(vinylBuffer());
// JS bundle from report renderer scripts.
const baseReportJs = streamFromString(ReportGenerator.reportJs, 'report.js');
const baseReportJs = streamFromString(htmlReportAssets.REPORT_JAVASCRIPT, 'report.js');
// JS bundle of library dependencies.
const deps = gulp.src([

13
typings/externs.d.ts поставляемый
Просмотреть файл

@ -41,6 +41,7 @@ declare global {
}
interface SharedFlagsSettings {
output?: 'json' | 'html' | 'csv';
maxWaitForLoad?: number;
blockedUrlPatterns?: string[] | null;
additionalTraceCategories?: string | null;
@ -77,14 +78,10 @@ declare global {
extraHeaders?: string;
}
export interface Results {
url: string;
audits: Audit.Results;
lighthouseVersion: string;
artifacts?: Object;
initialUrl: string;
fetchedAt: string;
reportCategories: ReportCategory[];
export interface RunnerResult {
lhr: Result;
report: string;
artifacts: Artifacts;
}
export interface ReportCategory {