Skip to content

core(driver): create eval code using interface #10816

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

Merged
merged 30 commits into from
Dec 2, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
90018b2
core(driver): create page code using structured interface
connorjclark May 20, 2020
f7e38f5
rename type
connorjclark May 20, 2020
78f386a
pr feedback
connorjclark May 20, 2020
32856e7
no strings
connorjclark May 20, 2020
e2dd63d
test
connorjclark May 20, 2020
f38bf41
Merge remote-tracking branch 'origin/master' into driver-eval-enhanced
connorjclark Jun 4, 2020
ce9839b
test
connorjclark Jun 4, 2020
4c903de
restrucutre
connorjclark Jun 5, 2020
0413a4a
Merge remote-tracking branch 'origin/master' into driver-eval-enhanced
connorjclark Jun 12, 2020
d4bd2bf
Merge remote-tracking branch 'origin/master' into driver-eval-enhanced
connorjclark Jun 17, 2020
02dc215
remove obj
connorjclark Jun 17, 2020
ce2827a
Merge remote-tracking branch 'origin/master' into driver-eval-enhanced
connorjclark Jul 15, 2020
c81a636
update master
connorjclark Nov 5, 2020
70d8210
rm dead code
connorjclark Nov 5, 2020
df5e8dd
Merge remote-tracking branch 'origin/master' into driver-eval-enhanced
connorjclark Nov 10, 2020
4e59476
adam feedback
connorjclark Nov 10, 2020
147b98c
fix tests
connorjclark Nov 10, 2020
a4051a1
fix mangle issues
connorjclark Nov 10, 2020
77afd88
fix
connorjclark Nov 10, 2020
fd94564
fix nasty types by using tuples
connorjclark Nov 10, 2020
9e7644e
no snapshot
connorjclark Nov 10, 2020
f42ca84
Merge remote-tracking branch 'origin/master' into driver-eval-enhanced
connorjclark Nov 12, 2020
b1fb5d3
fix pr
connorjclark Nov 12, 2020
c4d3a58
oops
connorjclark Nov 12, 2020
ee6f930
require empty args array
connorjclark Nov 13, 2020
6be38eb
inline
connorjclark Nov 13, 2020
97b0525
last bits
connorjclark Nov 13, 2020
81ea5b5
Merge remote-tracking branch 'origin/master' into driver-eval-enhanced
connorjclark Nov 13, 2020
4843291
Merge remote-tracking branch 'origin/master' into driver-eval-enhanced
connorjclark Dec 1, 2020
3c9f573
tests
connorjclark Dec 2, 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
16 changes: 16 additions & 0 deletions lighthouse-core/gather/driver.js
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,22 @@ class Driver {
return this._executionContext.evaluateAsync(expression, options);
}

/**
* Evaluate a function in the context of the current page.
* If `useIsolation` is true, the function will be evaluated in a content script that has
* access to the page's DOM but whose JavaScript state is completely separate.
* Returns a promise that resolves on a value of `mainFn`'s return type.
* @template {any[]} T, R
* @param {((...args: T) => R)} mainFn The main function to call.
* @param {{args: T, useIsolation?: boolean, deps?: Array<Function|string>}} options `args` should
* match the args of `mainFn`, and can be any serializable value. `deps` are functions that must be
* defined for `mainFn` to work.
* @return {Promise<R>}
*/
async evaluate(mainFn, options) {
return this._executionContext.evaluate(mainFn, options);
}

/**
* @return {Promise<{url: string, data: string}|null>}
*/
Expand Down
29 changes: 26 additions & 3 deletions lighthouse-core/gather/driver/execution-context.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,9 @@ class ExecutionContext {
// 3. Ensure that errors captured in the Promise are converted into plain-old JS Objects
// so that they can be serialized properly b/c JSON.stringify(new Error('foo')) === '{}'
expression: `(function wrapInNativePromise() {
const __nativePromise = window.__nativePromise || Promise;
const URL = window.__nativeURL || window.URL;
window.__lighthouseExecutionContextId = ${contextId};
const __nativePromise = globalThis.__nativePromise || Promise;
const URL = globalThis.__nativeURL || globalThis.URL;
globalThis.__lighthouseExecutionContextId = ${contextId};
return new __nativePromise(function (resolve) {
return __nativePromise.resolve()
.then(_ => ${expression})
Expand Down Expand Up @@ -149,6 +149,29 @@ class ExecutionContext {
throw err;
}
}

/**
* Evaluate a function in the context of the current page.
* If `useIsolation` is true, the function will be evaluated in a content script that has
* access to the page's DOM but whose JavaScript state is completely separate.
* Returns a promise that resolves on a value of `mainFn`'s return type.
* @template {any[]} T, R
* @param {((...args: T) => R)} mainFn The main function to call.
* @param {{args: T, useIsolation?: boolean, deps?: Array<Function|string>}} options `args` should
* match the args of `mainFn`, and can be any serializable value. `deps` are functions that must be
* defined for `mainFn` to work.
* @return {Promise<R>}
*/
evaluate(mainFn, options) {
const argsSerialized = options.args.map(arg => JSON.stringify(arg)).join(',');
const depsSerialized = options.deps ? options.deps.join('\n') : '';
const expression = `(() => {
${depsSerialized}
${mainFn}
return ${mainFn.name}(${argsSerialized});
})()`;
return this.evaluateAsync(expression, options);
}
}

module.exports = ExecutionContext;
3 changes: 2 additions & 1 deletion lighthouse-core/gather/fetcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,8 @@ class Fetcher {
requestInterceptionPromise,
]).finally(() => clearTimeout(timeoutHandle));

const injectionPromise = this.driver.evaluateAsync(`${injectIframe}(${JSON.stringify(url)})`, {
const injectionPromise = this.driver.evaluate(injectIframe, {
args: [url],
useIsolation: true,
});

Expand Down
17 changes: 9 additions & 8 deletions lighthouse-core/gather/gatherers/link-elements.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const LinkHeader = require('http-link-header');
const Gatherer = require('./gatherer.js');
const {URL} = require('../../lib/url-shim.js');
const NetworkAnalyzer = require('../../lib/dependency-graph/simulator/network-analyzer.js');
const {getElementsInDocumentString, getNodeDetailsString} = require('../../lib/page-functions.js');
const pageFunctions = require('../../lib/page-functions.js');

/* globals HTMLLinkElement getNodeDetails */

Expand Down Expand Up @@ -86,13 +86,14 @@ class LinkElements extends Gatherer {
static getLinkElementsInDOM(passContext) {
// We'll use evaluateAsync because the `node.getAttribute` method doesn't actually normalize
// the values like access from JavaScript does.
return passContext.driver.evaluateAsync(`(() => {
${getElementsInDocumentString};
${getLinkElementsInDOM};
${getNodeDetailsString};

return getLinkElementsInDOM();
})()`, {useIsolation: true});
return passContext.driver.evaluate(getLinkElementsInDOM, {
args: [],
useIsolation: true,
deps: [
pageFunctions.getNodeDetailsString,
pageFunctions.getElementsInDocument,
],
});
}

/**
Expand Down
43 changes: 29 additions & 14 deletions lighthouse-core/gather/gatherers/meta-elements.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,30 @@
'use strict';

const Gatherer = require('./gatherer.js');
const getElementsInDocumentString = require('../../lib/page-functions.js').getElementsInDocumentString; // eslint-disable-line max-len
const pageFunctions = require('../../lib/page-functions.js');

/* globals getElementsInDocument */

/* istanbul ignore next */
function collectMetaElements() {
// @ts-expect-error - getElementsInDocument put into scope via stringification
const metas = /** @type {HTMLMetaElement[]} */ (getElementsInDocument('head meta'));
return metas.map(meta => {
/** @param {string} name */
const getAttribute = name => {
const attr = meta.attributes.getNamedItem(name);
if (!attr) return;
return attr.value;
};
return {
name: meta.name.toLowerCase(),
content: meta.content,
property: getAttribute('property'),
httpEquiv: meta.httpEquiv ? meta.httpEquiv.toLowerCase() : undefined,
charset: getAttribute('charset'),
};
});
}

class MetaElements extends Gatherer {
/**
Expand All @@ -18,19 +41,11 @@ class MetaElements extends Gatherer {

// We'll use evaluateAsync because the `node.getAttribute` method doesn't actually normalize
// the values like access from JavaScript does.
return driver.evaluateAsync(`(() => {
${getElementsInDocumentString};

return getElementsInDocument('head meta').map(meta => {
return {
name: meta.name.toLowerCase(),
content: meta.content,
property: meta.attributes.property ? meta.attributes.property.value : undefined,
httpEquiv: meta.httpEquiv ? meta.httpEquiv.toLowerCase() : undefined,
charset: meta.attributes.charset ? meta.attributes.charset.value : undefined,
};
});
})()`, {useIsolation: true});
return driver.evaluate(collectMetaElements, {
args: [],
useIsolation: true,
deps: [pageFunctions.getElementsInDocument],
});
}
}

Expand Down
15 changes: 8 additions & 7 deletions lighthouse-core/gather/gatherers/script-elements.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
const Gatherer = require('./gatherer.js');
const NetworkAnalyzer = require('../../lib/dependency-graph/simulator/network-analyzer.js');
const NetworkRequest = require('../../lib/network-request.js');
const getElementsInDocumentString = require('../../lib/page-functions.js').getElementsInDocumentString; // eslint-disable-line max-len
const pageFunctions = require('../../lib/page-functions.js');

/* global getNodeDetails */
Expand Down Expand Up @@ -72,12 +71,14 @@ class ScriptElements extends Gatherer {
const driver = passContext.driver;
const mainResource = NetworkAnalyzer.findMainDocument(loadData.networkRecords, passContext.url);

/** @type {LH.Artifacts['ScriptElements']} */
const scripts = await driver.evaluateAsync(`(() => {
${getElementsInDocumentString}
${pageFunctions.getNodeDetailsString};
return (${collectAllScriptElements.toString()})();
})()`, {useIsolation: true});
const scripts = await driver.evaluate(collectAllScriptElements, {
args: [],
useIsolation: true,
deps: [
pageFunctions.getNodeDetailsString,
pageFunctions.getElementsInDocument,
],
});

for (const script of scripts) {
if (script.content) script.requestId = mainResource.requestId;
Expand Down
48 changes: 25 additions & 23 deletions lighthouse-core/gather/gatherers/seo/tap-targets.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const tapTargetsSelector = TARGET_SELECTORS.join(',');

/**
* @param {HTMLElement} element
* @returns {boolean}
* @return {boolean}
*/
/* istanbul ignore next */
function elementIsVisible(element) {
Expand All @@ -42,7 +42,7 @@ function elementIsVisible(element) {

/**
* @param {Element} element
* @returns {LH.Artifacts.Rect[]}
* @return {LH.Artifacts.Rect[]}
*/
/* istanbul ignore next */
function getClientRects(element) {
Expand All @@ -65,7 +65,7 @@ function getClientRects(element) {
/**
* @param {Element} element
* @param {string} tapTargetsSelector
* @returns {boolean}
* @return {boolean}
*/
/* istanbul ignore next */
function elementHasAncestorTapTarget(element, tapTargetsSelector) {
Expand Down Expand Up @@ -119,7 +119,7 @@ function hasTextNodeSiblingsFormingTextBlock(element) {
* Makes a reasonable guess, but for example gets it wrong if the element is surrounded by other
* HTML elements instead of direct text nodes.
* @param {Element} element
* @returns {boolean}
* @return {boolean}
*/
/* istanbul ignore next */
function elementIsInTextBlock(element) {
Expand Down Expand Up @@ -160,7 +160,7 @@ function elementCenterIsAtZAxisTop(el, elCenterPoint) {
/**
* Finds all position sticky/absolute elements on the page and adds a class
* that disables pointer events on them.
* @returns {() => void} - undo function to re-enable pointer events
* @return {() => void} - undo function to re-enable pointer events
*/
/* istanbul ignore next */
function disableFixedAndStickyElementPointerEvents() {
Expand All @@ -186,7 +186,7 @@ function disableFixedAndStickyElementPointerEvents() {

/**
* @param {string} tapTargetsSelector
* @returns {LH.Artifacts.TapTarget[]}
* @return {LH.Artifacts.TapTarget[]}
*/
/* istanbul ignore next */
function gatherTapTargets(tapTargetsSelector) {
Expand Down Expand Up @@ -285,23 +285,25 @@ class TapTargets extends Gatherer {
* @return {Promise<LH.Artifacts.TapTarget[]>} All visible tap targets with their positions and sizes
*/
afterPass(passContext) {
const expression = `(function() {
${pageFunctions.getElementsInDocumentString};
${disableFixedAndStickyElementPointerEvents.toString()};
${elementIsVisible.toString()};
${elementHasAncestorTapTarget.toString()};
${elementCenterIsAtZAxisTop.toString()}
${getClientRects.toString()};
${hasTextNodeSiblingsFormingTextBlock.toString()};
${elementIsInTextBlock.toString()};
${RectHelpers.getRectCenterPoint.toString()};
${pageFunctions.getNodeDetailsString};
${gatherTapTargets.toString()};

return gatherTapTargets("${tapTargetsSelector}");
})()`;

return passContext.driver.evaluateAsync(expression, {useIsolation: true});
return passContext.driver.evaluate(gatherTapTargets, {
args: [tapTargetsSelector],
useIsolation: true,
deps: [
pageFunctions.getNodeDetailsString,
pageFunctions.getElementsInDocument,
disableFixedAndStickyElementPointerEvents,
elementIsVisible,
elementHasAncestorTapTarget,
elementCenterIsAtZAxisTop,
getClientRects,
hasTextNodeSiblingsFormingTextBlock,
elementIsInTextBlock,
RectHelpers.getRectCenterPoint,
pageFunctions.getNodePath,
pageFunctions.getNodeSelector,
pageFunctions.getNodeLabel,
],
});
}
}

Expand Down
19 changes: 15 additions & 4 deletions lighthouse-core/lib/page-functions.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,21 @@
// @ts-nocheck
'use strict';

/* global window document Node ShadowRoot */

/**
* @fileoverview
* Helper functions that are passed by `toString()` by Driver to be evaluated in target page.
*
* Important: this module should only be imported like this:
* const pageFunctions = require('...');
* Never like this:
* const {justWhatINeed} = require('...');
* Otherwise, minification will mangle the variable names and break usage.
*/

/** @typedef {HTMLElementTagNameMap & {[id: string]: HTMLElement}} HTMLElementByTagName */

/* global window document Node ShadowRoot */

/**
* The `exceptionDetails` provided by the debugger protocol does not contain the useful
* information such as name, message, and stack trace of the error when it's wrapped in a
Expand Down Expand Up @@ -78,9 +87,10 @@ function checkTimeSinceLastLongTask() {
}

/**
* @param {string=} selector Optional simple CSS selector to filter nodes on.
* @template {string} T
* @param {T} selector Optional simple CSS selector to filter nodes on.
* Combinators are not supported.
* @return {Array<HTMLElement>}
* @return {Array<HTMLElementByTagName[T]>}
*/
/* istanbul ignore next */
function getElementsInDocument(selector) {
Expand Down Expand Up @@ -501,6 +511,7 @@ module.exports = {
wrapRuntimeEvalErrorInBrowserString: wrapRuntimeEvalErrorInBrowser.toString(),
registerPerformanceObserverInPageString: registerPerformanceObserverInPage.toString(),
checkTimeSinceLastLongTaskString: checkTimeSinceLastLongTask.toString(),
getElementsInDocument,
getElementsInDocumentString: getElementsInDocument.toString(),
getOuterHTMLSnippetString: getOuterHTMLSnippet.toString(),
getOuterHTMLSnippet: getOuterHTMLSnippet,
Expand Down
Loading