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.
- package/dist/report/bundle.esm.js +38 -24
- package/dist/report/flow.js +5 -5
- package/dist/report/standalone.js +9 -9
- package/flow-report/src/topbar.tsx +1 -1
- package/flow-report/src/wrappers/report.tsx +2 -1
- package/flow-report/test/topbar-test.tsx +1 -1
- package/lighthouse-cli/bin.js +1 -4
- package/lighthouse-cli/run.js +1 -1
- package/lighthouse-cli/test/smokehouse/core-tests.js +4 -5
- package/lighthouse-cli/test/smokehouse/readme.md +4 -0
- package/lighthouse-cli/test/smokehouse/report-assert.js +36 -0
- package/lighthouse-core/audits/autocomplete.js +34 -37
- package/lighthouse-core/audits/byte-efficiency/uses-long-cache-ttl.js +1 -0
- package/lighthouse-core/audits/seo/hreflang.js +2 -17
- package/lighthouse-core/config/default-config.js +2 -2
- package/lighthouse-core/fraggle-rock/config/default-config.js +3 -3
- package/lighthouse-core/fraggle-rock/gather/navigation-runner.js +50 -31
- package/lighthouse-core/fraggle-rock/gather/snapshot-runner.js +4 -5
- package/lighthouse-core/fraggle-rock/gather/timespan-runner.js +4 -5
- package/lighthouse-core/fraggle-rock/user-flow.js +8 -6
- package/lighthouse-core/gather/driver/navigation.js +29 -10
- package/lighthouse-core/gather/driver/network-monitor.js +8 -6
- package/lighthouse-core/gather/driver/prepare.js +13 -7
- package/lighthouse-core/gather/driver/wait-for-condition.js +11 -4
- package/lighthouse-core/gather/gather-runner.js +1 -1
- package/lighthouse-core/gather/gatherers/full-page-screenshot.js +35 -9
- package/lighthouse-core/gather/gatherers/inputs.js +113 -0
- package/lighthouse-core/index.js +3 -3
- package/lighthouse-core/lib/page-functions.js +11 -1
- package/lighthouse-core/lib/sentry.js +39 -24
- package/lighthouse-core/runner.js +79 -52
- package/lighthouse-core/util-commonjs.js +4 -0
- package/package.json +2 -3
- package/readme.md +1 -1
- package/report/assets/templates.html +2 -5
- package/report/renderer/components.js +3 -3
- package/report/renderer/dom.js +1 -3
- package/report/renderer/report-ui-features.js +25 -17
- package/report/renderer/topbar-features.js +5 -1
- package/report/renderer/util.js +4 -0
- package/report/test/renderer/__snapshots__/report-renderer-axe-test.js.snap +107 -0
- package/report/test/renderer/report-renderer-axe-test.js +38 -17
- package/report/test-assets/faux-psi.js +2 -1
- package/report/types/report-renderer.d.ts +21 -2
- package/shared/localization/locales/en-US.json +7 -1
- package/shared/localization/locales/en-XL.json +7 -1
- package/third-party/axe/LICENSE +362 -0
- package/third-party/axe/README.md +6 -0
- package/third-party/axe/valid-langs.js +110 -0
- package/third-party/snyk/snapshot.json +1 -0
- package/tsconfig.json +1 -0
- package/types/artifacts.d.ts +14 -14
- package/types/global-lh.d.ts +1 -0
- package/types/parse-cache-control/index.d.ts +1 -0
- package/types/smokehouse.d.ts +3 -0
- package/changelog.md +0 -5803
- package/lighthouse-core/gather/gatherers/form-elements.js +0 -113
- 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
|
-
|
|
36
|
+
disableFireworks: true,
|
|
37
|
+
disableDarkMode: true,
|
|
37
38
|
omitTopbar: true,
|
|
38
39
|
omitGlobalStyles: true,
|
|
39
40
|
onPageAnchorRendered: link => convertAnchor(link, hashState.index),
|
package/lighthouse-cli/bin.js
CHANGED
|
@@ -137,12 +137,9 @@ async function begin() {
|
|
|
137
137
|
url: urlUnderTest,
|
|
138
138
|
flags: cliFlags,
|
|
139
139
|
environmentData: {
|
|
140
|
-
|
|
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
|
}
|
package/lighthouse-cli/run.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
-
|
|
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: ['
|
|
198
|
+
requiredArtifacts: ['Inputs'],
|
|
199
199
|
};
|
|
200
200
|
}
|
|
201
201
|
|
|
202
202
|
/**
|
|
203
|
-
* @param {LH.Artifacts.
|
|
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
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
237
|
+
foundPrediction = true;
|
|
240
238
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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']} */
|
|
@@ -11,9 +11,8 @@
|
|
|
11
11
|
|
|
12
12
|
const Audit = require('../audit.js');
|
|
13
13
|
const i18n = require('../../lib/i18n/i18n.js');
|
|
14
|
-
const
|
|
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
|
|
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://
|
|
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
|
-
'
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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 {
|
|
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,
|
|
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,
|
|
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 =
|
|
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({
|
|
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
|
-
|
|
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,
|
|
96
|
+
const {driver, config, requestor} = navigationContext;
|
|
97
97
|
|
|
98
98
|
try {
|
|
99
|
-
const {finalUrl, warnings} = await gotoURL(driver,
|
|
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
|
-
|
|
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,
|
|
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 {
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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 = {
|
|
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
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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
|
|
294
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|