Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

core(full-page-screenshot): add full page screenshot to artifacts #9064

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lighthouse-cli/test/cli/__snapshots__/index-test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1165,6 +1165,9 @@ Object {
Object {
"path": "accessibility",
},
Object {
"path": "full-page-screenshot",
},
],
"networkQuietThresholdMs": 1000,
"passName": "defaultPass",
Expand Down
5 changes: 0 additions & 5 deletions lighthouse-core/audits/final-screenshot.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

const Audit = require('./audit.js');
const LHError = require('../lib/lh-error.js');
const TraceOfTab = require('../computed/trace-of-tab.js');
const Screenshots = require('../computed/screenshots.js');

class FinalScreenshot extends Audit {
Expand All @@ -31,9 +30,7 @@ class FinalScreenshot extends Audit {
*/
static async audit(artifacts, context) {
const trace = artifacts.traces[Audit.DEFAULT_PASS];
const traceOfTab = await TraceOfTab.request(trace, context);
const screenshots = await Screenshots.request(trace, context);
const {navigationStart} = traceOfTab.timestamps;
const finalScreenshot = screenshots[screenshots.length - 1];

if (!finalScreenshot) {
Expand All @@ -44,8 +41,6 @@ class FinalScreenshot extends Audit {
score: 1,
details: {
type: 'screenshot',
timing: Math.round((finalScreenshot.timestamp - navigationStart) / 1000),
timestamp: finalScreenshot.timestamp,
data: finalScreenshot.datauri,
},
};
Expand Down
39 changes: 39 additions & 0 deletions lighthouse-core/audits/full-page-screenshot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* @license Copyright 2019 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';

const Audit = require('./audit.js');

class FullPageScreenshot extends Audit {
/**
* @return {LH.Audit.Meta}
*/
static get meta() {
return {
id: 'full-page-screenshot',
scoreDisplayMode: Audit.SCORING_MODES.INFORMATIVE,
title: 'Full-page screenshot',
description: 'A full-height screenshot of the final rendered page',
requiredArtifacts: ['FullPageScreenshot'],
};
}

/**
* @param {LH.Artifacts} artifacts
* @return {Promise<LH.Audit.Product>}
*/
static async audit(artifacts) {
return {
score: 1,
details: {
type: 'screenshot',
...artifacts.FullPageScreenshot,
},
};
}
}

module.exports = FullPageScreenshot;
3 changes: 3 additions & 0 deletions lighthouse-core/config/default-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ const defaultConfig = {
'seo/tap-targets',
// Always run axe last because it scrolls the page down to the bottom
'accessibility',
'full-page-screenshot',
],
},
{
Expand Down Expand Up @@ -169,6 +170,8 @@ const defaultConfig = {
'metrics/speed-index',
'screenshot-thumbnails',
'final-screenshot',
// Disable for now since we don't need it in the LHR yet and it's quite big
// 'full-page-screenshot',
'metrics/estimated-input-latency',
'metrics/max-potential-fid',
'errors-in-console',
Expand Down
8 changes: 5 additions & 3 deletions lighthouse-core/gather/driver.js
Original file line number Diff line number Diff line change
Expand Up @@ -1423,13 +1423,15 @@ class Driver {

/**
* @param {LH.Config.Settings} settings
* @param {{height?: number, screenHeight: number?, deviceScaleFactor?: number}} [deviceMetricOverrides]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @param {{height?: number, screenHeight: number?, deviceScaleFactor?: number}} [deviceMetricOverrides]
* @param {{height?: number, screenHeight?: number, deviceScaleFactor?: number}} [deviceMetricOverrides]

* Override device size for taking screenshots.
* @return {Promise<void>}
*/
async beginEmulation(settings) {
async beginEmulation(settings, deviceMetricOverrides) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my initial thought looking at this was that I was worried about emulatedFormFactor conflicting with deviceMetricOverrides, maybe add a brief comment explaining the use case for deviceMetricOverrides?

creating a nexus 5x with arbitrary screen height seems counter-intuitive at first :)

if (settings.emulatedFormFactor === 'mobile') {
await emulation.enableNexus5X(this);
await emulation.enableNexus5X(this, deviceMetricOverrides);
} else if (settings.emulatedFormFactor === 'desktop') {
await emulation.enableDesktop(this);
await emulation.enableDesktop(this, deviceMetricOverrides);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be appropriate to hook the options on the driver itself?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean by that?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the extra param could be removed in favor for having it as a prop on this

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure we want to clutter driver with additional properties just for the method here. It's a pretty busy class already 😆

}

await this.setThrottling(settings, {useThrottling: true});
Expand Down
71 changes: 71 additions & 0 deletions lighthouse-core/gather/gatherers/full-page-screenshot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* @license Copyright 2019 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';

const Gatherer = require('./gatherer.js');

// JPEG quality setting
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we link to that awesome exploration doc you made for how we landed on 30?

// Exploration and examples of reports using different quality settings: https://docs.google.com/document/d/1ZSffucIca9XDW2eEwfoevrk-OTl7WQFeMf0CgeJAA8M/edit#
const FULL_PAGE_SCREENSHOT_QUALITY = 30;
// Maximum screenshot height in Chrome https://bugs.chromium.org/p/chromium/issues/detail?id=770769
const MAX_SCREENSHOT_HEIGHT = 16384;
// Maximum data URL size in Chrome https://bugs.chromium.org/p/chromium/issues/detail?id=69227
const MAX_DATA_URL_SIZE = 2 * 1024 * 1024;

class FullPageScreenshot extends Gatherer {
/**
* @param {LH.Gatherer.PassContext} passContext
* @param {number} maxScreenshotHeight
* @return {Promise<LH.Artifacts.FullPageScreenshot>}
*/
async _takeScreenshot(passContext, maxScreenshotHeight) {
const driver = passContext.driver;
const metrics = await driver.sendCommand('Page.getLayoutMetrics');
const width = await driver.evaluateAsync(`window.innerWidth`);
const height = Math.min(metrics.contentSize.height, maxScreenshotHeight);

await driver.beginEmulation(passContext.settings, {
height,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mostly talking to the air here did we ever figure out what screenHeight does separately from height? desktop doesn't set screenHeight at the moment at all just wonder if that'll impact anything

it sets the screen rect but not sure what impact that has...
https://cs.chromium.org/chromium/src/content/renderer/render_widget_screen_metrics_emulator.cc?type=cs&sq=package:chromium&g=0&l=78-83

seems like it notifies some observers and kicks off orientation changes, but doesn't actually affect viewport size?
https://cs.chromium.org/chromium/src/content/renderer/render_widget.cc?type=cs&sq=package:chromium&g=0&l=2183

🤷‍♂

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

screenHeight: height,
deviceScaleFactor: 1,
});

const result = await driver.sendCommand('Page.captureScreenshot', {
format: 'jpeg',
quality: FULL_PAGE_SCREENSHOT_QUALITY,
});
const data = 'data:image/jpeg;base64,' + result.data;

// Revert resized page
await driver.beginEmulation(passContext.settings);

return {
data,
width,
height,
};
}

/**
* @param {LH.Gatherer.PassContext} passContext
* @return {Promise<LH.Artifacts.FullPageScreenshot>}
*/
async afterPass(passContext) {
let screenshot = await this._takeScreenshot(passContext, MAX_SCREENSHOT_HEIGHT);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we scroll to the top of the page first? I think devtools does that.


if (screenshot.data.length > MAX_DATA_URL_SIZE) {
// Hitting the data URL size limit is rare, it only happens for pages on tall
// desktop sites with lots of images.
// So just cutting down the height a bit fixes the issue.
screenshot = await this._takeScreenshot(passContext, 5000);
}

return screenshot;
}
}

module.exports = FullPageScreenshot;
module.exports.MAX_SCREENSHOT_HEIGHT = MAX_SCREENSHOT_HEIGHT;
62 changes: 48 additions & 14 deletions lighthouse-core/lib/emulation.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,30 +64,51 @@ const NO_CPU_THROTTLE_METRICS = {

/**
* @param {Driver} driver
* @return {Promise<void>}
* @param {EnableEmulationOptions} emulationOptions
*/
async function enableNexus5X(driver) {
await Promise.all([
driver.sendCommand('Emulation.setDeviceMetricsOverride', NEXUS5X_EMULATION_METRICS),
async function enableEmulation(
driver,
{deviceMetrics, setTouchEmulationEnabled, userAgent, deviceMetricOverrides}
) {
if (deviceMetricOverrides) {
deviceMetrics = Object.assign({}, deviceMetrics, deviceMetricOverrides);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
deviceMetrics = Object.assign({}, deviceMetrics, deviceMetricOverrides);
deviceMetrics = {...deviceMetrics, ...deviceMetricOverrides};

}

return Promise.all([
driver.sendCommand('Emulation.setDeviceMetricsOverride', deviceMetrics),
// Network.enable must be called for UA overriding to work
driver.sendCommand('Network.enable'),
driver.sendCommand('Network.setUserAgentOverride', {userAgent: NEXUS5X_USERAGENT}),
driver.sendCommand('Emulation.setTouchEmulationEnabled', {enabled: true}),
driver.sendCommand('Network.setUserAgentOverride', {userAgent}),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we be balancing domain enable / disable?

driver.sendCommand('Emulation.setTouchEmulationEnabled', {enabled: setTouchEmulationEnabled}),
]);
}

/**
* @param {Driver} driver
* @param {DeviceMetricOverrides} [deviceMetricOverrides]
* @return {Promise<void>}
*/
async function enableDesktop(driver) {
await Promise.all([
driver.sendCommand('Emulation.setDeviceMetricsOverride', DESKTOP_EMULATION_METRICS),
// Network.enable must be called for UA overriding to work
driver.sendCommand('Network.enable'),
driver.sendCommand('Network.setUserAgentOverride', {userAgent: DESKTOP_USERAGENT}),
driver.sendCommand('Emulation.setTouchEmulationEnabled', {enabled: false}),
]);
async function enableNexus5X(driver, deviceMetricOverrides) {
await enableEmulation(driver, {
deviceMetrics: NEXUS5X_EMULATION_METRICS,
userAgent: NEXUS5X_USERAGENT,
setTouchEmulationEnabled: true,
deviceMetricOverrides,
});
}

/**
* @param {Driver} driver
* @param {DeviceMetricOverrides} [deviceMetricOverrides]
* @return {Promise<void>}
*/
async function enableDesktop(driver, deviceMetricOverrides) {
await enableEmulation(driver, {
deviceMetrics: DESKTOP_EMULATION_METRICS,
userAgent: DESKTOP_USERAGENT,
setTouchEmulationEnabled: false,
deviceMetricOverrides,
});
}

/**
Expand Down Expand Up @@ -155,3 +176,16 @@ module.exports = {
MOBILE_USERAGENT: NEXUS5X_USERAGENT,
DESKTOP_USERAGENT,
};

/** @typedef {{
* deviceMetrics: LH.Crdp.Emulation.SetDeviceMetricsOverrideRequest;
* userAgent: string;
* setTouchEmulationEnabled: boolean;
* deviceMetricOverrides?: DeviceMetricOverrides;
* }} EnableEmulationOptions */

/** @typedef {{
* height?: number;
* screenHeight: number?;
* deviceScaleFactor?: number;
* }} DeviceMetricOverrides */
2 changes: 0 additions & 2 deletions lighthouse-core/test/audits/final-screenshot-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ describe('Final screenshot', () => {
const results = await FinalScreenshotAudit.audit(artifacts, {computedCache: new Map()});

assert.equal(results.score, 1);
assert.equal(results.details.timing, 818);
assert.equal(results.details.timestamp, 225414990064);
assert.ok(results.details.data.startsWith('data:image/jpeg;base64,/9j/4AAQSkZJRgABA'));
});
});
Loading