From 47e742724950af49c7b232a36f7d75978960f39a Mon Sep 17 00:00:00 2001 From: Matt Zeunert Date: Tue, 16 Apr 2019 11:47:35 +0100 Subject: [PATCH 01/36] Structured-data-automatic-audit --- .../test/cli/__snapshots__/index-test.js.snap | 8 + .../test/fixtures/seo/seo-failure-cases.html | 8 + .../test/fixtures/seo/seo-tester.html | 8 + .../test/smokehouse/seo/expectations.js | 6 + .../audits/seo/structured-data-automatic.js | 174 ++++++++++++++++++ lighthouse-core/config/default-config.js | 2 + lighthouse-core/lib/i18n/en-US.json | 16 ++ .../seo/structured-data-automatic-test.js | 124 +++++++++++++ lighthouse-core/test/results/sample_v2.json | 25 +++ proto/sample_v2_round_trip.json | 19 +- 10 files changed, 389 insertions(+), 1 deletion(-) create mode 100644 lighthouse-core/audits/seo/structured-data-automatic.js create mode 100644 lighthouse-core/test/audits/seo/structured-data-automatic-test.js diff --git a/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap b/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap index ac884fe745c8..ff46bc763f68 100644 --- a/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap +++ b/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap @@ -369,6 +369,9 @@ Object { Object { "path": "seo/canonical", }, + Object { + "path": "seo/structured-data-automatic", + }, Object { "path": "seo/manual/structured-data", }, @@ -972,6 +975,11 @@ Object { "id": "canonical", "weight": 1, }, + Object { + "group": "seo-content", + "id": "structured-data-automatic", + "weight": 1, + }, Object { "group": "seo-mobile", "id": "font-size", diff --git a/lighthouse-cli/test/fixtures/seo/seo-failure-cases.html b/lighthouse-cli/test/fixtures/seo/seo-failure-cases.html index f9aded0c5ac7..6b0883bc8012 100644 --- a/lighthouse-cli/test/fixtures/seo/seo-failure-cases.html +++ b/lighthouse-cli/test/fixtures/seo/seo-failure-cases.html @@ -20,6 +20,14 @@ + +

SEO

diff --git a/lighthouse-cli/test/fixtures/seo/seo-tester.html b/lighthouse-cli/test/fixtures/seo/seo-tester.html index 8eed1643184a..3b8178c03bdf 100644 --- a/lighthouse-cli/test/fixtures/seo/seo-tester.html +++ b/lighthouse-cli/test/fixtures/seo/seo-tester.html @@ -19,6 +19,14 @@ + + + + + + + + + + + + + + + + +

Ripping off some webfont smoke tests

+

Do we need such text

+

+ + + diff --git a/lighthouse-cli/test/smokehouse/dobetterweb/dbw-expectations.js b/lighthouse-cli/test/smokehouse/dobetterweb/dbw-expectations.js index 230403e1be26..c39d18480360 100644 --- a/lighthouse-cli/test/smokehouse/dobetterweb/dbw-expectations.js +++ b/lighthouse-cli/test/smokehouse/dobetterweb/dbw-expectations.js @@ -155,12 +155,12 @@ module.exports = [ }, 'dom-size': { score: 1, - numericValue: 31, + numericValue: 34, details: { items: [ - {statistic: 'Total DOM Elements', value: '31'}, + {statistic: 'Total DOM Elements', value: '34'}, {statistic: 'Maximum DOM Depth', value: '3'}, - {statistic: 'Maximum Child Elements', value: '29'}, + {statistic: 'Maximum Child Elements', value: '32'}, ], }, }, diff --git a/lighthouse-cli/test/smokehouse/perf/expectations.js b/lighthouse-cli/test/smokehouse/perf/expectations.js index 3fcebd574d0c..b0d64f39f607 100644 --- a/lighthouse-cli/test/smokehouse/perf/expectations.js +++ b/lighthouse-cli/test/smokehouse/perf/expectations.js @@ -6,7 +6,7 @@ 'use strict'; /** - * Expected Lighthouse audit values for --preset=perf tests + * Expected Lighthouse audit values for perf tests. */ module.exports = [ { @@ -15,19 +15,19 @@ module.exports = [ finalUrl: 'http://localhost:10200/preload.html', audits: { 'speed-index': { - score: '>=0.80', + score: '>=0.80', // primarily just making sure it didn't fail/go crazy, specific value isn't that important }, 'first-meaningful-paint': { - score: '>=0.90', + score: '>=0.90', // primarily just making sure it didn't fail/go crazy, specific value isn't that important }, 'first-cpu-idle': { - score: '>=0.90', + score: '>=0.90', // primarily just making sure it didn't fail/go crazy, specific value isn't that important }, 'interactive': { - score: '>=0.90', + score: '>=0.90', // primarily just making sure it didn't fail/go crazy, specific value isn't that important }, 'time-to-first-byte': { - // Can be flaky, so test float numericValue instead of boolean score + // Can be flaky, so test float numericValue instead of binary score numericValue: '<1000', }, 'network-requests': { @@ -65,6 +65,84 @@ module.exports = [ }, }, }, + { + lhr: { + requestedUrl: 'http://localhost:10200/perf/perf-budgets/load-things.html', + finalUrl: 'http://localhost:10200/perf/perf-budgets/load-things.html', + audits: { + 'resource-summary': { + score: null, + displayValue: '11 requests • 164 KB', + details: { + items: [ + {resourceType: 'total', requestCount: 11, size: '168000±1000'}, + {resourceType: 'font', requestCount: 2, size: '80000±1000'}, + {resourceType: 'script', requestCount: 3, size: '55000±1000'}, + {resourceType: 'image', requestCount: 2, size: '28000±1000'}, + {resourceType: 'document', requestCount: 1, size: '2100±100'}, + {resourceType: 'other', requestCount: 2, size: '1250±50'}, + {resourceType: 'stylesheet', requestCount: 1, size: '450±100'}, + {resourceType: 'media', requestCount: 0, size: 0}, + {resourceType: 'third-party', requestCount: 0, size: 0}, + ], + }, + }, + 'performance-budget': { + score: null, + details: { + // Undefined items are asserting that the property isn't included in the table item. + items: [ + { + resourceType: 'total', + countOverBudget: '3 requests', + sizeOverBudget: '65000±1000', + }, + { + resourceType: 'script', + countOverBudget: '2 requests', + sizeOverBudget: '25000±1000', + }, + { + resourceType: 'font', + countOverBudget: undefined, + sizeOverBudget: '4000±500', + }, + { + resourceType: 'document', + countOverBudget: '1 request', + sizeOverBudget: '1100±50', + }, + { + resourceType: 'stylesheet', + countOverBudget: undefined, + sizeOverBudget: '450±100', + }, + { + resourceType: 'image', + countOverBudget: '1 request', + sizeOverBudget: undefined, + }, + { + resourceType: 'media', + countOverBudget: undefined, + sizeOverBudget: undefined, + }, + { + resourceType: 'other', + countOverBudget: '1 request', + sizeOverBudget: undefined, + }, + { + resourceType: 'third-party', + countOverBudget: undefined, + sizeOverBudget: undefined, + }, + ], + }, + }, + }, + }, + }, { lhr: { requestedUrl: 'http://localhost:10200/perf/fonts.html', diff --git a/lighthouse-cli/test/smokehouse/perf/lantern-expectations.js b/lighthouse-cli/test/smokehouse/perf/lantern-expectations.js index 49a768160d68..64b1ebc9fca2 100644 --- a/lighthouse-cli/test/smokehouse/perf/lantern-expectations.js +++ b/lighthouse-cli/test/smokehouse/perf/lantern-expectations.js @@ -33,7 +33,8 @@ module.exports = [ audits: { 'interactive': { // Make sure all of the CPU time is reflected in the perf metrics as well. - score: '<.2', + // The scripts stalls for 3 seconds and lantern has a 4x multiplier so 12s minimum. + numericValue: '>12000', }, 'bootup-time': { details: { @@ -56,7 +57,8 @@ module.exports = [ audits: { 'interactive': { // Make sure all of the CPU time is reflected in the perf metrics as well. - score: '<.2', + // The scripts stalls for 3 seconds and lantern has a 4x multiplier so 12s minimum. + numericValue: '>12000', }, 'bootup-time': { details: { diff --git a/lighthouse-cli/test/smokehouse/perf/perf-config.js b/lighthouse-cli/test/smokehouse/perf/perf-config.js new file mode 100644 index 000000000000..6b22341a9df2 --- /dev/null +++ b/lighthouse-cli/test/smokehouse/perf/perf-config.js @@ -0,0 +1,50 @@ +/** + * @license Copyright 2019 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'; + +/** @type {LH.Config.Json} */ +const perfConfig = { + extends: 'lighthouse:default', + settings: { + throttlingMethod: 'devtools', + onlyCategories: ['performance'], + + // A mixture of under, over, and meeting budget to exercise all paths. + budgets: [{ + resourceCounts: [ + {resourceType: 'total', budget: 8}, + {resourceType: 'stylesheet', budget: 1}, // meets budget + {resourceType: 'image', budget: 1}, + {resourceType: 'media', budget: 0}, + {resourceType: 'font', budget: 2}, // meets budget + {resourceType: 'script', budget: 1}, + {resourceType: 'document', budget: 0}, + {resourceType: 'other', budget: 1}, + {resourceType: 'third-party', budget: 0}, + ], + resourceSizes: [ + {resourceType: 'total', budget: 100}, + {resourceType: 'stylesheet', budget: 0}, + {resourceType: 'image', budget: 30}, // meets budget + {resourceType: 'media', budget: 0}, + {resourceType: 'font', budget: 75}, + {resourceType: 'script', budget: 30}, + {resourceType: 'document', budget: 1}, + {resourceType: 'other', budget: 2}, // meets budget + {resourceType: 'third-party', budget: 0}, + ], + timings: [ + {metric: 'first-contentful-paint', budget: 2000, tolerance: 100}, + {metric: 'first-cpu-idle', budget: 2000, tolerance: 100}, + {metric: 'interactive', budget: 2000, tolerance: 100}, + {metric: 'first-meaningful-paint', budget: 2000, tolerance: 100}, + {metric: 'estimated-input-latency', budget: 2000, tolerance: 100}, + ], + }], + }, +}; + +module.exports = perfConfig; diff --git a/lighthouse-cli/test/smokehouse/pwa-expectations.js b/lighthouse-cli/test/smokehouse/pwa-expectations.js index b4a91c98c0b1..85616240be77 100644 --- a/lighthouse-cli/test/smokehouse/pwa-expectations.js +++ b/lighthouse-cli/test/smokehouse/pwa-expectations.js @@ -66,6 +66,9 @@ module.exports = [ 'content-width': { score: 1, }, + 'apple-touch-icon': { + score: 1, + }, // "manual" audits. Just verify in the results. 'pwa-cross-browser': { @@ -128,6 +131,9 @@ module.exports = [ 'content-width': { score: 1, }, + 'apple-touch-icon': { + score: 1, + }, // "manual" audits. Just verify in the results. 'pwa-cross-browser': { diff --git a/lighthouse-cli/test/smokehouse/pwa2-expectations.js b/lighthouse-cli/test/smokehouse/pwa2-expectations.js index 3a56038d3e74..b8b93d09b62d 100644 --- a/lighthouse-cli/test/smokehouse/pwa2-expectations.js +++ b/lighthouse-cli/test/smokehouse/pwa2-expectations.js @@ -61,6 +61,12 @@ module.exports = [ 'content-width': { score: 1, }, + 'apple-touch-icon': { + score: 1, + warnings: [ + /apple-touch-icon-precomposed/, + ], + }, // "manual" audits. Just verify in the results. 'pwa-cross-browser': { @@ -123,6 +129,9 @@ module.exports = [ 'content-width': { score: 1, }, + 'apple-touch-icon': { + score: 0, + }, // "manual" audits. Just verify in the results. 'pwa-cross-browser': { diff --git a/lighthouse-cli/test/smokehouse/pwa3-expectations.js b/lighthouse-cli/test/smokehouse/pwa3-expectations.js index 351a34272daf..d05f37387f03 100644 --- a/lighthouse-cli/test/smokehouse/pwa3-expectations.js +++ b/lighthouse-cli/test/smokehouse/pwa3-expectations.js @@ -58,6 +58,9 @@ module.exports = [ 'content-width': { score: 1, }, + 'apple-touch-icon': { + score: 1, + }, // "manual" audits. Just verify in the results. 'pwa-cross-browser': { diff --git a/lighthouse-cli/test/smokehouse/smoke-test-dfns.js b/lighthouse-cli/test/smokehouse/smoke-test-dfns.js index 261eedd149f9..c12b14fcdffe 100644 --- a/lighthouse-cli/test/smokehouse/smoke-test-dfns.js +++ b/lighthouse-cli/test/smokehouse/smoke-test-dfns.js @@ -67,7 +67,7 @@ const SMOKE_TEST_DFNS = [{ }, { id: 'perf', expectations: 'perf/expectations.js', - config: 'lighthouse-core/config/perf-config.js', + config: 'perf/perf-config.js', batch: 'perf-metric', }, { id: 'lantern', diff --git a/lighthouse-cli/test/smokehouse/smokehouse-report.js b/lighthouse-cli/test/smokehouse/smokehouse-report.js index 33b50b502dd0..656561f32152 100644 --- a/lighthouse-cli/test/smokehouse/smokehouse-report.js +++ b/lighthouse-cli/test/smokehouse/smokehouse-report.js @@ -10,7 +10,7 @@ const log = require('lighthouse-logger'); const VERBOSE = Boolean(process.env.LH_SMOKE_VERBOSE); const NUMBER_REGEXP = /(?:\d|\.)+/.source; -const OPS_REGEXP = /<=?|>=?|\+\/-/.source; +const OPS_REGEXP = /<=?|>=?|\+\/-|±/.source; // An optional number, optional whitespace, an operator, optional whitespace, a number. const NUMERICAL_EXPECTATION_REGEXP = new RegExp(`^(${NUMBER_REGEXP})?\\s*(${OPS_REGEXP})\\s*(${NUMBER_REGEXP})$`); @@ -20,6 +20,7 @@ const NUMERICAL_EXPECTATION_REGEXP = * - Greater than/less than operators, e.g. "<100", ">90" * - Regular expressions * - Strict equality + * - plus or minus a margin of error, e.g. '10+/-5', '100±10' * * @param {*} actual * @param {*} expected @@ -39,6 +40,7 @@ function matchesExpectation(actual, expected) { case '<=': return actual <= postfixNumber; case '+/-': + case '±': return Math.abs(actual - prefixNumber) <= postfixNumber; default: throw new Error(`unexpected operator ${operator}`); @@ -80,16 +82,13 @@ function findDifference(path, actual, expected) { } // We only care that all expected's own properties are on actual (and not the other way around). + // Note an expected `undefined` can match an actual that is either `undefined` or not defined. for (const key of Object.keys(expected)) { // Bracket numbers, but property names requiring quotes will still be unquoted. const keyAccessor = /^\d+$/.test(key) ? `[${key}]` : `.${key}`; const keyPath = path + keyAccessor; const expectedValue = expected[key]; - if (!(key in actual)) { - return {path: keyPath, actual: undefined, expected: expectedValue}; - } - const actualValue = actual[key]; const subDifference = findDifference(keyPath, actualValue, expectedValue); diff --git a/lighthouse-cli/test/smokehouse/smokehouse.js b/lighthouse-cli/test/smokehouse/smokehouse.js index 3373aa0c34d9..603af8bc3b55 100755 --- a/lighthouse-cli/test/smokehouse/smokehouse.js +++ b/lighthouse-cli/test/smokehouse/smokehouse.js @@ -49,15 +49,17 @@ function runLighthouse(url, configPath, isDebug) { isDebug = isDebug || Boolean(process.env.LH_SMOKE_DEBUG); const command = 'node'; - const outputPath = `smokehouse-${Math.round(Math.random() * 100000)}.report.json`; - const artifactsDirectory = './.tmp/smokehouse-artifacts'; + const randInt = Math.round(Math.random() * 100000); + const outputPath = `smokehouse-${randInt}.report.json`; + const artifactsDirectory = `./.tmp/smokehouse-artifacts-${randInt}`; const args = [ 'lighthouse-cli/index.js', url, `--config-path=${configPath}`, `--output-path=${outputPath}`, '--output=json', - `-GA=${artifactsDirectory}`, + `-G=${artifactsDirectory}`, + `-A=${artifactsDirectory}`, '--quiet', '--port=0', ]; @@ -114,8 +116,11 @@ function runLighthouse(url, configPath, isDebug) { } } - const artifacts = JSON.parse( - fs.readFileSync(`${artifactsDirectory}/artifacts.json`, 'utf8')); + // Artifacts are undefined if they weren't written to disk (e.g. if there was an error). + let artifacts; + try { + artifacts = JSON.parse(fs.readFileSync(`${artifactsDirectory}/artifacts.json`, 'utf8')); + } catch (e) {} return { errorCode, diff --git a/lighthouse-cli/test/smokehouse/tricky-metrics/expectations.js b/lighthouse-cli/test/smokehouse/tricky-metrics/expectations.js index c94244363e05..1aafb8e72c71 100644 --- a/lighthouse-cli/test/smokehouse/tricky-metrics/expectations.js +++ b/lighthouse-cli/test/smokehouse/tricky-metrics/expectations.js @@ -15,11 +15,11 @@ module.exports = [ finalUrl: 'http://localhost:10200/tricky-tti.html', audits: { 'first-cpu-idle': { - score: '<75', + // stalls for 5 seconds, 5 seconds out, so should be around 10s numericValue: '>9000', }, 'interactive': { - score: '<75', + // stalls for 5 seconds, 5 seconds out, so should be around 10s numericValue: '>9000', }, }, diff --git a/lighthouse-core/audits/apple-touch-icon.js b/lighthouse-core/audits/apple-touch-icon.js new file mode 100644 index 000000000000..46313a29e1e6 --- /dev/null +++ b/lighthouse-core/audits/apple-touch-icon.js @@ -0,0 +1,71 @@ +/** + * @license Copyright 2019 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 Audit = require('./audit.js'); +const i18n = require('../lib/i18n/i18n.js'); + +/** + * @fileoverview Audits if a page has an `apple-touch-icon` link element with a valid href. + */ + +const UIStrings = { + /** Title of a Lighthouse audit that tells the user that their site contains a vaild touch icon. This descriptive title is shown when the page contains a valid iOS touch icon. "apple-touch-icon" is an HTML attribute value and should not be translated. */ + title: 'Provides a valid `apple-touch-icon`', + /** Title of a Lighthouse audit that tells the user that their site contains a vaild touch icon. This descriptive title is shown when the page does not contain a valid iOS touch icon. "apple-touch-icon" is an HTML attribute value and should not be translated. */ + failureTitle: 'Does not provide a valid `apple-touch-icon`', + /** Description of a Lighthouse audit that tells the user what having a valid apple-touch-icon does. This is displayed after a user expands the section to see more. No character length limits. 'Learn More' becomes link text to additional documentation. "apple-touch-icon" is an HTML attribute value and should not be translated. */ + description: 'For ideal appearance on iOS when users add to the home screen, define an ' + + 'apple-touch-icon. It must point to a non-transparent 192px (or 180px) square PNG. ' + + '[Learn More](https://developers.google.com/web/fundamentals/design-and-ux/browser-customization/).', + /** Warning that HTML attribute `apple-touch-icon-precomposed` should not be used in favor of `apple-touch-icon`. "apple-touch-icon-precomposed" and "apple-touch-icon" are HTML attribute values and should not be translated. */ + precomposedWarning: '`apple-touch-icon-precomposed` is out of date; ' + + '`apple-touch-icon` is preferred.', +}; + +const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings); + +class AppleTouchIcon extends Audit { + /** + * @return {LH.Audit.Meta} + */ + static get meta() { + return { + id: 'apple-touch-icon', + title: str_(UIStrings.title), + failureTitle: str_(UIStrings.failureTitle), + description: str_(UIStrings.description), + requiredArtifacts: ['LinkElements'], + }; + } + + /** + * @param {LH.Artifacts} artifacts + * @return {LH.Audit.Product} + */ + static audit(artifacts) { + const appleTouchIcons = artifacts.LinkElements + .filter(el => el.rel === 'apple-touch-icon' || el.rel === 'apple-touch-icon-precomposed') + .filter(el => !!el.href); + + // Audit passes if an `apple-touch-icon` exists. + const passed = appleTouchIcons.length !== 0; + + const warnings = []; + if (appleTouchIcons.filter(el => el.rel === 'apple-touch-icon-precomposed').length !== 0 + && appleTouchIcons.filter(el => el.rel === 'apple-touch-icon').length === 0) { + warnings.push(str_(UIStrings.precomposedWarning)); + } + + return { + score: passed ? 1 : 0, + warnings, + }; + } +} + +module.exports = AppleTouchIcon; +module.exports.UIStrings = UIStrings; diff --git a/lighthouse-core/audits/metrics/max-potential-fid.js b/lighthouse-core/audits/metrics/max-potential-fid.js index c53521b83193..411007ee1930 100644 --- a/lighthouse-core/audits/metrics/max-potential-fid.js +++ b/lighthouse-core/audits/metrics/max-potential-fid.js @@ -11,7 +11,7 @@ const i18n = require('../../lib/i18n/i18n'); const UIStrings = { /** The name of the metric "Maximum Potential First Input Delay" that marks the maximum estimated time between the page receiving input (a user clicking, tapping, or typing) and the page responding. Shown to users as the label for the numeric metric value. Ideally fits within a ~40 character limit. */ - title: 'Max Potential FID', + title: 'Max Potential First Input Delay', /** Description of the Maximum Potential First Input Delay metric that marks the maximum estimated time between the page receiving input (a user clicking, tapping, or typing) and the page responding. This description is displayed within a tooltip when the user hovers on the metric name to see more. No character length limits. 'Learn More' becomes link text to additional documentation. */ description: 'The maximum potential First Input Delay that your users could experience is the ' + 'duration, in milliseconds, of the longest task. [Learn more](https://developers.google.com/web/updates/2018/05/first-input-delay).', diff --git a/lighthouse-core/audits/performance-budget.js b/lighthouse-core/audits/performance-budget.js new file mode 100644 index 000000000000..71f3630e908b --- /dev/null +++ b/lighthouse-core/audits/performance-budget.js @@ -0,0 +1,148 @@ +/** + * @license Copyright 2019 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 Audit = require('./audit.js'); +const ResourceSummary = require('../computed/resource-summary.js'); +const i18n = require('../lib/i18n/i18n.js'); + +const UIStrings = { + /** Title of a Lighthouse audit that compares the size and quantity of page resources against targets set by the user. These targets are thought of as "performance budgets" because these metrics impact page performance (i.e. how quickly a page loads). */ + title: 'Performance budget', + /** Description of a Lighthouse audit where a user sets budgets for the quantity and size of page resources. No character length limits. 'Learn More' becomes link text to additional documentation. */ + description: 'Keep the quantity and size of network requests under the targets ' + + 'set by the provided performance budget. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/budgets).', + /** [ICU Syntax] Entry in a data table identifying the number of network requests of a particular type. Count will be a whole number. String should be as short as possible to be able to fit well into the table. */ + requestCountOverBudget: `{count, plural, + =1 {1 request} + other {# requests} + }`, +}; + +const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings); + +/** @typedef {{count: number, size: number}} ResourceEntry */ +/** @typedef {{resourceType: LH.Budget.ResourceType, label: string, requestCount: number, size: number, sizeOverBudget: number | undefined, countOverBudget: string | undefined}} BudgetItem */ + +class ResourceBudget extends Audit { + /** + * @return {LH.Audit.Meta} + */ + static get meta() { + return { + id: 'performance-budget', + title: str_(UIStrings.title), + description: str_(UIStrings.description), + scoreDisplayMode: Audit.SCORING_MODES.INFORMATIVE, + requiredArtifacts: ['devtoolsLogs', 'URL'], + }; + } + + /** + * @param {LH.Budget.ResourceType} resourceType + * @return {string} + */ + static getRowLabel(resourceType) { + /** @type {Record} */ + const strMappings = { + 'total': i18n.UIStrings.totalResourceType, + 'document': i18n.UIStrings.documentResourceType, + 'script': i18n.UIStrings.scriptResourceType, + 'stylesheet': i18n.UIStrings.stylesheetResourceType, + 'image': i18n.UIStrings.imageResourceType, + 'media': i18n.UIStrings.mediaResourceType, + 'font': i18n.UIStrings.fontResourceType, + 'other': i18n.UIStrings.otherResourceType, + 'third-party': i18n.UIStrings.thirdPartyResourceType, + }; + return strMappings[resourceType]; + } + + /** + * @param {LH.Budget} budget + * @param {Record} summary + * @return {Array} + */ + static tableItems(budget, summary) { + const resourceTypes = /** @type {Array} */ (Object.keys(summary)); + return resourceTypes.map((resourceType) => { + const label = str_(this.getRowLabel(resourceType)); + const requestCount = summary[resourceType].count; + const size = summary[resourceType].size; + + let sizeOverBudget; + let countOverBudget; + + if (budget.resourceSizes) { + const sizeBudget = budget.resourceSizes.find(b => b.resourceType === resourceType); + if (sizeBudget && (size > (sizeBudget.budget * 1024))) { + sizeOverBudget = size - (sizeBudget.budget * 1024); + } + } + if (budget.resourceCounts) { + const countBudget = budget.resourceCounts.find(b => b.resourceType === resourceType); + if (countBudget && (requestCount > countBudget.budget)) { + const requestDifference = requestCount - countBudget.budget; + countOverBudget = str_(UIStrings.requestCountOverBudget, {count: requestDifference}); + } + } + return { + resourceType, + label, + requestCount, + size, + countOverBudget, + sizeOverBudget, + }; + }).filter((row) => { + // Only resources with budgets should be included in the table + if (budget.resourceSizes) { + if (budget.resourceSizes.some(b => b.resourceType === row.resourceType)) return true; + } + if (budget.resourceCounts) { + if (budget.resourceCounts.some(b => b.resourceType === row.resourceType)) return true; + } + return false; + }).sort((a, b) => { + return (b.sizeOverBudget || 0) - (a.sizeOverBudget || 0); + }); + } + + /** + * @param {LH.Artifacts} artifacts + * @param {LH.Audit.Context} context + * @return {Promise} + */ + static async audit(artifacts, context) { + const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS]; + const summary = await ResourceSummary.request({devtoolsLog, URL: artifacts.URL}, context); + const budget = context.settings.budgets ? context.settings.budgets[0] : undefined; + + if (!budget) { + return { + score: 0, + notApplicable: true, + }; + } + + /** @type { LH.Audit.Details.Table['headings'] } */ + const headers = [ + {key: 'label', itemType: 'text', text: 'Resource Type'}, + {key: 'requestCount', itemType: 'numeric', text: 'Requests'}, + {key: 'size', itemType: 'bytes', text: 'Transfer Size'}, + {key: 'countOverBudget', itemType: 'text', text: ''}, + {key: 'sizeOverBudget', itemType: 'bytes', text: 'Over Budget'}, + ]; + + return { + details: Audit.makeTableDetails(headers, this.tableItems(budget, summary)), + score: 1, + }; + } +} + +module.exports = ResourceBudget; +module.exports.UIStrings = UIStrings; diff --git a/lighthouse-core/audits/resource-summary.js b/lighthouse-core/audits/resource-summary.js index b39add3ae424..d7835a9cbecd 100644 --- a/lighthouse-core/audits/resource-summary.js +++ b/lighthouse-core/audits/resource-summary.js @@ -12,9 +12,9 @@ const i18n = require('../lib/i18n/i18n.js'); const UIStrings = { /** Imperative title of a Lighthouse audit that tells the user to minimize the size and quantity of resources used to load the page. */ title: 'Keep request counts low and transfer sizes small', - /** Description of a Lighthouse audit that tells the user that they can setup a budgets for the quantity and size of page resources. No character length limits. */ + /** Description of a Lighthouse audit that tells the user that they can setup a budgets for the quantity and size of page resources. No character length limits. 'Learn More' becomes link text to additional documentation. */ description: 'To set budgets for the quantity and size of page resources,' + - ' add a budget.json file.', + ' add a budget.json file. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/budgets).', /** [ICU Syntax] Label for an audit identifying the number of requests and kilobytes used to load the page. */ displayValue: `{requestCount, plural, =1 {1 request} other {# requests}}` + ` • { byteCount, number, bytes } KB`, diff --git a/lighthouse-core/config/default-config.js b/lighthouse-core/config/default-config.js index ee1b7df16a23..ed51866a3e88 100644 --- a/lighthouse-core/config/default-config.js +++ b/lighthouse-core/config/default-config.js @@ -13,6 +13,10 @@ const i18n = require('../lib/i18n/i18n.js'); const UIStrings = { /** Title of the Performance category of audits. Equivalent to 'Web performance', this term is inclusive of all web page speed and loading optimization topics. Also used as a label of a score gauge; try to limit to 20 characters. */ performanceCategoryTitle: 'Performance', + /** Title of the Budgets section of the Performance Category. 'Budgets' refers to a budget (like a financial budget), but applied to the amount of resources on a page, rather than money. */ + budgetsGroupTitle: 'Budgets', + /** Description of the Budgets section of the Performance category. Within this section the budget results are displayed. */ + budgetsGroupDescription: 'Performance budgets set standards for the performance of your site.', /** Title of the speed metrics section of the Performance category. Within this section are various speed metrics which quantify the pageload performance into values presented in seconds and milliseconds. */ metricGroupTitle: 'Metrics', /** Title of the opportunity section of the Performance category. Within this section are audits with imperative titles that suggest actions the user can take to improve the loading performance of their web page. 'Suggestion'/'Optimization'/'Recommendation' are reasonable synonyms for 'opportunity' in this case. */ @@ -175,6 +179,7 @@ const defaultConfig = { 'critical-request-chains', 'redirects', 'installable-manifest', + 'apple-touch-icon', 'splash-screen', 'themed-omnibox', 'content-width', @@ -192,6 +197,7 @@ const defaultConfig = { 'main-thread-tasks', 'metrics', 'offline-start-url', + 'performance-budget', 'resource-summary', 'manual/pwa-cross-browser', 'manual/pwa-page-transitions', @@ -288,6 +294,10 @@ const defaultConfig = { title: str_(UIStrings.loadOpportunitiesGroupTitle), description: str_(UIStrings.loadOpportunitiesGroupDescription), }, + 'budgets': { + title: str_(UIStrings.budgetsGroupTitle), + description: str_(UIStrings.budgetsGroupDescription), + }, 'diagnostics': { title: str_(UIStrings.diagnosticsGroupTitle), description: str_(UIStrings.diagnosticsGroupDescription), @@ -380,6 +390,7 @@ const defaultConfig = { {id: 'bootup-time', weight: 0, group: 'diagnostics'}, {id: 'mainthread-work-breakdown', weight: 0, group: 'diagnostics'}, {id: 'font-display', weight: 0, group: 'diagnostics'}, + {id: 'performance-budget', weight: 0, group: 'budgets'}, {id: 'resource-summary', weight: 0, group: 'diagnostics'}, // Audits past this point don't belong to a group and will not be shown automatically {id: 'network-requests', weight: 0}, @@ -396,42 +407,46 @@ const defaultConfig = { title: str_(UIStrings.a11yCategoryTitle), description: str_(UIStrings.a11yCategoryDescription), manualDescription: str_(UIStrings.a11yCategoryManualDescription), + // Audit weights are meant to match the aXe scoring system of + // minor, serious, and critical. + // See the audits listed at dequeuniversity.com/rules/axe/3.2. + // Click on an audit and check the right hand column to see its severity. auditRefs: [ {id: 'accesskeys', weight: 3, group: 'a11y-navigation'}, - {id: 'aria-allowed-attr', weight: 3, group: 'a11y-aria'}, - {id: 'aria-required-attr', weight: 2, group: 'a11y-aria'}, - {id: 'aria-required-children', weight: 5, group: 'a11y-aria'}, - {id: 'aria-required-parent', weight: 2, group: 'a11y-aria'}, - {id: 'aria-roles', weight: 3, group: 'a11y-aria'}, - {id: 'aria-valid-attr-value', weight: 2, group: 'a11y-aria'}, - {id: 'aria-valid-attr', weight: 5, group: 'a11y-aria'}, - {id: 'audio-caption', weight: 4, group: 'a11y-audio-video'}, + {id: 'aria-allowed-attr', weight: 10, group: 'a11y-aria'}, + {id: 'aria-required-attr', weight: 10, group: 'a11y-aria'}, + {id: 'aria-required-children', weight: 10, group: 'a11y-aria'}, + {id: 'aria-required-parent', weight: 10, group: 'a11y-aria'}, + {id: 'aria-roles', weight: 10, group: 'a11y-aria'}, + {id: 'aria-valid-attr-value', weight: 10, group: 'a11y-aria'}, + {id: 'aria-valid-attr', weight: 10, group: 'a11y-aria'}, + {id: 'audio-caption', weight: 10, group: 'a11y-audio-video'}, {id: 'button-name', weight: 10, group: 'a11y-names-labels'}, - {id: 'bypass', weight: 10, group: 'a11y-navigation'}, - {id: 'color-contrast', weight: 6, group: 'a11y-color-contrast'}, - {id: 'definition-list', weight: 1, group: 'a11y-tables-lists'}, - {id: 'dlitem', weight: 1, group: 'a11y-tables-lists'}, - {id: 'document-title', weight: 2, group: 'a11y-names-labels'}, - {id: 'duplicate-id', weight: 5, group: 'a11y-best-practices'}, - {id: 'frame-title', weight: 5, group: 'a11y-names-labels'}, - {id: 'html-has-lang', weight: 4, group: 'a11y-language'}, - {id: 'html-lang-valid', weight: 1, group: 'a11y-language'}, - {id: 'image-alt', weight: 8, group: 'a11y-names-labels'}, - {id: 'input-image-alt', weight: 1, group: 'a11y-names-labels'}, + {id: 'bypass', weight: 3, group: 'a11y-navigation'}, + {id: 'color-contrast', weight: 3, group: 'a11y-color-contrast'}, + {id: 'definition-list', weight: 3, group: 'a11y-tables-lists'}, + {id: 'dlitem', weight: 3, group: 'a11y-tables-lists'}, + {id: 'document-title', weight: 3, group: 'a11y-names-labels'}, + {id: 'duplicate-id', weight: 1, group: 'a11y-best-practices'}, + {id: 'frame-title', weight: 3, group: 'a11y-names-labels'}, + {id: 'html-has-lang', weight: 3, group: 'a11y-language'}, + {id: 'html-lang-valid', weight: 3, group: 'a11y-language'}, + {id: 'image-alt', weight: 10, group: 'a11y-names-labels'}, + {id: 'input-image-alt', weight: 10, group: 'a11y-names-labels'}, {id: 'label', weight: 10, group: 'a11y-names-labels'}, - {id: 'layout-table', weight: 1, group: 'a11y-tables-lists'}, - {id: 'link-name', weight: 9, group: 'a11y-names-labels'}, - {id: 'list', weight: 5, group: 'a11y-tables-lists'}, - {id: 'listitem', weight: 4, group: 'a11y-tables-lists'}, - {id: 'meta-refresh', weight: 1, group: 'a11y-best-practices'}, - {id: 'meta-viewport', weight: 3, group: 'a11y-best-practices'}, - {id: 'object-alt', weight: 4, group: 'a11y-names-labels'}, - {id: 'tabindex', weight: 4, group: 'a11y-navigation'}, - {id: 'td-headers-attr', weight: 1, group: 'a11y-tables-lists'}, - {id: 'th-has-data-cells', weight: 1, group: 'a11y-tables-lists'}, - {id: 'valid-lang', weight: 1, group: 'a11y-language'}, - {id: 'video-caption', weight: 4, group: 'a11y-audio-video'}, - {id: 'video-description', weight: 3, group: 'a11y-audio-video'}, + {id: 'layout-table', weight: 3, group: 'a11y-tables-lists'}, + {id: 'link-name', weight: 3, group: 'a11y-names-labels'}, + {id: 'list', weight: 3, group: 'a11y-tables-lists'}, + {id: 'listitem', weight: 3, group: 'a11y-tables-lists'}, + {id: 'meta-refresh', weight: 10, group: 'a11y-best-practices'}, + {id: 'meta-viewport', weight: 10, group: 'a11y-best-practices'}, + {id: 'object-alt', weight: 3, group: 'a11y-names-labels'}, + {id: 'tabindex', weight: 3, group: 'a11y-navigation'}, + {id: 'td-headers-attr', weight: 3, group: 'a11y-tables-lists'}, + {id: 'th-has-data-cells', weight: 3, group: 'a11y-tables-lists'}, + {id: 'valid-lang', weight: 3, group: 'a11y-language'}, + {id: 'video-caption', weight: 10, group: 'a11y-audio-video'}, + {id: 'video-description', weight: 10, group: 'a11y-audio-video'}, // Manual audits {id: 'logical-tab-order', weight: 0}, {id: 'focusable-controls', weight: 0}, @@ -511,6 +526,7 @@ const defaultConfig = { {id: 'content-width', weight: 1, group: 'pwa-optimized'}, {id: 'viewport', weight: 2, group: 'pwa-optimized'}, {id: 'without-javascript', weight: 1, group: 'pwa-optimized'}, + {id: 'apple-touch-icon', weight: 1, group: 'pwa-optimized'}, // Manual audits {id: 'pwa-cross-browser', weight: 0}, {id: 'pwa-page-transitions', weight: 0}, diff --git a/lighthouse-core/index.js b/lighthouse-core/index.js index 09bca84b0187..4604ee2d7d66 100644 --- a/lighthouse-core/index.js +++ b/lighthouse-core/index.js @@ -73,5 +73,6 @@ lighthouse.getAuditList = Runner.getAuditList; lighthouse.traceCategories = require('./gather/driver').traceCategories; lighthouse.Audit = require('./audits/audit'); lighthouse.Gatherer = require('./gather/gatherers/gatherer'); +lighthouse.NetworkRecords = require('./computed/network-records.js'); module.exports = lighthouse; diff --git a/lighthouse-core/lib/i18n/en-US.json b/lighthouse-core/lib/i18n/en-US.json index aaa9f97a1065..0b8d88a9934e 100644 --- a/lighthouse-core/lib/i18n/en-US.json +++ b/lighthouse-core/lib/i18n/en-US.json @@ -423,6 +423,22 @@ "message": "`