diff --git a/docs/recipes/custom-gatherer-puppeteer/custom-gatherer.js b/docs/recipes/custom-gatherer-puppeteer/custom-gatherer.js index cba9ab0b5ca1..faeb46443b25 100644 --- a/docs/recipes/custom-gatherer-puppeteer/custom-gatherer.js +++ b/docs/recipes/custom-gatherer-puppeteer/custom-gatherer.js @@ -33,7 +33,7 @@ class CustomGatherer extends Gatherer { el.type = 'number'; document.body.append(el); } - await driver.evaluateAsync(`(${makeInput})()`); + await driver.evaluate(makeInput, {args: []}); await new Promise(resolve => setTimeout(resolve, 100)); // Prove that `driver` (Lighthouse) and `page` (Puppeteer) are talking to the same page. diff --git a/lighthouse-core/fraggle-rock/gather/driver.js b/lighthouse-core/fraggle-rock/gather/driver.js index 568790061194..04d69fb0f095 100644 --- a/lighthouse-core/fraggle-rock/gather/driver.js +++ b/lighthouse-core/fraggle-rock/gather/driver.js @@ -55,6 +55,19 @@ class Driver { if (!this._executionContext) throw new Error('Driver not connected to page'); return this._executionContext.evaluateAsync(expression, options); } + + /** + * @template {any[]} T, R + * @param {((...args: T) => R)} mainFn The main function to call. + * @param {{args: T, useIsolation?: boolean, deps?: Array}} 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 {FlattenedPromise} + */ + evaluate(mainFn, options) { + if (!this._executionContext) throw new Error('Driver not connected to page'); + return this._executionContext.evaluate(mainFn, options); + } } module.exports = Driver; diff --git a/lighthouse-core/gather/driver.js b/lighthouse-core/gather/driver.js index a575e66d6924..020698ac3593 100644 --- a/lighthouse-core/gather/driver.js +++ b/lighthouse-core/gather/driver.js @@ -466,6 +466,11 @@ class Driver { } /** + * Note: Prefer `evaluate` instead. + * Evaluate an expression in the context of the current page. If useIsolation is true, the expression + * 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 the expression's value. * @param {string} expression * @param {{useIsolation?: boolean}=} options * @return {Promise<*>} @@ -484,9 +489,9 @@ class Driver { * @param {{args: T, useIsolation?: boolean, deps?: Array}} 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} + * @return {FlattenedPromise} */ - async evaluate(mainFn, options) { + evaluate(mainFn, options) { return this._executionContext.evaluate(mainFn, options); } diff --git a/lighthouse-core/gather/driver/execution-context.js b/lighthouse-core/gather/driver/execution-context.js index 8964219c39cb..ef4fe1ffa8c9 100644 --- a/lighthouse-core/gather/driver/execution-context.js +++ b/lighthouse-core/gather/driver/execution-context.js @@ -160,7 +160,7 @@ class ExecutionContext { * @param {{args: T, useIsolation?: boolean, deps?: Array}} 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} + * @return {FlattenedPromise} */ evaluate(mainFn, options) { const argsSerialized = options.args.map(arg => JSON.stringify(arg)).join(','); diff --git a/lighthouse-core/gather/driver/wait-for-condition.js b/lighthouse-core/gather/driver/wait-for-condition.js index e9a159b8665b..88533f6dbc4a 100644 --- a/lighthouse-core/gather/driver/wait-for-condition.js +++ b/lighthouse-core/gather/driver/wait-for-condition.js @@ -213,7 +213,6 @@ function waitForCPUIdle(session, waitForCPUQuiet) { let lastTimeout; let canceled = false; - const checkForQuietExpression = `(${pageFunctions.checkTimeSinceLastLongTaskString})()`; /** * @param {ExecutionContext} executionContext * @param {() => void} resolve @@ -221,7 +220,8 @@ function waitForCPUIdle(session, waitForCPUQuiet) { */ async function checkForQuiet(executionContext, resolve) { if (canceled) return; - const timeSinceLongTask = await executionContext.evaluateAsync(checkForQuietExpression); + const timeSinceLongTask = + await executionContext.evaluate(pageFunctions.checkTimeSinceLastLongTask, {args: []}); if (canceled) return; if (typeof timeSinceLongTask === 'number') { diff --git a/lighthouse-core/gather/gatherers/accessibility.js b/lighthouse-core/gather/gatherers/accessibility.js index 580fd398fb0d..64f9fce41745 100644 --- a/lighthouse-core/gather/gatherers/accessibility.js +++ b/lighthouse-core/gather/gatherers/accessibility.js @@ -107,13 +107,15 @@ class Accessibility extends Gatherer { */ afterPass(passContext) { const driver = passContext.driver; - const expression = `(function () { - ${pageFunctions.getNodeDetailsString}; - ${axeLibSource}; - return (${runA11yChecks.toString()}()); - })()`; - return driver.evaluateAsync(expression, {useIsolation: true}).then(returnedValue => { + return driver.evaluate(runA11yChecks, { + args: [], + useIsolation: true, + deps: [ + axeLibSource, + pageFunctions.getNodeDetailsString, + ], + }).then(returnedValue => { if (!returnedValue) { throw new Error('No axe-core results returned'); } diff --git a/lighthouse-core/gather/gatherers/anchor-elements.js b/lighthouse-core/gather/gatherers/anchor-elements.js index 275bc5d95a28..ad368a032550 100644 --- a/lighthouse-core/gather/gatherers/anchor-elements.js +++ b/lighthouse-core/gather/gatherers/anchor-elements.js @@ -95,15 +95,15 @@ class AnchorElements extends Gatherer { */ async afterPass(passContext) { const driver = passContext.driver; - const expression = `(() => { - ${pageFunctions.getElementsInDocumentString}; - ${pageFunctions.getNodeDetailsString}; - return (${collectAnchorElements})(); - })()`; - - /** @type {LH.Artifacts['AnchorElements']} */ - const anchors = await driver.evaluateAsync(expression, {useIsolation: true}); + const anchors = await driver.evaluate(collectAnchorElements, { + args: [], + useIsolation: true, + deps: [ + pageFunctions.getElementsInDocumentString, + pageFunctions.getNodeDetailsString, + ], + }); await driver.sendCommand('DOM.enable'); // DOM.getDocument is necessary for pushNodesByBackendIdsToFrontend to properly retrieve nodeIds if the `DOM` domain was enabled before this gatherer, invoke it to be safe. diff --git a/lighthouse-core/gather/gatherers/cache-contents.js b/lighthouse-core/gather/gatherers/cache-contents.js index fdf2fe6d489b..e04c10801289 100644 --- a/lighthouse-core/gather/gatherers/cache-contents.js +++ b/lighthouse-core/gather/gatherers/cache-contents.js @@ -46,12 +46,7 @@ class CacheContents extends Gatherer { async afterPass(passContext) { const driver = passContext.driver; - /** @type {Array|void} */ - const cacheUrls = await driver.evaluateAsync(`(${getCacheContents.toString()}())`); - if (!cacheUrls || !Array.isArray(cacheUrls)) { - throw new Error('Unable to retrieve cache contents'); - } - + const cacheUrls = await driver.evaluate(getCacheContents, {args: []}); return cacheUrls; } } diff --git a/lighthouse-core/gather/gatherers/dobetterweb/doctype.js b/lighthouse-core/gather/gatherers/dobetterweb/doctype.js index ee0579496587..d404e713f942 100644 --- a/lighthouse-core/gather/gatherers/dobetterweb/doctype.js +++ b/lighthouse-core/gather/gatherers/dobetterweb/doctype.js @@ -29,7 +29,7 @@ class Doctype extends Gatherer { */ afterPass(passContext) { const driver = passContext.driver; - return driver.evaluateAsync(`(${getDoctype.toString()}())`); + return driver.evaluate(getDoctype, {args: []}); } } diff --git a/lighthouse-core/gather/gatherers/dobetterweb/domstats.js b/lighthouse-core/gather/gatherers/dobetterweb/domstats.js index 0bdb4dfcd8af..ac67c051cfa2 100644 --- a/lighthouse-core/gather/gatherers/dobetterweb/domstats.js +++ b/lighthouse-core/gather/gatherers/dobetterweb/domstats.js @@ -3,20 +3,19 @@ * 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. */ -// @ts-nocheck + /** * @fileoverview Gathers stats about the max height and width of the DOM tree * and total number of elements used on the page. */ -/* global getNodeDetails */ +/* global getNodeDetails document */ 'use strict'; const Gatherer = require('../gatherer.js'); const pageFunctions = require('../../../lib/page-functions.js'); - /** * Calculates the maximum tree depth of the DOM. * @param {HTMLElement} element Root of the tree to look in. @@ -24,7 +23,7 @@ const pageFunctions = require('../../../lib/page-functions.js'); * @return {LH.Artifacts.DOMStats} */ /* istanbul ignore next */ -function getDOMStats(element, deep = true) { +function getDOMStats(element = document.body, deep = true) { let deepestElement = null; let maxDepth = -1; let maxWidth = -1; @@ -32,7 +31,7 @@ function getDOMStats(element, deep = true) { let parentWithMostChildren = null; /** - * @param {Element} element + * @param {Element|ShadowRoot} element * @param {number} depth */ const _calcDOMWidthAndHeight = function(element, depth = 1) { @@ -64,10 +63,12 @@ function getDOMStats(element, deep = true) { return { depth: { max: result.maxDepth, + // @ts-expect-error - getNodeDetails put into scope via stringification ...getNodeDetails(deepestElement), }, width: { max: result.maxWidth, + // @ts-expect-error - getNodeDetails put into scope via stringification ...getNodeDetails(parentWithMostChildren), }, totalBodyElements: result.numElements, @@ -82,12 +83,12 @@ class DOMStats extends Gatherer { async afterPass(passContext) { const driver = passContext.driver; - const expression = `(function() { - ${pageFunctions.getNodeDetailsString}; - return (${getDOMStats.toString()}(document.body)); - })()`; await driver.sendCommand('DOM.enable'); - const results = await driver.evaluateAsync(expression, {useIsolation: true}); + const results = await driver.evaluate(getDOMStats, { + args: [], + useIsolation: true, + deps: [pageFunctions.getNodeDetailsString], + }); await driver.sendCommand('DOM.disable'); return results; } diff --git a/lighthouse-core/gather/gatherers/dobetterweb/password-inputs-with-prevented-paste.js b/lighthouse-core/gather/gatherers/dobetterweb/password-inputs-with-prevented-paste.js index 3e418af7ff25..d6e5cf5ae186 100644 --- a/lighthouse-core/gather/gatherers/dobetterweb/password-inputs-with-prevented-paste.js +++ b/lighthouse-core/gather/gatherers/dobetterweb/password-inputs-with-prevented-paste.js @@ -34,12 +34,10 @@ class PasswordInputsWithPreventedPaste extends Gatherer { * @return {Promise} */ afterPass(passContext) { - const expression = `(() => { - ${pageFunctions.getNodeDetailsString}; - return (${findPasswordInputsWithPreventedPaste.toString()}()); - })()`; - - return passContext.driver.evaluateAsync(expression); + return passContext.driver.evaluate(findPasswordInputsWithPreventedPaste, { + args: [], + deps: [pageFunctions.getNodeDetailsString], + }); } } diff --git a/lighthouse-core/gather/gatherers/form-elements.js b/lighthouse-core/gather/gatherers/form-elements.js index dc3f90f0ab3e..d8d45399bf6b 100644 --- a/lighthouse-core/gather/gatherers/form-elements.js +++ b/lighthouse-core/gather/gatherers/form-elements.js @@ -91,14 +91,14 @@ class FormElements extends Gatherer { async afterPass(passContext) { const driver = passContext.driver; - const expression = `(() => { - ${pageFunctions.getElementsInDocumentString}; - ${pageFunctions.getNodeDetailsString}; - return (${collectFormElements})(); - })()`; - - /** @type {LH.Artifacts['FormElements']} */ - const formElements = await driver.evaluateAsync(expression, {useIsolation: true}); + const formElements = await driver.evaluate(collectFormElements, { + args: [], + useIsolation: true, + deps: [ + pageFunctions.getElementsInDocumentString, + pageFunctions.getNodeDetailsString, + ], + }); return formElements; } } diff --git a/lighthouse-core/gather/gatherers/full-page-screenshot.js b/lighthouse-core/gather/gatherers/full-page-screenshot.js index a807965f0651..4aa8e4f63ef3 100644 --- a/lighthouse-core/gather/gatherers/full-page-screenshot.js +++ b/lighthouse-core/gather/gatherers/full-page-screenshot.js @@ -5,7 +5,7 @@ */ 'use strict'; -/* globals window getBoundingClientRect */ +/* globals window document getBoundingClientRect */ const Gatherer = require('./gatherer.js'); const pageFunctions = require('../../lib/page-functions.js'); @@ -93,18 +93,23 @@ class FullPageScreenshot extends Gatherer { return nodes; } - const expression = `(function () { - ${pageFunctions.getBoundingClientRectString}; - return (${resolveNodes.toString()}()); - })()`; + + /** + * @param {{useIsolation: boolean}} _ + */ + function resolveNodesInPage({useIsolation}) { + return passContext.driver.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 passContext.driver.evaluateAsync(expression, {useIsolation: false}); - const isolatedContextResult = - await passContext.driver.evaluateAsync(expression, {useIsolation: true}); + const pageContextResult = await resolveNodesInPage({useIsolation: false}); + const isolatedContextResult = await resolveNodesInPage({useIsolation: true}); return {...pageContextResult, ...isolatedContextResult}; } @@ -136,20 +141,28 @@ class FullPageScreenshot extends Gatherer { // 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 - const observedDeviceMetrics = await driver.evaluateAsync(`(function() { + + // 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: window.screen.orientation.type, + type: screenOrientationType, angle: window.screen.orientation.angle, }, deviceScaleFactor: window.devicePixelRatio, }; - })()`, {useIsolation: true}); - // Convert the Web API's snake case (landscape-primary) to camel case (landscapePrimary). - observedDeviceMetrics.screenOrientation.type = - snakeCaseToCamelCase(observedDeviceMetrics.screenOrientation.type); + } + + const observedDeviceMetrics = await driver.evaluate(getObservedDeviceMetrics, { + args: [], + useIsolation: true, + deps: [snakeCaseToCamelCase], + }); await driver.sendCommand('Emulation.setDeviceMetricsOverride', { mobile: passContext.settings.formFactor === 'mobile', ...observedDeviceMetrics, diff --git a/lighthouse-core/gather/gatherers/iframe-elements.js b/lighthouse-core/gather/gatherers/iframe-elements.js index 39faa61f69bf..ec234938c486 100644 --- a/lighthouse-core/gather/gatherers/iframe-elements.js +++ b/lighthouse-core/gather/gatherers/iframe-elements.js @@ -43,15 +43,15 @@ class IFrameElements extends Gatherer { async afterPass(passContext) { const driver = passContext.driver; - const expression = `(() => { - ${pageFunctions.getElementsInDocumentString}; - ${pageFunctions.isPositionFixedString}; - ${pageFunctions.getNodeDetailsString}; - return (${collectIFrameElements})(); - })()`; - - /** @type {LH.Artifacts['IFrameElements']} */ - const iframeElements = await driver.evaluateAsync(expression, {useIsolation: true}); + const iframeElements = await driver.evaluate(collectIFrameElements, { + args: [], + useIsolation: true, + deps: [ + pageFunctions.getElementsInDocumentString, + pageFunctions.isPositionFixedString, + pageFunctions.getNodeDetailsString, + ], + }); return iframeElements; } } diff --git a/lighthouse-core/gather/gatherers/image-elements.js b/lighthouse-core/gather/gatherers/image-elements.js index 08caf579255b..afb966743ebf 100644 --- a/lighthouse-core/gather/gatherers/image-elements.js +++ b/lighthouse-core/gather/gatherers/image-elements.js @@ -223,7 +223,7 @@ class ImageElements extends Gatherer { * @return {Promise} */ async fetchElementWithSizeInformation(driver, element) { - const url = JSON.stringify(element.src); + const url = element.src; if (this._naturalSizeCache.has(url)) { return Object.assign(element, this._naturalSizeCache.get(url)); } @@ -231,8 +231,9 @@ class ImageElements extends Gatherer { try { // We don't want this to take forever, 250ms should be enough for images that are cached driver.setNextProtocolTimeout(250); - /** @type {{naturalWidth: number, naturalHeight: number}} */ - const size = await driver.evaluateAsync(`(${determineNaturalSize.toString()})(${url})`); + const size = await driver.evaluate(determineNaturalSize, { + args: [url], + }); this._naturalSizeCache.set(url, size); return Object.assign(element, size); } catch (_) { @@ -286,21 +287,18 @@ class ImageElements extends Gatherer { return map; }, /** @type {Object} */ ({})); - const expression = `(function() { - ${pageFunctions.getElementsInDocumentString}; // define function on page - ${pageFunctions.getBoundingClientRectString}; - ${pageFunctions.getNodeDetailsString}; - ${getClientRect.toString()}; - ${getPosition.toString()}; - ${getHTMLImages.toString()}; - ${getCSSImages.toString()}; - ${collectImageElementInfo.toString()}; - - return collectImageElementInfo(); - })()`; - - /** @type {Array} */ - const elements = await driver.evaluateAsync(expression); + const elements = await driver.evaluate(collectImageElementInfo, { + args: [], + deps: [ + pageFunctions.getElementsInDocumentString, + pageFunctions.getBoundingClientRectString, + pageFunctions.getNodeDetailsString, + getClientRect, + getPosition, + getHTMLImages, + getCSSImages, + ], + }); /** @type {Array} */ const imageUsage = []; diff --git a/lighthouse-core/gather/gatherers/seo/embedded-content.js b/lighthouse-core/gather/gatherers/seo/embedded-content.js index 4d510aeee91c..a4554d40d672 100644 --- a/lighthouse-core/gather/gatherers/seo/embedded-content.js +++ b/lighthouse-core/gather/gatherers/seo/embedded-content.js @@ -5,36 +5,42 @@ */ 'use strict'; +/* globals getElementsInDocument */ + const Gatherer = require('../gatherer.js'); const pageFunctions = require('../../../lib/page-functions.js'); +function getEmbeddedContent() { + const selector = 'object, embed, applet'; + /** @type {HTMLElement[]} */ + // @ts-expect-error - getElementsInDocument put into scope via stringification + const elements = getElementsInDocument(selector); + return elements + .map(node => ({ + tagName: node.tagName, + type: node.getAttribute('type'), + src: node.getAttribute('src'), + data: node.getAttribute('data'), + code: node.getAttribute('code'), + params: Array.from(node.children) + .filter(el => el.tagName === 'PARAM') + .map(el => ({ + name: el.getAttribute('name') || '', + value: el.getAttribute('value') || '', + })), + })); +} + class EmbeddedContent extends Gatherer { /** * @param {LH.Gatherer.PassContext} passContext * @return {Promise} */ afterPass(passContext) { - const expression = `(function() { - ${pageFunctions.getElementsInDocumentString}; // define function on page - const selector = 'object, embed, applet'; - const elements = getElementsInDocument(selector); - return elements - .map(node => ({ - tagName: node.tagName, - type: node.getAttribute('type'), - src: node.getAttribute('src'), - data: node.getAttribute('data'), - code: node.getAttribute('code'), - params: Array.from(node.children) - .filter(el => el.tagName === 'PARAM') - .map(el => ({ - name: el.getAttribute('name') || '', - value: el.getAttribute('value') || '', - })), - })); - })()`; - - return passContext.driver.evaluateAsync(expression); + return passContext.driver.evaluate(getEmbeddedContent, { + args: [], + deps: [pageFunctions.getElementsInDocument], + }); } } diff --git a/lighthouse-core/gather/gatherers/seo/robots-txt.js b/lighthouse-core/gather/gatherers/seo/robots-txt.js index 749827f94a53..7a0b1294ef01 100644 --- a/lighthouse-core/gather/gatherers/seo/robots-txt.js +++ b/lighthouse-core/gather/gatherers/seo/robots-txt.js @@ -32,7 +32,8 @@ class RobotsTxt extends Gatherer { * @return {Promise} */ afterPass(passContext) { - return passContext.driver.evaluateAsync(`(${getRobotsTxtContent.toString()}())`, { + return passContext.driver.evaluate(getRobotsTxtContent, { + args: [], useIsolation: true, }); } diff --git a/lighthouse-core/gather/gatherers/viewport-dimensions.js b/lighthouse-core/gather/gatherers/viewport-dimensions.js index f5376df26cd9..173775084d51 100644 --- a/lighthouse-core/gather/gatherers/viewport-dimensions.js +++ b/lighthouse-core/gather/gatherers/viewport-dimensions.js @@ -10,20 +10,20 @@ const Gatherer = require('./gatherer.js'); /* global window */ /** - * @return {Promise} + * @return {LH.Artifacts.ViewportDimensions} */ /* istanbul ignore next */ function getViewportDimensions() { // window.innerWidth to get the scrollable size of the window (irrespective of zoom) // window.outerWidth to get the size of the visible area // window.devicePixelRatio to get ratio of logical pixels to physical pixels - return Promise.resolve({ + return { innerWidth: window.innerWidth, innerHeight: window.innerHeight, outerWidth: window.outerWidth, outerHeight: window.outerHeight, devicePixelRatio: window.devicePixelRatio, - }); + }; } class ViewportDimensions extends Gatherer { @@ -34,9 +34,10 @@ class ViewportDimensions extends Gatherer { async afterPass(passContext) { const driver = passContext.driver; - /** @type {LH.Artifacts.ViewportDimensions} */ - const dimensions = await driver.evaluateAsync(`(${getViewportDimensions.toString()}())`, - {useIsolation: true}); + const dimensions = await driver.evaluate(getViewportDimensions, { + args: [], + useIsolation: true, + }); const allNumeric = Object.values(dimensions).every(Number.isFinite); if (!allNumeric) { diff --git a/lighthouse-core/lib/page-functions.js b/lighthouse-core/lib/page-functions.js index 07f1e7ff1d3a..8c9d62234b24 100644 --- a/lighthouse-core/lib/page-functions.js +++ b/lighthouse-core/lib/page-functions.js @@ -68,6 +68,7 @@ function registerPerformanceObserverInPage() { /** * Used by _waitForCPUIdle and executed in the context of the page, returns time since last long task. + * @return {number} */ /* istanbul ignore next */ function checkTimeSinceLastLongTask() { @@ -513,7 +514,7 @@ const getNodeDetailsString = `function getNodeDetails(element) { module.exports = { wrapRuntimeEvalErrorInBrowserString: wrapRuntimeEvalErrorInBrowser.toString(), registerPerformanceObserverInPageString: registerPerformanceObserverInPage.toString(), - checkTimeSinceLastLongTaskString: checkTimeSinceLastLongTask.toString(), + checkTimeSinceLastLongTask, getElementsInDocument, getElementsInDocumentString: getElementsInDocument.toString(), getOuterHTMLSnippetString: getOuterHTMLSnippet.toString(), diff --git a/lighthouse-core/lib/stack-collector.js b/lighthouse-core/lib/stack-collector.js index 01b95817aaa0..f3afbd42771c 100644 --- a/lighthouse-core/lib/stack-collector.js +++ b/lighthouse-core/lib/stack-collector.js @@ -82,13 +82,11 @@ async function detectLibraries() { async function collectStacks(passContext) { const status = {msg: 'Collect stacks', id: 'lh:gather:collectStacks'}; log.time(status); - const expression = `(function () { - ${libDetectorSource}; - return (${detectLibraries.toString()}()); - })()`; - /** @type {JSLibrary[]} */ - const jsLibraries = await passContext.driver.evaluateAsync(expression); + const jsLibraries = await passContext.driver.evaluate(detectLibraries, { + args: [], + deps: [libDetectorSource], + }); /** @type {LH.Artifacts['Stacks']} */ const stacks = jsLibraries.map(lib => ({ diff --git a/lighthouse-core/test/gather/fake-driver.js b/lighthouse-core/test/gather/fake-driver.js index db166317525b..4af9decceef1 100644 --- a/lighthouse-core/test/gather/fake-driver.js +++ b/lighthouse-core/test/gather/fake-driver.js @@ -71,6 +71,9 @@ function makeFakeDriver({protocolGetVersionResponse}) { evaluateAsync() { return Promise.resolve({}); }, + evaluate() { + return Promise.resolve({}); + }, /** @param {{x: number, y: number}} position */ scrollTo(position) { scrollPosition = position; diff --git a/lighthouse-core/test/gather/gatherers/accessibility-test.js b/lighthouse-core/test/gather/gatherers/accessibility-test.js index c64d21aab8c5..8b4d6413535d 100644 --- a/lighthouse-core/test/gather/gatherers/accessibility-test.js +++ b/lighthouse-core/test/gather/gatherers/accessibility-test.js @@ -20,9 +20,7 @@ describe('Accessibility gatherer', () => { it('fails if nothing is returned', () => { return accessibilityGather.afterPass({ driver: { - evaluateAsync() { - return Promise.resolve(); - }, + async evaluate() {}, }, }).then( _ => assert.ok(false), @@ -32,10 +30,10 @@ describe('Accessibility gatherer', () => { it('fails if result has no violations array', () => { return accessibilityGather.afterPass({ driver: { - evaluateAsync() { - return Promise.resolve({ + async evaluate() { + return { url: 'https://example.com', - }); + }; }, }, }).then( @@ -47,8 +45,8 @@ describe('Accessibility gatherer', () => { const error = 'There was an error.'; return accessibilityGather.afterPass({ driver: { - evaluateAsync() { - return Promise.reject(new Error(error)); + async evaluate() { + throw new Error(error); }, }, }).then( diff --git a/lighthouse-core/test/gather/gatherers/dobetterweb/password-inputs-with-prevented-paste-test.js b/lighthouse-core/test/gather/gatherers/dobetterweb/password-inputs-with-prevented-paste-test.js index b3990730fd12..2ddf5a4ba3c8 100644 --- a/lighthouse-core/test/gather/gatherers/dobetterweb/password-inputs-with-prevented-paste-test.js +++ b/lighthouse-core/test/gather/gatherers/dobetterweb/password-inputs-with-prevented-paste-test.js @@ -21,14 +21,14 @@ describe('PasswordInputsWithPreventedPaste gatherer', () => { return gatherer .afterPass({ driver: { - evaluateAsync() { - return Promise.resolve([ + async evaluate() { + return [ { node: { snippet: '', }, }, - ]); + ]; }, }, }) diff --git a/lighthouse-core/test/gather/gatherers/full-page-screenshot-test.js b/lighthouse-core/test/gather/gatherers/full-page-screenshot-test.js index 12282d72313b..7b7f3ed5bc53 100644 --- a/lighthouse-core/test/gather/gatherers/full-page-screenshot-test.js +++ b/lighthouse-core/test/gather/gatherers/full-page-screenshot-test.js @@ -14,22 +14,23 @@ const FullPageScreenshotGatherer = require('../../../gather/gatherers/full-page- */ function createMockDriver({contentSize, screenSize, screenshotData}) { return { - evaluateAsync: async function(code) { - if (code === 'window.innerWidth') { - return contentSize.width; - } - if (code.includes('document.documentElement.clientWidth')) { + evaluate: async function(fn) { + if (fn.name === 'resolveNodes') { + return {}; + } else if (fn.name === 'getObservedDeviceMetrics') { return { width: screenSize.width, height: screenSize.height, screenWidth: screenSize.width, screenHeight: screenSize.height, screenOrientation: { - type: 'landscape-primary', + type: 'landscapePrimary', angle: 30, }, deviceScaleFactor: screenSize.dpr, }; + } else { + throw new Error(`unexpected fn ${fn.name}`); } }, beginEmulation: jest.fn(), diff --git a/lighthouse-core/test/gather/gatherers/viewport-dimensions-test.js b/lighthouse-core/test/gather/gatherers/viewport-dimensions-test.js index 8946314a6d21..4cdc467fed5b 100644 --- a/lighthouse-core/test/gather/gatherers/viewport-dimensions-test.js +++ b/lighthouse-core/test/gather/gatherers/viewport-dimensions-test.js @@ -20,14 +20,14 @@ describe('ViewportDimensions gatherer', () => { it('returns an artifact', () => { return gatherer.afterPass({ driver: { - evaluateAsync() { - return Promise.resolve({ + async evaluate() { + return { innerWidth: 400, outerWidth: 400, innerHeight: 600, outerHeight: 600, devicePixelRatio: 2, - }); + }; }, }, }).then(artifact => { diff --git a/lighthouse-core/test/lib/stack-collector-test.js b/lighthouse-core/test/lib/stack-collector-test.js index 97147df27391..bf2008081b76 100644 --- a/lighthouse-core/test/lib/stack-collector-test.js +++ b/lighthouse-core/test/lib/stack-collector-test.js @@ -10,15 +10,15 @@ const collectStacks = require('../../lib/stack-collector.js'); describe('stack collector', () => { - /** @type {{driver: {evaluateAsync: jest.Mock}}} */ + /** @type {{driver: {evaluate: jest.Mock}}} */ let passContext; beforeEach(() => { - passContext = {driver: {evaluateAsync: jest.fn()}}; + passContext = {driver: {evaluate: jest.fn()}}; }); it('returns the detected stacks', async () => { - passContext.driver.evaluateAsync.mockResolvedValue([ + passContext.driver.evaluate.mockResolvedValue([ {id: 'jquery', name: 'jQuery', version: '2.1.0', npm: 'jquery'}, {id: 'angular', name: 'Angular', version: '', npm: ''}, {id: 'magento', name: 'Magento', version: 2}, diff --git a/types/externs.d.ts b/types/externs.d.ts index 3509347d73f2..5b8a4243819e 100644 --- a/types/externs.d.ts +++ b/types/externs.d.ts @@ -64,6 +64,8 @@ declare global { type FirstParamType any> = T extends (arg1: infer P, ...args: any[]) => any ? P : never; + type FlattenedPromise = Promise ? X : A>; + /** * Split string `S` on delimiter `D`. * From https://github.com/microsoft/TypeScript/pull/40336#issue-476562046 diff --git a/types/gatherer.d.ts b/types/gatherer.d.ts index 4cf2c339d46f..05c7fa1fb302 100644 --- a/types/gatherer.d.ts +++ b/types/gatherer.d.ts @@ -26,6 +26,7 @@ declare global { export interface FRTransitionalDriver { defaultSession: FRProtocolSession; evaluateAsync(expression: string, options?: {useIsolation?: boolean}): Promise; + evaluate(mainFn: (...args: T) => R, options: {args: T, useIsolation?: boolean, deps?: Array}): FlattenedPromise; } /** The limited context interface shared between pre and post Fraggle Rock Lighthouse. */