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.
@@ -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
- return Runner.run(
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
- 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
  }
@@ -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 gatherFn = () => {
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.run(gatherFn, options);
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 {(runnerData: {config: TConfig, driverMock?: Driver}) => Promise<LH.Artifacts>} gatherFn
32
- * @param {{config: TConfig, computedCache: Map<string, ArbitraryEqualityMap>, driverMock?: Driver}} runOpts
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 run(gatherFn, runOpts) {
36
- const settings = runOpts.config.settings;
35
+ static async audit(artifacts, options) {
36
+ const {config, computedCache} = options;
37
+ const settings = config.settings;
38
+
37
39
  try {
38
- const runnerStatus = {msg: 'Runner setup', id: 'lh:runner:run'};
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
- // Audit phase
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, runOpts.config.audits, artifacts,
64
- lighthouseRunWarnings, runOpts.computedCache);
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 (runOpts.config.categories) {
86
- categories = ReportScoring.scoreAllCategories(runOpts.config.categories, auditResultsById);
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: runOpts.config.groups || undefined,
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
- // i18n LighthouseError strings.
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 gatherAndManageArtifacts(gatherFn, options) {
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
- let artifacts;
162
- if (settings.auditMode && !settings.gatherMode) {
163
- // No browser required, just load the artifacts from disk.
164
- const path = this._getDataSavePath(settings);
165
- artifacts = assetSaver.loadArtifacts(path);
166
- const requestedUrl = artifacts.URL.requestedUrl;
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
- // -G means save these to disk (e.g. ./latest-run).
178
- if (settings.gatherMode) {
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
- await assetSaver.saveArtifacts(artifacts, path);
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
- return artifacts;
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
- // As entries can share a name, dedupe based on the startTime timestamp
201
- ].map(entry => /** @type {[number, PerformanceEntry]} */ ([entry.startTime, entry]));
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 runnerEntry = timingEntries.find(e => e.name === 'lh:runner:run');
216
- return {entries: timingEntries, total: runnerEntry?.duration || 0};
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lighthouse",
3
- "version": "9.3.1-dev.20220202",
3
+ "version": "9.3.1-dev.20220203",
4
4
  "description": "Automated auditing, performance metrics, and best practices for the web.",
5
5
  "main": "./lighthouse-core/index.js",
6
6
  "bin": {
@@ -20,6 +20,9 @@ declare global {
20
20
  code?: any;
21
21
  message?: any;
22
22
  };
23
+ timing?: {
24
+ entries?: any
25
+ }
23
26
  }
24
27
 
25
28
  export type ExpectedRunnerResult = {