lighthouse 9.3.1-dev.20220202 → 9.3.1-dev.20220203
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/lighthouse-cli/test/smokehouse/core-tests.js +2 -0
- package/lighthouse-cli/test/smokehouse/readme.md +4 -0
- package/lighthouse-cli/test/smokehouse/report-assert.js +36 -0
- package/lighthouse-core/fraggle-rock/gather/navigation-runner.js +4 -5
- 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/index.js +3 -3
- package/lighthouse-core/runner.js +79 -52
- package/package.json +1 -1
- package/types/smokehouse.d.ts +3 -0
|
@@ -60,6 +60,7 @@ import seoPassing from './test-definitions/seo-passing.js';
|
|
|
60
60
|
import seoStatus403 from './test-definitions/seo-status-403.js';
|
|
61
61
|
import seoTapTargets from './test-definitions/seo-tap-targets.js';
|
|
62
62
|
import sourceMaps from './test-definitions/source-maps.js';
|
|
63
|
+
import timing from './test-definitions/timing.js';
|
|
63
64
|
|
|
64
65
|
/** @type {ReadonlyArray<Smokehouse.TestDfn>} */
|
|
65
66
|
const smokeTests = [
|
|
@@ -119,6 +120,7 @@ const smokeTests = [
|
|
|
119
120
|
seoStatus403,
|
|
120
121
|
seoTapTargets,
|
|
121
122
|
sourceMaps,
|
|
123
|
+
timing,
|
|
122
124
|
];
|
|
123
125
|
|
|
124
126
|
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
|
|
|
@@ -287,7 +287,8 @@ async function navigation(options) {
|
|
|
287
287
|
skipAboutBlank: configContext.skipAboutBlank,
|
|
288
288
|
};
|
|
289
289
|
|
|
290
|
-
|
|
290
|
+
const runnerOptions = {config, computedCache};
|
|
291
|
+
const artifacts = await Runner.gather(
|
|
291
292
|
async () => {
|
|
292
293
|
const driver = new Driver(page);
|
|
293
294
|
const requestedUrl = URL.normalizeUrl(url);
|
|
@@ -298,11 +299,9 @@ async function navigation(options) {
|
|
|
298
299
|
|
|
299
300
|
return finalizeArtifacts(baseArtifacts, artifacts);
|
|
300
301
|
},
|
|
301
|
-
|
|
302
|
-
config,
|
|
303
|
-
computedCache: new Map(),
|
|
304
|
-
}
|
|
302
|
+
runnerOptions
|
|
305
303
|
);
|
|
304
|
+
return Runner.audit(artifacts, runnerOptions);
|
|
306
305
|
}
|
|
307
306
|
|
|
308
307
|
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
|
}
|
package/lighthouse-core/index.js
CHANGED
|
@@ -47,11 +47,11 @@ async function lighthouse(url, flags = {}, configJSON, userConnection) {
|
|
|
47
47
|
const connection = userConnection || new ChromeProtocol(flags.port, flags.hostname);
|
|
48
48
|
|
|
49
49
|
// kick off a lighthouse run
|
|
50
|
-
const
|
|
50
|
+
const artifacts = await Runner.gather(() => {
|
|
51
51
|
const requestedUrl = URL.normalizeUrl(url);
|
|
52
52
|
return Runner._gatherArtifactsFromBrowser(requestedUrl, options, connection);
|
|
53
|
-
};
|
|
54
|
-
return Runner.
|
|
53
|
+
}, options);
|
|
54
|
+
return Runner.audit(artifacts, options);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/**
|
|
@@ -28,14 +28,16 @@ const {version: lighthouseVersion} = require('../package.json');
|
|
|
28
28
|
class Runner {
|
|
29
29
|
/**
|
|
30
30
|
* @template {LH.Config.Config | LH.Config.FRConfig} TConfig
|
|
31
|
-
* @param {
|
|
32
|
-
* @param {{config: TConfig, computedCache: Map<string, ArbitraryEqualityMap
|
|
31
|
+
* @param {LH.Artifacts} artifacts
|
|
32
|
+
* @param {{config: TConfig, driverMock?: Driver, computedCache: Map<string, ArbitraryEqualityMap>}} options
|
|
33
33
|
* @return {Promise<LH.RunnerResult|undefined>}
|
|
34
34
|
*/
|
|
35
|
-
static async
|
|
36
|
-
const
|
|
35
|
+
static async audit(artifacts, options) {
|
|
36
|
+
const {config, computedCache} = options;
|
|
37
|
+
const settings = config.settings;
|
|
38
|
+
|
|
37
39
|
try {
|
|
38
|
-
const runnerStatus = {msg: '
|
|
40
|
+
const runnerStatus = {msg: 'Audit phase', id: 'lh:runner:audit'};
|
|
39
41
|
log.time(runnerStatus, 'verbose');
|
|
40
42
|
|
|
41
43
|
/**
|
|
@@ -44,24 +46,14 @@ class Runner {
|
|
|
44
46
|
*/
|
|
45
47
|
const lighthouseRunWarnings = [];
|
|
46
48
|
|
|
47
|
-
const sentryContext = Sentry.getContext();
|
|
48
|
-
Sentry.captureBreadcrumb({
|
|
49
|
-
message: 'Run started',
|
|
50
|
-
category: 'lifecycle',
|
|
51
|
-
data: sentryContext?.extra,
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
const artifacts = await this.gatherAndManageArtifacts(gatherFn, runOpts);
|
|
55
|
-
|
|
56
49
|
// Potentially quit early
|
|
57
50
|
if (settings.gatherMode && !settings.auditMode) return;
|
|
58
51
|
|
|
59
|
-
|
|
60
|
-
if (!runOpts.config.audits) {
|
|
52
|
+
if (!config.audits) {
|
|
61
53
|
throw new Error('No audits to evaluate.');
|
|
62
54
|
}
|
|
63
|
-
const auditResultsById = await Runner._runAudits(settings,
|
|
64
|
-
lighthouseRunWarnings,
|
|
55
|
+
const auditResultsById = await Runner._runAudits(settings, config.audits, artifacts,
|
|
56
|
+
lighthouseRunWarnings, computedCache);
|
|
65
57
|
|
|
66
58
|
// LHR construction phase
|
|
67
59
|
const resultsStatus = {msg: 'Generating results...', id: 'lh:runner:generate'};
|
|
@@ -82,8 +74,8 @@ class Runner {
|
|
|
82
74
|
|
|
83
75
|
/** @type {Record<string, LH.RawIcu<LH.Result.Category>>} */
|
|
84
76
|
let categories = {};
|
|
85
|
-
if (
|
|
86
|
-
categories = ReportScoring.scoreAllCategories(
|
|
77
|
+
if (config.categories) {
|
|
78
|
+
categories = ReportScoring.scoreAllCategories(config.categories, auditResultsById);
|
|
87
79
|
}
|
|
88
80
|
|
|
89
81
|
log.timeEnd(resultsStatus);
|
|
@@ -108,7 +100,7 @@ class Runner {
|
|
|
108
100
|
audits: auditResultsById,
|
|
109
101
|
configSettings: settings,
|
|
110
102
|
categories,
|
|
111
|
-
categoryGroups:
|
|
103
|
+
categoryGroups: config.groups || undefined,
|
|
112
104
|
stackPacks: stackPacks.getStackPacks(artifacts.Stacks),
|
|
113
105
|
timing: this._getTiming(artifacts),
|
|
114
106
|
i18n: {
|
|
@@ -134,12 +126,7 @@ class Runner {
|
|
|
134
126
|
|
|
135
127
|
return {lhr, artifacts, report};
|
|
136
128
|
} catch (err) {
|
|
137
|
-
|
|
138
|
-
if (err.friendlyMessage) {
|
|
139
|
-
err.friendlyMessage = format.getFormatted(err.friendlyMessage, settings.locale);
|
|
140
|
-
}
|
|
141
|
-
await Sentry.captureException(err, {level: 'fatal'});
|
|
142
|
-
throw err;
|
|
129
|
+
throw Runner.createRunnerError(err, settings);
|
|
143
130
|
}
|
|
144
131
|
}
|
|
145
132
|
|
|
@@ -150,38 +137,72 @@ class Runner {
|
|
|
150
137
|
*
|
|
151
138
|
* @template {LH.Config.Config | LH.Config.FRConfig} TConfig
|
|
152
139
|
* @param {(runnerData: {config: TConfig, driverMock?: Driver}) => Promise<LH.Artifacts>} gatherFn
|
|
153
|
-
* @param {{config: TConfig, driverMock?: Driver}} options
|
|
140
|
+
* @param {{config: TConfig, driverMock?: Driver, computedCache: Map<string, ArbitraryEqualityMap>}} options
|
|
154
141
|
* @return {Promise<LH.Artifacts>}
|
|
155
142
|
*/
|
|
156
|
-
static async
|
|
143
|
+
static async gather(gatherFn, options) {
|
|
157
144
|
const settings = options.config.settings;
|
|
158
145
|
|
|
159
|
-
// Gather phase
|
|
160
146
|
// Either load saved artifacts from disk or from the browser.
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
if (!requestedUrl) {
|
|
169
|
-
throw new Error('Cannot run audit mode on empty URL');
|
|
170
|
-
}
|
|
171
|
-
} else {
|
|
172
|
-
artifacts = await gatherFn({
|
|
173
|
-
config: options.config,
|
|
174
|
-
driverMock: options.driverMock,
|
|
147
|
+
try {
|
|
148
|
+
const sentryContext = Sentry.getContext();
|
|
149
|
+
Sentry.captureBreadcrumb({
|
|
150
|
+
message: 'Run started',
|
|
151
|
+
category: 'lifecycle',
|
|
152
|
+
data: sentryContext?.extra,
|
|
175
153
|
});
|
|
176
154
|
|
|
177
|
-
|
|
178
|
-
|
|
155
|
+
/** @type {LH.Artifacts} */
|
|
156
|
+
let artifacts;
|
|
157
|
+
if (settings.auditMode && !settings.gatherMode) {
|
|
158
|
+
// No browser required, just load the artifacts from disk.
|
|
179
159
|
const path = this._getDataSavePath(settings);
|
|
180
|
-
|
|
160
|
+
artifacts = assetSaver.loadArtifacts(path);
|
|
161
|
+
const requestedUrl = artifacts.URL.requestedUrl;
|
|
162
|
+
|
|
163
|
+
if (!requestedUrl) {
|
|
164
|
+
throw new Error('Cannot run audit mode on empty URL');
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
const runnerStatus = {msg: 'Gather phase', id: 'lh:runner:gather'};
|
|
168
|
+
log.time(runnerStatus, 'verbose');
|
|
169
|
+
|
|
170
|
+
artifacts = await gatherFn({
|
|
171
|
+
config: options.config,
|
|
172
|
+
driverMock: options.driverMock,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
log.timeEnd(runnerStatus);
|
|
176
|
+
|
|
177
|
+
// If `gather` is run multiple times before `audit`, the timing entries for each `gather` can pollute one another.
|
|
178
|
+
// We need to clear the timing entries at the end of gathering.
|
|
179
|
+
// Set artifacts.Timing again to ensure lh:runner:gather is included.
|
|
180
|
+
artifacts.Timing = log.takeTimeEntries();
|
|
181
|
+
|
|
182
|
+
// -G means save these to disk (e.g. ./latest-run).
|
|
183
|
+
if (settings.gatherMode) {
|
|
184
|
+
const path = this._getDataSavePath(settings);
|
|
185
|
+
await assetSaver.saveArtifacts(artifacts, path);
|
|
186
|
+
}
|
|
181
187
|
}
|
|
188
|
+
|
|
189
|
+
return artifacts;
|
|
190
|
+
} catch (err) {
|
|
191
|
+
throw Runner.createRunnerError(err, settings);
|
|
182
192
|
}
|
|
193
|
+
}
|
|
183
194
|
|
|
184
|
-
|
|
195
|
+
/**
|
|
196
|
+
* @param {any} err
|
|
197
|
+
* @param {LH.Config.Settings} settings
|
|
198
|
+
*/
|
|
199
|
+
static createRunnerError(err, settings) {
|
|
200
|
+
// i18n LighthouseError strings.
|
|
201
|
+
if (err.friendlyMessage) {
|
|
202
|
+
err.friendlyMessage = format.getFormatted(err.friendlyMessage, settings.locale);
|
|
203
|
+
}
|
|
204
|
+
Sentry.captureException(err, {level: 'fatal'});
|
|
205
|
+
return err;
|
|
185
206
|
}
|
|
186
207
|
|
|
187
208
|
/**
|
|
@@ -197,8 +218,11 @@ class Runner {
|
|
|
197
218
|
const timingEntriesKeyValues = [
|
|
198
219
|
...timingEntriesFromArtifacts,
|
|
199
220
|
...timingEntriesFromRunner,
|
|
200
|
-
|
|
201
|
-
|
|
221
|
+
].map(entry => /** @type {[string, PerformanceEntry]} */ ([
|
|
222
|
+
// As entries can share a name and start time, dedupe based on the name, startTime and duration
|
|
223
|
+
`${entry.startTime}-${entry.name}-${entry.duration}`,
|
|
224
|
+
entry,
|
|
225
|
+
]));
|
|
202
226
|
const timingEntries = Array.from(new Map(timingEntriesKeyValues).values())
|
|
203
227
|
// Truncate timestamps to hundredths of a millisecond saves ~4KB. No need for microsecond
|
|
204
228
|
// resolution.
|
|
@@ -212,8 +236,11 @@ class Runner {
|
|
|
212
236
|
entryType: entry.entryType,
|
|
213
237
|
};
|
|
214
238
|
}).sort((a, b) => a.startTime - b.startTime);
|
|
215
|
-
const
|
|
216
|
-
|
|
239
|
+
const gatherEntry = timingEntries.find(e => e.name === 'lh:runner:gather');
|
|
240
|
+
const auditEntry = timingEntries.find(e => e.name === 'lh:runner:audit');
|
|
241
|
+
const gatherTiming = gatherEntry?.duration || 0;
|
|
242
|
+
const auditTiming = auditEntry?.duration || 0;
|
|
243
|
+
return {entries: timingEntries, total: gatherTiming + auditTiming};
|
|
217
244
|
}
|
|
218
245
|
|
|
219
246
|
/**
|
package/package.json
CHANGED