lighthouse 9.5.0-dev.20230112 → 9.5.0-dev.20230114

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 (36) hide show
  1. package/cli/cli-flags.js +5 -1
  2. package/cli/test/smokehouse/core-tests.js +2 -0
  3. package/cli/test/smokehouse/report-assert.js +11 -7
  4. package/core/config/constants.js +1 -0
  5. package/core/config/default-config.js +0 -1
  6. package/core/config/filters.js +8 -4
  7. package/core/gather/gatherers/accessibility.js +15 -1
  8. package/core/gather/gatherers/full-page-screenshot.js +51 -40
  9. package/core/gather/gatherers/seo/tap-targets.js +39 -19
  10. package/core/legacy/config/config.js +8 -10
  11. package/core/legacy/gather/driver.js +0 -17
  12. package/core/legacy/gather/gather-runner.js +0 -6
  13. package/core/runner.js +2 -0
  14. package/core/util.cjs +30 -0
  15. package/dist/report/bundle.esm.js +40 -19
  16. package/dist/report/flow.js +5 -5
  17. package/dist/report/standalone.js +4 -4
  18. package/flow-report/src/common.tsx +3 -3
  19. package/flow-report/src/util.ts +0 -11
  20. package/flow-report/test/common-test.tsx +4 -7
  21. package/flow-report/test/summary/summary-test.tsx +1 -1
  22. package/package.json +1 -1
  23. package/readme.md +1 -0
  24. package/report/renderer/details-renderer.js +1 -2
  25. package/report/renderer/element-screenshot-renderer.js +4 -4
  26. package/report/renderer/report-renderer.js +3 -7
  27. package/report/renderer/report-ui-features.js +3 -7
  28. package/report/renderer/util.js +30 -0
  29. package/report/test/renderer/util-test.js +20 -0
  30. package/report/test-assets/faux-psi.js +3 -3
  31. package/types/artifacts.d.ts +1 -11
  32. package/types/lhr/audit-details.d.ts +0 -17
  33. package/types/lhr/lhr.d.ts +13 -0
  34. package/types/lhr/settings.d.ts +2 -0
  35. package/types/smokehouse.d.ts +2 -1
  36. package/core/audits/full-page-screenshot.js +0 -42
package/cli/cli-flags.js CHANGED
@@ -205,12 +205,16 @@ function getYargsParser(manualArgv) {
205
205
  type: 'string',
206
206
  describe: 'The path to the budget.json file for LightWallet.',
207
207
  },
208
+ 'disable-full-page-screenshot': {
209
+ type: 'boolean',
210
+ describe: 'Disables collection of the full page screenshot, which can be quite large',
211
+ },
208
212
  })
209
213
  .group([
210
214
  'save-assets', 'list-all-audits', 'list-locales', 'list-trace-categories', 'additional-trace-categories',
211
215
  'config-path', 'preset', 'chrome-flags', 'port', 'hostname', 'form-factor', 'screenEmulation', 'emulatedUserAgent',
212
216
  'max-wait-for-load', 'enable-error-reporting', 'gather-mode', 'audit-mode',
213
- 'only-audits', 'only-categories', 'skip-audits', 'budget-path',
217
+ 'only-audits', 'only-categories', 'skip-audits', 'budget-path', 'disable-full-page-screenshot',
214
218
  ], 'Configuration:')
215
219
 
216
220
  // Output
@@ -15,6 +15,7 @@ import errorsIframeExpiredSsl from './test-definitions/errors-iframe-expired-ssl
15
15
  import errorsInfiniteLoop from './test-definitions/errors-infinite-loop.js';
16
16
  import formsAutoComplete from './test-definitions/forms-autocomplete.js';
17
17
  import fpsMax from './test-definitions/fps-max.js';
18
+ import fpsMaxPassive from './test-definitions/fps-max-passive.js';
18
19
  import fpsScaled from './test-definitions/fps-scaled.js';
19
20
  import issuesMixedContent from './test-definitions/issues-mixed-content.js';
20
21
  import lanternFetch from './test-definitions/lantern-fetch.js';
@@ -79,6 +80,7 @@ const smokeTests = [
79
80
  formsAutoComplete,
80
81
  fpsMax,
81
82
  fpsScaled,
83
+ fpsMaxPassive,
82
84
  issuesMixedContent,
83
85
  lanternFetch,
84
86
  lanternIdleCallbackLong,
@@ -391,30 +391,34 @@ function collateResults(localConsole, actual, expected) {
391
391
  return makeComparison(auditName + ' audit', actualResult, expectedResult);
392
392
  });
393
393
 
394
- const timingAssertions = [];
394
+ /** @type {Comparison[]} */
395
+ const extraAssertions = [];
396
+
395
397
  if (expected.lhr.timing) {
396
398
  const comparison = makeComparison('timing', actual.lhr.timing, expected.lhr.timing);
397
- timingAssertions.push(comparison);
399
+ extraAssertions.push(comparison);
398
400
  }
399
401
 
400
- /** @type {Comparison[]} */
401
- const requestCountAssertion = [];
402
402
  if (expected.networkRequests) {
403
- requestCountAssertion.push(makeComparison(
403
+ extraAssertions.push(makeComparison(
404
404
  'Requests',
405
405
  actual.networkRequests,
406
406
  expected.networkRequests
407
407
  ));
408
408
  }
409
409
 
410
+ if (expected.lhr.fullPageScreenshot) {
411
+ extraAssertions.push(makeComparison('fullPageScreenshot', actual.lhr.fullPageScreenshot,
412
+ expected.lhr.fullPageScreenshot));
413
+ }
414
+
410
415
  return [
411
416
  makeComparison('final url', actual.lhr.finalDisplayedUrl, expected.lhr.finalDisplayedUrl),
412
417
  runtimeErrorAssertion,
413
418
  runWarningsAssertion,
414
- ...requestCountAssertion,
415
419
  ...artifactAssertions,
416
420
  ...auditAssertions,
417
- ...timingAssertions,
421
+ ...extraAssertions,
418
422
  ];
419
423
  }
420
424
 
@@ -111,6 +111,7 @@ const defaultSettings = {
111
111
  debugNavigation: false,
112
112
  channel: 'node',
113
113
  usePassiveGathering: false,
114
+ disableFullPageScreenshot: false,
114
115
  skipAboutBlank: false,
115
116
  blankPage: 'about:blank',
116
117
 
@@ -278,7 +278,6 @@ const defaultConfig = {
278
278
  'valid-source-maps',
279
279
  'preload-lcp-image',
280
280
  'csp-xss',
281
- 'full-page-screenshot',
282
281
  'script-treemap-data',
283
282
  'manual/pwa-cross-browser',
284
283
  'manual/pwa-page-transitions',
@@ -27,12 +27,13 @@ const baseArtifactKeys = Object.keys(baseArtifactKeySource);
27
27
 
28
28
  // Some audits are used by the report for additional information.
29
29
  // Keep these audits unless they are *directly* skipped with `skipAudits`.
30
- const filterResistantAuditIds = ['full-page-screenshot'];
30
+ /** @type {string[]} */
31
+ const filterResistantAuditIds = [];
31
32
 
32
33
  // Some artifacts are used by the report for additional information.
33
34
  // Always run these artifacts even if audits do not request them.
34
35
  // These are similar to base artifacts but they cannot be run in all 3 modes.
35
- const filterResistantArtifactIds = ['Stacks', 'NetworkUserAgent'];
36
+ const filterResistantArtifactIds = ['Stacks', 'NetworkUserAgent', 'FullPageScreenshot'];
36
37
 
37
38
  /**
38
39
  * Returns the set of audit IDs used in the list of categories.
@@ -302,7 +303,7 @@ function filterConfigByExplicitFilters(resolvedConfig, filters) {
302
303
  [
303
304
  ...baseAuditIds, // Start with our base audits.
304
305
  ...(onlyAudits || []), // Additionally include the opt-in audits from `onlyAudits`.
305
- ...filterResistantAuditIds, // Always include our filter-resistant audits (full-page-screenshot).
306
+ ...filterResistantAuditIds, // Always include any filter-resistant audits.
306
307
  ].filter(auditId => !skipAudits || !skipAudits.includes(auditId))
307
308
  );
308
309
 
@@ -313,7 +314,10 @@ function filterConfigByExplicitFilters(resolvedConfig, filters) {
313
314
  const availableCategories =
314
315
  filterCategoriesByAvailableAudits(resolvedConfig.categories, audits || []);
315
316
  const categories = filterCategoriesByExplicitFilters(availableCategories, onlyCategories);
316
- const artifacts = filterArtifactsByAvailableAudits(resolvedConfig.artifacts, audits);
317
+ let artifacts = filterArtifactsByAvailableAudits(resolvedConfig.artifacts, audits);
318
+ if (artifacts && resolvedConfig.settings.disableFullPageScreenshot) {
319
+ artifacts = artifacts.filter(({id}) => id !== 'FullPageScreenshot');
320
+ }
317
321
  const navigations =
318
322
  filterNavigationsByAvailableArtifacts(resolvedConfig.navigations, artifacts || []);
319
323
 
@@ -81,6 +81,19 @@ async function runA11yChecks() {
81
81
  };
82
82
  }
83
83
 
84
+ async function runA11yChecksAndResetScroll() {
85
+ const originalScrollPosition = {
86
+ x: window.scrollX,
87
+ y: window.scrollY,
88
+ };
89
+
90
+ try {
91
+ return await runA11yChecks();
92
+ } finally {
93
+ window.scrollTo(originalScrollPosition.x, originalScrollPosition.y);
94
+ }
95
+ }
96
+
84
97
  /**
85
98
  * @param {import('axe-core/axe').Result} result
86
99
  * @return {LH.Artifacts.AxeRuleResult}
@@ -168,13 +181,14 @@ class Accessibility extends FRGatherer {
168
181
  getArtifact(passContext) {
169
182
  const driver = passContext.driver;
170
183
 
171
- return driver.executionContext.evaluate(runA11yChecks, {
184
+ return driver.executionContext.evaluate(runA11yChecksAndResetScroll, {
172
185
  args: [],
173
186
  useIsolation: true,
174
187
  deps: [
175
188
  axeSource,
176
189
  pageFunctions.getNodeDetails,
177
190
  createAxeRuleResultArtifact,
191
+ runA11yChecks,
178
192
  ],
179
193
  });
180
194
  }
@@ -47,7 +47,7 @@ function getObservedDeviceMetrics() {
47
47
  }
48
48
 
49
49
  /**
50
- * The screenshot dimensions are sized to `window.outerHeight` / `window.innerWidth`,
50
+ * The screenshot dimensions are sized to `window.outerHeight` / `window.outerWidth`,
51
51
  * however the bounding boxes of the elements are relative to `window.innerHeight` / `window.innerWidth`.
52
52
  */
53
53
  function getScreenshotAreaSize() {
@@ -74,9 +74,8 @@ class FullPageScreenshot extends FRGatherer {
74
74
  /**
75
75
  * @param {LH.Gatherer.FRTransitionalContext} context
76
76
  * @param {{height: number, width: number, mobile: boolean}} deviceMetrics
77
- * @return {Promise<LH.Artifacts.FullPageScreenshot['screenshot']>}
78
77
  */
79
- async _takeScreenshot(context, deviceMetrics) {
78
+ async _resizeViewport(context, deviceMetrics) {
80
79
  const session = context.driver.defaultSession;
81
80
  const metrics = await session.sendCommand('Page.getLayoutMetrics');
82
81
 
@@ -118,8 +117,14 @@ class FullPageScreenshot extends FRGatherer {
118
117
 
119
118
  // Now that new resources are (probably) fetched, wait long enough for a layout.
120
119
  await context.driver.executionContext.evaluate(waitForDoubleRaf, {args: []});
120
+ }
121
121
 
122
- const result = await session.sendCommand('Page.captureScreenshot', {
122
+ /**
123
+ * @param {LH.Gatherer.FRTransitionalContext} context
124
+ * @return {Promise<LH.Result.FullPageScreenshot['screenshot']>}
125
+ */
126
+ async _takeScreenshot(context) {
127
+ const result = await context.driver.defaultSession.sendCommand('Page.captureScreenshot', {
123
128
  format: 'webp',
124
129
  quality: FULL_PAGE_SCREENSHOT_QUALITY,
125
130
  });
@@ -129,8 +134,8 @@ class FullPageScreenshot extends FRGatherer {
129
134
  await context.driver.executionContext.evaluate(getScreenshotAreaSize, {
130
135
  args: [],
131
136
  useIsolation: true,
132
- deps: [kebabCaseToCamelCase],
133
137
  });
138
+
134
139
  return {
135
140
  data,
136
141
  width: screenshotAreaSize.width,
@@ -146,11 +151,11 @@ class FullPageScreenshot extends FRGatherer {
146
151
  * to re-collect the bounding client rectangle.
147
152
  * @see pageFunctions.getNodeDetails
148
153
  * @param {LH.Gatherer.FRTransitionalContext} context
149
- * @return {Promise<LH.Artifacts.FullPageScreenshot['nodes']>}
154
+ * @return {Promise<LH.Result.FullPageScreenshot['nodes']>}
150
155
  */
151
156
  async _resolveNodes(context) {
152
157
  function resolveNodes() {
153
- /** @type {LH.Artifacts.FullPageScreenshot['nodes']} */
158
+ /** @type {LH.Result.FullPageScreenshot['nodes']} */
154
159
  const nodes = {};
155
160
  if (!window.__lighthouseNodesDontTouchOrAllVarianceGoesAway) return nodes;
156
161
 
@@ -197,44 +202,50 @@ class FullPageScreenshot extends FRGatherer {
197
202
  /** @type {{width: number, height: number, deviceScaleFactor: number, mobile: boolean}} */
198
203
  const deviceMetrics = {...settings.screenEmulation};
199
204
 
200
- // In case some other program is controlling emulation, remember what the device looks like now and reset after gatherer is done.
201
- // If we're gathering with mobile screenEmulation on (overlay scrollbars, etc), continue to use that for this screenshot.
202
- if (!lighthouseControlsEmulation) {
203
- const observedDeviceMetrics = await executionContext.evaluate(getObservedDeviceMetrics, {
204
- args: [],
205
- useIsolation: true,
206
- deps: [kebabCaseToCamelCase],
207
- });
208
- deviceMetrics.height = observedDeviceMetrics.height;
209
- deviceMetrics.width = observedDeviceMetrics.width;
210
- deviceMetrics.deviceScaleFactor = observedDeviceMetrics.deviceScaleFactor;
211
- // If screen emulation is disabled, use formFactor to determine if we are on mobile.
212
- deviceMetrics.mobile = settings.formFactor === 'mobile';
213
- }
214
-
215
205
  try {
206
+ if (!settings.usePassiveGathering) {
207
+ // In case some other program is controlling emulation, remember what the device looks like now and reset after gatherer is done.
208
+ // If we're gathering with mobile screenEmulation on (overlay scrollbars, etc), continue to use that for this screenshot.
209
+ if (!lighthouseControlsEmulation) {
210
+ const observedDeviceMetrics = await executionContext.evaluate(getObservedDeviceMetrics, {
211
+ args: [],
212
+ useIsolation: true,
213
+ deps: [kebabCaseToCamelCase],
214
+ });
215
+ deviceMetrics.height = observedDeviceMetrics.height;
216
+ deviceMetrics.width = observedDeviceMetrics.width;
217
+ deviceMetrics.deviceScaleFactor = observedDeviceMetrics.deviceScaleFactor;
218
+ // If screen emulation is disabled, use formFactor to determine if we are on mobile.
219
+ deviceMetrics.mobile = settings.formFactor === 'mobile';
220
+ }
221
+
222
+ await this._resizeViewport(context, deviceMetrics);
223
+ }
224
+
216
225
  return {
217
- screenshot: await this._takeScreenshot(context, deviceMetrics),
226
+ screenshot: await this._takeScreenshot(context),
218
227
  nodes: await this._resolveNodes(context),
219
228
  };
220
229
  } finally {
221
- // Revert resized page.
222
- if (lighthouseControlsEmulation) {
223
- await emulation.emulate(session, settings);
224
- } else {
225
- // Best effort to reset emulation to what it was.
226
- // https://github.com/GoogleChrome/lighthouse/pull/10716#discussion_r428970681
227
- // TODO: seems like this would be brittle. Should at least work for devtools, but what
228
- // about scripted puppeteer usages? Better to introduce a "setEmulation" callback
229
- // in the LH runner api, which for ex. puppeteer consumers would setup puppeteer emulation,
230
- // and then just call that to reset?
231
- // https://github.com/GoogleChrome/lighthouse/issues/11122
232
- await session.sendCommand('Emulation.setDeviceMetricsOverride', {
233
- mobile: deviceMetrics.mobile,
234
- deviceScaleFactor: deviceMetrics.deviceScaleFactor,
235
- height: deviceMetrics.height,
236
- width: 0, // Leave width unchanged
237
- });
230
+ if (!settings.usePassiveGathering) {
231
+ // Revert resized page.
232
+ if (lighthouseControlsEmulation) {
233
+ await emulation.emulate(session, settings);
234
+ } else {
235
+ // Best effort to reset emulation to what it was.
236
+ // https://github.com/GoogleChrome/lighthouse/pull/10716#discussion_r428970681
237
+ // TODO: seems like this would be brittle. Should at least work for devtools, but what
238
+ // about scripted puppeteer usages? Better to introduce a "setEmulation" callback
239
+ // in the LH runner api, which for ex. puppeteer consumers would setup puppeteer emulation,
240
+ // and then just call that to reset?
241
+ // https://github.com/GoogleChrome/lighthouse/issues/11122
242
+ await session.sendCommand('Emulation.setDeviceMetricsOverride', {
243
+ mobile: deviceMetrics.mobile,
244
+ deviceScaleFactor: deviceMetrics.deviceScaleFactor,
245
+ height: deviceMetrics.height,
246
+ width: 0, // Leave width unchanged
247
+ });
248
+ }
238
249
  }
239
250
  }
240
251
  }
@@ -281,6 +281,24 @@ function gatherTapTargets(tapTargetsSelector, className) {
281
281
 
282
282
  return targets;
283
283
  }
284
+
285
+ /**
286
+ * @param {string} tapTargetsSelector
287
+ * @param {string} className
288
+ * @return {LH.Artifacts.TapTarget[]}
289
+ */
290
+ function gatherTapTargetsAndResetScroll(tapTargetsSelector, className) {
291
+ const originalScrollPosition = {
292
+ x: window.scrollX,
293
+ y: window.scrollY,
294
+ };
295
+
296
+ try {
297
+ return gatherTapTargets(tapTargetsSelector, className);
298
+ } finally {
299
+ window.scrollTo(originalScrollPosition.x, originalScrollPosition.y);
300
+ }
301
+ }
284
302
  /* c8 ignore stop */
285
303
 
286
304
  class TapTargets extends FRGatherer {
@@ -337,25 +355,27 @@ class TapTargets extends FRGatherer {
337
355
  const className = 'lighthouse-disable-pointer-events';
338
356
  const styleSheetId = await this.addStyleRule(session, className);
339
357
 
340
- const tapTargets = await passContext.driver.executionContext.evaluate(gatherTapTargets, {
341
- args: [tapTargetsSelector, className],
342
- useIsolation: true,
343
- deps: [
344
- pageFunctions.getNodeDetails,
345
- pageFunctions.getElementsInDocument,
346
- disableFixedAndStickyElementPointerEvents,
347
- elementIsVisible,
348
- elementHasAncestorTapTarget,
349
- elementCenterIsAtZAxisTop,
350
- getClientRects,
351
- hasTextNodeSiblingsFormingTextBlock,
352
- elementIsInTextBlock,
353
- RectHelpers.getRectCenterPoint,
354
- pageFunctions.getNodePath,
355
- pageFunctions.getNodeSelector,
356
- pageFunctions.getNodeLabel,
357
- ],
358
- });
358
+ const tapTargets =
359
+ await passContext.driver.executionContext.evaluate(gatherTapTargetsAndResetScroll, {
360
+ args: [tapTargetsSelector, className],
361
+ useIsolation: true,
362
+ deps: [
363
+ pageFunctions.getNodeDetails,
364
+ pageFunctions.getElementsInDocument,
365
+ disableFixedAndStickyElementPointerEvents,
366
+ elementIsVisible,
367
+ elementHasAncestorTapTarget,
368
+ elementCenterIsAtZAxisTop,
369
+ getClientRects,
370
+ hasTextNodeSiblingsFormingTextBlock,
371
+ elementIsInTextBlock,
372
+ RectHelpers.getRectCenterPoint,
373
+ pageFunctions.getNodePath,
374
+ pageFunctions.getNodeSelector,
375
+ pageFunctions.getNodeLabel,
376
+ gatherTapTargets,
377
+ ],
378
+ });
359
379
 
360
380
  await this.removeStyleRule(session, styleSheetId);
361
381
 
@@ -79,7 +79,7 @@ function assertValidPasses(passes, audits) {
79
79
  const gatherer = gathererDefn.instance;
80
80
  foundGatherers.add(gatherer.name);
81
81
  const isGatherRequiredByAudits = requestedGatherers.has(gatherer.name);
82
- if (!isGatherRequiredByAudits) {
82
+ if (!isGatherRequiredByAudits && gatherer.name !== 'FullPageScreenshot') {
83
83
  const msg = `${gatherer.name} gatherer requested, however no audit requires it.`;
84
84
  log.warn('config', msg);
85
85
  }
@@ -313,7 +313,8 @@ class LegacyResolvedConfig {
313
313
  */
314
314
  static filterConfigIfNeeded(config) {
315
315
  const settings = config.settings;
316
- if (!settings.onlyCategories && !settings.onlyAudits && !settings.skipAudits) {
316
+ // eslint-disable-next-line max-len
317
+ if (!settings.onlyCategories && !settings.onlyAudits && !settings.skipAudits && !settings.disableFullPageScreenshot) {
317
318
  return;
318
319
  }
319
320
 
@@ -328,6 +329,11 @@ class LegacyResolvedConfig {
328
329
  // 3. Resolve which gatherers will need to run
329
330
  const requestedGathererIds = LegacyResolvedConfig.getGatherersRequestedByAudits(audits);
330
331
 
332
+ // Always include FullPageScreenshot, unless explictly told not to.
333
+ if (!settings.disableFullPageScreenshot) {
334
+ requestedGathererIds.add('FullPageScreenshot');
335
+ }
336
+
331
337
  // 4. Filter to only the neccessary passes
332
338
  const passes =
333
339
  LegacyResolvedConfig.generatePassesNeededByGatherers(config.passes, requestedGathererIds);
@@ -413,14 +419,6 @@ class LegacyResolvedConfig {
413
419
  }
414
420
  });
415
421
 
416
- // The `full-page-screenshot` audit belongs to no category, but we still want to include
417
- // it (unless explictly excluded) because there are audits in every category that can use it.
418
- const explicitlyExcludesFullPageScreenshot =
419
- settings.skipAudits && settings.skipAudits.includes('full-page-screenshot');
420
- if (!explicitlyExcludesFullPageScreenshot && (settings.onlyCategories || settings.skipAudits)) {
421
- includedAudits.add('full-page-screenshot');
422
- }
423
-
424
422
  return {categories, requestedAuditNames: includedAudits};
425
423
  }
426
424
 
@@ -405,23 +405,6 @@ class Driver {
405
405
  return fetchResponseBodyFromCache(this.defaultSession, requestId, timeout);
406
406
  }
407
407
 
408
- /**
409
- * @param {{x: number, y: number}} position
410
- * @return {Promise<void>}
411
- */
412
- scrollTo(position) {
413
- const scrollExpression = `window.scrollTo(${position.x}, ${position.y})`;
414
- return this.executionContext.evaluateAsync(scrollExpression, {useIsolation: true});
415
- }
416
-
417
- /**
418
- * @return {Promise<{x: number, y: number}>}
419
- */
420
- getScrollPosition() {
421
- return this.executionContext.evaluateAsync(`({x: window.scrollX, y: window.scrollY})`,
422
- {useIsolation: true});
423
- }
424
-
425
408
  /**
426
409
  * @param {{additionalTraceCategories?: string|null}=} settings
427
410
  * @return {Promise<void>}
@@ -313,17 +313,12 @@ class GatherRunner {
313
313
  * @return {Promise<void>}
314
314
  */
315
315
  static async afterPass(passContext, loadData, gathererResults) {
316
- const driver = passContext.driver;
317
316
  const config = passContext.passConfig;
318
317
  const gatherers = config.gatherers;
319
318
 
320
319
  const apStatus = {msg: `Running afterPass methods`, id: `lh:gather:afterPass`};
321
320
  log.time(apStatus, 'verbose');
322
321
 
323
- // Some gatherers scroll the page which can cause unexpected results for other gatherers.
324
- // We reset the scroll position in between each gatherer.
325
- const scrollPosition = await driver.getScrollPosition();
326
-
327
322
  for (const gathererDefn of gatherers) {
328
323
  const gatherer = gathererDefn.instance;
329
324
  const status = {
@@ -339,7 +334,6 @@ class GatherRunner {
339
334
  gathererResult.push(artifactPromise);
340
335
  gathererResults[gatherer.name] = gathererResult;
341
336
  await artifactPromise.catch(() => {});
342
- await driver.scrollTo(scrollPosition);
343
337
  log.timeEnd(status);
344
338
  }
345
339
  log.timeEnd(apStatus);
package/core/runner.js CHANGED
@@ -110,6 +110,8 @@ class Runner {
110
110
  categories,
111
111
  categoryGroups: resolvedConfig.groups || undefined,
112
112
  stackPacks: stackPacks.getStackPacks(artifacts.Stacks),
113
+ fullPageScreenshot: resolvedConfig.settings.disableFullPageScreenshot ?
114
+ undefined : artifacts.FullPageScreenshot,
113
115
  timing: this._getTiming(artifacts),
114
116
  i18n: {
115
117
  rendererFormattedStrings: format.getRendererFormattedStrings(settings.locale),
package/core/util.cjs CHANGED
@@ -216,9 +216,39 @@ class Util {
216
216
  });
217
217
  }
218
218
 
219
+ // In 10.0, full-page-screenshot became a top-level property on the LHR.
220
+ if (clone.audits['full-page-screenshot']) {
221
+ const details = /** @type {LH.Result.FullPageScreenshot=} */ (
222
+ clone.audits['full-page-screenshot'].details);
223
+ if (details) {
224
+ clone.fullPageScreenshot = {
225
+ screenshot: details.screenshot,
226
+ nodes: details.nodes,
227
+ };
228
+ } else {
229
+ clone.fullPageScreenshot = null;
230
+ }
231
+ delete clone.audits['full-page-screenshot'];
232
+ }
233
+
219
234
  return clone;
220
235
  }
221
236
 
237
+ /**
238
+ * @param {LH.Result} lhr
239
+ * @return {LH.Result.FullPageScreenshot=}
240
+ */
241
+ static getFullPageScreenshot(lhr) {
242
+ if (lhr.fullPageScreenshot) {
243
+ return lhr.fullPageScreenshot;
244
+ }
245
+
246
+ // Prior to 10.0.
247
+ const details = /** @type {LH.Result.FullPageScreenshot=} */ (
248
+ lhr.audits['full-page-screenshot']?.details);
249
+ return details;
250
+ }
251
+
222
252
  /**
223
253
  * Used to determine if the "passed" for the purposes of showing up in the "failed" or "passed"
224
254
  * sections of the report.
@@ -212,9 +212,39 @@ class Util {
212
212
  });
213
213
  }
214
214
 
215
+ // In 10.0, full-page-screenshot became a top-level property on the LHR.
216
+ if (clone.audits['full-page-screenshot']) {
217
+ const details = /** @type {LH.Result.FullPageScreenshot=} */ (
218
+ clone.audits['full-page-screenshot'].details);
219
+ if (details) {
220
+ clone.fullPageScreenshot = {
221
+ screenshot: details.screenshot,
222
+ nodes: details.nodes,
223
+ };
224
+ } else {
225
+ clone.fullPageScreenshot = null;
226
+ }
227
+ delete clone.audits['full-page-screenshot'];
228
+ }
229
+
215
230
  return clone;
216
231
  }
217
232
 
233
+ /**
234
+ * @param {LH.Result} lhr
235
+ * @return {LH.Result.FullPageScreenshot=}
236
+ */
237
+ static getFullPageScreenshot(lhr) {
238
+ if (lhr.fullPageScreenshot) {
239
+ return lhr.fullPageScreenshot;
240
+ }
241
+
242
+ // Prior to 10.0.
243
+ const details = /** @type {LH.Result.FullPageScreenshot=} */ (
244
+ lhr.audits['full-page-screenshot']?.details);
245
+ return details;
246
+ }
247
+
218
248
  /**
219
249
  * Used to determine if the "passed" for the purposes of showing up in the "failed" or "passed"
220
250
  * sections of the report.
@@ -2636,7 +2666,7 @@ const CRCRenderer = CriticalRequestChainRenderer;
2636
2666
  */
2637
2667
 
2638
2668
  /**
2639
- * @param {LH.Audit.Details.FullPageScreenshot['screenshot']} screenshot
2669
+ * @param {LH.Result.FullPageScreenshot['screenshot']} screenshot
2640
2670
  * @param {LH.Audit.Details.Rect} rect
2641
2671
  * @return {boolean}
2642
2672
  */
@@ -2742,7 +2772,7 @@ class ElementScreenshotRenderer {
2742
2772
  * Allows for multiple Lighthouse reports to be rendered on the page, each with their
2743
2773
  * own full page screenshot.
2744
2774
  * @param {HTMLElement} el
2745
- * @param {LH.Audit.Details.FullPageScreenshot['screenshot']} screenshot
2775
+ * @param {LH.Result.FullPageScreenshot['screenshot']} screenshot
2746
2776
  */
2747
2777
  static installFullPageScreenshot(el, screenshot) {
2748
2778
  el.style.setProperty('--element-screenshot-url', `url('${screenshot.data}')`);
@@ -2824,7 +2854,7 @@ class ElementScreenshotRenderer {
2824
2854
  * Used to render both the thumbnail preview in details tables and the full-page screenshot in the lightbox.
2825
2855
  * Returns null if element rect is outside screenshot bounds.
2826
2856
  * @param {DOM} dom
2827
- * @param {LH.Audit.Details.FullPageScreenshot['screenshot']} screenshot
2857
+ * @param {LH.Result.FullPageScreenshot['screenshot']} screenshot
2828
2858
  * @param {Rect} elementRectSC Region of screenshot to highlight.
2829
2859
  * @param {Size} maxRenderSizeDC e.g. maxThumbnailSize or maxLightboxSize.
2830
2860
  * @return {Element|null}
@@ -2919,7 +2949,7 @@ const URL_PREFIXES = ['http://', 'https://', 'data:'];
2919
2949
  class DetailsRenderer {
2920
2950
  /**
2921
2951
  * @param {DOM} dom
2922
- * @param {{fullPageScreenshot?: LH.Audit.Details.FullPageScreenshot}} [options]
2952
+ * @param {{fullPageScreenshot?: LH.Result.FullPageScreenshot}} [options]
2923
2953
  */
2924
2954
  constructor(dom, options = {}) {
2925
2955
  this._dom = dom;
@@ -2945,7 +2975,6 @@ class DetailsRenderer {
2945
2975
  // Internal-only details, not for rendering.
2946
2976
  case 'screenshot':
2947
2977
  case 'debugdata':
2948
- case 'full-page-screenshot':
2949
2978
  case 'treemap-data':
2950
2979
  return null;
2951
2980
 
@@ -4529,12 +4558,8 @@ class ReportRenderer {
4529
4558
  Util.i18n = i18n;
4530
4559
  Util.reportJson = report;
4531
4560
 
4532
- const fullPageScreenshot =
4533
- report.audits['full-page-screenshot']?.details &&
4534
- report.audits['full-page-screenshot'].details.type === 'full-page-screenshot' ?
4535
- report.audits['full-page-screenshot'].details : undefined;
4536
4561
  const detailsRenderer = new DetailsRenderer(this._dom, {
4537
- fullPageScreenshot,
4562
+ fullPageScreenshot: report.fullPageScreenshot ?? undefined,
4538
4563
  });
4539
4564
 
4540
4565
  const categoryRenderer = new CategoryRenderer(this._dom, detailsRenderer);
@@ -4605,9 +4630,9 @@ class ReportRenderer {
4605
4630
  reportSection.append(this._renderReportFooter(report));
4606
4631
  reportContainer.append(headerContainer, reportSection);
4607
4632
 
4608
- if (fullPageScreenshot) {
4633
+ if (report.fullPageScreenshot) {
4609
4634
  ElementScreenshotRenderer.installFullPageScreenshot(
4610
- this._dom.rootEl, fullPageScreenshot.screenshot);
4635
+ this._dom.rootEl, report.fullPageScreenshot.screenshot);
4611
4636
  }
4612
4637
 
4613
4638
  return reportFragment;
@@ -5466,6 +5491,7 @@ class ReportUIFeatures {
5466
5491
  */
5467
5492
  initFeatures(lhr) {
5468
5493
  this.json = lhr;
5494
+ this._fullPageScreenshot = Util.getFullPageScreenshot(lhr);
5469
5495
 
5470
5496
  if (this._topbar) {
5471
5497
  this._topbar.enable(lhr);
@@ -5705,18 +5731,13 @@ class ReportUIFeatures {
5705
5731
  * @param {Element} rootEl
5706
5732
  */
5707
5733
  _setupElementScreenshotOverlay(rootEl) {
5708
- const fullPageScreenshot =
5709
- this.json.audits['full-page-screenshot'] &&
5710
- this.json.audits['full-page-screenshot'].details &&
5711
- this.json.audits['full-page-screenshot'].details.type === 'full-page-screenshot' &&
5712
- this.json.audits['full-page-screenshot'].details;
5713
- if (!fullPageScreenshot) return;
5734
+ if (!this._fullPageScreenshot) return;
5714
5735
 
5715
5736
  ElementScreenshotRenderer.installOverlayFeature({
5716
5737
  dom: this._dom,
5717
5738
  rootEl: rootEl,
5718
5739
  overlayContainerEl: rootEl,
5719
- fullPageScreenshot,
5740
+ fullPageScreenshot: this._fullPageScreenshot,
5720
5741
  });
5721
5742
  }
5722
5743