-
Notifications
You must be signed in to change notification settings - Fork 9.4k
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
Changes from 11 commits
fa68fbf
d9f4d2e
97fbdae
a2391d2
1b6ba16
93960c5
90190c6
fe7b0f8
987c676
827a19f
437e3af
553eb21
551c53b
df7a9f2
c592163
f54e01d
315150c
77b72d9
5fb4dd6
69e71bb
8f6c886
3b62754
445b614
da33697
8566593
6f16838
7cee565
a18ead2
f043e2d
cf3359b
32b0dea
7783001
9ff001c
9cadb0b
4f82ee3
6cfe947
b4f129f
76835b1
0484a53
20ce34a
df63018
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,6 +14,7 @@ const NetworkRecorder = require('../lib/network-recorder.js'); | |
const constants = require('../config/constants.js'); | ||
const i18n = require('../lib/i18n/i18n.js'); | ||
const URL = require('../lib/url-shim.js'); | ||
const pageFunctions = require('../lib/page-functions.js'); | ||
|
||
const UIStrings = { | ||
/** | ||
|
@@ -501,6 +502,60 @@ class GatherRunner { | |
}; | ||
} | ||
|
||
/** | ||
* 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. We also update the devtools node path. | ||
* The old devtools node path is used as a lookup key. We walk the entire artifacts object | ||
* to update all objects that reference an old devtools node path. | ||
* @param {Driver} driver | ||
* @param {Partial<LH.Artifacts>} artifacts | ||
*/ | ||
static async resolveNodes(driver, artifacts) { | ||
function resolveNodes() { | ||
return (window.__nodes || []).map(({key, node}) => { | ||
return { | ||
key, | ||
newBoundingRect: getBoundingClientRect(node), | ||
newDevtoolsNodePath: getNodePath(node), | ||
}; | ||
}); | ||
} | ||
const expression = `(function () { | ||
${pageFunctions.getBoundingClientRectString}; | ||
${pageFunctions.getNodePathString}; | ||
return (${resolveNodes.toString()}()); | ||
})()`; | ||
|
||
/** @type {Array<{key: string, newBoundingRect: any, newDevtoolsNodePath: string}>} */ | ||
const resolved = await driver.evaluateAsync(expression, {useIsolation: true}); | ||
|
||
console.log(' resolved ===='); | ||
console.log(resolved); | ||
|
||
const walk = require('../lib/sd-validation/helpers/walk-object.js'); | ||
walk(artifacts, (name, value) => { | ||
if (!value || typeof value !== 'object') return; | ||
if (!value.path && !value.devtoolsNodePath) return; | ||
|
||
const resolvedNode = resolved.find(r => r.key === value.devtoolsNodePath); | ||
if (!resolvedNode) return; | ||
|
||
console.log(resolvedNode.key, 'set to rect', resolvedNode.newBoundingRect); | ||
|
||
// Rects are stored on properties named either `boundingRect` or `clientRect`. | ||
if (value.boundingRect) { | ||
value.boundingRect = resolvedNode.newBoundingRect; | ||
} else if (value.clientRect) { | ||
value.clientRect = resolvedNode.newBoundingRect; | ||
} | ||
|
||
value.devtoolsNodePath = resolvedNode.newDevtoolsNodePath; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suppose this could break things if updating the path (which is the lookup key...) as we go. Perhaps should create a |
||
}); | ||
} | ||
|
||
/** | ||
* Return an initialized but mostly empty set of base artifacts, to be | ||
* populated as the run continues. | ||
|
@@ -783,7 +838,11 @@ class GatherRunner { | |
|
||
// Run `afterPass()` on gatherers and return collected artifacts. | ||
await GatherRunner.afterPass(passContext, loadData, gathererResults); | ||
const artifacts = GatherRunner.collectArtifacts(gathererResults); | ||
const artifacts = await GatherRunner.collectArtifacts(gathererResults); | ||
|
||
if (process.env.RESOLVE_NODES) { | ||
await this.resolveNodes(driver, artifacts.artifacts); | ||
} | ||
|
||
log.timeEnd(status); | ||
return artifacts; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -447,19 +447,30 @@ function wrapRequestIdleCallback(cpuSlowdownMultiplier) { | |
}; | ||
} | ||
|
||
const getNodeDetailsString = `function getNodeDetails(elem) { | ||
/** | ||
* @param {HTMLElement} element | ||
*/ | ||
function getNodeDetails(element) { | ||
const details = { | ||
devtoolsNodePath: getNodePath(element), | ||
selector: getNodeSelector(element), | ||
boundingRect: getBoundingClientRect(element), | ||
snippet: getOuterHTMLSnippet(element), | ||
nodeLabel: getNodeLabel(element), | ||
}; | ||
window.__nodes = window.__nodes || []; | ||
window.__nodes.push({key: details.devtoolsNodePath, node: element}); | ||
return details; | ||
} | ||
|
||
const getNodeDetailsString = `function getNodeDetails(element) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we just put There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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()}; | ||
return { | ||
devtoolsNodePath: getNodePath(elem), | ||
selector: getNodeSelector(elem), | ||
boundingRect: getBoundingClientRect(elem), | ||
snippet: getOuterHTMLSnippet(elem), | ||
nodeLabel: getNodeLabel(elem), | ||
}; | ||
${getNodeDetails.toString()}; | ||
return getNodeDetails(element); | ||
}`; | ||
|
||
module.exports = { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just leaving this for posterity...
the _taptarget artifact_ has a `clientRects` prop (note the plural).. this can have multiple client rects per node even tho it has just a single bounding rect.
the iframe and image element artifacts have a
clientRect
prop. and yes those are identical in definition toboundingRect
.