Skip to content

Commit

Permalink
core(fr): split out DOM utilities from legacy driver (#12431)
Browse files Browse the repository at this point in the history
  • Loading branch information
patrickhulce authored May 4, 2021
1 parent be0aea2 commit 94ac618
Show file tree
Hide file tree
Showing 11 changed files with 158 additions and 307 deletions.
3 changes: 3 additions & 0 deletions lighthouse-core/fraggle-rock/config/default-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const artifacts = {
DevtoolsLog: '',
Trace: '',
Accessibility: '',
AnchorElements: '',
AppCacheManifest: '',
CacheContents: '',
ConsoleMessages: '',
Expand Down Expand Up @@ -49,6 +50,7 @@ const defaultConfig = {

/* eslint-disable max-len */
{id: artifacts.Accessibility, gatherer: 'accessibility'},
{id: artifacts.AnchorElements, gatherer: 'anchor-elements'},
{id: artifacts.AppCacheManifest, gatherer: 'dobetterweb/appcache'},
{id: artifacts.CacheContents, gatherer: 'cache-contents'},
{id: artifacts.ConsoleMessages, gatherer: 'console-messages'},
Expand Down Expand Up @@ -83,6 +85,7 @@ const defaultConfig = {
artifacts.Trace,

artifacts.Accessibility,
artifacts.AnchorElements,
artifacts.AppCacheManifest,
artifacts.CacheContents,
artifacts.ConsoleMessages,
Expand Down
86 changes: 0 additions & 86 deletions lighthouse-core/gather/driver.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

const Fetcher = require('./fetcher.js');
const ExecutionContext = require('./driver/execution-context.js');
const LHElement = require('../lib/lh-element.js');
const LHError = require('../lib/lh-error.js');
const NetworkRequest = require('../lib/network-request.js');
const EventEmitter = require('events').EventEmitter;
Expand Down Expand Up @@ -379,29 +378,6 @@ class Driver {
return !!this._domainEnabledCounts.get(domain);
}

/**
* @param {string} objectId Object ID for the resolved DOM node
* @param {string} propName Name of the property
* @return {Promise<string|null>} The property value, or null, if property not found
*/
async getObjectProperty(objectId, propName) {
const propertiesResponse = await this.sendCommand('Runtime.getProperties', {
objectId,
accessorPropertiesOnly: true,
generatePreview: false,
ownProperties: false,
});

const propertyForName = propertiesResponse.result
.find(property => property.name === propName);

if (propertyForName && propertyForName.value) {
return propertyForName.value.value;
} else {
return null;
}
}

/**
* Return the body of the response with the given ID. Rejects if getting the
* body times out.
Expand All @@ -419,68 +395,6 @@ class Driver {
return result.body;
}

/**
* @param {string} selector Selector to find in the DOM
* @return {Promise<LHElement|null>} The found element, or null, resolved in a promise
*/
async querySelector(selector) {
const documentResponse = await this.sendCommand('DOM.getDocument');
const rootNodeId = documentResponse.root.nodeId;

const targetNode = await this.sendCommand('DOM.querySelector', {
nodeId: rootNodeId,
selector,
});

if (targetNode.nodeId === 0) {
return null;
}
return new LHElement(targetNode, this);
}

/**
* Resolves a backend node ID (from a trace event, protocol, etc) to the object ID for use with
* `Runtime.callFunctionOn`. `undefined` means the node could not be found.
*
* @param {number} backendNodeId
* @return {Promise<string|undefined>}
*/
async resolveNodeIdToObjectId(backendNodeId) {
try {
const resolveNodeResponse = await this.sendCommand('DOM.resolveNode', {backendNodeId});
return resolveNodeResponse.object.objectId;
} catch (err) {
if (/No node.*found/.test(err.message) ||
/Node.*does not belong to the document/.test(err.message)) return undefined;
throw err;
}
}

/**
* Resolves a proprietary devtools node path (created from page-function.js) to the object ID for use
* with `Runtime.callFunctionOn`. `undefined` means the node could not be found.
* Requires `DOM.getDocument` to have been called since the object's creation or it will always be `undefined`.
*
* @param {string} devtoolsNodePath
* @return {Promise<string|undefined>}
*/
async resolveDevtoolsNodePathToObjectId(devtoolsNodePath) {
try {
const {nodeId} = await this.sendCommand('DOM.pushNodeByPathToFrontend', {
path: devtoolsNodePath,
});

const {object: {objectId}} = await this.sendCommand('DOM.resolveNode', {
nodeId,
});

return objectId;
} catch (err) {
if (/No node.*found/.test(err.message)) return undefined;
throw err;
}
}

/**
* @param {{x: number, y: number}} position
* @return {Promise<void>}
Expand Down
58 changes: 58 additions & 0 deletions lighthouse-core/gather/driver/dom.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* @license Copyright 2021 The Lighthouse Authors. 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';

/**
* @param {Error} err
* @return {undefined}
*/
function handlePotentialMissingNodeError(err) {
if (
/No node.*found/.test(err.message) ||
/Node.*does not belong to the document/.test(err.message)
) {
return undefined;
}
throw err;
}

/**
* Resolves a backend node ID (from a trace event, protocol, etc) to the object ID for use with
* `Runtime.callFunctionOn`. `undefined` means the node could not be found.
*
* @param {LH.Gatherer.FRProtocolSession} session
* @param {number} backendNodeId
* @return {Promise<string|undefined>}
*/
async function resolveNodeIdToObjectId(session, backendNodeId) {
try {
const resolveNodeResponse = await session.sendCommand('DOM.resolveNode', {backendNodeId});
return resolveNodeResponse.object.objectId;
} catch (err) {
return handlePotentialMissingNodeError(err);
}
}

/**
* Resolves a proprietary devtools node path (created from page-function.js) to the object ID for use
* with `Runtime.callFunctionOn`. `undefined` means the node could not be found.
* Requires `DOM.getDocument` to have been called since the object's creation or it will always be `undefined`.
*
* @param {LH.Gatherer.FRProtocolSession} session
* @param {string} path
* @return {Promise<string|undefined>}
*/
async function resolveDevtoolsNodePathToObjectId(session, path) {
try {
const {nodeId} = await session.sendCommand('DOM.pushNodeByPathToFrontend', {path});
const {object: {objectId}} = await session.sendCommand('DOM.resolveNode', {nodeId});
return objectId;
} catch (err) {
return handlePotentialMissingNodeError(err);
}
}

module.exports = {resolveNodeIdToObjectId, resolveDevtoolsNodePathToObjectId};
34 changes: 20 additions & 14 deletions lighthouse-core/gather/gatherers/anchor-elements.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@

/* global getNodeDetails */

const Gatherer = require('./gatherer.js');
const FRGatherer = require('../../fraggle-rock/gather/base-gatherer.js');
const dom = require('../driver/dom.js');
const pageFunctions = require('../../lib/page-functions.js');

/* eslint-env browser, node */
Expand Down Expand Up @@ -74,43 +75,48 @@ function collectAnchorElements() {
/* c8 ignore stop */

/**
* @param {LH.Gatherer.PassContext['driver']} driver
* @param {LH.Gatherer.FRProtocolSession} session
* @param {string} devtoolsNodePath
* @return {Promise<Array<{type: string}>>}
*/
async function getEventListeners(driver, devtoolsNodePath) {
const objectId = await driver.resolveDevtoolsNodePathToObjectId(devtoolsNodePath);
async function getEventListeners(session, devtoolsNodePath) {
const objectId = await dom.resolveDevtoolsNodePathToObjectId(session, devtoolsNodePath);
if (!objectId) return [];

const response = await driver.sendCommand('DOMDebugger.getEventListeners', {
const response = await session.sendCommand('DOMDebugger.getEventListeners', {
objectId,
});

return response.listeners.map(({type}) => ({type}));
}

class AnchorElements extends Gatherer {
class AnchorElements extends FRGatherer {
/** @type {LH.Gatherer.GathererMeta} */
meta = {
supportedModes: ['snapshot', 'navigation'],
}

/**
* @param {LH.Gatherer.PassContext} passContext
* @param {LH.Gatherer.FRTransitionalContext} passContext
* @return {Promise<LH.Artifacts['AnchorElements']>}
*/
async afterPass(passContext) {
const driver = passContext.driver;
async getArtifact(passContext) {
const session = passContext.driver.defaultSession;

const anchors = await driver.executionContext.evaluate(collectAnchorElements, {
const anchors = await passContext.driver.executionContext.evaluate(collectAnchorElements, {
args: [],
useIsolation: true,
deps: [
pageFunctions.getElementsInDocumentString,
pageFunctions.getNodeDetailsString,
],
});
await driver.sendCommand('DOM.enable');
await session.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.
await driver.sendCommand('DOM.getDocument', {depth: -1, pierce: true});
await session.sendCommand('DOM.getDocument', {depth: -1, pierce: true});
const anchorsWithEventListeners = anchors.map(async anchor => {
const listeners = await getEventListeners(driver, anchor.node.devtoolsNodePath);
const listeners = await getEventListeners(session, anchor.node.devtoolsNodePath);

return {
...anchor,
Expand All @@ -119,7 +125,7 @@ class AnchorElements extends Gatherer {
});

const result = await Promise.all(anchorsWithEventListeners);
await driver.sendCommand('DOM.disable');
await session.sendCommand('DOM.disable');
return result;
}
}
Expand Down
3 changes: 2 additions & 1 deletion lighthouse-core/gather/gatherers/trace-elements.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
*/

const Gatherer = require('./gatherer.js');
const dom = require('../driver/dom.js');
const pageFunctions = require('../../lib/page-functions.js');
const TraceProcessor = require('../../lib/tracehouse/trace-processor.js');
const RectHelpers = require('../../lib/rect-helpers.js');
Expand Down Expand Up @@ -275,7 +276,7 @@ class TraceElements extends Gatherer {
const backendNodeId = backendNodeData[i].nodeId;
let response;
try {
const objectId = await driver.resolveNodeIdToObjectId(backendNodeId);
const objectId = await dom.resolveNodeIdToObjectId(driver.defaultSession, backendNodeId);
if (!objectId) continue;
response = await driver.sendCommand('Runtime.callFunctionOn', {
objectId,
Expand Down
71 changes: 0 additions & 71 deletions lighthouse-core/lib/lh-element.js

This file was deleted.

4 changes: 2 additions & 2 deletions lighthouse-core/test/fraggle-rock/api-test-pptr.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ describe('Fraggle Rock API', () => {

const {auditResults, erroredAudits, failedAudits} = getAuditsBreakdown(lhr);
// TODO(FR-COMPAT): This assertion can be removed when full compatibility is reached.
expect(auditResults.length).toMatchInlineSnapshot(`72`);
expect(auditResults.length).toMatchInlineSnapshot(`73`);

expect(erroredAudits).toHaveLength(0);
expect(failedAudits.map(audit => audit.id)).toContain('label');
Expand Down Expand Up @@ -159,7 +159,7 @@ describe('Fraggle Rock API', () => {
const {lhr} = result;
const {auditResults, failedAudits, erroredAudits} = getAuditsBreakdown(lhr);
// TODO(FR-COMPAT): This assertion can be removed when full compatibility is reached.
expect(auditResults.length).toMatchInlineSnapshot(`102`);
expect(auditResults.length).toMatchInlineSnapshot(`103`);
expect(erroredAudits).toHaveLength(0);

const failedAuditIds = failedAudits.map(audit => audit.id);
Expand Down
Loading

0 comments on commit 94ac618

Please sign in to comment.