From cff829c7db71a6bef926654e07a0209b1900f210 Mon Sep 17 00:00:00 2001 From: Brendan Kenny Date: Thu, 21 Apr 2016 01:55:06 -0700 Subject: [PATCH] Adds full report to CLI and extension; upgrades printer. * Refactors for a 'full report' in CLI & extension. * Upgrades the printer; adds tests. --- aggregators/aggregate.js | 9 + aggregators/can-load-offline/index.js | 8 + aggregators/is-performant/index.js | 8 + aggregators/is-secure/index.js | 8 + .../is-sized-for-mobile-screen/index.js | 8 + .../launches-with-splash-screen/index.js | 8 + aggregators/omnibox-is-themed/index.js | 8 + .../index.js | 8 + audits/audit.js | 11 +- audits/performance/first-meaningful-paint.js | 9 +- cli.js | 15 +- cli/printer.js | 203 ++++++++--- extension/app/manifest.json | 6 +- extension/app/pages/images/spinner.png | Bin 0 -> 4278 bytes extension/app/pages/report.html | 33 ++ extension/app/pages/styles/report.css | 57 +++ extension/app/popup.html | 19 +- extension/app/scripts.babel/app.js | 14 +- extension/app/scripts.babel/background.js | 19 +- extension/app/scripts.babel/report.js | 56 +++ extension/app/styles/lighthouse.css | 2 +- extension/gulpfile.babel.js | 7 +- extension/package.json | 1 + gather-scheduler.js | 4 +- package.json | 1 + report/report.js | 111 ++++++ report/styles/report.css | 324 ++++++++++++++++++ report/templates/report.html | 93 +++++ runner.js | 8 +- test/aggregators/aggregate.js | 7 +- test/aggregators/aggregators.js | 8 + test/cli/printer.js | 92 +++++ test/results/sample.json | 1 + 33 files changed, 1101 insertions(+), 65 deletions(-) create mode 100644 extension/app/pages/images/spinner.png create mode 100644 extension/app/pages/report.html create mode 100644 extension/app/pages/styles/report.css create mode 100644 extension/app/scripts.babel/report.js create mode 100644 report/report.js create mode 100644 report/styles/report.css create mode 100644 report/templates/report.html create mode 100644 test/cli/printer.js create mode 100644 test/results/sample.json diff --git a/aggregators/aggregate.js b/aggregators/aggregate.js index 9199ed5071a6..03482ab3e94f 100644 --- a/aggregators/aggregate.js +++ b/aggregators/aggregate.js @@ -27,6 +27,14 @@ class Aggregate { throw new Error('Aggregate name must be overridden'); } + /** + * @throws {Error} + * @return {string} The short name for this aggregation. + */ + static get shortName() { + throw new Error('Aggregate shortName must be overridden'); + } + /** * @throws {Error} * @return {!AggregationCriteria} The criteria for this aggregation. @@ -188,6 +196,7 @@ class Aggregate { static aggregate(results) { return { name: this.name, + shortName: this.shortName, score: Aggregate.compare(results, this.criteria) }; } diff --git a/aggregators/can-load-offline/index.js b/aggregators/can-load-offline/index.js index 93c7dd71030b..60e4784eefcc 100644 --- a/aggregators/can-load-offline/index.js +++ b/aggregators/can-load-offline/index.js @@ -39,6 +39,14 @@ class WorksOffline extends Aggregate { return 'Works Offline'; } + /** + * @override + * @return {string} + */ + static get shortName() { + return 'Works Offline'; + } + /** * @override * @return {!AggregationCriteria} diff --git a/aggregators/is-performant/index.js b/aggregators/is-performant/index.js index 2c03da0cf194..a037bd6c646e 100644 --- a/aggregators/is-performant/index.js +++ b/aggregators/is-performant/index.js @@ -32,6 +32,14 @@ class IsPerformant extends Aggregate { return 'Is Performant'; } + /** + * @override + * @return {string} + */ + static get shortName() { + return 'Performance'; + } + /** * @override * @return {!AggregationCriteria} diff --git a/aggregators/is-secure/index.js b/aggregators/is-secure/index.js index f5345a0804b9..a6aec4f8247d 100644 --- a/aggregators/is-secure/index.js +++ b/aggregators/is-secure/index.js @@ -32,6 +32,14 @@ class IsSecure extends Aggregate { return 'Is Secure'; } + /** + * @override + * @return {string} + */ + static get shortName() { + return 'Secure'; + } + /** * @override * @return {!AggregationCriteria} diff --git a/aggregators/is-sized-for-mobile-screen/index.js b/aggregators/is-sized-for-mobile-screen/index.js index 5bc3d5f29cea..4be25c7ba832 100644 --- a/aggregators/is-sized-for-mobile-screen/index.js +++ b/aggregators/is-sized-for-mobile-screen/index.js @@ -35,6 +35,14 @@ class MobileFriendly extends Aggregate { return 'Is Mobile Friendly'; } + /** + * @override + * @return {string} + */ + static get shortName() { + return 'Mobile Friendly'; + } + /** * @override * @return {!AggregationCriteria} diff --git a/aggregators/launches-with-splash-screen/index.js b/aggregators/launches-with-splash-screen/index.js index c94d4c4ed05b..70d53719fae5 100644 --- a/aggregators/launches-with-splash-screen/index.js +++ b/aggregators/launches-with-splash-screen/index.js @@ -44,6 +44,14 @@ class SplashScreen extends Aggregate { return 'Will Launch With A Splash Screen'; } + /** + * @override + * @return {string} + */ + static get shortName() { + return 'Splash Screen'; + } + /** * An app that was installed to homescreen can get a custom splash screen * while launching. diff --git a/aggregators/omnibox-is-themed/index.js b/aggregators/omnibox-is-themed/index.js index c623a168808f..abc08827887c 100644 --- a/aggregators/omnibox-is-themed/index.js +++ b/aggregators/omnibox-is-themed/index.js @@ -38,6 +38,14 @@ class OmniboxThemeColor extends Aggregate { return 'Omnibox Matches Brand Colors'; } + /** + * @override + * @return {string} + */ + static get shortName() { + return 'Omnibox'; + } + /** * For the omnibox to adopt a theme color, Chrome needs the following: * - has valid manifest diff --git a/aggregators/will-get-add-to-homescreen-prompt/index.js b/aggregators/will-get-add-to-homescreen-prompt/index.js index 2a36c50c74e0..80b9849c13e7 100644 --- a/aggregators/will-get-add-to-homescreen-prompt/index.js +++ b/aggregators/will-get-add-to-homescreen-prompt/index.js @@ -47,6 +47,14 @@ class AddToHomescreen extends Aggregate { return 'Will Get Add to Homescreen Prompt'; } + /** + * @override + * @return {string} + */ + static get shortName() { + return 'Add to Homescreen'; + } + /** * For the install-to-homescreen / install-app-banner prompt to show, * Chrome needs the following: diff --git a/audits/audit.js b/audits/audit.js index 87f5da441ab6..208b7d6f0ccf 100644 --- a/audits/audit.js +++ b/audits/audit.js @@ -38,17 +38,26 @@ class Audit { throw new Error('Audit description must be overridden'); } + /** + * @return {?(boolean|number|string|undefined)} + */ + static get optimalValue() { + return undefined; + } + /** * @param {(boolean|number|string)} value * @param {?(boolean|number|string)=} rawValue * @param {string=} debugString Optional string to describe any error condition encountered. + * @param {?(boolean|number|string)=} optimalValue * @return {!AuditResult} */ - static generateAuditResult(value, rawValue, debugString) { + static generateAuditResult(value, rawValue, debugString, optimalValue) { return { value, rawValue, debugString, + optimalValue, name: this.name, tags: this.tags, description: this.description diff --git a/audits/performance/first-meaningful-paint.js b/audits/performance/first-meaningful-paint.js index 0d7864a67b6a..ded5cca71081 100644 --- a/audits/performance/first-meaningful-paint.js +++ b/audits/performance/first-meaningful-paint.js @@ -42,6 +42,13 @@ class FirstMeaningfulPaint extends Audit { return 'Fast first paint of content'; } + /** + * @override + */ + static get optimalValue() { + return '1,000ms'; + } + /** * Audits the page to give a score for First Meaningful Paint. * @see https://github.com/GoogleChrome/lighthouse/issues/26 @@ -79,7 +86,7 @@ class FirstMeaningfulPaint extends Audit { }) .then(result => { return FirstMeaningfulPaint.generateAuditResult(result.score, - result.duration, result.debugString); + result.duration, result.debugString, this.optimalValue); }); } } diff --git a/cli.js b/cli.js index d7803808a755..507a45f1519f 100755 --- a/cli.js +++ b/cli.js @@ -32,10 +32,11 @@ const cli = meow(` --version Current version of package --verbose Displays verbose logging --quiet Displays no progress or debug logs - --json Output results as JSON --mobile Emulates a Nexus 5X (default=true) --load-page Loads the page (default=true) --save-trace Save the trace contents to disk + --output How to output the page(default=pretty) + --output-path The location to output the response(default=stdout) `); const defaultUrl = 'https://operasoftware.github.io/pwa-list/'; @@ -50,8 +51,16 @@ lighthouse({ url: url, flags: cli.flags }).then(results => { - Printer[cli.flags.json ? 'json' : 'prettyPrint'](log, console, url, results); -}).catch(err => { + const outputMode = cli.flags.output || 'pretty'; + const outputPath = cli.flags.outputPath || 'stdout'; + return Printer.write(results, outputMode, outputPath); +}) +.then(status => { + if (status) { + log.info('printer', status); + } +}) +.catch(err => { if (err.code === 'ECONNREFUSED') { console.error('Unable to connect to Chrome. Did you run ./launch-chrome.sh?'); } else { diff --git a/cli/printer.js b/cli/printer.js index ba1981657176..b21abf19dcc5 100644 --- a/cli/printer.js +++ b/cli/printer.js @@ -1,46 +1,167 @@ +/** + * @license + * Copyright 2016 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 Printer = { - /** - * @param {{info: function(...)}} debugLog - * @param {{log: function(...)}} output - * @param {string} url - * @param {!Array} results - */ - json: function(debugLog, output, url, results) { - debugLog.info('\n\n\nLighthouse results (JSON):', url); - - output.log(JSON.stringify(results, null, 2)); - }, - - /** - * @param {{info: function(...)}} debugLog - * @param {string} url - * @param {{log: function(...)}} output - * @param {!Array} results - */ - prettyPrint: function(debugLog, output, url, results) { - debugLog.info('\n\n\nLighthouse results:', url); - - // TODO: colorise - results.forEach(item => { - let score = (item.score.overall * 100).toFixed(0); - output.log(`${item.name}: ${score}%`); - - item.score.subItems.forEach(subitem => { - let lineItem = ` -- ${subitem.description}: ${subitem.value}`; - if (subitem.rawValue) { - lineItem += ` (${subitem.rawValue})`; - } - output.log(lineItem); - if (subitem.debugString) { - output.log(` ${subitem.debugString}`); - } - }); - - output.log(''); +const fs = require('fs'); +const Report = require('../report/report'); + +const log = (typeof process !== 'undefined' && 'version' in process) ? + require('npmlog').log : console.log.bind(console); + +/** + * An enumeration of acceptable output modes: + * + * @enum {string} + */ +const OUTPUT_MODE = { + pretty: 'pretty', + json: 'json', + html: 'html' +}; + +/** + * Verify output mode. + * @param {string} mode + * @return {OUTPUT_MODE} + */ +function checkOutputMode(mode) { + if (!OUTPUT_MODE.hasOwnProperty(mode)) { + log('warn', `Unknown output mode ${mode}; using pretty`); + return OUTPUT_MODE.pretty; + } + + return OUTPUT_MODE[mode]; +} + +/** + * Verify output path to use, either stdout or a file path. + * @param {string} path + */ +function checkOutputPath(path) { + if (!path) { + log('warn', 'No output path set; using stdout'); + return 'stdout'; + } + + return path; +} + +/** + * Creates the results output in a format based on the `mode`. + * + * @param {{url: string, aggregations: !Array<*>}} results + * @param {OUTPUT_MODE} outputMode + * @return {string} + */ +function createOutput(results, outputMode) { + const report = new Report(); + + // HTML report. + if (outputMode === 'html') { + return report.generateHTML(results); + } + + // JSON report. + if (outputMode === 'json') { + return JSON.stringify(results.aggregations, null, 2); + } + + // Pretty printed. + let output = ''; + results.aggregations.forEach(item => { + let score = (item.score.overall * 100).toFixed(0); + output += `${item.name}: ${score}%\n`; + + item.score.subItems.forEach(subitem => { + let lineItem = ` -- ${subitem.description}: ${subitem.value}`; + if (subitem.rawValue) { + lineItem += ` (${subitem.rawValue})`; + } + output += `${lineItem}\n`; + if (subitem.debugString) { + output += ` ${subitem.debugString}\n`; + } + }); + + output += '\n'; + }); + + return output; +} + +/** + * Writes the output to stdout. + * + * @param {string} output + * @return {!Promise} + */ +function writeToStdout(output) { + return Promise.resolve(process.stdout.write(`${output}\n`)); +} + +/** + * Writes the output to a file. + * + * @param {string} filePath The destination path + * @param {string} output The output to write + * @return {Promise} + */ +function writeFile(filePath, output) { + return new Promise((resolve, reject) => { + // TODO: make this mkdir to the filePath. + fs.writeFile(filePath, output, 'utf8', err => { + if (err) { + return reject(err); + } + + resolve(`Output written to ${filePath}`); }); + }); +} + +/** + * Writes the results. + * + * @param {{url: string, aggregations: !Array<*>}} results + * @param {string} mode Output mode; either 'pretty', 'json', or 'html'. + * @param {string} path The output path to use, either stdout or a file path. + * @return {!Promise} + */ +function write(results, mode, path) { + const outputMode = checkOutputMode(mode); + const outputPath = checkOutputPath(path); + + const output = createOutput(results, outputMode); + + if (outputPath === 'stdout') { + return writeToStdout(output); } -}; -module.exports = Printer; + return writeFile(outputPath, output); +} + +module.exports = { + checkOutputMode, + checkOutputPath, + createOutput, + write +}; diff --git a/extension/app/manifest.json b/extension/app/manifest.json index 5a66a092c38b..76b661c41fa7 100644 --- a/extension/app/manifest.json +++ b/extension/app/manifest.json @@ -17,7 +17,8 @@ }, "permissions": [ "activeTab", - "debugger" + "debugger", + "tabs" ], "browser_action": { "default_icon": { @@ -25,5 +26,6 @@ }, "default_title": "Lighthouse", "default_popup": "popup.html" - } + }, + "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'none'" } diff --git a/extension/app/pages/images/spinner.png b/extension/app/pages/images/spinner.png new file mode 100644 index 0000000000000000000000000000000000000000..ccfc9f6bdbee7282ce66135a3200336e7f6fe3f6 GIT binary patch literal 4278 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 literal 0 HcmV?d00001 diff --git a/extension/app/pages/report.html b/extension/app/pages/report.html new file mode 100644 index 000000000000..9826c50e476d --- /dev/null +++ b/extension/app/pages/report.html @@ -0,0 +1,33 @@ + + + + + + + Lighthouse report + + + +
+
+
+ + + + diff --git a/extension/app/pages/styles/report.css b/extension/app/pages/styles/report.css new file mode 100644 index 000000000000..8be59260d890 --- /dev/null +++ b/extension/app/pages/styles/report.css @@ -0,0 +1,57 @@ +/** + * Copyright 2016 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. + */ + +* { + box-sizing: border-box; +} + +html, body { + padding: 0; + margin: 0; + background: #FAFAFA; + color: #444; + font-family: Arial, sans-serif; +} + +body { + min-width: 600px; + padding: 0 16px; +} + +a { + color: #57A0A8; +} + +.spinner-container { + width: 100%; + height: 100%; + position: fixed; + top: 0; + left: 0; + display: flex; + justify-content: space-around; + align-items: center; + pointer-events: none; +} + +.spinner { + width: 34px; + height: 34px; + border-radius: 50%; + background: url(../images/spinner.png) center center no-repeat; + background-size: 34px 34px; + will-change: transform; +} diff --git a/extension/app/popup.html b/extension/app/popup.html index acaee42d1a36..74a754fd77a9 100644 --- a/extension/app/popup.html +++ b/extension/app/popup.html @@ -1,3 +1,20 @@ + @@ -15,7 +32,7 @@

Lighthouse

...

Beta - +
diff --git a/extension/app/scripts.babel/app.js b/extension/app/scripts.babel/app.js index 648281b523bd..14b4a6e35ae7 100644 --- a/extension/app/scripts.babel/app.js +++ b/extension/app/scripts.babel/app.js @@ -19,25 +19,27 @@ document.addEventListener('DOMContentLoaded', _ => { const background = chrome.extension.getBackgroundPage(); const siteNameEl = window.document.querySelector('header h2'); const resultsEl = document.body.querySelector('.results'); - const reloadPage = document.body.querySelector('.reload-all'); + const generateFullReportEl = document.body.querySelector('.generate-full-report'); background.runAudits({ flags: { mobile: false, loadPage: false } - }).then(ret => { - resultsEl.innerHTML = ret; + }) + .then(results => { + resultsEl.innerHTML = background.createResultsHTML(results); }); - reloadPage.addEventListener('click', () => { + generateFullReportEl.addEventListener('click', () => { background.runAudits({ flags: { mobile: true, loadPage: true } - }).then(ret => { - resultsEl.innerHTML = ret; + }) + .then(results => { + background.createPageAndPopulate(results); }); }); diff --git a/extension/app/scripts.babel/background.js b/extension/app/scripts.babel/background.js index 1e4d53cdfe4f..9103d50751c7 100644 --- a/extension/app/scripts.babel/background.js +++ b/extension/app/scripts.babel/background.js @@ -21,6 +21,18 @@ const ExtensionProtocol = require('../../../helpers/extension/driver.js'); const runner = require('../../../runner'); const NO_SCORE_PROVIDED = '-1'; +window.createPageAndPopulate = function(results) { + const tabURL = chrome.extension.getURL('/pages/report.html'); + chrome.tabs.create({url: tabURL}, tab => { + // Have a timeout here so that the receiving side has time to load + // and register an event listener for onMessage. Otherwise the + // message sent with the results will be lost. + setTimeout(_ => { + chrome.tabs.sendMessage(tab.id, results); + }, 1000); + }); +}; + window.runAudits = function(options) { const driver = new ExtensionProtocol(); @@ -29,7 +41,6 @@ window.runAudits = function(options) { // Add in the URL to the options. return runner(driver, Object.assign({}, options, {url})); }) - .then(results => createResultsHTML(results)) .catch(returnError); }; @@ -46,10 +57,10 @@ function escapeHTML(str) { .replace(/`/g, '`'); } -function createResultsHTML(results) { +window.createResultsHTML = function(results) { let resultsHTML = ''; - results.forEach(item => { + results.aggregations.forEach(item => { const score = (item.score.overall * 100).toFixed(0); const groupHasErrors = (score < 100); const groupClass = 'group ' + @@ -82,7 +93,7 @@ function createResultsHTML(results) { }); return resultsHTML; -} +}; chrome.runtime.onInstalled.addListener(details => { console.log('previousVersion', details.previousVersion); diff --git a/extension/app/scripts.babel/report.js b/extension/app/scripts.babel/report.js new file mode 100644 index 000000000000..6a3d6762adb7 --- /dev/null +++ b/extension/app/scripts.babel/report.js @@ -0,0 +1,56 @@ +/** + * @license + * Copyright 2016 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. + */ +const Report = require('../../../report/report.js'); + +class ReportLoader { + constructor() { + this.spinnerEl = document.querySelector('.js-spinner'); + this.startSpinner(); + } + + startSpinner() { + this.spinnerEl.classList.remove('spinner--hidden'); + this.spinnerAnimation = this.spinnerEl.animate([ + {transform: 'rotate(0deg)'}, + {transform: 'rotate(359deg)'} + ], { + duration: 1000, + iterations: Infinity + }); + } + + stopSpinner() { + this.spinnerAnimation.cancel(); + this.spinnerEl.classList.add('spinner--hidden'); + } + + write(results) { + const report = new Report(); + report.generateHTML(results).then(html => { + this.stopSpinner(); + document.documentElement.innerHTML = html; + }); + } +} + +const report = new ReportLoader(); + +if (chrome && chrome.runtime && chrome.runtime.onMessage) { + chrome.runtime.onMessage.addListener(data => { + report.write(data); + }); +} diff --git a/extension/app/styles/lighthouse.css b/extension/app/styles/lighthouse.css index 82825df38d5b..237915de5f62 100644 --- a/extension/app/styles/lighthouse.css +++ b/extension/app/styles/lighthouse.css @@ -163,7 +163,7 @@ header a { color: #D0021B; } -.reload-all { +.generate-full-report { display: block; font-size: 14px; padding: 5px; diff --git a/extension/gulpfile.babel.js b/extension/gulpfile.babel.js index 9ce0475e3d4a..4ce4940714f2 100644 --- a/extension/gulpfile.babel.js +++ b/extension/gulpfile.babel.js @@ -3,6 +3,7 @@ import gulp from 'gulp'; import gulpLoadPlugins from 'gulp-load-plugins'; import del from 'del'; import browserify from 'gulp-browserify'; +import brfs from 'brfs'; import runSequence from 'run-sequence'; var debug = require('gulp-debug'); @@ -84,7 +85,8 @@ gulp.task('babel', () => { return gulp.src([ 'app/scripts.babel/app.js', 'app/scripts.babel/chromereload.js', - 'app/scripts.babel/background.js']) + 'app/scripts.babel/background.js', + 'app/scripts.babel/report.js']) .pipe($.rollup()) .pipe($.babel({ presets: ['es2015'] @@ -93,7 +95,8 @@ gulp.task('babel', () => { ignore: [ 'npmlog', 'chrome-remote-interface' - ] + ], + transform: ['brfs'] })) .pipe(gulp.dest('app/scripts')) .pipe(gulp.dest('dist/scripts')); diff --git a/extension/package.json b/extension/package.json index 37d175bbfb5f..a294adeac937 100644 --- a/extension/package.json +++ b/extension/package.json @@ -15,6 +15,7 @@ "eslint-config-google": "^0.4.0", "gulp": "^3.9.1", "gulp-babel": "^6.1.2", + "gulp-brfs": "^0.1.0", "gulp-browserify": "^0.5.1", "gulp-cache": "^0.4.2", "gulp-chrome-manifest": "0.0.13", diff --git a/gather-scheduler.js b/gather-scheduler.js index ac098f0282f6..3c9fbcd29c08 100644 --- a/gather-scheduler.js +++ b/gather-scheduler.js @@ -16,6 +16,8 @@ */ 'use strict'; +const fs = require('fs'); + const log = (typeof process !== 'undefined' && 'version' in process) ? require('npmlog').log : console.log.bind(console); @@ -142,7 +144,7 @@ class GatherScheduler { const hostname = url.match(/^.*?\/\/(.*?)(:?\/|$)/)[1]; const filename = (hostname + '_' + date.toISOString() + '.trace.json') .replace(/[\/\?<>\\:\*\|":]/g, '-'); - require('fs').writeFileSync(filename, JSON.stringify(tracingData.traceContents, null, 2)); + fs.writeFileSync(filename, JSON.stringify(tracingData.traceContents, null, 2)); log('info', 'trace file saved to disk', filename); } } diff --git a/package.json b/package.json index 41852b170757..f3784e000fed 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "chrome-devtools-frontend": "1.0.381789", "chrome-remote-interface": "^0.11.0", "devtools-timeline-model": "1.0.19", + "handlebars": "^4.0.5", "meow": "^3.7.0", "npmlog": "^2.0.3", "semver": "^5.1.0", diff --git a/report/report.js b/report/report.js new file mode 100644 index 000000000000..b51acb32b7fb --- /dev/null +++ b/report/report.js @@ -0,0 +1,111 @@ +/** + * @license + * Copyright 2016 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 Intl */ + +const Handlebars = require('handlebars'); +const fs = require('fs'); +const path = require('path'); + +class Report { + + constructor() { + Handlebars.registerHelper('generated', _ => { + const options = { + day: 'numeric', month: 'numeric', year: 'numeric', + hour: 'numeric', minute: 'numeric', second: 'numeric', + timeZoneName: 'short' + }; + const formatter = new Intl.DateTimeFormat('en-US', options); + return formatter.format(new Date()); + }); + + Handlebars.registerHelper('generateAnchor', shortName => { + return shortName.toLowerCase().replace(/\s/gim, ''); + }); + + Handlebars.registerHelper('getItemValue', value => { + if (typeof value === 'boolean') { + return value ? 'Yes' : 'No'; + } + + return value; + }); + + Handlebars.registerHelper('getItemRating', value => { + if (typeof value === 'boolean') { + return value ? 'good' : 'poor'; + } + + let rating = 'poor'; + if (value > 0.33) { + rating = 'average'; + } + if (value > 0.66) { + rating = 'good'; + } + + return rating; + }); + + Handlebars.registerHelper('convertToPercentage', value => { + return Math.floor(value * 100); + }); + + Handlebars.registerHelper('getItemRawValue', subItem => { + let value = ''; + if (typeof subItem.rawValue !== 'undefined') { + let optimalValue = ''; + if (typeof subItem.optimalValue !== 'undefined') { + optimalValue = ` / ${subItem.optimalValue}`; + } + + value = ` (${subItem.rawValue}${optimalValue})`; + } + + return value; + }); + } + + getReportHTML() { + return fs.readFileSync(path.join(__dirname, './templates/report.html'), 'utf8'); + } + + getReportCSS() { + return fs.readFileSync(path.join(__dirname, './styles/report.css'), 'utf8'); + } + + generateHTML(results) { + const totalScore = + (results.aggregations.reduce((prev, aggregation) => { + return prev + aggregation.score.overall; + }, 0) / + results.aggregations.length); + + const template = Handlebars.compile(this.getReportHTML()); + // TODO(bckenny): is this async? + return template({ + url: results.url, + totalScore: Math.round(totalScore * 100), + css: this.getReportCSS(), + aggregations: results.aggregations + }); + } +} + +module.exports = Report; diff --git a/report/styles/report.css b/report/styles/report.css new file mode 100644 index 000000000000..db7fe4600dd1 --- /dev/null +++ b/report/styles/report.css @@ -0,0 +1,324 @@ +/** + * Copyright 2016 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. + */ + +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 100; + src: local('Roboto Thin'), local('Roboto-Thin') format('woff'); +} +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + src: local('Roboto'), local('Roboto-Regular') format('woff'); +} +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 500; + src: local('Roboto Medium'), local('Roboto-Medium') format('woff'); +} + +* { + box-sizing: border-box; +} + +html, body { + padding: 0; + margin: 0; + background: #FAFAFA; + color: #444; + font-family: Arial, sans-serif; + font-size: 16px; +} + +body { + min-width: 600px; + padding: 0 16px; +} + +a { + color: #57A0A8; +} + +.report { + width: 100%; + margin: 16px auto 100px auto; + border-radius: 4px; + max-width: 1280px; + background: #FFF; + box-shadow: 0px 4px 6px 0px rgba(0,0,0,0.26); +} + +.header { + height: 180px; + background: #2D3441; + display: flex; + flex-direction: row; + border-radius: 4px 4px 0 0; + font-family: 'Roboto', Arial, sans-serif; + position: relative; +} + +.header::after { + content: ''; + display: block; + width: 90px; + height: 90px; + position: absolute; + top: 0; + right: 0; + background: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODYiIGhlaWdodD0iODYiIHZpZXdCb3g9IjAgMCA4NiA4NiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PHRpdGxlPkJldGE8L3RpdGxlPjxkZWZzPjxwYXRoIGlkPSJiIiBkPSJNLTExLjcwNCAxMy4xNDRIMTI1LjU4djMwSC0xMS43MDN6Ii8+PGZpbHRlciB4PSItNTAlIiB5PSItNTAlIiB3aWR0aD0iMjAwJSIgaGVpZ2h0PSIyMDAlIiBmaWx0ZXJVbml0cz0ib2JqZWN0Qm91bmRpbmdCb3giIGlkPSJhIj48ZmVPZmZzZXQgZHk9IjEiIGluPSJTb3VyY2VBbHBoYSIgcmVzdWx0PSJzaGFkb3dPZmZzZXRPdXRlcjEiLz48ZmVHYXVzc2lhbkJsdXIgc3RkRGV2aWF0aW9uPSIxIiBpbj0ic2hhZG93T2Zmc2V0T3V0ZXIxIiByZXN1bHQ9InNoYWRvd0JsdXJPdXRlcjEiLz48ZmVDb2xvck1hdHJpeCB2YWx1ZXM9IjAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAuNSAwIiBpbj0ic2hhZG93Qmx1ck91dGVyMSIvPjwvZmlsdGVyPjx0ZXh0IGlkPSJkIiBmb250LWZhbWlseT0iUm9ib3RvLUJvbGQsIFJvYm90byIgZm9udC1zaXplPSIxNCIgZm9udC13ZWlnaHQ9ImJvbGQiIGZpbGw9IiNGRkYiPjx0c3BhbiB4PSI0My41NTYiIHk9IjM3LjU1NiI+QkVUQTwvdHNwYW4+PC90ZXh0PjxmaWx0ZXIgeD0iLTUwJSIgeT0iLTUwJSIgd2lkdGg9IjIwMCUiIGhlaWdodD0iMjAwJSIgZmlsdGVyVW5pdHM9Im9iamVjdEJvdW5kaW5nQm94IiBpZD0iYyI+PGZlT2Zmc2V0IGR5PSIxIiBpbj0iU291cmNlQWxwaGEiIHJlc3VsdD0ic2hhZG93T2Zmc2V0T3V0ZXIxIi8+PGZlR2F1c3NpYW5CbHVyIHN0ZERldmlhdGlvbj0iLjUiIGluPSJzaGFkb3dPZmZzZXRPdXRlcjEiIHJlc3VsdD0ic2hhZG93Qmx1ck91dGVyMSIvPjxmZUNvbG9yTWF0cml4IHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMC4xNDA5NjQ2NzQgMCIgaW49InNoYWRvd0JsdXJPdXRlcjEiLz48L2ZpbHRlcj48cGF0aCBpZD0iZiIgZD0iTS40IDE2Ljk3MmgxMTl2MjguNEguNHoiLz48ZmlsdGVyIHg9Ii01MCUiIHk9Ii01MCUiIHdpZHRoPSIyMDAlIiBoZWlnaHQ9IjIwMCUiIGZpbHRlclVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgaWQ9ImUiPjxmZUdhdXNzaWFuQmx1ciBzdGREZXZpYXRpb249IjMuNSIgaW49IlNvdXJjZUFscGhhIiByZXN1bHQ9InNoYWRvd0JsdXJJbm5lcjEiLz48ZmVPZmZzZXQgaW49InNoYWRvd0JsdXJJbm5lcjEiIHJlc3VsdD0ic2hhZG93T2Zmc2V0SW5uZXIxIi8+PGZlQ29tcG9zaXRlIGluPSJzaGFkb3dPZmZzZXRJbm5lcjEiIGluMj0iU291cmNlQWxwaGEiIG9wZXJhdG9yPSJhcml0aG1ldGljIiBrMj0iLTEiIGszPSIxIiByZXN1bHQ9InNoYWRvd0lubmVySW5uZXIxIi8+PGZlQ29sb3JNYXRyaXggdmFsdWVzPSIwIDAgMCAwIDEgMCAwIDAgMCAxIDAgMCAwIDAgMSAwIDAgMCAwLjY4OTUwOTczNyAwIiBpbj0ic2hhZG93SW5uZXJJbm5lcjEiLz48L2ZpbHRlcj48L2RlZnM+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj48ZyBtYXNrPSJ1cmwoI21hc2stMikiIHRyYW5zZm9ybT0icm90YXRlKDQ1IDU1LjQ0IDI0LjUyMykiPjx1c2UgZmlsbD0iIzAwMCIgZmlsdGVyPSJ1cmwoI2EpIiB4bGluazpocmVmPSIjYiIvPjx1c2UgZmlsbD0iI0NGM0EzQyIgeGxpbms6aHJlZj0iI2IiLz48L2c+PGcgbWFzaz0idXJsKCNtYXNrLTIpIiB0cmFuc2Zvcm09InJvdGF0ZSg0NSA1OS41NTYgMjguOTM1KSIgZmlsbD0iI0ZGRiI+PHVzZSBmaWx0ZXI9InVybCgjYykiIHhsaW5rOmhyZWY9IiNkIi8+PHVzZSB4bGluazpocmVmPSIjZCIvPjwvZz48dXNlIGZpbHRlcj0idXJsKCNlKSIgeGxpbms6aHJlZj0iI2YiIG1hc2s9InVybCgjbWFzay0yKSIgdHJhbnNmb3JtPSJyb3RhdGUoNDUgNTguNCAyNy41NSkiIGZpbGw9IiMwMDAiLz48cGF0aCBkPSJNOC41LS41bDg4LjIwNCA4OC4yMDRNOC41LTM5LjVsODguMjA0IDg4LjIwNCIgc3Ryb2tlPSIjRkZGIiBzdHJva2UtbGluZWNhcD0ic3F1YXJlIiBzdHJva2UtZGFzaGFycmF5PSIxLDIiIG9wYWNpdHk9Ii4zODYiIG1hc2s9InVybCgjbWFzay0yKSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTMpIi8+PC9nPjwvc3ZnPg==') top right no-repeat; +} + +.header-titles { + height: 100%; + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; + padding: 0 0 0 5%; +} + +.header-titles__main, +.header-titles__url { + margin: 0; + -webkit-font-smoothing: antialiased; +} + +.header-titles__main { + font-size: 36px; + color: #FFF; + font-weight: 400; +} + +.header-titles__url { + color: #FFF; + opacity: 0.65; + font-size: 18px; + font-weight: 400; +} + +.header__score { + display: flex; + flex-direction: column; + justify-content: center; + border-radius: 0 4px 0 0; + height: 100%; + padding-left: 120px; + position: relative; + padding: 0 5% 0 0; + background: #45535F; +} + +.header__score::before { + content: ''; + display: block; + width: 120px; + height: 100%; + background: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzY0IiBoZWlnaHQ9IjE4NCIgdmlld0JveD0iMCAwIDM2NCAxODQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHRpdGxlPlBhdGggNDY8L3RpdGxlPjxwYXRoIHN0cm9rZT0iI0ZGRiIgZmlsbD0iIzQ1NTM1RiIgZD0iTTEgMTgzLjMxTDEwNC41NiAxaDI1OC4yMnYxODIuMzF6IiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHN0cm9rZS1vcGFjaXR5PSIuMTUiLz48L3N2Zz4K'); + background-position: 0 -1px; + position: absolute; + left: -120px; + top: 0; +} + +.score-container__overall-score { + color: #FFF; + font-size: 92px; + font-weight: 100; + position: relative; + display: inline-block; + text-align: center; + min-width: 70px; +} + +.score-container__overall-score::after { + content: 'Your score'; + position: absolute; + bottom: -4px; + font-size: 14px; + font-weight: 500; + text-align: center; + width: 100%; + left: 0; + opacity: 0.5; +} + +.score-container__max-score { + color: #57A0A8; + font-size: 28px; + font-weight: 500; +} + +.report-body__content { + padding: 0 5%; + display: flex; + flex-direction: row; + align-items: flex-start; +} + +.report-body__menu { + width: 22%; + min-width: 230px; + margin: -54px 3% 0 -20px; + background: #FFFFFF; + box-shadow: 0px 2px 3px 0px rgba(0,0,0,0.20); + border-radius: 2px; +} + +.menu__header { + border-radius: 2px 2px 0 0; + background: #3F545F; + padding: 0 20px; + height: 54px; + line-height: 54px; + color: #FFF; + font-family: 'Roboto', Arial, sans-serif; + font-size: 18px; +} + +.menu__nav { + list-style: none; + margin: 0; + padding: 0; +} + +.menu__nav-item { + height: 40px; + line-height: 40px; + border-top: 1px solid #EBEBEB; +} + +.menu__link { + padding: 0 20px; + text-decoration: none; + color: #777; + display: flex; +} + +.menu__link:hover { + background-color: #57A0A8; + color: #FFF; +} + +.menu__link-label { + flex: 1; + color: #49525F; + font-weight: 500; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.menu__link-score { + padding-left: 20px; +} + +.menu__link-score--good, +.report-section__score--good, +.report-section__item-value--good { + color: #76B530; +} + +.menu__link-score--average, +.report-section__score--average, +.report-section__item-value--average { + color: #F5A623; +} + +.menu__link-score--poor, +.report-section__score--poor, +.report-section__item-value--poor { + color: #D0021B; +} + +.menu__link:hover .menu__link-label, +.menu__link:hover .menu__link-score { + color: #FFF; +} + +.report-body__breakdown { + flex: 1; + padding-top: 20px; +} + +.report-body__breakdown-item { + padding-bottom: 40px; + border-bottom: 1px solid #EBEBEB; +} + +.report-body__breakdown-item:last-of-type { + border: none; +} + +.report-body__header { + height: 80px; + border-bottom: 1px solid #EBEBEB; +} + +.report-section__title { + -webkit-font-smoothing: antialiased; + font-family: 'Roboto', Arial, sans-serif; + font-size: 28px; + font-weight: 400; + color: #49525F; + display: flex; +} + +.report-section__subtitle { + -webkit-font-smoothing: antialiased; + font-family: 'Roboto', Arial, sans-serif; + font-size: 16px; + font-weight: normal; + color: #719EA8; +} + +.report-section__label { + flex: 1; +} + +.report-section__individual-results { + list-style: none; + padding: 0; + margin: 0; +} + +.report-section__item { + display: flex; + padding-left: 32px; + background: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNXB4IiBoZWlnaHQ9IjVweCIgdmlld0JveD0iMCAwIDUgNSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICAgIDxjaXJjbGUgaWQ9Ik92YWwtNzAiIHN0cm9rZT0ibm9uZSIgZmlsbD0iIzY0NjQ2NCIgZmlsbC1ydWxlPSJldmVub2RkIiBjeD0iMi41IiBjeT0iMi41IiByPSIyLjUiPjwvY2lyY2xlPgo8L3N2Zz4K') 14px center no-repeat; + line-height: 24px; +} + +.report-section__item-raw-value { + color: #777; +} + +.report-section__item-description { + flex: 1; +} + +.footer { + margin-top: 40px; + height: 130px; + line-height: 90px; + text-align: center; + font-size: 12px; + border-top: 1px solid #EBEBEB; + color: #999; +} diff --git a/report/templates/report.html b/report/templates/report.html new file mode 100644 index 000000000000..6600d411c5ac --- /dev/null +++ b/report/templates/report.html @@ -0,0 +1,93 @@ + + + + + + + Lighthouse report + + + +
+
+
+

Lighthouse

+

{{ url }}

+
+ + + +
+ +
+
+
+
+
+ + +
+
+ {{#each aggregations}} +
+

+ + {{ convertToPercentage this.score.overall }} +

+ +

Individual results:

+
    + {{#each this.score.subItems }} +
  • + {{ this.description }} + {{ getItemValue this.value }} + {{{ getItemRawValue this }}} +
  • + {{/each}} +
+
+ {{/each}} +
+
+ + +
+
+ + diff --git a/runner.js b/runner.js index 74723f02d674..22ed4918a270 100644 --- a/runner.js +++ b/runner.js @@ -76,5 +76,11 @@ module.exports = function(driver, opts) { return GatherScheduler .run(gatherers, Object.assign({}, opts, {driver})) .then(artifacts => Auditor.audit(artifacts, audits)) - .then(results => Aggregator.aggregate(aggregators, results)); + .then(results => Aggregator.aggregate(aggregators, results)) + .then(aggregations => { + return { + url: opts.url, + aggregations + }; + }); }; diff --git a/test/aggregators/aggregate.js b/test/aggregators/aggregate.js index 228589589905..c8936dc02145 100644 --- a/test/aggregators/aggregate.js +++ b/test/aggregators/aggregate.js @@ -24,9 +24,14 @@ describe('Aggregate', () => { 'Aggregate name must be overridden'); }); + it('throws when shortName is called directly', () => { + return assert.throws(_ => Aggregate.shortName, + 'Aggregate name must be overridden'); + }); + it('throws when criteria is called directly', () => { return assert.throws(_ => Aggregate.criteria, - 'Aggregate name must be overridden'); + 'Aggregate criteria must be overridden'); }); it('filters empty results', () => { diff --git a/test/aggregators/aggregators.js b/test/aggregators/aggregators.js index 0e5c2a5c0d0c..c857d742058f 100644 --- a/test/aggregators/aggregators.js +++ b/test/aggregators/aggregators.js @@ -47,6 +47,14 @@ describe('Aggregators', () => { }); }); + it('has no aggregators failing when shortName is called', () => { + return walkTree.then(aggregators => { + aggregators.forEach(aggregator => { + assert.doesNotThrow(_ => aggregator.shortName); + }); + }); + }); + it('has no aggregators failing when criteria is called', () => { return walkTree.then(aggregators => { aggregators.forEach(aggregator => { diff --git a/test/cli/printer.js b/test/cli/printer.js new file mode 100644 index 000000000000..9acd959f5676 --- /dev/null +++ b/test/cli/printer.js @@ -0,0 +1,92 @@ +/** + * Copyright 2016 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 Printer = require('../../cli/printer.js'); +const assert = require('assert'); +const fs = require('fs'); +const sampleResults = require('../results/sample.json'); + +/* global describe, it */ + +describe('Printer', () => { + it('accepts valid output modes', () => { + const mode = 'json'; + assert.equal(Printer.checkOutputMode(mode), mode); + }); + + it('rejects invalid output modes', () => { + const mode = 'bacon'; + assert.notEqual(Printer.checkOutputMode(mode), mode); + }); + + it('accepts valid output paths', () => { + const path = '/path/to/output'; + assert.equal(Printer.checkOutputPath(path), path); + }); + + it('rejects invalid output paths', () => { + const path = undefined; + assert.notEqual(Printer.checkOutputPath(path), path); + }); + + it('creates JSON for results', () => { + const mode = 'json'; + const jsonOutput = Printer.createOutput(sampleResults, mode); + assert.doesNotThrow(_ => JSON.parse(jsonOutput)); + }); + + it('creates Pretty Printed results', () => { + const mode = 'pretty'; + const prettyOutput = Printer.createOutput(sampleResults, mode); + + // Just check there's no HTML / JSON there. + assert.throws(_ => JSON.parse(prettyOutput)); + assert.equal(/ { + const mode = 'html'; + const htmlOutput = Printer.createOutput(sampleResults, mode); + assert(/ { + const mode = 'html'; + const path = './.test-file.html'; + const htmlOutput = Printer.createOutput(sampleResults, mode); + + // Now do a second pass where the file is written out. + return Printer.write(sampleResults, mode, path).then(_ => { + const fileContents = fs.readFileSync(path); + fs.unlinkSync(path); + assert.equal(fileContents, htmlOutput); + }); + }); + + it('throws for invalid paths', () => { + const mode = 'html'; + const path = '!/#@.html'; + return Printer.write(sampleResults, mode, path).then(_ => { + // If the then is called, something went askew. + assert(false); + }) + .catch(err => { + assert(err.code === 'ENOENT'); + }); + }); +}); diff --git a/test/results/sample.json b/test/results/sample.json new file mode 100644 index 000000000000..6c775c7b4dbf --- /dev/null +++ b/test/results/sample.json @@ -0,0 +1 @@ +{"url":"https://voice-memos.appspot.com/","aggregations":[{"name":"Will Get Add to Homescreen Prompt","shortName":"Add to Homescreen","score":{"overall":1,"subItems":[{"value":true,"name":"service-worker","tags":["Offline"],"description":"Has a registered Service Worker"},{"value":true,"name":"manifest-exists","tags":["Manifest"],"description":"Manifest exists"},{"value":true,"name":"manifest-start-url","tags":["Manifest"],"description":"Manifest contains start_url"},{"value":true,"debugString":"Found icons of sizes: 192x192,384x384","name":"manifest-icons-min-144","tags":["Manifest"],"description":"Manifest contains icons at least 144px"},{"value":true,"name":"manifest-short-name","tags":["Manifest"],"description":"Manifest contains short_name"},{"value":true,"name":"manifest-short-name-length","tags":["Manifest"],"description":"App short_name won't be truncated"}]}},{"name":"Will Launch With A Splash Screen","shortName":"Splash Screen","score":{"overall":1,"subItems":[{"value":true,"name":"manifest-exists","tags":["Manifest"],"description":"Manifest exists"},{"value":true,"name":"manifest-name","tags":["Manifest"],"description":"Manifest contains name"},{"value":true,"name":"manifest-background-color","tags":["Manifest"],"description":"Manifest contains background_color"},{"value":true,"name":"manifest-theme-color","tags":["Manifest"],"description":"Manifest contains theme_color"},{"value":true,"debugString":"Found icons of sizes: 192x192,384x384","name":"manifest-icons-min-192","tags":["Manifest"],"description":"Manifest contains icons at least 192px"}]}},{"name":"Omnibox Matches Brand Colors","shortName":"Omnibox","score":{"overall":1,"subItems":[{"value":true,"name":"manifest-exists","tags":["Manifest"],"description":"Manifest exists"},{"value":true,"name":"manifest-theme-color","tags":["Manifest"],"description":"Manifest contains theme_color"},{"value":true,"rawValue":"#4527A0","name":"theme-color-meta","tags":["HTML"],"description":"HTML has a theme-color "}]}},{"name":"Works Offline","shortName":"Works Offline","score":{"overall":1,"subItems":[{"value":true,"name":"service-worker","tags":["Offline"],"description":"Has a registered Service Worker"},{"value":true,"name":"works offline","tags":["Offline"],"description":"URL responds with a 200 when offline"}]}},{"name":"Is Secure","shortName":"Secure","score":{"overall":1,"subItems":[{"value":true,"name":"is-on-https","tags":["Security"],"description":"Site is on HTTPS"}]}},{"name":"Is Performant","shortName":"Performance","score":{"overall":1,"subItems":[{"value":100,"rawValue":"557.55ms","optimalValue":"1,000ms","name":"first-contentful-paint","tags":["Performance"],"description":"Fast first paint of content"}]}},{"name":"Is Mobile Friendly","shortName":"Mobile Friendly","score":{"overall":1,"subItems":[{"value":true,"name":"viewport","tags":["Mobile Friendly"],"description":"HTML has a viewport "},{"value":true,"rawValue":"standalone","debugString":"Manifest display property should be standalone or fullscreen.","name":"manifest-display","tags":["Manifest"],"description":"Manifest has suggested display property"}]}}]} \ No newline at end of file