lighthouse 9.3.1 → 9.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/report/bundle.esm.js +38 -24
  2. package/dist/report/flow.js +5 -5
  3. package/dist/report/standalone.js +9 -9
  4. package/flow-report/src/topbar.tsx +1 -1
  5. package/flow-report/src/wrappers/report.tsx +2 -1
  6. package/flow-report/test/topbar-test.tsx +1 -1
  7. package/lighthouse-cli/bin.js +1 -4
  8. package/lighthouse-cli/run.js +1 -1
  9. package/lighthouse-cli/test/smokehouse/core-tests.js +4 -5
  10. package/lighthouse-cli/test/smokehouse/readme.md +4 -0
  11. package/lighthouse-cli/test/smokehouse/report-assert.js +36 -0
  12. package/lighthouse-core/audits/autocomplete.js +34 -37
  13. package/lighthouse-core/audits/byte-efficiency/uses-long-cache-ttl.js +1 -0
  14. package/lighthouse-core/audits/seo/hreflang.js +2 -17
  15. package/lighthouse-core/config/default-config.js +2 -2
  16. package/lighthouse-core/fraggle-rock/config/default-config.js +3 -3
  17. package/lighthouse-core/fraggle-rock/gather/navigation-runner.js +50 -31
  18. package/lighthouse-core/fraggle-rock/gather/snapshot-runner.js +4 -5
  19. package/lighthouse-core/fraggle-rock/gather/timespan-runner.js +4 -5
  20. package/lighthouse-core/fraggle-rock/user-flow.js +8 -6
  21. package/lighthouse-core/gather/driver/navigation.js +29 -10
  22. package/lighthouse-core/gather/driver/network-monitor.js +8 -6
  23. package/lighthouse-core/gather/driver/prepare.js +13 -7
  24. package/lighthouse-core/gather/driver/wait-for-condition.js +11 -4
  25. package/lighthouse-core/gather/gather-runner.js +1 -1
  26. package/lighthouse-core/gather/gatherers/full-page-screenshot.js +35 -9
  27. package/lighthouse-core/gather/gatherers/inputs.js +113 -0
  28. package/lighthouse-core/index.js +3 -3
  29. package/lighthouse-core/lib/page-functions.js +11 -1
  30. package/lighthouse-core/lib/sentry.js +39 -24
  31. package/lighthouse-core/runner.js +79 -52
  32. package/lighthouse-core/util-commonjs.js +4 -0
  33. package/package.json +2 -3
  34. package/readme.md +1 -1
  35. package/report/assets/templates.html +2 -5
  36. package/report/renderer/components.js +3 -3
  37. package/report/renderer/dom.js +1 -3
  38. package/report/renderer/report-ui-features.js +25 -17
  39. package/report/renderer/topbar-features.js +5 -1
  40. package/report/renderer/util.js +4 -0
  41. package/report/test/renderer/__snapshots__/report-renderer-axe-test.js.snap +107 -0
  42. package/report/test/renderer/report-renderer-axe-test.js +38 -17
  43. package/report/test-assets/faux-psi.js +2 -1
  44. package/report/types/report-renderer.d.ts +21 -2
  45. package/shared/localization/locales/en-US.json +7 -1
  46. package/shared/localization/locales/en-XL.json +7 -1
  47. package/third-party/axe/LICENSE +362 -0
  48. package/third-party/axe/README.md +6 -0
  49. package/third-party/axe/valid-langs.js +110 -0
  50. package/third-party/snyk/snapshot.json +1 -0
  51. package/tsconfig.json +1 -0
  52. package/types/artifacts.d.ts +14 -14
  53. package/types/global-lh.d.ts +1 -0
  54. package/types/parse-cache-control/index.d.ts +1 -0
  55. package/types/smokehouse.d.ts +3 -0
  56. package/changelog.md +0 -5803
  57. package/lighthouse-core/gather/gatherers/form-elements.js +0 -113
  58. package/lighthouse-core/scripts/package.json +0 -4
@@ -16,7 +16,7 @@ import {saveFile} from '../../report/renderer/api';
16
16
 
17
17
  function saveHtml(flowResult: LH.FlowResult, htmlStr: string) {
18
18
  const blob = new Blob([htmlStr], {type: 'text/html'});
19
- const filename = getFlowResultFilenamePrefix(flowResult);
19
+ const filename = getFlowResultFilenamePrefix(flowResult) + '.html';
20
20
  saveFile(blob, filename);
21
21
  }
22
22
 
@@ -33,7 +33,8 @@ export const Report: FunctionComponent<{hashState: LH.FlowResult.HashState}> =
33
33
  ({hashState}) => {
34
34
  const ref = useExternalRenderer<HTMLDivElement>(() => {
35
35
  return renderReport(hashState.currentLhr, {
36
- disableAutoDarkModeAndFireworks: true,
36
+ disableFireworks: true,
37
+ disableDarkMode: true,
37
38
  omitTopbar: true,
38
39
  omitGlobalStyles: true,
39
40
  onPageAnchorRendered: link => convertAnchor(link, hashState.index),
@@ -56,7 +56,7 @@ it('save button opens save dialog for HTML file', async () => {
56
56
 
57
57
  expect(mockSaveFile).toHaveBeenCalledWith(
58
58
  expect.any(Blob),
59
- 'User-flow_2021-09-14_22-24-22'
59
+ 'User-flow_2021-09-14_22-24-22.html'
60
60
  );
61
61
  });
62
62
 
@@ -137,12 +137,9 @@ async function begin() {
137
137
  url: urlUnderTest,
138
138
  flags: cliFlags,
139
139
  environmentData: {
140
- name: 'redacted', // prevent sentry from using hostname
140
+ serverName: 'redacted', // prevent sentry from using hostname
141
141
  environment: isDev() ? 'development' : 'production',
142
142
  release: pkg.version,
143
- tags: {
144
- channel: 'cli',
145
- },
146
143
  },
147
144
  });
148
145
  }
@@ -208,7 +208,7 @@ async function runLighthouseWithFraggleRock(url, flags, config, launchedChrome)
208
208
  const page = await browser.newPage();
209
209
  flags.channel = 'fraggle-rock-cli';
210
210
  const configContext = {configPath: flags.configPath, settingsOverrides: flags};
211
- return fraggleRock.navigation({url, page, config, configContext});
211
+ return fraggleRock.navigation(url, {page, config, configContext});
212
212
  }
213
213
 
214
214
  /**
@@ -10,12 +10,11 @@ import byteEfficiency from './test-definitions/byte-efficiency.js';
10
10
  import byteGzip from './test-definitions/byte-gzip.js';
11
11
  import cspAllowAll from './test-definitions/csp-allow-all.js';
12
12
  import cspBlockAll from './test-definitions/csp-block-all.js';
13
- import cspBlockAllM91 from './test-definitions/csp-block-all-m91.js';
14
13
  import dbw from './test-definitions/dobetterweb.js';
15
14
  import errorsExpiredSsl from './test-definitions/errors-expired-ssl.js';
16
15
  import errorsIframeExpiredSsl from './test-definitions/errors-iframe-expired-ssl.js';
17
16
  import errorsInfiniteLoop from './test-definitions/errors-infinite-loop.js';
18
- // import formsAutoComplete from './test-definitions/forms-autocomplete.js';
17
+ import formsAutoComplete from './test-definitions/forms-autocomplete.js';
19
18
  import issuesMixedContent from './test-definitions/issues-mixed-content.js';
20
19
  import lanternFetch from './test-definitions/lantern-fetch.js';
21
20
  import lanternIdleCallbackLong from './test-definitions/lantern-idle-callback-long.js';
@@ -60,6 +59,7 @@ import seoPassing from './test-definitions/seo-passing.js';
60
59
  import seoStatus403 from './test-definitions/seo-status-403.js';
61
60
  import seoTapTargets from './test-definitions/seo-tap-targets.js';
62
61
  import sourceMaps from './test-definitions/source-maps.js';
62
+ import timing from './test-definitions/timing.js';
63
63
 
64
64
  /** @type {ReadonlyArray<Smokehouse.TestDfn>} */
65
65
  const smokeTests = [
@@ -68,13 +68,11 @@ const smokeTests = [
68
68
  byteGzip,
69
69
  cspAllowAll,
70
70
  cspBlockAll,
71
- cspBlockAllM91,
72
71
  dbw,
73
72
  errorsExpiredSsl,
74
73
  errorsIframeExpiredSsl,
75
74
  errorsInfiniteLoop,
76
- // TODO: restore when --enable-features=AutofillShowTypePredictions is not needed.
77
- // formsAutoComplete,
75
+ formsAutoComplete,
78
76
  issuesMixedContent,
79
77
  lanternOnline,
80
78
  lanternSetTimeout,
@@ -119,6 +117,7 @@ const smokeTests = [
119
117
  seoStatus403,
120
118
  seoTapTargets,
121
119
  sourceMaps,
120
+ timing,
122
121
  ];
123
122
 
124
123
  export default smokeTests;
@@ -64,11 +64,15 @@ Individual elements of an array can be asserted by using numeric properties in a
64
64
 
65
65
  However, if an array literal is used as the expectation, an extra condition is enforced that the actual array _must_ have the same length as the provided expected array.
66
66
 
67
+ Arrays can be checked against a subset of elements using the special `_includes` property. The value of `_includes` _must_ be an array. Each assertion in `_includes` will remove the matching item from consideration for the rest.
68
+
67
69
  **Examples**:
68
70
  | Actual | Expected | Result |
69
71
  | -- | -- | -- |
70
72
  | `[{url: 'http://badssl.com'}, {url: 'http://example.com'}]` | `{1: {url: 'http://example.com'}}` | ✅ PASS |
71
73
  | `[{timeInMs: 5}, {timeInMs: 15}]` | `{length: 2}` | ✅ PASS |
74
+ | `[{timeInMs: 5}, {timeInMs: 15}]` | `{_includes: [{timeInMs: 5}]}` | ✅ PASS |
75
+ | `[{timeInMs: 5}, {timeInMs: 15}]` | `{_includes: [{timeInMs: 5}, {timeInMs: 5}]}` | ❌ FAIL |
72
76
  | `[{timeInMs: 5}, {timeInMs: 15}]` | `[{timeInMs: 5}]` | ❌ FAIL |
73
77
 
74
78
  ### Special environment checks
@@ -111,6 +111,35 @@ function findDifference(path, actual, expected) {
111
111
  const keyPath = path + keyAccessor;
112
112
  const expectedValue = expected[key];
113
113
 
114
+ if (key === '_includes') {
115
+ if (!Array.isArray(expectedValue)) throw new Error('Array subset must be array');
116
+ if (!Array.isArray(actual)) {
117
+ return {
118
+ path,
119
+ actual: 'Actual value is not an array',
120
+ expected,
121
+ };
122
+ }
123
+
124
+ const actualCopy = [...actual];
125
+ for (const expectedEntry of expectedValue) {
126
+ const matchingIndex =
127
+ actualCopy.findIndex(actualEntry => !findDifference(keyPath, actualEntry, expectedEntry));
128
+ if (matchingIndex !== -1) {
129
+ actualCopy.splice(matchingIndex, 1);
130
+ continue;
131
+ }
132
+
133
+ return {
134
+ path,
135
+ actual: 'Item not found in array',
136
+ expected: expectedEntry,
137
+ };
138
+ }
139
+
140
+ continue;
141
+ }
142
+
114
143
  const actualValue = actual[key];
115
144
  const subDifference = findDifference(keyPath, actualValue, expectedValue);
116
145
 
@@ -305,6 +334,12 @@ function collateResults(localConsole, actual, expected) {
305
334
  return makeComparison(auditName + ' audit', actualResult, expectedResult);
306
335
  });
307
336
 
337
+ const timingAssertions = [];
338
+ if (expected.lhr.timing) {
339
+ const comparison = makeComparison('timing', actual.lhr.timing, expected.lhr.timing);
340
+ timingAssertions.push(comparison);
341
+ }
342
+
308
343
  /** @type {Comparison[]} */
309
344
  const requestCountAssertion = [];
310
345
  if (expected.networkRequests) {
@@ -322,6 +357,7 @@ function collateResults(localConsole, actual, expected) {
322
357
  ...requestCountAssertion,
323
358
  ...artifactAssertions,
324
359
  ...auditAssertions,
360
+ ...timingAssertions,
325
361
  ];
326
362
  }
327
363
 
@@ -195,12 +195,12 @@ class AutocompleteAudit extends Audit {
195
195
  title: str_(UIStrings.title),
196
196
  failureTitle: str_(UIStrings.failureTitle),
197
197
  description: str_(UIStrings.description),
198
- requiredArtifacts: ['FormElements'],
198
+ requiredArtifacts: ['Inputs'],
199
199
  };
200
200
  }
201
201
 
202
202
  /**
203
- * @param {LH.Artifacts.FormInput} input
203
+ * @param {LH.Artifacts.InputElement} input
204
204
  * @return {{hasValidTokens: boolean, isValidOrder?: boolean}}
205
205
  */
206
206
  static checkAttributeValidity(input) {
@@ -224,47 +224,44 @@ class AutocompleteAudit extends Audit {
224
224
  * @return {LH.Audit.Product}
225
225
  */
226
226
  static audit(artifacts) {
227
- const forms = artifacts.FormElements;
228
227
  const failingFormsData = [];
229
228
  const warnings = [];
230
229
  let foundPrediction = false;
231
- for (const form of forms) {
232
- for (const input of form.inputs) {
233
- const validity = this.checkAttributeValidity(input);
234
- if (validity.hasValidTokens && validity.isValidOrder) continue;
235
- if (!input.autocomplete.prediction) continue;
236
- if (noPrediction.includes(input.autocomplete.prediction) &&
237
- !input.autocomplete.attribute) continue;
230
+ for (const input of artifacts.Inputs.inputs) {
231
+ const validity = this.checkAttributeValidity(input);
232
+ if (validity.hasValidTokens && validity.isValidOrder) continue;
233
+ if (!input.autocomplete.prediction) continue;
234
+ if (noPrediction.includes(input.autocomplete.prediction) &&
235
+ !input.autocomplete.attribute) continue;
238
236
 
239
- foundPrediction = true;
237
+ foundPrediction = true;
240
238
 
241
- // @ts-ignore
242
- let suggestion = predictionTypesToTokens[input.autocomplete.prediction];
243
- // This is here to satisfy typescript because the possible null value of autocomplete.attribute is not compatible with Audit details.
244
- if (!input.autocomplete.attribute) input.autocomplete.attribute = '';
245
- // Warning is created because while there is an autocomplete attribute, the autocomplete property does not exsist, thus the attribute's value is invalid.
246
- if (input.autocomplete.attribute) {
247
- warnings.push(str_(UIStrings.warningInvalid, {token: input.autocomplete.attribute,
248
- snippet: input.node.snippet}));
249
- }
250
- if (validity.isValidOrder === false) {
251
- warnings.push(str_(UIStrings.warningOrder, {tokens: input.autocomplete.attribute,
252
- snippet: input.node.snippet}));
253
- suggestion = UIStrings.reviewOrder;
254
- }
255
- // If the autofill prediction is not in our autofill suggestion mapping, then we warn
256
- if (!(input.autocomplete.prediction in predictionTypesToTokens) &&
257
- validity.isValidOrder) {
258
- log.warn(`Autocomplete prediction (${input.autocomplete.prediction})
259
- not found in our mapping`);
260
- continue;
261
- }
262
- failingFormsData.push({
263
- node: Audit.makeNodeItem(input.node),
264
- suggestion: suggestion,
265
- current: input.autocomplete.attribute,
266
- });
239
+ // @ts-ignore
240
+ let suggestion = predictionTypesToTokens[input.autocomplete.prediction];
241
+ // This is here to satisfy typescript because the possible null value of autocomplete.attribute is not compatible with Audit details.
242
+ if (!input.autocomplete.attribute) input.autocomplete.attribute = '';
243
+ // Warning is created because while there is an autocomplete attribute, the autocomplete property does not exsist, thus the attribute's value is invalid.
244
+ if (input.autocomplete.attribute) {
245
+ warnings.push(str_(UIStrings.warningInvalid, {token: input.autocomplete.attribute,
246
+ snippet: input.node.snippet}));
267
247
  }
248
+ if (validity.isValidOrder === false) {
249
+ warnings.push(str_(UIStrings.warningOrder, {tokens: input.autocomplete.attribute,
250
+ snippet: input.node.snippet}));
251
+ suggestion = UIStrings.reviewOrder;
252
+ }
253
+ // If the autofill prediction is not in our autofill suggestion mapping, then we warn
254
+ if (!(input.autocomplete.prediction in predictionTypesToTokens) &&
255
+ validity.isValidOrder) {
256
+ log.warn(`Autocomplete prediction (${input.autocomplete.prediction})
257
+ not found in our mapping`);
258
+ continue;
259
+ }
260
+ failingFormsData.push({
261
+ node: Audit.makeNodeItem(input.node),
262
+ suggestion: suggestion,
263
+ current: input.autocomplete.attribute,
264
+ });
268
265
  }
269
266
 
270
267
  /** @type {LH.Audit.Details.Table['headings']} */
@@ -181,6 +181,7 @@ class CacheHeaders extends Audit {
181
181
  cacheControl['must-revalidate'] ||
182
182
  cacheControl['no-cache'] ||
183
183
  cacheControl['no-store'] ||
184
+ cacheControl['stale-while-revalidate'] ||
184
185
  cacheControl['private'])) {
185
186
  return true;
186
187
  }
@@ -11,9 +11,8 @@
11
11
 
12
12
  const Audit = require('../audit.js');
13
13
  const i18n = require('../../lib/i18n/i18n.js');
14
- const axeLibSource = require('../../lib/axe.js').source;
14
+ const {isValidLang} = require('../../../third-party/axe/valid-langs.js');
15
15
 
16
- const VALID_LANGS = importValidLangs();
17
16
  const NO_LANGUAGE = 'x-default';
18
17
 
19
18
  const UIStrings = {
@@ -33,20 +32,6 @@ const UIStrings = {
33
32
 
34
33
  const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings);
35
34
 
36
- /**
37
- * Import list of valid languages from axe core.
38
- * This is a huge array of language codes that can be stored more efficiently if we will need to
39
- * shrink the bundle size.
40
- * @return {Array<string>}
41
- */
42
- function importValidLangs() {
43
- // Define a window-ish object so axe will export to it.
44
- const window = {getComputedStyle: () => {}};
45
- eval(axeLibSource);
46
- // @ts-expect-error
47
- return window.axe.utils.validLangs();
48
- }
49
-
50
35
  /**
51
36
  * @param {string} href
52
37
  * @return {boolean}
@@ -66,7 +51,7 @@ function isExpectedLanguageCode(hreflang) {
66
51
 
67
52
  // hreflang can consist of language-script-region, we are validating only language
68
53
  const [lang] = hreflang.split('-');
69
- return VALID_LANGS.includes(lang.toLowerCase());
54
+ return isValidLang(lang.toLowerCase());
70
55
  }
71
56
 
72
57
  class Hreflang extends Audit {
@@ -39,7 +39,7 @@ const UIStrings = {
39
39
  /** Title of the Accessibility category of audits. This section contains audits focused on making web content accessible to all users. Also used as a label of a score gauge; try to limit to 20 characters. */
40
40
  a11yCategoryTitle: 'Accessibility',
41
41
  /** Description of the Accessibility category. This is displayed at the top of a list of audits focused on making web content accessible to all users. No character length limits. 'improve the accessibility of your web app' becomes link text to additional documentation. */
42
- a11yCategoryDescription: 'These checks highlight opportunities to [improve the accessibility of your web app](https://developers.google.com/web/fundamentals/accessibility). Only a subset of accessibility issues can be automatically detected so manual testing is also encouraged.',
42
+ a11yCategoryDescription: 'These checks highlight opportunities to [improve the accessibility of your web app](https://web.dev/lighthouse-accessibility/). Only a subset of accessibility issues can be automatically detected so manual testing is also encouraged.',
43
43
  /** Description of the Accessibility manual checks category. This description is displayed above a list of accessibility audits that currently have no automated test and so must be verified manually by the user. No character length limits. 'conducting an accessibility review' becomes link text to additional documentation. */
44
44
  a11yCategoryManualDescription: 'These items address areas which an automated testing tool cannot cover. Learn more in our guide on [conducting an accessibility review](https://developers.google.com/web/fundamentals/accessibility/how-to-review).',
45
45
  /** Title of the best practices section of the Accessibility category. Within this section are audits with descriptive titles that highlight common accessibility best practices. */
@@ -144,7 +144,7 @@ const defaultConfig = {
144
144
  'meta-elements',
145
145
  'script-elements',
146
146
  'iframe-elements',
147
- 'form-elements',
147
+ 'inputs',
148
148
  'main-document-content',
149
149
  'global-listeners',
150
150
  'dobetterweb/doctype',
@@ -46,7 +46,7 @@ const artifacts = {
46
46
  DOMStats: '',
47
47
  EmbeddedContent: '',
48
48
  FontSize: '',
49
- FormElements: '',
49
+ Inputs: '',
50
50
  FullPageScreenshot: '',
51
51
  GlobalListeners: '',
52
52
  IFrameElements: '',
@@ -96,7 +96,7 @@ const defaultConfig = {
96
96
  {id: artifacts.DOMStats, gatherer: 'dobetterweb/domstats'},
97
97
  {id: artifacts.EmbeddedContent, gatherer: 'seo/embedded-content'},
98
98
  {id: artifacts.FontSize, gatherer: 'seo/font-size'},
99
- {id: artifacts.FormElements, gatherer: 'form-elements'},
99
+ {id: artifacts.Inputs, gatherer: 'inputs'},
100
100
  {id: artifacts.FullPageScreenshot, gatherer: 'full-page-screenshot'},
101
101
  {id: artifacts.GlobalListeners, gatherer: 'global-listeners'},
102
102
  {id: artifacts.IFrameElements, gatherer: 'iframe-elements'},
@@ -148,7 +148,7 @@ const defaultConfig = {
148
148
  artifacts.DOMStats,
149
149
  artifacts.EmbeddedContent,
150
150
  artifacts.FontSize,
151
- artifacts.FormElements,
151
+ artifacts.Inputs,
152
152
  artifacts.GlobalListeners,
153
153
  artifacts.IFrameElements,
154
154
  artifacts.ImageElements,
@@ -35,7 +35,7 @@ const NetworkRecords = require('../../computed/network-records.js');
35
35
  * @property {Driver} driver
36
36
  * @property {LH.Config.FRConfig} config
37
37
  * @property {LH.Config.NavigationDefn} navigation
38
- * @property {string} requestedUrl
38
+ * @property {LH.NavigationRequestor} requestor
39
39
  * @property {LH.FRBaseArtifacts} baseArtifacts
40
40
  * @property {Map<string, LH.ArbitraryEqualityMap>} computedCache
41
41
  * @property {InternalOptions} [options]
@@ -44,17 +44,17 @@ const NetworkRecords = require('../../computed/network-records.js');
44
44
  /** @typedef {Omit<Parameters<typeof collectPhaseArtifacts>[0], 'phase'>} PhaseState */
45
45
 
46
46
  /**
47
- * @param {{driver: Driver, config: LH.Config.FRConfig, requestedUrl: string, options?: InternalOptions}} args
47
+ * @param {{driver: Driver, config: LH.Config.FRConfig, requestor: LH.NavigationRequestor, options?: InternalOptions}} args
48
48
  * @return {Promise<{baseArtifacts: LH.FRBaseArtifacts}>}
49
49
  */
50
- async function _setup({driver, config, requestedUrl, options}) {
50
+ async function _setup({driver, config, requestor, options}) {
51
51
  await driver.connect();
52
52
  if (!options?.skipAboutBlank) {
53
53
  await gotoURL(driver, defaultNavigationConfig.blankPage, {waitUntil: ['navigated']});
54
54
  }
55
55
 
56
56
  const baseArtifacts = await getBaseArtifacts(config, driver, {gatherMode: 'navigation'});
57
- baseArtifacts.URL.requestedUrl = requestedUrl;
57
+ if (typeof requestor === 'string') baseArtifacts.URL.requestedUrl = requestor;
58
58
 
59
59
  await prepare.prepareTargetForNavigationMode(driver, config.settings);
60
60
 
@@ -65,7 +65,7 @@ async function _setup({driver, config, requestedUrl, options}) {
65
65
  * @param {NavigationContext} navigationContext
66
66
  * @return {Promise<{warnings: Array<LH.IcuMessage>}>}
67
67
  */
68
- async function _setupNavigation({requestedUrl, driver, navigation, config, options}) {
68
+ async function _setupNavigation({requestor, driver, navigation, config, options}) {
69
69
  if (!options?.skipAboutBlank) {
70
70
  await gotoURL(driver, navigation.blankPage, {...navigation, waitUntil: ['navigated']});
71
71
  }
@@ -74,7 +74,7 @@ async function _setupNavigation({requestedUrl, driver, navigation, config, optio
74
74
  config.settings,
75
75
  {
76
76
  ...navigation,
77
- url: requestedUrl,
77
+ requestor,
78
78
  }
79
79
  );
80
80
 
@@ -90,25 +90,25 @@ async function _cleanupNavigation({driver}) {
90
90
 
91
91
  /**
92
92
  * @param {NavigationContext} navigationContext
93
- * @return {Promise<{finalUrl: string, navigationError: LH.LighthouseError | undefined, warnings: Array<LH.IcuMessage>}>}
93
+ * @return {Promise<{requestedUrl: string, finalUrl: string, navigationError: LH.LighthouseError | undefined, warnings: Array<LH.IcuMessage>}>}
94
94
  */
95
95
  async function _navigate(navigationContext) {
96
- const {driver, config, requestedUrl} = navigationContext;
96
+ const {driver, config, requestor} = navigationContext;
97
97
 
98
98
  try {
99
- const {finalUrl, warnings} = await gotoURL(driver, requestedUrl, {
99
+ const {requestedUrl, finalUrl, warnings} = await gotoURL(driver, requestor, {
100
100
  ...navigationContext.navigation,
101
101
  debugNavigation: config.settings.debugNavigation,
102
102
  maxWaitForFcp: config.settings.maxWaitForFcp,
103
103
  maxWaitForLoad: config.settings.maxWaitForLoad,
104
104
  waitUntil: navigationContext.navigation.pauseAfterFcpMs ? ['fcp', 'load'] : ['load'],
105
105
  });
106
-
107
- return {finalUrl, navigationError: undefined, warnings};
106
+ return {requestedUrl, finalUrl, navigationError: undefined, warnings};
108
107
  } catch (err) {
109
108
  if (!(err instanceof LighthouseError)) throw err;
110
109
  if (err.code !== 'NO_FCP' && err.code !== 'PAGE_HUNG') throw err;
111
- return {finalUrl: requestedUrl, navigationError: err, warnings: []};
110
+ if (typeof requestor !== 'string') throw err;
111
+ return {requestedUrl: requestor, finalUrl: requestor, navigationError: err, warnings: []};
112
112
  }
113
113
  }
114
114
 
@@ -152,7 +152,7 @@ async function _collectDebugData(navigationContext, phaseState) {
152
152
  * @param {PhaseState} phaseState
153
153
  * @param {Awaited<ReturnType<typeof _setupNavigation>>} setupResult
154
154
  * @param {Awaited<ReturnType<typeof _navigate>>} navigateResult
155
- * @return {Promise<{finalUrl: string, artifacts: Partial<LH.GathererArtifacts>, warnings: Array<LH.IcuMessage>, pageLoadError: LH.LighthouseError | undefined}>}
155
+ * @return {Promise<{requestedUrl: string, finalUrl: string, artifacts: Partial<LH.GathererArtifacts>, warnings: Array<LH.IcuMessage>, pageLoadError: LH.LighthouseError | undefined}>}
156
156
  */
157
157
  async function _computeNavigationResult(
158
158
  navigationContext,
@@ -174,7 +174,7 @@ async function _computeNavigationResult(
174
174
  if (pageLoadError) {
175
175
  const locale = navigationContext.config.settings.locale;
176
176
  const localizedMessage = format.getFormatted(pageLoadError.friendlyMessage, locale);
177
- log.error('NavigationRunner', localizedMessage, navigationContext.requestedUrl);
177
+ log.error('NavigationRunner', localizedMessage, navigateResult.requestedUrl);
178
178
 
179
179
  /** @type {Partial<LH.GathererArtifacts>} */
180
180
  const artifacts = {};
@@ -183,6 +183,7 @@ async function _computeNavigationResult(
183
183
  if (debugData.trace) artifacts.traces = {[pageLoadErrorId]: debugData.trace};
184
184
 
185
185
  return {
186
+ requestedUrl: navigateResult.requestedUrl,
186
187
  finalUrl,
187
188
  pageLoadError,
188
189
  artifacts,
@@ -192,7 +193,13 @@ async function _computeNavigationResult(
192
193
  await collectPhaseArtifacts({phase: 'getArtifact', ...phaseState});
193
194
 
194
195
  const artifacts = await awaitArtifacts(phaseState.artifactState);
195
- return {finalUrl, artifacts, warnings, pageLoadError: undefined};
196
+ return {
197
+ requestedUrl: navigateResult.requestedUrl,
198
+ finalUrl,
199
+ artifacts,
200
+ warnings,
201
+ pageLoadError: undefined,
202
+ };
196
203
  }
197
204
  }
198
205
 
@@ -224,10 +231,10 @@ async function _navigation(navigationContext) {
224
231
  }
225
232
 
226
233
  /**
227
- * @param {{driver: Driver, config: LH.Config.FRConfig, requestedUrl: string; baseArtifacts: LH.FRBaseArtifacts, computedCache: NavigationContext['computedCache'], options?: InternalOptions}} args
234
+ * @param {{driver: Driver, config: LH.Config.FRConfig, requestor: LH.NavigationRequestor; baseArtifacts: LH.FRBaseArtifacts, computedCache: NavigationContext['computedCache'], options?: InternalOptions}} args
228
235
  * @return {Promise<{artifacts: Partial<LH.FRArtifacts & LH.FRBaseArtifacts>}>}
229
236
  */
230
- async function _navigations({driver, config, requestedUrl, baseArtifacts, computedCache, options}) {
237
+ async function _navigations({driver, config, requestor, baseArtifacts, computedCache, options}) {
231
238
  if (!config.navigations) throw new Error('No navigations configured');
232
239
 
233
240
  /** @type {Partial<LH.FRArtifacts & LH.FRBaseArtifacts>} */
@@ -239,7 +246,7 @@ async function _navigations({driver, config, requestedUrl, baseArtifacts, comput
239
246
  const navigationContext = {
240
247
  driver,
241
248
  navigation,
242
- requestedUrl,
249
+ requestor,
243
250
  config,
244
251
  baseArtifacts,
245
252
  computedCache,
@@ -254,7 +261,10 @@ async function _navigations({driver, config, requestedUrl, baseArtifacts, comput
254
261
  shouldHaltNavigations = true;
255
262
  }
256
263
 
257
- artifacts.URL = {requestedUrl, finalUrl: navigationResult.finalUrl};
264
+ artifacts.URL = {
265
+ requestedUrl: navigationResult.requestedUrl,
266
+ finalUrl: navigationResult.finalUrl,
267
+ };
258
268
  }
259
269
 
260
270
  LighthouseRunWarnings.push(...navigationResult.warnings);
@@ -266,43 +276,52 @@ async function _navigations({driver, config, requestedUrl, baseArtifacts, comput
266
276
  }
267
277
 
268
278
  /**
269
- * @param {{requestedUrl: string, driver: Driver, config: LH.Config.FRConfig}} args
279
+ * @param {{requestedUrl?: string, driver: Driver, config: LH.Config.FRConfig}} args
270
280
  */
271
281
  async function _cleanup({requestedUrl, driver, config}) {
272
- const didResetStorage = !config.settings.disableStorageReset;
282
+ const didResetStorage = !config.settings.disableStorageReset && requestedUrl;
273
283
  if (didResetStorage) await storage.clearDataForOrigin(driver.defaultSession, requestedUrl);
274
284
 
275
285
  await driver.disconnect();
276
286
  }
277
287
 
278
288
  /**
279
- * @param {{url: string, page: import('puppeteer').Page, config?: LH.Config.Json, configContext?: LH.Config.FRContext}} options
289
+ * @param {LH.NavigationRequestor} requestor
290
+ * @param {{page: import('puppeteer').Page, config?: LH.Config.Json, configContext?: LH.Config.FRContext}} options
280
291
  * @return {Promise<LH.RunnerResult|undefined>}
281
292
  */
282
- async function navigation(options) {
283
- const {url, page, configContext = {}} = options;
293
+ async function navigation(requestor, options) {
294
+ const {page, configContext = {}} = options;
284
295
  const {config} = initializeConfig(options.config, {...configContext, gatherMode: 'navigation'});
285
296
  const computedCache = new Map();
286
297
  const internalOptions = {
287
298
  skipAboutBlank: configContext.skipAboutBlank,
288
299
  };
289
300
 
290
- return Runner.run(
301
+ // We can't trigger the navigation through user interaction if we reset the page before starting.
302
+ if (typeof requestor !== 'string') {
303
+ internalOptions.skipAboutBlank = true;
304
+ }
305
+
306
+ const runnerOptions = {config, computedCache};
307
+ const artifacts = await Runner.gather(
291
308
  async () => {
292
309
  const driver = new Driver(page);
293
- const requestedUrl = URL.normalizeUrl(url);
294
- const context = {driver, config, requestedUrl, options: internalOptions};
310
+ const context = {
311
+ driver,
312
+ config,
313
+ requestor: typeof requestor === 'string' ? URL.normalizeUrl(requestor) : requestor,
314
+ options: internalOptions,
315
+ };
295
316
  const {baseArtifacts} = await _setup(context);
296
317
  const {artifacts} = await _navigations({...context, baseArtifacts, computedCache});
297
318
  await _cleanup(context);
298
319
 
299
320
  return finalizeArtifacts(baseArtifacts, artifacts);
300
321
  },
301
- {
302
- config,
303
- computedCache: new Map(),
304
- }
322
+ runnerOptions
305
323
  );
324
+ return Runner.audit(artifacts, runnerOptions);
306
325
  }
307
326
 
308
327
  module.exports = {
@@ -26,7 +26,8 @@ async function snapshot(options) {
26
26
  const computedCache = new Map();
27
27
  const url = await options.page.url();
28
28
 
29
- return Runner.run(
29
+ const runnerOptions = {config, computedCache};
30
+ const artifacts = await Runner.gather(
30
31
  async () => {
31
32
  const baseArtifacts = await getBaseArtifacts(config, driver, {gatherMode: 'snapshot'});
32
33
  baseArtifacts.URL.requestedUrl = url;
@@ -50,11 +51,9 @@ async function snapshot(options) {
50
51
  const artifacts = await awaitArtifacts(artifactState);
51
52
  return finalizeArtifacts(baseArtifacts, artifacts);
52
53
  },
53
- {
54
- config,
55
- computedCache,
56
- }
54
+ runnerOptions
57
55
  );
56
+ return Runner.audit(artifacts, runnerOptions);
58
57
  }
59
58
 
60
59
  module.exports = {
@@ -50,7 +50,8 @@ async function startTimespan(options) {
50
50
  return {
51
51
  async endTimespan() {
52
52
  const finalUrl = await options.page.url();
53
- return Runner.run(
53
+ const runnerOptions = {config, computedCache};
54
+ const artifacts = await Runner.gather(
54
55
  async () => {
55
56
  baseArtifacts.URL.requestedUrl = requestedUrl;
56
57
  baseArtifacts.URL.finalUrl = finalUrl;
@@ -63,11 +64,9 @@ async function startTimespan(options) {
63
64
  const artifacts = await awaitArtifacts(artifactState);
64
65
  return finalizeArtifacts(baseArtifacts, artifacts);
65
66
  },
66
- {
67
- config,
68
- computedCache,
69
- }
67
+ runnerOptions
70
68
  );
69
+ return Runner.audit(artifacts, runnerOptions);
71
70
  },
72
71
  };
73
72
  }