forked from GoogleChrome/lighthouse
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathfull-page-screenshot.js
195 lines (172 loc) · 7.82 KB
/
full-page-screenshot.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
/**
* @license Copyright 2020 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
'use strict';
/* globals window document getBoundingClientRect */
const FRGatherer = require('../../fraggle-rock/gather/base-gatherer.js');
const emulation = require('../../lib/emulation.js');
const pageFunctions = require('../../lib/page-functions.js');
// JPEG quality setting
// Exploration and examples of reports using different quality settings: https://docs.google.com/document/d/1ZSffucIca9XDW2eEwfoevrk-OTl7WQFeMf0CgeJAA8M/edit#
const FULL_PAGE_SCREENSHOT_QUALITY = 30;
/**
* @param {string} str
*/
function snakeCaseToCamelCase(str) {
return str.replace(/(-\w)/g, m => m[1].toUpperCase());
}
class FullPageScreenshot extends FRGatherer {
/** @type {LH.Gatherer.GathererMeta} */
meta = {
supportedModes: ['snapshot', 'timespan', 'navigation'],
}
/**
* @param {LH.Gatherer.FRTransitionalContext} context
* @return {Promise<number>}
* @see https://bugs.chromium.org/p/chromium/issues/detail?id=770769
*/
async getMaxScreenshotHeight(context) {
return await context.driver.executionContext.evaluate(pageFunctions.getMaxTextureSize, {
args: [],
useIsolation: true,
deps: [],
});
}
/**
* @param {LH.Gatherer.FRTransitionalContext} context
* @return {Promise<LH.Artifacts.FullPageScreenshot['screenshot']>}
*/
async _takeScreenshot(context) {
const session = context.driver.defaultSession;
const maxScreenshotHeight = await this.getMaxScreenshotHeight(context);
const metrics = await session.sendCommand('Page.getLayoutMetrics');
// Width should match emulated width, without considering content overhang.
// Both layoutViewport and visualViewport capture this. visualViewport accounts
// for page zoom/scale, which we currently don't account for (or expect). So we use layoutViewport.width.
// Note: If the page is zoomed, many assumptions fail.
//
// Height should be as tall as the content. So we use contentSize.height
const width = Math.min(metrics.layoutViewport.clientWidth, maxScreenshotHeight);
const height = Math.min(metrics.contentSize.height, maxScreenshotHeight);
await session.sendCommand('Emulation.setDeviceMetricsOverride', {
// If we're gathering with mobile screenEmulation on (overlay scrollbars, etc), continue to use that for this screenshot.
mobile: context.settings.screenEmulation.mobile,
height,
width,
deviceScaleFactor: 1,
scale: 1,
screenOrientation: {angle: 0, type: 'portraitPrimary'},
});
// TODO: elements collected earlier in gathering are likely to have been shifted by now.
// The lower in the page, the more likely (footer elements especially).
// https://github.com/GoogleChrome/lighthouse/issues/11118
const result = await session.sendCommand('Page.captureScreenshot', {
format: 'jpeg',
quality: FULL_PAGE_SCREENSHOT_QUALITY,
});
const data = 'data:image/jpeg;base64,' + result.data;
return {
data,
width,
height,
};
}
/**
* Gatherers can collect details about DOM nodes, including their position on the page.
* Layout shifts occuring after a gatherer runs can cause these positions to be incorrect,
* resulting in a poor experience for element screenshots.
* `getNodeDetails` maintains a collection of DOM objects in the page, which we can iterate
* to re-collect the bounding client rectangle.
* @see pageFunctions.getNodeDetails
* @param {LH.Gatherer.FRTransitionalContext} context
* @return {Promise<LH.Artifacts.FullPageScreenshot['nodes']>}
*/
async _resolveNodes(context) {
function resolveNodes() {
/** @type {LH.Artifacts.FullPageScreenshot['nodes']} */
const nodes = {};
if (!window.__lighthouseNodesDontTouchOrAllVarianceGoesAway) return nodes;
const lhIdToElements = window.__lighthouseNodesDontTouchOrAllVarianceGoesAway;
for (const [node, id] of lhIdToElements.entries()) {
// @ts-expect-error - getBoundingClientRect put into scope via stringification
const rect = getBoundingClientRect(node);
nodes[id] = rect;
}
return nodes;
}
/**
* @param {{useIsolation: boolean}} _
*/
function resolveNodesInPage({useIsolation}) {
return context.driver.executionContext.evaluate(resolveNodes, {
args: [],
useIsolation,
deps: [pageFunctions.getBoundingClientRectString],
});
}
// Collect nodes with the page context (`useIsolation: false`) and with our own, reused
// context (`useIsolation: true`). Gatherers use both modes when collecting node details,
// so we must do the same here too.
const pageContextResult = await resolveNodesInPage({useIsolation: false});
const isolatedContextResult = await resolveNodesInPage({useIsolation: true});
return {...pageContextResult, ...isolatedContextResult};
}
/**
* @param {LH.Gatherer.FRTransitionalContext} context
* @return {Promise<LH.Artifacts['FullPageScreenshot']>}
*/
async getArtifact(context) {
const session = context.driver.defaultSession;
const executionContext = context.driver.executionContext;
const settings = context.settings;
// In case some other program is controlling emulation, try to remember what the device looks
// like now and reset after gatherer is done.
const lighthouseControlsEmulation = !settings.screenEmulation.disabled;
try {
return {
screenshot: await this._takeScreenshot(context),
nodes: await this._resolveNodes(context),
};
} finally {
// Revert resized page.
if (lighthouseControlsEmulation) {
await emulation.emulate(session, settings);
} else {
// Best effort to reset emulation to what it was.
// https://github.com/GoogleChrome/lighthouse/pull/10716#discussion_r428970681
// TODO: seems like this would be brittle. Should at least work for devtools, but what
// about scripted puppeteer usages? Better to introduce a "setEmulation" callback
// in the LH runner api, which for ex. puppeteer consumers would setup puppeteer emulation,
// and then just call that to reset?
// https://github.com/GoogleChrome/lighthouse/issues/11122
// eslint-disable-next-line no-inner-declarations
function getObservedDeviceMetrics() {
// Convert the Web API's snake case (landscape-primary) to camel case (landscapePrimary).
const screenOrientationType = /** @type {LH.Crdp.Emulation.ScreenOrientationType} */ (
snakeCaseToCamelCase(window.screen.orientation.type));
return {
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight,
screenOrientation: {
type: screenOrientationType,
angle: window.screen.orientation.angle,
},
deviceScaleFactor: window.devicePixelRatio,
};
}
const observedDeviceMetrics = await executionContext.evaluate(getObservedDeviceMetrics, {
args: [],
useIsolation: true,
deps: [snakeCaseToCamelCase],
});
await session.sendCommand('Emulation.setDeviceMetricsOverride', {
mobile: settings.formFactor === 'mobile',
...observedDeviceMetrics,
});
}
}
}
}
module.exports = FullPageScreenshot;