From 23b83f8230dd5c3bd99d0d133aedce0cd322c764 Mon Sep 17 00:00:00 2001 From: Patrick Hulce Date: Tue, 23 Feb 2021 14:56:16 -0600 Subject: [PATCH 1/4] core(fr): convert base artifacts to gatherer artifacts --- lighthouse-core/fraggle-rock/config/config.js | 1 + .../fraggle-rock/config/default-config.js | 8 ++ .../fraggle-rock/gather/base-artifacts.js | 49 ++++++--- lighthouse-core/fraggle-rock/gather/driver.js | 5 + .../fraggle-rock/gather/navigation-runner.js | 3 +- .../fraggle-rock/gather/snapshot-runner.js | 11 +- .../fraggle-rock/gather/timespan-runner.js | 18 +++- lighthouse-core/gather/driver.js | 70 +----------- lighthouse-core/gather/driver/environment.js | 43 ++++++++ lighthouse-core/gather/gather-runner.js | 69 +++--------- .../gather/gatherers/installability-errors.js | 48 +++++++++ .../gather/gatherers/network-user-agent.js | 41 +++++++ .../gatherers/stacks.js} | 69 +++++++----- .../gather/gatherers/web-app-manifest.js | 102 ++++++++++++++++++ .../test/fraggle-rock/api-test-pptr.js | 20 ++-- .../test/fraggle-rock/gather/driver-test.js | 9 +- .../test/fraggle-rock/gather/mock-driver.js | 14 ++- .../gather/navigation-runner-test.js | 7 ++ .../gather/snapshot-runner-test.js | 5 + .../gather/timespan-runner-test.js | 5 + lighthouse-core/test/gather/driver-test.js | 36 ------- .../test/gather/gather-runner-test.js | 61 ----------- .../gatherers/installability-errors-test.js | 32 ++++++ .../gatherers/network-user-agent-test.js | 25 +++++ .../gatherers/stacks-test.js} | 16 +-- .../gather/gatherers/web-app-manifest-test.js | 82 ++++++++++++++ lighthouse-core/test/test-utils.js | 11 +- tsconfig.json | 2 +- types/artifacts.d.ts | 51 ++++++--- types/gatherer.d.ts | 13 ++- 30 files changed, 619 insertions(+), 307 deletions(-) create mode 100644 lighthouse-core/gather/driver/environment.js create mode 100644 lighthouse-core/gather/gatherers/installability-errors.js create mode 100644 lighthouse-core/gather/gatherers/network-user-agent.js rename lighthouse-core/{lib/stack-collector.js => gather/gatherers/stacks.js} (70%) create mode 100644 lighthouse-core/gather/gatherers/web-app-manifest.js create mode 100644 lighthouse-core/test/gather/gatherers/installability-errors-test.js create mode 100644 lighthouse-core/test/gather/gatherers/network-user-agent-test.js rename lighthouse-core/test/{lib/stack-collector-test.js => gather/gatherers/stacks-test.js} (73%) create mode 100644 lighthouse-core/test/gather/gatherers/web-app-manifest-test.js diff --git a/lighthouse-core/fraggle-rock/config/config.js b/lighthouse-core/fraggle-rock/config/config.js index 9d017eeb89e3..feabb311d0d8 100644 --- a/lighthouse-core/fraggle-rock/config/config.js +++ b/lighthouse-core/fraggle-rock/config/config.js @@ -191,6 +191,7 @@ function initializeConfig(configJSON, context) { // TODO(FR-COMPAT): validate audits // TODO(FR-COMPAT): validate categories // TODO(FR-COMPAT): filter config using onlyAudits/onlyCategories + // TODO(FR-COMPAT): always keep base/shared artifacts/audits (Stacks, FullPageScreenshot, etc) config = filterConfigByGatherMode(config, context.gatherMode); diff --git a/lighthouse-core/fraggle-rock/config/default-config.js b/lighthouse-core/fraggle-rock/config/default-config.js index 4672c8144e8c..04f734f0ec30 100644 --- a/lighthouse-core/fraggle-rock/config/default-config.js +++ b/lighthouse-core/fraggle-rock/config/default-config.js @@ -26,11 +26,15 @@ const defaultConfig = { {id: 'FormElements', gatherer: 'form-elements'}, {id: 'GlobalListeners', gatherer: 'global-listeners'}, {id: 'IframeElements', gatherer: 'iframe-elements'}, + {id: 'InstallabillityErrors', gatherer: 'installability-errors'}, {id: 'MetaElements', gatherer: 'meta-elements'}, + {id: 'NetworkUserAgent', gatherer: 'network-user-agent'}, {id: 'PasswordInputsWithPreventedPaste', gatherer: 'dobetterweb/password-inputs-with-prevented-paste'}, {id: 'RobotsTxt', gatherer: 'seo/robots-txt'}, + {id: 'Stacks', gatherer: 'stacks'}, {id: 'TapTargets', gatherer: 'seo/tap-targets'}, {id: 'ViewportDimensions', gatherer: 'viewport-dimensions'}, + {id: 'WebAppManifest', gatherer: 'web-app-manifest'}, /* eslint-enable max-len */ // Artifact copies are renamed for compatibility with legacy artifacts. @@ -56,11 +60,15 @@ const defaultConfig = { 'FormElements', 'GlobalListeners', 'IframeElements', + 'InstallabillityErrors', 'MetaElements', + 'NetworkUserAgent', 'PasswordInputsWithPreventedPaste', 'RobotsTxt', + 'Stacks', 'TapTargets', 'ViewportDimensions', + 'WebAppManifest', // Compat artifacts come last. 'devtoolsLogs', diff --git a/lighthouse-core/fraggle-rock/gather/base-artifacts.js b/lighthouse-core/fraggle-rock/gather/base-artifacts.js index 5ff65d1f0083..8b758f716593 100644 --- a/lighthouse-core/fraggle-rock/gather/base-artifacts.js +++ b/lighthouse-core/fraggle-rock/gather/base-artifacts.js @@ -5,30 +5,51 @@ */ 'use strict'; +const {getBrowserVersion, getBenchmarkIndex} = require('../../gather/driver/environment.js'); + /** * @param {LH.Config.FRConfig} config - * @return {LH.BaseArtifacts} + * @param {LH.Gatherer.FRTransitionalDriver} driver + * @return {Promise} */ -function getBaseArtifacts(config) { - // TODO(FR-COMPAT): convert these to regular artifacts +async function getBaseArtifacts(config, driver) { + const HostUserAgent = (await getBrowserVersion(driver.defaultSession)).userAgent; + + // Whether Lighthouse was run on a mobile device (i.e. not on a desktop machine). + const HostFormFactor = + HostUserAgent.includes('Android') || HostUserAgent.includes('Mobile') ? 'mobile' : 'desktop'; + + const BenchmarkIndex = await getBenchmarkIndex(driver.executionContext); + + /** @type {Array} */ + const LighthouseRunWarnings = []; + + // TODO(FR-COMPAT): support slow host CPU warning + // TODO(FR-COMPAT): support redirected URL warning return { + // Meta artifacts. fetchTime: new Date().toJSON(), - LighthouseRunWarnings: [], - URL: {requestedUrl: '', finalUrl: ''}, Timing: [], - Stacks: [], + LighthouseRunWarnings, settings: config.settings, - HostFormFactor: 'mobile', - HostUserAgent: 'unknown', - NetworkUserAgent: 'unknown', - BenchmarkIndex: 0, - InstallabilityErrors: {errors: []}, + // Environment artifacts that can always be computed. + HostFormFactor, + HostUserAgent, + BenchmarkIndex, + // Contextual artifacts whose collection changes based on gather mode. + URL: {requestedUrl: '', finalUrl: ''}, + PageLoadError: null, // TODO(FR-COMPAT): support PageLoadError + // Artifacts that have been replaced by regular gatherers in Fraggle Rock. + Stacks: [], + NetworkUserAgent: '', + WebAppManifest: null, // replaced by standard gatherer + InstallabilityErrors: {errors: []}, // replaced by standard gatherer traces: {}, devtoolsLogs: {}, - WebAppManifest: null, - PageLoadError: null, }; } -module.exports = {getBaseArtifacts}; +module.exports = { + getBaseArtifacts, +}; diff --git a/lighthouse-core/fraggle-rock/gather/driver.js b/lighthouse-core/fraggle-rock/gather/driver.js index a02b61b2d73f..22d432c39328 100644 --- a/lighthouse-core/fraggle-rock/gather/driver.js +++ b/lighthouse-core/fraggle-rock/gather/driver.js @@ -47,6 +47,11 @@ class Driver { return this._executionContext; } + /** @return {Promise} */ + async url() { + return this._page.url(); + } + /** @return {Promise} */ async connect() { if (this._session) return; diff --git a/lighthouse-core/fraggle-rock/gather/navigation-runner.js b/lighthouse-core/fraggle-rock/gather/navigation-runner.js index 3f20ebc8d771..8a407605228b 100644 --- a/lighthouse-core/fraggle-rock/gather/navigation-runner.js +++ b/lighthouse-core/fraggle-rock/gather/navigation-runner.js @@ -40,7 +40,7 @@ async function _setup({driver, config, requestedUrl}) { // TODO(FR-COMPAT): setupDriver - const baseArtifacts = getBaseArtifacts(config); + const baseArtifacts = await getBaseArtifacts(config, driver); baseArtifacts.URL.requestedUrl = requestedUrl; return {baseArtifacts}; @@ -99,6 +99,7 @@ async function _collectPhaseArtifacts({navigationContext, artifacts, phase}) { : {}; return gatherer[phase]({ + url: await navigationContext.driver.url(), driver: navigationContext.driver, gatherMode: 'navigation', dependencies, diff --git a/lighthouse-core/fraggle-rock/gather/snapshot-runner.js b/lighthouse-core/fraggle-rock/gather/snapshot-runner.js index d58720a9bde7..b5fcf04d2d32 100644 --- a/lighthouse-core/fraggle-rock/gather/snapshot-runner.js +++ b/lighthouse-core/fraggle-rock/gather/snapshot-runner.js @@ -21,7 +21,7 @@ async function snapshot(options) { return Runner.run( async () => { - const baseArtifacts = getBaseArtifacts(config); + const baseArtifacts = await getBaseArtifacts(config, driver); baseArtifacts.URL.requestedUrl = url; baseArtifacts.URL.finalUrl = url; @@ -32,8 +32,15 @@ async function snapshot(options) { const {id, gatherer} = artifactDefn; const artifactName = /** @type {keyof LH.GathererArtifacts} */ (id); const dependencies = await collectArtifactDependencies(artifactDefn, artifacts); + /** @type {LH.Gatherer.FRTransitionalContext} */ + const context = { + gatherMode: 'snapshot', + url, + driver, + dependencies, + }; const artifact = await Promise.resolve() - .then(() => gatherer.instance.snapshot({gatherMode: 'snapshot', driver, dependencies})) + .then(() => gatherer.instance.snapshot(context)) .catch(err => err); artifacts[artifactName] = artifact; diff --git a/lighthouse-core/fraggle-rock/gather/timespan-runner.js b/lighthouse-core/fraggle-rock/gather/timespan-runner.js index 7f1dd66b0b82..c7172b107f60 100644 --- a/lighthouse-core/fraggle-rock/gather/timespan-runner.js +++ b/lighthouse-core/fraggle-rock/gather/timespan-runner.js @@ -27,7 +27,12 @@ async function startTimespan(options) { for (const {id, gatherer} of config.artifacts || []) { artifactErrors[id] = Promise.resolve().then(() => - gatherer.instance.beforeTimespan({gatherMode: 'timespan', driver, dependencies: {}}) + gatherer.instance.beforeTimespan({ + gatherMode: 'timespan', + url: requestedUrl, + driver, + dependencies: {}, + }) ); // Run each beforeTimespan serially, but handle errors in the next pass. @@ -39,7 +44,7 @@ async function startTimespan(options) { const finalUrl = await options.page.url(); return Runner.run( async () => { - const baseArtifacts = getBaseArtifacts(config); + const baseArtifacts = await getBaseArtifacts(config, driver); baseArtifacts.URL.requestedUrl = requestedUrl; baseArtifacts.URL.finalUrl = finalUrl; @@ -50,9 +55,16 @@ async function startTimespan(options) { const {id, gatherer} = artifactDefn; const artifactName = /** @type {keyof LH.GathererArtifacts} */ (id); const dependencies = await collectArtifactDependencies(artifactDefn, artifacts); + /** @type {LH.Gatherer.FRTransitionalContext} */ + const context = { + gatherMode: 'timespan', + url: finalUrl, + driver, + dependencies, + }; const artifact = await artifactErrors[id] .then(() => - gatherer.instance.afterTimespan({gatherMode: 'timespan', driver, dependencies}) + gatherer.instance.afterTimespan(context) ) .catch(err => err); diff --git a/lighthouse-core/gather/driver.js b/lighthouse-core/gather/driver.js index 897f6a6f452e..f309efbc4ebd 100644 --- a/lighthouse-core/gather/driver.js +++ b/lighthouse-core/gather/driver.js @@ -27,6 +27,7 @@ const pageFunctions = require('../lib/page-functions.js'); // eslint-disable-next-line no-unused-vars const Connection = require('./connections/connection.js'); const NetworkMonitor = require('./driver/network-monitor.js'); +const {getBrowserVersion} = require('./driver/environment.js'); const UIStrings = { /** @@ -145,27 +146,7 @@ class Driver { * @return {Promise} */ async getBrowserVersion() { - const status = {msg: 'Getting browser version', id: 'lh:gather:getVersion'}; - log.time(status, 'verbose'); - const version = await this.sendCommand('Browser.getVersion'); - const match = version.product.match(/\/(\d+)/); // eg 'Chrome/71.0.3577.0' - const milestone = match ? parseInt(match[1]) : 0; - log.timeEnd(status); - return Object.assign(version, {milestone}); - } - - /** - * Computes the benchmark index to get a rough estimate of device class. - * @return {Promise} - */ - async getBenchmarkIndex() { - const status = {msg: 'Benchmarking machine', id: 'lh:gather:getBenchmarkIndex'}; - log.time(status); - const indexVal = await this.executionContext.evaluate(pageFunctions.computeBenchmarkIndex, { - args: [], - }); - log.timeEnd(status); - return indexVal; + return getBrowserVersion(this); } /** @@ -442,53 +423,6 @@ class Driver { }); } - /** - * @return {Promise<{url: string, data: string}|null>} - */ - async getAppManifest() { - // In all environments but LR, Page.getAppManifest finishes very quickly. - // In LR, there is a bug that causes this command to hang until outgoing - // requests finish. This has been seen in long polling (where it will never - // return) and when other requests take a long time to finish. We allow 10 seconds - // for outgoing requests to finish. Anything more, and we continue the run without - // a manifest. - // Googlers, see: http://b/124008171 - this.setNextProtocolTimeout(10000); - let response; - try { - response = await this.sendCommand('Page.getAppManifest'); - } catch (err) { - if (err.code === 'PROTOCOL_TIMEOUT') { - // LR will timeout fetching the app manifest in some cases, move on without one. - // https://github.com/GoogleChrome/lighthouse/issues/7147#issuecomment-461210921 - log.error('Driver', 'Failed fetching manifest', err); - return null; - } - - throw err; - } - - let data = response.data; - - // We're not reading `response.errors` however it may contain critical and noncritical - // errors from Blink's manifest parser: - // https://chromedevtools.github.io/debugger-protocol-viewer/tot/Page/#type-AppManifestError - if (!data) { - // If the data is empty, the page had no manifest. - return null; - } - - const BOM_LENGTH = 3; - const BOM_FIRSTCHAR = 65279; - const isBomEncoded = data.charCodeAt(0) === BOM_FIRSTCHAR; - - if (isBomEncoded) { - data = Buffer.from(data).slice(BOM_LENGTH).toString(); - } - - return {...response, data}; - } - /** * @return {Promise} */ diff --git a/lighthouse-core/gather/driver/environment.js b/lighthouse-core/gather/driver/environment.js new file mode 100644 index 000000000000..9dcb0dab974d --- /dev/null +++ b/lighthouse-core/gather/driver/environment.js @@ -0,0 +1,43 @@ +/** + * @license Copyright 2021 The Lighthouse Authors. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ +'use strict'; + +const log = require('lighthouse-logger'); +const pageFunctions = require('../../lib/page-functions.js'); + +/** + * @param {LH.Gatherer.FRProtocolSession} session + * @return {Promise} + */ +async function getBrowserVersion(session) { + const status = {msg: 'Getting browser version', id: 'lh:gather:getVersion'}; + log.time(status, 'verbose'); + const version = await session.sendCommand('Browser.getVersion'); + const match = version.product.match(/\/(\d+)/); // eg 'Chrome/71.0.3577.0' + const milestone = match ? parseInt(match[1]) : 0; + log.timeEnd(status); + return Object.assign(version, {milestone}); +} + +/** + * Computes the benchmark index to get a rough estimate of device class. + * @param {LH.Gatherer.FRTransitionalDriver['executionContext']} executionContext + * @return {Promise} + */ +async function getBenchmarkIndex(executionContext) { + const status = {msg: 'Benchmarking machine', id: 'lh:gather:getBenchmarkIndex'}; + log.time(status); + const indexVal = await executionContext.evaluate(pageFunctions.computeBenchmarkIndex, { + args: [], + }); + log.timeEnd(status); + return indexVal; +} + +module.exports = { + getBrowserVersion, + getBenchmarkIndex, +}; diff --git a/lighthouse-core/gather/gather-runner.js b/lighthouse-core/gather/gather-runner.js index a6170daca46f..a9563fe5885f 100644 --- a/lighthouse-core/gather/gather-runner.js +++ b/lighthouse-core/gather/gather-runner.js @@ -6,14 +6,17 @@ 'use strict'; const log = require('lighthouse-logger'); -const manifestParser = require('../lib/manifest-parser.js'); -const stacksGatherer = require('../lib/stack-collector.js'); const LHError = require('../lib/lh-error.js'); const NetworkAnalyzer = require('../lib/dependency-graph/simulator/network-analyzer.js'); const NetworkRecorder = require('../lib/network-recorder.js'); const constants = require('../config/constants.js'); const i18n = require('../lib/i18n/i18n.js'); const URL = require('../lib/url-shim.js'); +const {getBenchmarkIndex} = require('./driver/environment.js'); +const WebAppManifest = require('./gatherers/web-app-manifest.js'); +const InstallabilityErrors = require('./gatherers/installability-errors.js'); +const NetworkUserAgent = require('./gatherers/network-user-agent.js'); +const Stacks = require('./gatherers/stacks.js'); const UIStrings = { /** @@ -575,27 +578,6 @@ class GatherRunner { }; } - /** - * Creates an Artifacts.InstallabilityErrors, tranforming data from the protocol - * for old versions of Chrome. - * @param {LH.Gatherer.PassContext} passContext - * @return {Promise} - */ - static async getInstallabilityErrors(passContext) { - const status = { - msg: 'Get webapp installability errors', - id: 'lh:gather:getInstallabilityErrors', - }; - log.time(status); - const response = - await passContext.driver.sendCommand('Page.getInstallabilityErrors'); - - const errors = response.installabilityErrors; - - log.timeEnd(status); - return {errors}; - } - /** * Populates the important base artifacts from a fully loaded test page. * Currently must be run before `start-url` gatherer so that `WebAppManifest` @@ -618,24 +600,19 @@ class GatherRunner { } // Fetch the manifest, if it exists. - baseArtifacts.WebAppManifest = await GatherRunner.getWebAppManifest(passContext); + baseArtifacts.WebAppManifest = await WebAppManifest.getWebAppManifest( + passContext.driver.defaultSession, passContext.url); if (baseArtifacts.WebAppManifest) { - baseArtifacts.InstallabilityErrors = await GatherRunner.getInstallabilityErrors(passContext); + baseArtifacts.InstallabilityErrors = await InstallabilityErrors.getInstallabilityErrors( + passContext.driver.defaultSession); } - baseArtifacts.Stacks = await stacksGatherer(passContext); + baseArtifacts.Stacks = await Stacks.collectStacks(passContext.driver.executionContext); // Find the NetworkUserAgent actually used in the devtoolsLogs. const devtoolsLog = baseArtifacts.devtoolsLogs[passContext.passConfig.passName]; - const userAgentEntry = devtoolsLog.find(entry => - entry.method === 'Network.requestWillBeSent' && - !!entry.params.request.headers['User-Agent'] - ); - if (userAgentEntry) { - // @ts-expect-error - guaranteed to exist by the find above - baseArtifacts.NetworkUserAgent = userAgentEntry.params.request.headers['User-Agent']; - } + baseArtifacts.NetworkUserAgent = NetworkUserAgent.getNetworkUserAgent(devtoolsLog); const slowCpuWarning = GatherRunner.getSlowHostCpuWarning(passContext); if (slowCpuWarning) baseArtifacts.LighthouseRunWarnings.push(slowCpuWarning); @@ -655,28 +632,6 @@ class GatherRunner { baseArtifacts.Timing = log.getTimeEntries(); } - /** - * Uses the debugger protocol to fetch the manifest from within the context of - * the target page, reusing any credentials, emulation, etc, already established - * there. - * - * Returns the parsed manifest or null if the page had no manifest. If the manifest - * was unparseable as JSON, manifest.value will be undefined and manifest.warning - * will have the reason. See manifest-parser.js for more information. - * - * @param {LH.Gatherer.PassContext} passContext - * @return {Promise} - */ - static async getWebAppManifest(passContext) { - const status = {msg: 'Get webapp manifest', id: 'lh:gather:getWebAppManifest'}; - log.time(status); - const response = await passContext.driver.getAppManifest(); - if (!response) return null; - const manifest = manifestParser(response.data, response.url, passContext.url); - log.timeEnd(status); - return manifest; - } - /** * @param {Array} passConfigs * @param {{driver: Driver, requestedUrl: string, settings: LH.Config.Settings}} options @@ -695,7 +650,7 @@ class GatherRunner { await GatherRunner.loadBlank(driver); const baseArtifacts = await GatherRunner.initializeBaseArtifacts(options); - baseArtifacts.BenchmarkIndex = await options.driver.getBenchmarkIndex(); + baseArtifacts.BenchmarkIndex = await getBenchmarkIndex(driver.executionContext); await GatherRunner.setupDriver(driver, options, baseArtifacts.LighthouseRunWarnings); diff --git a/lighthouse-core/gather/gatherers/installability-errors.js b/lighthouse-core/gather/gatherers/installability-errors.js new file mode 100644 index 000000000000..f0eda793197d --- /dev/null +++ b/lighthouse-core/gather/gatherers/installability-errors.js @@ -0,0 +1,48 @@ +/** + * @license Copyright 2021 The Lighthouse Authors. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ +'use strict'; + +const log = require('lighthouse-logger'); +const FRGatherer = require('../../fraggle-rock/gather/base-gatherer.js'); + +class InstallabilityErrors extends FRGatherer { + /** @type {LH.Gatherer.GathererMeta} */ + meta = { + supportedModes: ['snapshot', 'navigation'], + }; + + /** + * Creates an Artifacts.InstallabilityErrors, tranforming data from the protocol + * for old versions of Chrome. + * @param {LH.Gatherer.FRProtocolSession} session + * @return {Promise} + */ + static async getInstallabilityErrors(session) { + const status = { + msg: 'Get webapp installability errors', + id: 'lh:gather:getInstallabilityErrors', + }; + log.time(status); + const response = await session.sendCommand('Page.getInstallabilityErrors'); + + const errors = response.installabilityErrors; + + log.timeEnd(status); + return {errors}; + } + + /** + * @param {LH.Gatherer.FRTransitionalContext} context + * @return {Promise} + */ + snapshot(context) { + const driver = context.driver; + + return InstallabilityErrors.getInstallabilityErrors(driver.defaultSession); + } +} + +module.exports = InstallabilityErrors; diff --git a/lighthouse-core/gather/gatherers/network-user-agent.js b/lighthouse-core/gather/gatherers/network-user-agent.js new file mode 100644 index 000000000000..ba2dcbe2333d --- /dev/null +++ b/lighthouse-core/gather/gatherers/network-user-agent.js @@ -0,0 +1,41 @@ +/** + * @license Copyright 2021 The Lighthouse Authors. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ +'use strict'; + +const FRGatherer = require('../../fraggle-rock/gather/base-gatherer.js'); + +/** @implements {LH.Gatherer.FRGathererInstance<'DevtoolsLog'>} */ +class NetworkUserAgent extends FRGatherer { + /** @type {LH.Gatherer.GathererMeta<'DevtoolsLog'>} */ + meta = { + supportedModes: ['timespan', 'navigation'], + dependencies: {DevtoolsLog: require('./devtools-log.js').symbol}, + }; + + /** + * @param {LH.Artifacts['DevtoolsLog']} devtoolsLog + * @return {string} + */ + static getNetworkUserAgent(devtoolsLog) { + for (const entry of devtoolsLog) { + if (entry.method !== 'Network.requestWillBeSent') continue; + const userAgent = entry.params.request.headers['User-Agent']; + if (userAgent) return userAgent; + } + + return ''; + } + + /** + * @param {LH.Gatherer.FRTransitionalContext<'DevtoolsLog'>} context + * @return {Promise} + */ + async afterTimespan(context) { + return NetworkUserAgent.getNetworkUserAgent(context.dependencies.DevtoolsLog); + } +} + +module.exports = NetworkUserAgent; diff --git a/lighthouse-core/lib/stack-collector.js b/lighthouse-core/gather/gatherers/stacks.js similarity index 70% rename from lighthouse-core/lib/stack-collector.js rename to lighthouse-core/gather/gatherers/stacks.js index 7b24ea4cc62d..bf4d7ac371f6 100644 --- a/lighthouse-core/lib/stack-collector.js +++ b/lighthouse-core/gather/gatherers/stacks.js @@ -3,6 +3,7 @@ * 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'; /** * @fileoverview Gathers a list of detected JS libraries and their versions. @@ -11,10 +12,9 @@ /* global window */ /* global d41d8cd98f00b204e9800998ecf8427e_LibraryDetectorTests */ -'use strict'; - const fs = require('fs'); const log = require('lighthouse-logger'); +const FRGatherer = require('../../fraggle-rock/gather/base-gatherer.js'); const libDetectorSource = fs.readFileSync( require.resolve('js-library-detector/library/libraries.js'), 'utf8'); @@ -76,29 +76,46 @@ async function detectLibraries() { } /* c8 ignore stop */ -/** - * @param {LH.Gatherer.PassContext} passContext - * @return {Promise} - */ -async function collectStacks(passContext) { - const status = {msg: 'Collect stacks', id: 'lh:gather:collectStacks'}; - log.time(status); - - const jsLibraries = await passContext.driver.executionContext.evaluate(detectLibraries, { - args: [], - deps: [libDetectorSource], - }); - - /** @type {LH.Artifacts['Stacks']} */ - const stacks = jsLibraries.map(lib => ({ - detector: 'js', - id: lib.id, - name: lib.name, - version: typeof lib.version === 'number' ? String(lib.version) : (lib.version || undefined), - npm: lib.npm || undefined, - })); - log.timeEnd(status); - return stacks; + +/** @implements {LH.Gatherer.FRGathererInstance} */ +class Stacks extends FRGatherer { + /** @type {LH.Gatherer.GathererMeta} */ + meta = { + supportedModes: ['snapshot', 'navigation'], + }; + + /** + * @param {LH.Gatherer.FRTransitionalDriver['executionContext']} executionContext + * @return {Promise} + */ + static async collectStacks(executionContext) { + const status = {msg: 'Collect stacks', id: 'lh:gather:collectStacks'}; + log.time(status); + + const jsLibraries = await executionContext.evaluate(detectLibraries, { + args: [], + deps: [libDetectorSource], + }); + + /** @type {LH.Artifacts['Stacks']} */ + const stacks = jsLibraries.map(lib => ({ + detector: 'js', + id: lib.id, + name: lib.name, + version: typeof lib.version === 'number' ? String(lib.version) : (lib.version || undefined), + npm: lib.npm || undefined, + })); + log.timeEnd(status); + return stacks; + } + + /** + * @param {LH.Gatherer.FRTransitionalContext} context + * @return {Promise} + */ + async snapshot(context) { + return Stacks.collectStacks(context.driver.executionContext); + } } -module.exports = collectStacks; +module.exports = Stacks; diff --git a/lighthouse-core/gather/gatherers/web-app-manifest.js b/lighthouse-core/gather/gatherers/web-app-manifest.js new file mode 100644 index 000000000000..a9ae8ca62cdd --- /dev/null +++ b/lighthouse-core/gather/gatherers/web-app-manifest.js @@ -0,0 +1,102 @@ +/** + * @license Copyright 2021 The Lighthouse Authors. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ +'use strict'; + +const log = require('lighthouse-logger'); +const manifestParser = require('../../lib/manifest-parser.js'); +const FRGatherer = require('../../fraggle-rock/gather/base-gatherer.js'); + +class WebAppManifest extends FRGatherer { + /** @type {LH.Gatherer.GathererMeta} */ + meta = { + supportedModes: ['snapshot', 'navigation'], + }; + + /** + * @param {LH.Gatherer.FRProtocolSession} session + * @return {Promise<{url: string, data: string}|null>} + */ + static async fetchAppManifest(session) { + // In all environments but LR, Page.getAppManifest finishes very quickly. + // In LR, there is a bug that causes this command to hang until outgoing + // requests finish. This has been seen in long polling (where it will never + // return) and when other requests take a long time to finish. We allow 10 seconds + // for outgoing requests to finish. Anything more, and we continue the run without + // a manifest. + // Googlers, see: http://b/124008171 + session.setNextProtocolTimeout(10000); + let response; + try { + response = await session.sendCommand('Page.getAppManifest'); + } catch (err) { + if (err.code === 'PROTOCOL_TIMEOUT') { + // LR will timeout fetching the app manifest in some cases, move on without one. + // https://github.com/GoogleChrome/lighthouse/issues/7147#issuecomment-461210921 + log.error('WebAppManifest', 'Failed fetching manifest', err); + return null; + } + + throw err; + } + + let data = response.data; + + // We're not reading `response.errors` however it may contain critical and noncritical + // errors from Blink's manifest parser: + // https://chromedevtools.github.io/debugger-protocol-viewer/tot/Page/#type-AppManifestError + if (!data) { + // If the data is empty, the page had no manifest. + return null; + } + + const BOM_LENGTH = 3; + const BOM_FIRSTCHAR = 65279; + const isBomEncoded = data.charCodeAt(0) === BOM_FIRSTCHAR; + + if (isBomEncoded) { + data = Buffer.from(data) + .slice(BOM_LENGTH) + .toString(); + } + + return {...response, data}; + } + + /** + * Uses the debugger protocol to fetch the manifest from within the context of + * the target page, reusing any credentials, emulation, etc, already established + * there. + * + * Returns the parsed manifest or null if the page had no manifest. If the manifest + * was unparseable as JSON, manifest.value will be undefined and manifest.warning + * will have the reason. See manifest-parser.js for more information. + * + * @param {LH.Gatherer.FRProtocolSession} session + * @param {string} pageUrl + * @return {Promise} + */ + static async getWebAppManifest(session, pageUrl) { + const status = {msg: 'Get webapp manifest', id: 'lh:gather:getWebAppManifest'}; + log.time(status); + const response = await WebAppManifest.fetchAppManifest(session); + if (!response) return null; + const manifest = manifestParser(response.data, response.url, pageUrl); + log.timeEnd(status); + return manifest; + } + + /** + * @param {LH.Gatherer.FRTransitionalContext} context + * @return {Promise} + */ + snapshot(context) { + const driver = context.driver; + + return WebAppManifest.getWebAppManifest(driver.defaultSession, context.url); + } +} + +module.exports = WebAppManifest; diff --git a/lighthouse-core/test/fraggle-rock/api-test-pptr.js b/lighthouse-core/test/fraggle-rock/api-test-pptr.js index 40c69b858c50..680cf3c78c0a 100644 --- a/lighthouse-core/test/fraggle-rock/api-test-pptr.js +++ b/lighthouse-core/test/fraggle-rock/api-test-pptr.js @@ -24,11 +24,13 @@ function getAuditsBreakdown(lhr) { audit => !irrelevantDisplayModes.has(audit.scoreDisplayMode) ); - const informativeAudits = applicableAudits - .filter(audit => audit.scoreDisplayMode === 'informative'); + const informativeAudits = applicableAudits.filter( + audit => audit.scoreDisplayMode === 'informative' + ); - const erroredAudits = applicableAudits - .filter(audit => audit.score === null && audit && !informativeAudits.includes(audit)); + const erroredAudits = applicableAudits.filter( + audit => audit.score === null && audit && !informativeAudits.includes(audit) + ); const failedAudits = applicableAudits.filter(audit => audit.score !== null && audit.score < 1); @@ -93,7 +95,7 @@ describe('Fraggle Rock API', () => { const {auditResults, erroredAudits, failedAudits} = getAuditsBreakdown(lhr); // TODO(FR-COMPAT): This assertion can be removed when full compatibility is reached. - expect(auditResults.length).toMatchInlineSnapshot(`65`); + expect(auditResults.length).toMatchInlineSnapshot(`67`); expect(erroredAudits).toHaveLength(0); expect(failedAudits.map(audit => audit.id)).toContain('label'); @@ -117,7 +119,9 @@ describe('Fraggle Rock API', () => { const bestPractices = lhr.categories['best-practices']; expect(bestPractices.score).toBeLessThan(1); - const {erroredAudits, failedAudits} = getAuditsBreakdown(lhr); + const {auditResults, erroredAudits, failedAudits} = getAuditsBreakdown(lhr); + // TODO(FR-COMPAT): This assertion can be removed when full compatibility is reached. + expect(auditResults.length).toMatchInlineSnapshot(`24`); expect(erroredAudits).toHaveLength(0); expect(failedAudits.map(audit => audit.id)).toContain('errors-in-console'); @@ -153,7 +157,9 @@ describe('Fraggle Rock API', () => { if (!result) throw new Error('Lighthouse failed to produce a result'); const {lhr} = result; - const {failedAudits, erroredAudits} = getAuditsBreakdown(lhr); + const {auditResults, failedAudits, erroredAudits} = getAuditsBreakdown(lhr); + // TODO(FR-COMPAT): This assertion can be removed when full compatibility is reached. + expect(auditResults.length).toMatchInlineSnapshot(`97`); expect(erroredAudits).toHaveLength(0); const failedAuditIds = failedAudits.map(audit => audit.id); diff --git a/lighthouse-core/test/fraggle-rock/gather/driver-test.js b/lighthouse-core/test/fraggle-rock/gather/driver-test.js index eaae521cc2ad..031d3e3ef621 100644 --- a/lighthouse-core/test/fraggle-rock/gather/driver-test.js +++ b/lighthouse-core/test/fraggle-rock/gather/driver-test.js @@ -30,7 +30,7 @@ let driver; beforeEach(() => { // @ts-expect-error - Individual mock functions are applied as necessary. - page = {target: () => pageTarget}; + page = {target: () => pageTarget, url: jest.fn()}; // @ts-expect-error - Individual mock functions are applied as necessary. pageTarget = {createCDPSession: () => puppeteerSession}; // @ts-expect-error - Individual mock functions are applied as necessary. @@ -60,6 +60,13 @@ for (const fnName of DELEGATED_FUNCTIONS) { }); } +describe('.url', () => { + it('should return the page url', async () => { + page.url = jest.fn().mockReturnValue('https://example.com'); + expect(await driver.url()).toEqual('https://example.com'); + }); +}); + describe('.executionContext', () => { it('should fail if called before connect', () => { expect(() => driver.executionContext).toThrow(); diff --git a/lighthouse-core/test/fraggle-rock/gather/mock-driver.js b/lighthouse-core/test/fraggle-rock/gather/mock-driver.js index 424c2565fde8..a367e60b9acd 100644 --- a/lighthouse-core/test/fraggle-rock/gather/mock-driver.js +++ b/lighthouse-core/test/fraggle-rock/gather/mock-driver.js @@ -23,9 +23,16 @@ const { function createMockSession() { return { sendCommand: createMockSendCommandFn({useSessionId: false}), + setNextProtocolTimeout: jest.fn(), once: createMockOnceFn(), on: createMockOnFn(), off: jest.fn(), + + /** @return {LH.Gatherer.FRProtocolSession} */ + asSession() { + // @ts-expect-error - We'll rely on the tests passing to know this matches. + return this; + }, }; } @@ -67,17 +74,18 @@ function createMockExecutionContext() { } function createMockDriver() { + const page = createMockPage(); const session = createMockSession(); const context = createMockExecutionContext(); return { - _page: createMockPage(), + _page: page, _executionContext: context, _session: session, + url: () => page.url(), defaultSession: session, connect: jest.fn(), - evaluate: context.evaluate, - evaluateAsync: context.evaluateAsync, + executionContext: context, /** @return {Driver} */ asDriver() { diff --git a/lighthouse-core/test/fraggle-rock/gather/navigation-runner-test.js b/lighthouse-core/test/fraggle-rock/gather/navigation-runner-test.js index 3a6e181a3f7a..c7e5ea01a67f 100644 --- a/lighthouse-core/test/fraggle-rock/gather/navigation-runner-test.js +++ b/lighthouse-core/test/fraggle-rock/gather/navigation-runner-test.js @@ -81,6 +81,13 @@ describe('NavigationRunner', () => { }); describe('_setup', () => { + beforeEach(() => { + mockDriver._session.sendCommand.mockResponse('Browser.getVersion', { + product: 'Chrome/88.0', + userAgent: 'Chrome', + }); + }); + it('should connect the driver', async () => { await runner._setup({driver, config, requestedUrl}); expect(mockDriver.connect).toHaveBeenCalled(); diff --git a/lighthouse-core/test/fraggle-rock/gather/snapshot-runner-test.js b/lighthouse-core/test/fraggle-rock/gather/snapshot-runner-test.js index 701d1c5794ee..11475aafbc86 100644 --- a/lighthouse-core/test/fraggle-rock/gather/snapshot-runner-test.js +++ b/lighthouse-core/test/fraggle-rock/gather/snapshot-runner-test.js @@ -45,6 +45,11 @@ describe('Snapshot Runner', () => { mockRunnerRun = jest.fn(); page = mockPage.asPage(); + mockDriver._session.sendCommand.mockResponse('Browser.getVersion', { + product: 'Chrome/88.0', + userAgent: 'Chrome', + }); + gathererA = createMockGathererInstance({supportedModes: ['snapshot']}); gathererA.snapshot.mockResolvedValue('Artifact A'); diff --git a/lighthouse-core/test/fraggle-rock/gather/timespan-runner-test.js b/lighthouse-core/test/fraggle-rock/gather/timespan-runner-test.js index b8df6c7431ff..df3ba6bc93e1 100644 --- a/lighthouse-core/test/fraggle-rock/gather/timespan-runner-test.js +++ b/lighthouse-core/test/fraggle-rock/gather/timespan-runner-test.js @@ -45,6 +45,11 @@ describe('Timespan Runner', () => { mockRunnerRun = jest.fn(); page = mockPage.asPage(); + mockDriver._session.sendCommand.mockResponse('Browser.getVersion', { + product: 'Chrome/88.0', + userAgent: 'Chrome', + }); + gathererA = createMockGathererInstance({supportedModes: ['timespan']}); gathererA.afterTimespan.mockResolvedValue('Artifact A'); diff --git a/lighthouse-core/test/gather/driver-test.js b/lighthouse-core/test/gather/driver-test.js index 11f9f1115124..ad2406536e3d 100644 --- a/lighthouse-core/test/gather/driver-test.js +++ b/lighthouse-core/test/gather/driver-test.js @@ -227,42 +227,6 @@ describe('.setExtraHTTPHeaders', () => { }); }); -describe('.getAppManifest', () => { - it('should return null when no manifest', async () => { - connectionStub.sendCommand = createMockSendCommandFn().mockResponse( - 'Page.getAppManifest', - {data: undefined, url: '/manifest'} - ); - const result = await driver.getAppManifest(); - expect(result).toEqual(null); - }); - - it('should return the manifest', async () => { - const manifest = {name: 'The App'}; - connectionStub.sendCommand = createMockSendCommandFn().mockResponse( - 'Page.getAppManifest', - {data: JSON.stringify(manifest), url: '/manifest'} - ); - const result = await driver.getAppManifest(); - expect(result).toEqual({data: JSON.stringify(manifest), url: '/manifest'}); - }); - - it('should handle BOM-encoded manifest', async () => { - const fs = require('fs'); - const manifestWithoutBOM = fs.readFileSync(__dirname + '/../fixtures/manifest.json').toString(); - const manifestWithBOM = fs - .readFileSync(__dirname + '/../fixtures/manifest-bom.json') - .toString(); - - connectionStub.sendCommand = createMockSendCommandFn().mockResponse( - 'Page.getAppManifest', - {data: manifestWithBOM, url: '/manifest'} - ); - const result = await driver.getAppManifest(); - expect(result).toEqual({data: manifestWithoutBOM, url: '/manifest'}); - }); -}); - describe('.goOffline', () => { it('should send offline emulation', async () => { connectionStub.sendCommand = createMockSendCommandFn() diff --git a/lighthouse-core/test/gather/gather-runner-test.js b/lighthouse-core/test/gather/gather-runner-test.js index 8694ea453257..b9e3f88830f8 100644 --- a/lighthouse-core/test/gather/gather-runner-test.js +++ b/lighthouse-core/test/gather/gather-runner-test.js @@ -28,12 +28,10 @@ const GatherRunner = { beginRecording: makeParamsOptional(GatherRunner_.beginRecording), collectArtifacts: makeParamsOptional(GatherRunner_.collectArtifacts), endRecording: makeParamsOptional(GatherRunner_.endRecording), - getInstallabilityErrors: makeParamsOptional(GatherRunner_.getInstallabilityErrors), getInterstitialError: makeParamsOptional(GatherRunner_.getInterstitialError), getNetworkError: makeParamsOptional(GatherRunner_.getNetworkError), getNonHtmlError: makeParamsOptional(GatherRunner_.getNonHtmlError), getPageLoadError: makeParamsOptional(GatherRunner_.getPageLoadError), - getWebAppManifest: makeParamsOptional(GatherRunner_.getWebAppManifest), getSlowHostCpuWarning: makeParamsOptional(GatherRunner_.getSlowHostCpuWarning), initializeBaseArtifacts: makeParamsOptional(GatherRunner_.initializeBaseArtifacts), loadPage: makeParamsOptional(GatherRunner_.loadPage), @@ -1648,28 +1646,6 @@ describe('GatherRunner', function() { }); }); - describe('.getInstallabilityErrors', () => { - /** @type {RecursivePartial} */ - let passContext; - - beforeEach(() => { - passContext = { - driver, - }; - }); - - it('should return the response from the protocol', async () => { - connectionStub.sendCommand - .mockResponse('Page.getInstallabilityErrors', { - installabilityErrors: [{errorId: 'no-icon-available', errorArguments: []}], - }); - const result = await GatherRunner.getInstallabilityErrors(passContext); - expect(result).toEqual({ - errors: [{errorId: 'no-icon-available', errorArguments: []}], - }); - }); - }); - describe('.getSlowHostCpuWarning', () => { /** @type {RecursivePartial} */ let passContext; @@ -1717,41 +1693,4 @@ describe('GatherRunner', function() { expect(GatherRunner.getSlowHostCpuWarning(passContext)).toBe(undefined); }); }); - - describe('.getWebAppManifest', () => { - const MANIFEST_URL = 'https://example.com/manifest.json'; - /** @type {RecursivePartial} */ - let passContext; - - beforeEach(() => { - passContext = { - url: 'https://example.com/index.html', - baseArtifacts: {}, - driver, - }; - }); - - it('should return null when there is no manifest', async () => { - connectionStub.sendCommand - .mockResponse('Page.getAppManifest', {}) - .mockResponse('Page.getInstallabilityErrors', {installabilityErrors: []}); - const result = await GatherRunner.getWebAppManifest(passContext); - expect(result).toEqual(null); - }); - - it('should parse the manifest when found', async () => { - const manifest = {name: 'App'}; - connectionStub.sendCommand - .mockResponse('Page.getAppManifest', {data: JSON.stringify(manifest), url: MANIFEST_URL}) - .mockResponse('Page.getInstallabilityErrors', {installabilityErrors: []}); - - const result = await GatherRunner.getWebAppManifest(passContext); - expect(result).toHaveProperty('raw', JSON.stringify(manifest)); - expect(result && result.value).toMatchObject({ - name: {value: 'App', raw: 'App'}, - start_url: {value: passContext.url, raw: undefined}, - }); - expect(result && result.url).toMatch(MANIFEST_URL); - }); - }); }); diff --git a/lighthouse-core/test/gather/gatherers/installability-errors-test.js b/lighthouse-core/test/gather/gatherers/installability-errors-test.js new file mode 100644 index 000000000000..8374961b57ef --- /dev/null +++ b/lighthouse-core/test/gather/gatherers/installability-errors-test.js @@ -0,0 +1,32 @@ +/** + * @license Copyright 2021 The Lighthouse Authors. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ +'use strict'; + +/* eslint-env jest */ + +const InstallabilityErrors = require('../../../gather/gatherers/installability-errors.js'); +const {createMockSession} = require('../../fraggle-rock/gather/mock-driver.js'); + + +describe('.getInstallabilityErrors', () => { + let session = createMockSession(); + + beforeEach(() => { + session = createMockSession(); + }); + + it('should return the response from the protocol', async () => { + session.sendCommand + .mockResponse('Page.getInstallabilityErrors', { + installabilityErrors: [{errorId: 'no-icon-available', errorArguments: []}], + }); + + const result = await InstallabilityErrors.getInstallabilityErrors(session.asSession()); + expect(result).toEqual({ + errors: [{errorId: 'no-icon-available', errorArguments: []}], + }); + }); +}); diff --git a/lighthouse-core/test/gather/gatherers/network-user-agent-test.js b/lighthouse-core/test/gather/gatherers/network-user-agent-test.js new file mode 100644 index 000000000000..8c2b1c902060 --- /dev/null +++ b/lighthouse-core/test/gather/gatherers/network-user-agent-test.js @@ -0,0 +1,25 @@ +/** + * @license Copyright 2021 The Lighthouse Authors. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ +'use strict'; + +/* eslint-env jest */ + +const NetworkUserAgent = require('../../../gather/gatherers/network-user-agent.js'); +const devtoolsLog = require('../../fixtures/traces/lcp-m78.devtools.log.json'); + + +describe('.getNetworkUserAgent', () => { + it('should return empty string when no network events available', async () => { + const result = await NetworkUserAgent.getNetworkUserAgent([]); + expect(result).toEqual(''); + }); + + it('should return the user agent that was used to make requests', async () => { + const result = await NetworkUserAgent.getNetworkUserAgent(devtoolsLog); + // eslint-disable-next-line max-len + expect(result).toEqual('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36'); + }); +}); diff --git a/lighthouse-core/test/lib/stack-collector-test.js b/lighthouse-core/test/gather/gatherers/stacks-test.js similarity index 73% rename from lighthouse-core/test/lib/stack-collector-test.js rename to lighthouse-core/test/gather/gatherers/stacks-test.js index 136a51f78a94..b3887f49fa71 100644 --- a/lighthouse-core/test/lib/stack-collector-test.js +++ b/lighthouse-core/test/gather/gatherers/stacks-test.js @@ -7,24 +7,26 @@ /* eslint-env jest */ -const collectStacks = require('../../lib/stack-collector.js'); +const StacksGatherer = require('../../../gather/gatherers/stacks.js'); -describe('stack collector', () => { - /** @type {{driver: {evaluate: jest.Mock}}} */ - let passContext; +describe('StacksGatherer', () => { + /** @type {{executionContext: {evaluate: jest.Mock}}} */ + let driver; beforeEach(() => { - passContext = {driver: {executionContext: {evaluate: jest.fn()}}}; + driver = {executionContext: {evaluate: jest.fn()}}; }); it('returns the detected stacks', async () => { - passContext.driver.executionContext.evaluate.mockResolvedValue([ + driver.executionContext.evaluate.mockResolvedValue([ {id: 'jquery', name: 'jQuery', version: '2.1.0', npm: 'jquery'}, {id: 'angular', name: 'Angular', version: '', npm: ''}, {id: 'magento', name: 'Magento', version: 2}, ]); - expect(await collectStacks(passContext)).toEqual([ + /** @type {*} */ + const executionContext = driver.executionContext; + expect(await StacksGatherer.collectStacks(executionContext)).toEqual([ {detector: 'js', id: 'jquery', name: 'jQuery', npm: 'jquery', version: '2.1.0'}, {detector: 'js', id: 'angular', name: 'Angular', npm: undefined, version: undefined}, {detector: 'js', id: 'magento', name: 'Magento', npm: undefined, version: '2'}, diff --git a/lighthouse-core/test/gather/gatherers/web-app-manifest-test.js b/lighthouse-core/test/gather/gatherers/web-app-manifest-test.js new file mode 100644 index 000000000000..af9b4f139dd5 --- /dev/null +++ b/lighthouse-core/test/gather/gatherers/web-app-manifest-test.js @@ -0,0 +1,82 @@ +/** + * @license Copyright 2021 The Lighthouse Authors. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ +'use strict'; + +/* eslint-env jest */ + +const WebAppManifest = require('../../../gather/gatherers/web-app-manifest.js'); +const {createMockSession} = require('../../fraggle-rock/gather/mock-driver.js'); + +describe('WebAppManifest Gatherer', () => { + let session = createMockSession(); + + beforeEach(() => { + session = createMockSession(); + }); + + describe('.getAppManifest', () => { + it('should return null when no manifest', async () => { + session.sendCommand.mockResponse('Page.getAppManifest', {data: undefined, url: '/manifest'}); + const result = await WebAppManifest.fetchAppManifest(session.asSession()); + expect(result).toEqual(null); + }); + + it('should return the manifest', async () => { + const manifest = {name: 'The App'}; + session.sendCommand.mockResponse('Page.getAppManifest', { + data: JSON.stringify(manifest), + url: '/manifest', + }); + const result = await WebAppManifest.fetchAppManifest(session.asSession()); + expect(result).toEqual({data: JSON.stringify(manifest), url: '/manifest'}); + }); + + it('should handle BOM-encoded manifest', async () => { + const fs = require('fs'); + const manifestWithoutBOM = fs + .readFileSync(__dirname + '/../../fixtures/manifest.json') + .toString(); + const manifestWithBOM = fs + .readFileSync(__dirname + '/../../fixtures/manifest-bom.json') + .toString(); + + session.sendCommand.mockResponse('Page.getAppManifest', { + data: manifestWithBOM, + url: '/manifest', + }); + const result = await WebAppManifest.fetchAppManifest(session.asSession()); + expect(result).toEqual({data: manifestWithoutBOM, url: '/manifest'}); + }); + }); + + describe('.getWebAppManifest', () => { + const MANIFEST_URL = 'https://example.com/manifest.json'; + const PAGE_URL = 'https://example.com/index.html'; + + it('should return null when there is no manifest', async () => { + session.sendCommand + .mockResponse('Page.getAppManifest', {}) + .mockResponse('Page.getInstallabilityErrors', {installabilityErrors: []}); + const result = await WebAppManifest.getWebAppManifest(session.asSession(), PAGE_URL); + expect(result).toEqual(null); + }); + + it('should parse the manifest when found', async () => { + const manifest = {name: 'App'}; + session.sendCommand + .mockResponse('Page.getAppManifest', {data: JSON.stringify(manifest), url: MANIFEST_URL}) + .mockResponse('Page.getInstallabilityErrors', {installabilityErrors: []}); + + const result = await WebAppManifest.getWebAppManifest(session.asSession(), PAGE_URL); + expect(result).toHaveProperty('raw', JSON.stringify(manifest)); + expect(result && result.value).toMatchObject({ + name: {value: 'App', raw: 'App'}, + start_url: {value: PAGE_URL, raw: undefined}, + }); + expect(result && result.url).toMatch(MANIFEST_URL); + }); + }); +}); diff --git a/lighthouse-core/test/test-utils.js b/lighthouse-core/test/test-utils.js index 10c860ee0831..7465e5191aaf 100644 --- a/lighthouse-core/test/test-utils.js +++ b/lighthouse-core/test/test-utils.js @@ -225,7 +225,16 @@ async function flushAllTimersAndMicrotasks(ms = 1000) { * shouldn't concern themselves about. */ function makeMocksForGatherRunner() { - jest.mock('../lib/stack-collector.js', () => () => Promise.resolve([])); + jest.mock('../gather/driver/environment.js', () => ({ + getBenchmarkIndex: () => Promise.resolve(150), + })); + jest.mock('../gather/gatherers/stacks.js', () => ({collectStacks: () => Promise.resolve([])})); + jest.mock('../gather/gatherers/installability-errors.js', () => ({ + getInstallabilityErrors: async () => ({errors: []}), + })); + jest.mock('../gather/gatherers/web-app-manifest.js', () => ({ + getWebAppManifest: async () => null, + })); } module.exports = { diff --git a/tsconfig.json b/tsconfig.json index 700b57ad173a..b145463b9a32 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -57,6 +57,7 @@ "lighthouse-core/test/gather/gatherers/seo/font-size-test.js", "lighthouse-core/test/gather/gatherers/service-worker-test.js", "lighthouse-core/test/gather/gatherers/source-maps-test.js", + "lighthouse-core/test/gather/gatherers/stack-collector-test.js", "lighthouse-core/test/gather/gatherers/start-url-test.js", "lighthouse-core/test/gather/gatherers/trace-elements-test.js", "lighthouse-core/test/gather/gatherers/viewport-dimensions-test.js", @@ -86,7 +87,6 @@ "lighthouse-core/test/lib/rect-helpers-test.js", "lighthouse-core/test/lib/sd-validation-test.js", "lighthouse-core/test/lib/sentry-test.js", - "lighthouse-core/test/lib/stack-collector-test.js", "lighthouse-core/test/lib/stack-packs-test.js", "lighthouse-core/test/lib/statistics-test.js", "lighthouse-core/test/lib/timing-trace-saver-test.js", diff --git a/types/artifacts.d.ts b/types/artifacts.d.ts index e4dacd82b3f4..59bbe80487d8 100644 --- a/types/artifacts.d.ts +++ b/types/artifacts.d.ts @@ -19,11 +19,16 @@ declare global { export interface Artifacts extends BaseArtifacts, GathererArtifacts {} /** - * Artifacts always created by GatherRunner. These artifacts are available to Lighthouse plugins. + * Artifacts always created by gathering. These artifacts are available to Lighthouse plugins. * NOTE: any breaking changes here are considered breaking Lighthouse changes that must be done * on a major version bump. */ - export interface BaseArtifacts { + export type BaseArtifacts = UniversalBaseArtifacts & ContextualBaseArtifacts & LegacyBaseArtifacts + + /** + * The set of base artifacts that are available in every mode of Lighthouse operation. + */ + export interface UniversalBaseArtifacts { /** The ISO-8601 timestamp of when the test page was fetched and artifacts collected. */ fetchTime: string; /** A set of warnings about unexpected things encountered while loading and testing the page. */ @@ -32,30 +37,42 @@ declare global { HostFormFactor: 'desktop'|'mobile'; /** The user agent string of the version of Chrome used. */ HostUserAgent: string; - /** The user agent string that Lighthouse used to load the page. */ - NetworkUserAgent: string; /** The benchmark index that indicates rough device class. */ BenchmarkIndex: number; - /** Parsed version of the page's Web App Manifest, or null if none found. */ - WebAppManifest: Artifacts.Manifest | null; - /** Errors preventing page being installable as PWA. */ - InstallabilityErrors: Artifacts.InstallabilityErrors; - /** Information on detected tech stacks (e.g. JS libraries) used by the page. */ - Stacks: Artifacts.DetectedStack[]; - /** A set of page-load traces, keyed by passName. */ - traces: {[passName: string]: Trace}; - /** A set of DevTools debugger protocol records, keyed by passName. */ - devtoolsLogs: {[passName: string]: DevtoolsLog}; /** An object containing information about the testing configuration used by Lighthouse. */ settings: Config.Settings; - /** The URL initially requested and the post-redirects URL that was actually loaded. */ - URL: {requestedUrl: string, finalUrl: string}; /** The timing instrumentation of the gather portion of a run. */ Timing: Artifacts.MeasureEntry[]; + } + + /** + * The set of base artifacts whose semantics differ or may be valueless in certain Lighthouse gather modes. + */ + export interface ContextualBaseArtifacts { + /** The URL initially requested and the post-redirects URL that was actually loaded. */ + URL: {requestedUrl: string, finalUrl: string}; /** If loading the page failed, value is the error that caused it. Otherwise null. */ PageLoadError: LighthouseError | null; } + /** + * The set of base artifacts that were replaced by standard gatherers in Fraggle Rock. + */ + export interface LegacyBaseArtifacts { + /** The user agent string that Lighthouse used to load the page. Set to the empty string if unknown. */ + NetworkUserAgent: string; + /** Information on detected tech stacks (e.g. JS libraries) used by the page. */ + Stacks: Artifacts.DetectedStack[]; + /** Parsed version of the page's Web App Manifest, or null if none found. This moved to a regular artifact in Fraggle Rock. */ + WebAppManifest: Artifacts.Manifest | null; + /** Errors preventing page being installable as PWA. This moved to a regular artifact in Fraggle Rock. */ + InstallabilityErrors: Artifacts.InstallabilityErrors; + /** A set of page-load traces, keyed by passName. */ + traces: {[passName: string]: Trace}; + /** A set of DevTools debugger protocol records, keyed by passName. */ + devtoolsLogs: {[passName: string]: DevtoolsLog}; + } + /** * Artifacts provided by the default gatherers that are exposed to plugins with a hardended API. * NOTE: any breaking changes here are considered breaking Lighthouse changes that must be done @@ -64,7 +81,7 @@ declare global { export interface PublicGathererArtifacts { /** ConsoleMessages deprecation and intervention warnings, console API calls, and exceptions logged by Chrome during page load. */ ConsoleMessages: Artifacts.ConsoleMessage[]; - /** All the iframe elements in the page.*/ + /** All the iframe elements in the page. */ IFrameElements: Artifacts.IFrameElement[]; /** The contents of the main HTML document network resource. */ MainDocumentContent: string; diff --git a/types/gatherer.d.ts b/types/gatherer.d.ts index ae735bb4a03e..6a064473ce71 100644 --- a/types/gatherer.d.ts +++ b/types/gatherer.d.ts @@ -33,8 +33,13 @@ declare global { /** The limited context interface shared between pre and post Fraggle Rock Lighthouse. */ export interface FRTransitionalContext { - gatherMode: GatherMode + /** The URL of the page that is currently active. Might be `about:blank` in the before phases */ + url: string; + /** The gather mode Lighthouse is currently in. */ + gatherMode: GatherMode; + /** The connection to the page being analyzed. */ driver: FRTransitionalDriver; + /** The set of available dependencies requested by the current gatherer. */ dependencies: TDependencies extends DefaultDependenciesKey ? {} : Pick>; @@ -58,7 +63,11 @@ declare global { trace?: Trace; } - export type PhaseArtifact = LH.GathererArtifacts[keyof LH.GathererArtifacts] | LH.Artifacts['devtoolsLogs'] | LH.Artifacts['traces'] + export type PhaseArtifact = LH.GathererArtifacts[keyof LH.GathererArtifacts] | + LH.Artifacts['devtoolsLogs'] | + LH.Artifacts['traces'] | + LH.Artifacts['WebAppManifest'] | + LH.Artifacts['InstallabilityErrors']; export type PhaseResultNonPromise = void|PhaseArtifact export type PhaseResult = PhaseResultNonPromise | Promise From 27224c789d49d718f5496b49d8fcafa1eb64cd48 Mon Sep 17 00:00:00 2001 From: Patrick Hulce Date: Tue, 23 Feb 2021 17:40:33 -0600 Subject: [PATCH 2/4] Update types/gatherer.d.ts Co-authored-by: Connor Clark --- types/gatherer.d.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/types/gatherer.d.ts b/types/gatherer.d.ts index 6a064473ce71..fe889235a075 100644 --- a/types/gatherer.d.ts +++ b/types/gatherer.d.ts @@ -63,7 +63,8 @@ declare global { trace?: Trace; } - export type PhaseArtifact = LH.GathererArtifacts[keyof LH.GathererArtifacts] | + export type PhaseArtifact = | + LH.GathererArtifacts[keyof LH.GathererArtifacts] | LH.Artifacts['devtoolsLogs'] | LH.Artifacts['traces'] | LH.Artifacts['WebAppManifest'] | From d08a6e8db22503387a898e8cc82507d4fa985472 Mon Sep 17 00:00:00 2001 From: Patrick Hulce Date: Wed, 24 Feb 2021 12:22:03 -0600 Subject: [PATCH 3/4] make browserify happy --- .../gather/gatherers/network-user-agent.js | 3 ++- lighthouse-core/gather/gatherers/stacks.js | 15 +++++++++++---- .../gather/gatherers/network-user-agent-test.js | 2 +- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/lighthouse-core/gather/gatherers/network-user-agent.js b/lighthouse-core/gather/gatherers/network-user-agent.js index ba2dcbe2333d..53584c62cb3f 100644 --- a/lighthouse-core/gather/gatherers/network-user-agent.js +++ b/lighthouse-core/gather/gatherers/network-user-agent.js @@ -6,13 +6,14 @@ 'use strict'; const FRGatherer = require('../../fraggle-rock/gather/base-gatherer.js'); +const DevtoolsLogGatherer = require('./devtools-log.js'); /** @implements {LH.Gatherer.FRGathererInstance<'DevtoolsLog'>} */ class NetworkUserAgent extends FRGatherer { /** @type {LH.Gatherer.GathererMeta<'DevtoolsLog'>} */ meta = { supportedModes: ['timespan', 'navigation'], - dependencies: {DevtoolsLog: require('./devtools-log.js').symbol}, + dependencies: {DevtoolsLog: DevtoolsLogGatherer.symbol}, }; /** diff --git a/lighthouse-core/gather/gatherers/stacks.js b/lighthouse-core/gather/gatherers/stacks.js index bf4d7ac371f6..9348dda4e979 100644 --- a/lighthouse-core/gather/gatherers/stacks.js +++ b/lighthouse-core/gather/gatherers/stacks.js @@ -79,10 +79,17 @@ async function detectLibraries() { /** @implements {LH.Gatherer.FRGathererInstance} */ class Stacks extends FRGatherer { - /** @type {LH.Gatherer.GathererMeta} */ - meta = { - supportedModes: ['snapshot', 'navigation'], - }; + constructor() { + super(); + + // Because this file uses `fs.readFile` it gets parsed by a different branch of the browserify internals + // that cannot handle the latest ECMAScript features. + // See https://github.com/GoogleChrome/lighthouse/issues/12134 + /** @type {LH.Gatherer.GathererMeta} */ + this.meta = { + supportedModes: ['snapshot', 'navigation'], + }; + } /** * @param {LH.Gatherer.FRTransitionalDriver['executionContext']} executionContext diff --git a/lighthouse-core/test/gather/gatherers/network-user-agent-test.js b/lighthouse-core/test/gather/gatherers/network-user-agent-test.js index 8c2b1c902060..bc6de1788521 100644 --- a/lighthouse-core/test/gather/gatherers/network-user-agent-test.js +++ b/lighthouse-core/test/gather/gatherers/network-user-agent-test.js @@ -8,9 +8,9 @@ /* eslint-env jest */ const NetworkUserAgent = require('../../../gather/gatherers/network-user-agent.js'); +/** @type {*} */ const devtoolsLog = require('../../fixtures/traces/lcp-m78.devtools.log.json'); - describe('.getNetworkUserAgent', () => { it('should return empty string when no network events available', async () => { const result = await NetworkUserAgent.getNetworkUserAgent([]); From c6b5ae132fbfb118cdb14c582eac84200f574bf3 Mon Sep 17 00:00:00 2001 From: Patrick Hulce Date: Thu, 25 Feb 2021 13:40:18 -0600 Subject: [PATCH 4/4] add typechecked artifact IDs --- .../fraggle-rock/config/default-config.js | 129 +++++++++++------- .../test/fraggle-rock/api-test-pptr.js | 4 +- types/artifacts.d.ts | 25 ++++ 3 files changed, 108 insertions(+), 50 deletions(-) diff --git a/lighthouse-core/fraggle-rock/config/default-config.js b/lighthouse-core/fraggle-rock/config/default-config.js index 04f734f0ec30..519b4f7d8c66 100644 --- a/lighthouse-core/fraggle-rock/config/default-config.js +++ b/lighthouse-core/fraggle-rock/config/default-config.js @@ -7,72 +7,105 @@ const legacyDefaultConfig = require('../../config/default-config.js'); +// Ensure all artifact IDs match the typedefs. +/** @type {Record} */ +const artifacts = { + DevtoolsLog: '', + Trace: '', + Accessibility: '', + AppCacheManifest: '', + CacheContents: '', + ConsoleMessages: '', + Doctype: '', + DOMStats: '', + EmbeddedContent: '', + FontSize: '', + FormElements: '', + GlobalListeners: '', + IFrameElements: '', + InstallabilityErrors: '', + MetaElements: '', + NetworkUserAgent: '', + PasswordInputsWithPreventedPaste: '', + RobotsTxt: '', + Stacks: '', + TapTargets: '', + ViewportDimensions: '', + WebAppManifest: '', + devtoolsLogs: '', + traces: '', +}; + +for (const key of Object.keys(artifacts)) { + artifacts[/** @type {keyof typeof artifacts} */ (key)] = key; +} + /** @type {LH.Config.Json} */ const defaultConfig = { artifacts: [ // Artifacts which can be depended on come first. - {id: 'DevtoolsLog', gatherer: 'devtools-log'}, - {id: 'Trace', gatherer: 'trace'}, + {id: artifacts.DevtoolsLog, gatherer: 'devtools-log'}, + {id: artifacts.Trace, gatherer: 'trace'}, /* eslint-disable max-len */ - {id: 'Accessibility', gatherer: 'accessibility'}, - {id: 'Appcache', gatherer: 'dobetterweb/appcache'}, - {id: 'CacheContents', gatherer: 'cache-contents'}, - {id: 'ConsoleMessages', gatherer: 'console-messages'}, - {id: 'Doctype', gatherer: 'dobetterweb/doctype'}, - {id: 'Domstats', gatherer: 'dobetterweb/domstats'}, - {id: 'EmbeddedContent', gatherer: 'seo/embedded-content'}, - {id: 'FontSize', gatherer: 'seo/font-size'}, - {id: 'FormElements', gatherer: 'form-elements'}, - {id: 'GlobalListeners', gatherer: 'global-listeners'}, - {id: 'IframeElements', gatherer: 'iframe-elements'}, - {id: 'InstallabillityErrors', gatherer: 'installability-errors'}, - {id: 'MetaElements', gatherer: 'meta-elements'}, - {id: 'NetworkUserAgent', gatherer: 'network-user-agent'}, - {id: 'PasswordInputsWithPreventedPaste', gatherer: 'dobetterweb/password-inputs-with-prevented-paste'}, - {id: 'RobotsTxt', gatherer: 'seo/robots-txt'}, - {id: 'Stacks', gatherer: 'stacks'}, - {id: 'TapTargets', gatherer: 'seo/tap-targets'}, - {id: 'ViewportDimensions', gatherer: 'viewport-dimensions'}, - {id: 'WebAppManifest', gatherer: 'web-app-manifest'}, + {id: artifacts.Accessibility, gatherer: 'accessibility'}, + {id: artifacts.AppCacheManifest, gatherer: 'dobetterweb/appcache'}, + {id: artifacts.CacheContents, gatherer: 'cache-contents'}, + {id: artifacts.ConsoleMessages, gatherer: 'console-messages'}, + {id: artifacts.Doctype, gatherer: 'dobetterweb/doctype'}, + {id: artifacts.DOMStats, gatherer: 'dobetterweb/domstats'}, + {id: artifacts.EmbeddedContent, gatherer: 'seo/embedded-content'}, + {id: artifacts.FontSize, gatherer: 'seo/font-size'}, + {id: artifacts.FormElements, gatherer: 'form-elements'}, + {id: artifacts.GlobalListeners, gatherer: 'global-listeners'}, + {id: artifacts.IFrameElements, gatherer: 'iframe-elements'}, + {id: artifacts.InstallabilityErrors, gatherer: 'installability-errors'}, + {id: artifacts.MetaElements, gatherer: 'meta-elements'}, + {id: artifacts.NetworkUserAgent, gatherer: 'network-user-agent'}, + {id: artifacts.PasswordInputsWithPreventedPaste, gatherer: 'dobetterweb/password-inputs-with-prevented-paste'}, + {id: artifacts.RobotsTxt, gatherer: 'seo/robots-txt'}, + {id: artifacts.Stacks, gatherer: 'stacks'}, + {id: artifacts.TapTargets, gatherer: 'seo/tap-targets'}, + {id: artifacts.ViewportDimensions, gatherer: 'viewport-dimensions'}, + {id: artifacts.WebAppManifest, gatherer: 'web-app-manifest'}, /* eslint-enable max-len */ // Artifact copies are renamed for compatibility with legacy artifacts. - {id: 'devtoolsLogs', gatherer: 'devtools-log-compat'}, - {id: 'traces', gatherer: 'trace-compat'}, + {id: artifacts.devtoolsLogs, gatherer: 'devtools-log-compat'}, + {id: artifacts.traces, gatherer: 'trace-compat'}, ], navigations: [ { id: 'default', artifacts: [ // Artifacts which can be depended on come first. - 'DevtoolsLog', - 'Trace', + artifacts.DevtoolsLog, + artifacts.Trace, - 'Accessibility', - 'Appcache', - 'CacheContents', - 'ConsoleMessages', - 'Doctype', - 'Domstats', - 'EmbeddedContent', - 'FontSize', - 'FormElements', - 'GlobalListeners', - 'IframeElements', - 'InstallabillityErrors', - 'MetaElements', - 'NetworkUserAgent', - 'PasswordInputsWithPreventedPaste', - 'RobotsTxt', - 'Stacks', - 'TapTargets', - 'ViewportDimensions', - 'WebAppManifest', + artifacts.Accessibility, + artifacts.AppCacheManifest, + artifacts.CacheContents, + artifacts.ConsoleMessages, + artifacts.Doctype, + artifacts.DOMStats, + artifacts.EmbeddedContent, + artifacts.FontSize, + artifacts.FormElements, + artifacts.GlobalListeners, + artifacts.IFrameElements, + artifacts.InstallabilityErrors, + artifacts.MetaElements, + artifacts.NetworkUserAgent, + artifacts.PasswordInputsWithPreventedPaste, + artifacts.RobotsTxt, + artifacts.Stacks, + artifacts.TapTargets, + artifacts.ViewportDimensions, + artifacts.WebAppManifest, // Compat artifacts come last. - 'devtoolsLogs', - 'traces', + artifacts.devtoolsLogs, + artifacts.traces, ], }, ], diff --git a/lighthouse-core/test/fraggle-rock/api-test-pptr.js b/lighthouse-core/test/fraggle-rock/api-test-pptr.js index 680cf3c78c0a..0a74df8518ce 100644 --- a/lighthouse-core/test/fraggle-rock/api-test-pptr.js +++ b/lighthouse-core/test/fraggle-rock/api-test-pptr.js @@ -95,7 +95,7 @@ describe('Fraggle Rock API', () => { const {auditResults, erroredAudits, failedAudits} = getAuditsBreakdown(lhr); // TODO(FR-COMPAT): This assertion can be removed when full compatibility is reached. - expect(auditResults.length).toMatchInlineSnapshot(`67`); + expect(auditResults.length).toMatchInlineSnapshot(`72`); expect(erroredAudits).toHaveLength(0); expect(failedAudits.map(audit => audit.id)).toContain('label'); @@ -159,7 +159,7 @@ describe('Fraggle Rock API', () => { const {lhr} = result; const {auditResults, failedAudits, erroredAudits} = getAuditsBreakdown(lhr); // TODO(FR-COMPAT): This assertion can be removed when full compatibility is reached. - expect(auditResults.length).toMatchInlineSnapshot(`97`); + expect(auditResults.length).toMatchInlineSnapshot(`102`); expect(erroredAudits).toHaveLength(0); const failedAuditIds = failedAudits.map(audit => audit.id); diff --git a/types/artifacts.d.ts b/types/artifacts.d.ts index 59bbe80487d8..21720620b809 100644 --- a/types/artifacts.d.ts +++ b/types/artifacts.d.ts @@ -18,6 +18,29 @@ declare global { module LH { export interface Artifacts extends BaseArtifacts, GathererArtifacts {} + export type FRArtifacts = StrictOmit; + /** * Artifacts always created by gathering. These artifacts are available to Lighthouse plugins. * NOTE: any breaking changes here are considered breaking Lighthouse changes that must be done @@ -25,6 +48,8 @@ declare global { */ export type BaseArtifacts = UniversalBaseArtifacts & ContextualBaseArtifacts & LegacyBaseArtifacts + export type FRBaseArtifacts = UniversalBaseArtifacts & ContextualBaseArtifacts; + /** * The set of base artifacts that are available in every mode of Lighthouse operation. */