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): resolve node rects during emulation #11536

Merged
merged 41 commits into from
Nov 30, 2020
Merged
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
fa68fbf
initial
connorjclark Sep 9, 2020
d9f4d2e
test
connorjclark Sep 9, 2020
97fbdae
test
connorjclark Oct 7, 2020
a2391d2
Merge remote-tracking branch 'origin/master' into resolve-nodes
connorjclark Oct 7, 2020
1b6ba16
update
connorjclark Oct 7, 2020
93960c5
Merge branch 'full-page-dpr-fix' into resolve-nodes
connorjclark Oct 7, 2020
90190c6
minor
connorjclark Oct 7, 2020
fe7b0f8
report: do not show element screenshot if out of bounds
connorjclark Oct 8, 2020
987c676
test
connorjclark Oct 8, 2020
827a19f
Revert "Merge branch 'full-page-dpr-fix' into resolve-nodes"
connorjclark Oct 8, 2020
437e3af
Merge branch 'screenshots-oob' into resolve-nodes
connorjclark Oct 8, 2020
553eb21
Merge remote-tracking branch 'origin/master' into resolve-nodes
connorjclark Nov 10, 2020
551c53b
refactor
connorjclark Nov 11, 2020
df7a9f2
read from all contexts
connorjclark Nov 11, 2020
c592163
tests
connorjclark Nov 11, 2020
f54e01d
expand smoke test
connorjclark Nov 12, 2020
315150c
lhId
connorjclark Nov 12, 2020
77b72d9
details renderer test
connorjclark Nov 12, 2020
5fb4dd6
axe
connorjclark Nov 12, 2020
69e71bb
mock fps gatherer
connorjclark Nov 12, 2020
8f6c886
fix mocks
connorjclark Nov 12, 2020
3b62754
expectations
connorjclark Nov 12, 2020
445b614
mockkkkkkssssss
connorjclark Nov 13, 2020
da33697
Merge remote-tracking branch 'origin/master' into resolve-nodes
connorjclark Nov 13, 2020
8566593
update
connorjclark Nov 16, 2020
6f16838
updatesmokes
connorjclark Nov 16, 2020
7cee565
Merge remote-tracking branch 'origin/master' into resolve-nodes
connorjclark Nov 17, 2020
a18ead2
timings
connorjclark Nov 17, 2020
f043e2d
xvfb the thing
connorjclark Nov 17, 2020
cf3359b
nah
connorjclark Nov 17, 2020
32b0dea
Merge remote-tracking branch 'origin/master' into resolve-nodes
connorjclark Nov 17, 2020
7783001
hmm
connorjclark Nov 17, 2020
9ff001c
nah
connorjclark Nov 17, 2020
9cadb0b
Merge remote-tracking branch 'origin/master' into resolve-nodes
connorjclark Nov 21, 2020
4f82ee3
move to base artifact
connorjclark Nov 21, 2020
6cfe947
Merge remote-tracking branch 'origin/master' into resolve-nodes
connorjclark Nov 24, 2020
b4f129f
ts
connorjclark Nov 24, 2020
76835b1
Merge remote-tracking branch 'origin/master' into resolve-nodes
connorjclark Nov 25, 2020
0484a53
oop
connorjclark Nov 25, 2020
20ce34a
html element shadow
connorjclark Nov 25, 2020
df63018
simple
connorjclark Nov 26, 2020
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
14 changes: 11 additions & 3 deletions lighthouse-cli/test/fixtures/screenshot.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,19 @@
</style>

<body>
<p id="textEl">Screenshot tester.</p>

<script>
const params = new URLSearchParams(document.location.search);
if (params.has('width')) document.body.style.width = params.get('width');
if (params.has('height')) document.body.style.height = params.get('height');
</script>

<p>Screenshot tester.</p>

textEl.style.color = 'gray';
textEl.style.backgroundColor = 'lightgrey';
let padding = 1;
setInterval(() => {
textEl.style.paddingTop = padding + 'px';
padding += 1;
}, 50);
</script>
</body>
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,42 @@ const expectations = [
{
artifacts: {
FullPageScreenshot: {
width: '>1000',
height: '>1000',
data: /data:image\/jpeg;base64,.{10000,}$/,
screenshot: {
width: '>1000',
height: '>1000',
data: /data:image\/jpeg;base64,.{10000,}$/,
},
nodes: {
'page-0-BODY': {
top: 8,
bottom: 1008,
left: 8,
right: 1008,
width: 1000,
height: 1000,
},
// The following 2 are the same element (from different JS contexts). This element
// starts with height ~18 and grows over time. See screenshot.html.
'page-1-P': {
connorjclark marked this conversation as resolved.
Show resolved Hide resolved
top: 8,
left: 8,
height: '>40',
},
'5-1-P': {
Copy link
Member

Choose a reason for hiding this comment

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

we should move screenshot-config off of experimental-config and to a stable set of gatherers or folks messing with experimental gatherers/audits are going to have to deal with changes to these IDs unrelated to what they're up to. (e.g. there's no reason the "The following 2" comment will still apply to these two elements)

top: 8,
left: 8,
height: '>40',
},
'5-2-BODY': {
Copy link
Collaborator

Choose a reason for hiding this comment

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

how are these sharing the same incremental map counter when they're in different execution contexts...

shouldn't it start over at 5-0-BODY?

Copy link
Collaborator Author

@connorjclark connorjclark Nov 16, 2020

Choose a reason for hiding this comment

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

Because different gatherers, in the same pass and execution context, are collecting node details on the same elements.

Copy link
Collaborator Author

@connorjclark connorjclark Nov 16, 2020

Choose a reason for hiding this comment

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

oh, and the reason you wouldn't see 5-0 or 5-1 but see 5-2 is perhaps because the first few nodes had zero size.

top: 8,
bottom: 1008,
left: 8,
right: 1008,
width: 1000,
height: 1000,
},
'5-3-HTML': {},
Copy link
Collaborator Author

@connorjclark connorjclark Nov 12, 2020

Choose a reason for hiding this comment

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

the sizes here are env-dependent so I just removed them.

This also illustrates that the same node can get assigned multiple ids (these rects are gonna be the same value, too). We could do a reverse lookup in getNodeDetailsImpl to dedupe this, but it'd only dedupe within the same pass. Should we?

Copy link
Collaborator

Choose a reason for hiding this comment

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

is there a reasonable range we could use? or are top/left at least consistent?

Copy link
Member

Choose a reason for hiding this comment

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

I like the idea of deduping them, it would make the results less confusing. Would it be possible to add a pass number to the lhid?

Copy link
Collaborator

Choose a reason for hiding this comment

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

+1 to deduping, I wouldn't love a pass number for FR migration, but this is going to be tricky anyway

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

alternative, if we provide more structure here we could do the de duping when collecting in full page screenshot. i'll experiment.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

wait no sorry, that'd involve editing the ids in the LHR which we are trying hard not to do.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

iirc top/left were non constant, which was strange. We aren't losing anything by not having more rect checks here imo.

what I'd really like is a way to mark "no other keys" on an object.

},
},
},
lhr: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ const experimentalConfig = require('../../../../../lighthouse-core/config/experi
module.exports = {
...experimentalConfig,
settings: {
onlyAudits: ['full-page-screenshot'],
emulatedFormFactor: 'desktop',
},
};
1 change: 1 addition & 0 deletions lighthouse-core/audits/accessibility/axe-audit.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ class AxeAudit extends Audit {
items = rule.nodes.map(node => ({
node: /** @type {LH.Audit.Details.NodeValue} */ ({
type: 'node',
lhId: node.lhId,
selector: node.selector,
path: node.devtoolsNodePath,
snippet: node.snippet,
Expand Down
2 changes: 1 addition & 1 deletion lighthouse-core/audits/full-page-screenshot.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class FullPageScreenshot extends Audit {
score: 1,
details: {
type: 'full-page-screenshot',
...artifacts.FullPageScreenshot,
fullPageScreenshot: artifacts.FullPageScreenshot,
},
};
}
Expand Down
1 change: 1 addition & 0 deletions lighthouse-core/audits/largest-contentful-paint-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class LargestContentfulPaintElement extends Audit {
lcpElementDetails.push({
node: /** @type {LH.Audit.Details.NodeValue} */ ({
type: 'node',
lhId: lcpElement.lhId,
path: lcpElement.devtoolsNodePath,
selector: lcpElement.selector,
nodeLabel: lcpElement.nodeLabel,
Expand Down
3 changes: 2 additions & 1 deletion lighthouse-core/audits/layout-shift-elements.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class LayoutShiftElements extends Audit {
title: str_(UIStrings.title),
description: str_(UIStrings.description),
scoreDisplayMode: Audit.SCORING_MODES.INFORMATIVE,
requiredArtifacts: ['TraceElements'],
requiredArtifacts: ['traces', 'TraceElements'],
};
}

Expand All @@ -45,6 +45,7 @@ class LayoutShiftElements extends Audit {
return {
node: /** @type {LH.Audit.Details.NodeValue} */ ({
type: 'node',
lhId: element.lhId,
path: element.devtoolsNodePath,
selector: element.selector,
nodeLabel: element.nodeLabel,
Expand Down
1 change: 1 addition & 0 deletions lighthouse-core/audits/seo/tap-targets.js
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ function targetToTableNode(target) {

return {
type: 'node',
lhId: target.lhId,
snippet: target.snippet,
path: target.devtoolsNodePath,
selector: target.selector,
Expand Down
1 change: 1 addition & 0 deletions lighthouse-core/gather/driver/execution-context.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ class ExecutionContext {
expression: `(function wrapInNativePromise() {
const __nativePromise = window.__nativePromise || Promise;
const URL = window.__nativeURL || window.URL;
window.__lighthouseExecutionContextId = ${contextId};
Copy link
Member

Choose a reason for hiding this comment

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

The context id ends up being a binary signal in the lhIds anyways (either page or a single execution context id, since if there had been other contexts in the pass they wouldn't still be around to query anyways :), so maybe consider a more generalized window._isLighthouseIsolatedContext = boolean.

(Ideally we could get away without using it at all, but I can't think of a way to do that while keeping the original IDs in each artifact untouched)

return new __nativePromise(function (resolve) {
return __nativePromise.resolve()
.then(_ => ${expression})
Expand Down
51 changes: 48 additions & 3 deletions lighthouse-core/gather/gatherers/full-page-screenshot.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
*/
'use strict';

/* globals window getBoundingClientRect */

const Gatherer = require('./gatherer.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#
Expand All @@ -23,7 +26,7 @@ function snakeCaseToCamelCase(str) {
class FullPageScreenshot extends Gatherer {
/**
* @param {LH.Gatherer.PassContext} passContext
* @return {Promise<LH.Artifacts.FullPageScreenshot>}
* @return {Promise<LH.Artifacts.FullPageScreenshot['screenshot']>}
*/
async _takeScreenshot(passContext) {
const driver = passContext.driver;
Expand Down Expand Up @@ -64,6 +67,46 @@ class FullPageScreenshot extends Gatherer {
};
}

/**
* 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.PassContext} passContext
* @return {Promise<LH.Artifacts.FullPageScreenshot['nodes']>}
*/
async _resolveNodes(passContext) {
function resolveNodes() {
/** @type {LH.Artifacts.FullPageScreenshot['nodes']} */
const nodes = {};
if (!window.__lighthouseNodesDontTouchOrAllVarianceGoesAway) return nodes;

const {lhIdToElements} = window.__lighthouseNodesDontTouchOrAllVarianceGoesAway;
for (const [id, node] of lhIdToElements.entries()) {
// @ts-expect-error - getBoundingClientRect put into scope via stringification
const rect = getBoundingClientRect(node);
if (rect.width || rect.height) nodes[id] = rect;
}

return nodes;
}
const expression = `(function () {
${pageFunctions.getBoundingClientRectString};
return (${resolveNodes.toString()}());
})()`;

// Collect nodes with the page context (`useIsolation: false`) and with our own, reused
// context (useIsolation: false). Gatherers use both modes when collecting node details,
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// context (useIsolation: false). Gatherers use both modes when collecting node details,
// context (useIsolation: true). Gatherers use both modes when collecting node details,

// so we must do the same here too.
const pageContextResult =
await passContext.driver.evaluateAsync(expression, {useIsolation: false});
const isolatedContextResult =
await passContext.driver.evaluateAsync(expression, {useIsolation: true});
return {...pageContextResult, ...isolatedContextResult};
connorjclark marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* @param {LH.Gatherer.PassContext} passContext
* @return {Promise<LH.Artifacts['FullPageScreenshot']>}
Expand All @@ -77,8 +120,10 @@ class FullPageScreenshot extends Gatherer {
!passContext.settings.internalDisableDeviceScreenEmulation;

try {
const screenshot = await this._takeScreenshot(passContext);
return screenshot;
return {
screenshot: await this._takeScreenshot(passContext),
nodes: await this._resolveNodes(passContext),
};
} finally {
// Revert resized page.
if (lighthouseControlsEmulation) {
Expand Down
57 changes: 48 additions & 9 deletions lighthouse-core/lib/page-functions.js
Original file line number Diff line number Diff line change
Expand Up @@ -446,20 +446,59 @@ function wrapRequestIdleCallback(cpuSlowdownMultiplier) {
};
}

const getNodeDetailsString = `function getNodeDetails(elem) {
/**
* @param {HTMLElement} element
*/
function getNodeDetailsImpl(element) {
// This bookkeeping is for the FullPageScreenshot gatherer.
if (!window.__lighthouseNodesDontTouchOrAllVarianceGoesAway) {
Copy link
Member

Choose a reason for hiding this comment

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

FWIW, I also don't understand what __lighthouseNodesDontTouchOrAllVarianceGoesAway is trying to communicate :) It might be worth switching to something more straightforward

window.__lighthouseNodesDontTouchOrAllVarianceGoesAway = {
lhIdToElements: new Map(),
elementToLhId: new Map(),
};
}

// Create an id that will be unique across all execution contexts.
// The id could be any arbitrary string, the exact value is not important.
// For example, tagName is added only because it might be useful for debugging.
// But execution id and map size are added to ensure uniqueness.
// We also dedupe this id so that details collected for an element within the same
// pass and execution context will share the same id. Not technically important, but
// cuts down on some duplication.
let lhId = window.__lighthouseNodesDontTouchOrAllVarianceGoesAway.elementToLhId.get(element);
if (!lhId) {
lhId = [
window.__lighthouseExecutionContextId !== undefined ?
window.__lighthouseExecutionContextId :
'page',
window.__lighthouseNodesDontTouchOrAllVarianceGoesAway.elementToLhId.size,
element.tagName,
].join('-');
window.__lighthouseNodesDontTouchOrAllVarianceGoesAway.lhIdToElements.set(lhId, element);
Copy link
Member

Choose a reason for hiding this comment

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

seems like we don't need this. the for (const [id, node] of lhIdToElements.entries()) { later on could just use elementToLhId and reverse the order of the destructured array. right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

ya but I'm assuming not performant. and this code is simpler/fewer lines.

Copy link
Member

Choose a reason for hiding this comment

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

ya but I'm assuming not performant. and this code is simpler/fewer lines.

hmmm.. maybe you're thinking of something else?
what i'm proposing is definitely less work. and its fewer lines

diff --git a/lighthouse-core/gather/gatherers/full-page-screenshot.js b/lighthouse-core/gather/gatherers/full-page-screenshot.js
index 474eca578..5806de603 100644
--- a/lighthouse-core/gather/gatherers/full-page-screenshot.js
+++ b/lighthouse-core/gather/gatherers/full-page-screenshot.js
@@ -82,9 +82,8 @@ class FullPageScreenshot extends Gatherer {
       /** @type {LH.Artifacts.FullPageScreenshot['nodes']} */
       const nodes = {};
       if (!window.__lighthouseNodesDontTouchOrAllVarianceGoesAway) return nodes;
-
-      const {lhIdToElements} = window.__lighthouseNodesDontTouchOrAllVarianceGoesAway;
-      for (const [id, node] of lhIdToElements.entries()) {
+      const elementToLhId = window.__lighthouseNodesDontTouchOrAllVarianceGoesAway;
+      for (const [node, id] of elementToLhId.entries()) {
         // @ts-expect-error - getBoundingClientRect put into scope via stringification
         const rect = getBoundingClientRect(node);
         if (rect.width || rect.height) nodes[id] = rect;
diff --git a/lighthouse-core/lib/page-functions.js b/lighthouse-core/lib/page-functions.js
index 9769877a9..a71bfe497 100644
--- a/lighthouse-core/lib/page-functions.js
+++ b/lighthouse-core/lib/page-functions.js
@@ -452,10 +452,7 @@ function wrapRequestIdleCallback(cpuSlowdownMultiplier) {
 function getNodeDetailsImpl(element) {
   // This bookkeeping is for the FullPageScreenshot gatherer.
   if (!window.__lighthouseNodesDontTouchOrAllVarianceGoesAway) {
-    window.__lighthouseNodesDontTouchOrAllVarianceGoesAway = {
-      lhIdToElements: new Map(),
-      elementToLhId: new Map(),
-    };
+    window.__lighthouseNodesDontTouchOrAllVarianceGoesAway = new Map();
   }
 
   // Create an id that will be unique across all execution contexts.
@@ -465,17 +462,16 @@ function getNodeDetailsImpl(element) {
   // We also dedupe this id so that details collected for an element within the same
   // pass and execution context will share the same id. Not technically important, but
   // cuts down on some duplication.
-  let lhId = window.__lighthouseNodesDontTouchOrAllVarianceGoesAway.elementToLhId.get(element);
+  let lhId = window.__lighthouseNodesDontTouchOrAllVarianceGoesAway.get(element);
   if (!lhId) {
     lhId = [
       window.__lighthouseExecutionContextId !== undefined ?
         window.__lighthouseExecutionContextId :
         'page',
-      window.__lighthouseNodesDontTouchOrAllVarianceGoesAway.elementToLhId.size,
+      window.__lighthouseNodesDontTouchOrAllVarianceGoesAway.size,
       element.tagName,
     ].join('-');
-    window.__lighthouseNodesDontTouchOrAllVarianceGoesAway.lhIdToElements.set(lhId, element);
-    window.__lighthouseNodesDontTouchOrAllVarianceGoesAway.elementToLhId.set(element, lhId);
+    window.__lighthouseNodesDontTouchOrAllVarianceGoesAway.set(element, lhId);
   }
 
   const htmlElement = element instanceof ShadowRoot ? element.host : element;
diff --git a/types/externs.d.ts b/types/externs.d.ts
index a04a09b08..a4727bb45 100644
--- a/types/externs.d.ts
+++ b/types/externs.d.ts
@@ -324,11 +324,8 @@ declare global {
   }
 
   interface Window {
-    /** Used by FullPageScreenshot gatherer. */
-    __lighthouseNodesDontTouchOrAllVarianceGoesAway: {
-      lhIdToElements: Map<string, HTMLElement>;
-      elementToLhId: Map<HTMLElement, string>;
-    };
+    /** Map of Element to lhID. Used by FullPageScreenshot gatherer. */
+    __lighthouseNodesDontTouchOrAllVarianceGoesAway: Map<HTMLElement, string>;
     __lighthouseExecutionContextId?: number;
   }
 }

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

ya this is good. i did not think to do just a Map<HTMLElement, string>, was stuck on thinking of it as Map<string, HTMLElement>

window.__lighthouseNodesDontTouchOrAllVarianceGoesAway.elementToLhId.set(element, lhId);
}

const htmlElement = element instanceof ShadowRoot ? element.host : element;
const details = {
lhId,
devtoolsNodePath: getNodePath(element),
selector: getNodeSelector(htmlElement),
boundingRect: getBoundingClientRect(htmlElement),
snippet: getOuterHTMLSnippet(element),
nodeLabel: getNodeLabel(htmlElement),
};

return details;
}

const getNodeDetailsString = `function getNodeDetails(element) {
Copy link
Member

Choose a reason for hiding this comment

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

Can we just put getNodeDetails.toString() in the exports and remove this?

Copy link
Member

Choose a reason for hiding this comment

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

yeah i considered that as well, though this fn is a little different from the rest in that it depends on all the others. so we need those stringified functions inlined here within this one.

when i worked on this with @adrianaixba it seems the most practical to just define this fn as a string and skip the conversion in order to make sure all the deps were available. (otherwise i figured whereever we used getNodeDetails in gatherers, we'd also need to inject each of its dependency fns as well.

i still feel pretty decent this was the right compromise for DX, but if there's another good solution we can def explore it.

Copy link
Collaborator Author

@connorjclark connorjclark Nov 12, 2020

Choose a reason for hiding this comment

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

I had to add non trivial code to this function so I split it out, but am keeping the stringified dep export.

${getNodePath.toString()};
${getNodeSelector.toString()};
${getBoundingClientRect.toString()};
${getOuterHTMLSnippet.toString()};
${getNodeLabel.toString()};
const htmlElem = elem instanceof ShadowRoot ? elem.host : elem;
return {
devtoolsNodePath: getNodePath(elem),
selector: getNodeSelector(htmlElem),
boundingRect: getBoundingClientRect(htmlElem),
snippet: getOuterHTMLSnippet(elem),
nodeLabel: getNodeLabel(htmlElem),
};
${getNodeDetailsImpl.toString()};
return getNodeDetailsImpl(element);
}`;

module.exports = {
Expand Down
14 changes: 8 additions & 6 deletions lighthouse-core/report/html/renderer/details-renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const URL_PREFIXES = ['http://', 'https://', 'data:'];
class DetailsRenderer {
/**
* @param {DOM} dom
* @param {{fullPageScreenshot?: LH.Audit.Details.FullPageScreenshot}} [options]
* @param {{fullPageScreenshot?: LH.Artifacts.FullPageScreenshot}} [options]
*/
constructor(dom, options = {}) {
this._dom = dom;
Expand Down Expand Up @@ -521,16 +521,18 @@ class DetailsRenderer {
if (item.selector) element.setAttribute('data-selector', item.selector);
if (item.snippet) element.setAttribute('data-snippet', item.snippet);

if (!item.boundingRect || !this._fullPageScreenshot) {
return element;
}
if (!this._fullPageScreenshot) return element;

const rect =
(item.lhId ? this._fullPageScreenshot.nodes[item.lhId] : null) || item.boundingRect;
if (!rect) return element;

const maxThumbnailSize = {width: 147, height: 100};
const elementScreenshot = ElementScreenshotRenderer.render(
this._dom,
this._templateContext,
this._fullPageScreenshot,
item.boundingRect,
this._fullPageScreenshot.screenshot,
rect,
maxThumbnailSize
);
if (elementScreenshot) element.prepend(elementScreenshot);
Expand Down
Loading