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.
- package/cli/cli-flags.js +5 -1
- package/cli/test/smokehouse/core-tests.js +2 -0
- package/cli/test/smokehouse/report-assert.js +11 -7
- package/core/config/constants.js +1 -0
- package/core/config/default-config.js +0 -1
- package/core/config/filters.js +8 -4
- package/core/gather/gatherers/accessibility.js +15 -1
- package/core/gather/gatherers/full-page-screenshot.js +51 -40
- package/core/gather/gatherers/seo/tap-targets.js +39 -19
- package/core/legacy/config/config.js +8 -10
- package/core/legacy/gather/driver.js +0 -17
- package/core/legacy/gather/gather-runner.js +0 -6
- package/core/runner.js +2 -0
- package/core/util.cjs +30 -0
- package/dist/report/bundle.esm.js +40 -19
- package/dist/report/flow.js +5 -5
- package/dist/report/standalone.js +4 -4
- package/flow-report/src/common.tsx +3 -3
- package/flow-report/src/util.ts +0 -11
- package/flow-report/test/common-test.tsx +4 -7
- package/flow-report/test/summary/summary-test.tsx +1 -1
- package/package.json +1 -1
- package/readme.md +1 -0
- package/report/renderer/details-renderer.js +1 -2
- package/report/renderer/element-screenshot-renderer.js +4 -4
- package/report/renderer/report-renderer.js +3 -7
- package/report/renderer/report-ui-features.js +3 -7
- package/report/renderer/util.js +30 -0
- package/report/test/renderer/util-test.js +20 -0
- package/report/test-assets/faux-psi.js +3 -3
- package/types/artifacts.d.ts +1 -11
- package/types/lhr/audit-details.d.ts +0 -17
- package/types/lhr/lhr.d.ts +13 -0
- package/types/lhr/settings.d.ts +2 -0
- package/types/smokehouse.d.ts +2 -1
- 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
|
-
|
|
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
|
-
|
|
399
|
+
extraAssertions.push(comparison);
|
|
398
400
|
}
|
|
399
401
|
|
|
400
|
-
/** @type {Comparison[]} */
|
|
401
|
-
const requestCountAssertion = [];
|
|
402
402
|
if (expected.networkRequests) {
|
|
403
|
-
|
|
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
|
-
...
|
|
421
|
+
...extraAssertions,
|
|
418
422
|
];
|
|
419
423
|
}
|
|
420
424
|
|
package/core/config/constants.js
CHANGED
package/core/config/filters.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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
|
|
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
|
-
|
|
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.
|
|
154
|
+
* @return {Promise<LH.Result.FullPageScreenshot['nodes']>}
|
|
150
155
|
*/
|
|
151
156
|
async _resolveNodes(context) {
|
|
152
157
|
function resolveNodes() {
|
|
153
|
-
/** @type {LH.
|
|
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
|
|
226
|
+
screenshot: await this._takeScreenshot(context),
|
|
218
227
|
nodes: await this._resolveNodes(context),
|
|
219
228
|
};
|
|
220
229
|
} finally {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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 =
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|