UNPKG

lighthouse

Version:

Automated auditing, performance metrics, and best practices for the web.

135 lines (116 loc) 4.19 kB
/** * @license * Copyright 2019 Google LLC * SPDX-License-Identifier: Apache-2.0 */ /* global getNodeDetails */ import BaseGatherer from '../base-gatherer.js'; import {pageFunctions} from '../../lib/page-functions.js'; import {resolveDevtoolsNodePathToObjectId} from '../driver/dom.js'; /* eslint-env browser, node */ /** * Function that is stringified and run in the page to collect anchor elements. * Additional complexity is introduced because anchors can be HTML or SVG elements. * * We use this evaluateAsync method because the `node.getAttribute` method doesn't actually normalize * the values like access from JavaScript in-page does. * * @return {LH.Artifacts['AnchorElements']} */ /* c8 ignore start */ function collectAnchorElements() { /** @param {string} url */ const resolveURLOrEmpty = url => { try { return new URL(url, window.location.href).href; } catch (_) { return ''; } }; /** @param {HTMLAnchorElement|SVGAElement} node */ function getTruncatedOnclick(node) { const onclick = node.getAttribute('onclick') || ''; return onclick.slice(0, 1024); } /** @type {Array<HTMLAnchorElement|SVGAElement>} */ // @ts-expect-error - put into scope via stringification const anchorElements = getElementsInDocument('a'); // eslint-disable-line no-undef return anchorElements.map(node => { if (node instanceof HTMLAnchorElement) { return { href: node.href, rawHref: node.getAttribute('href') || '', onclick: getTruncatedOnclick(node), role: node.getAttribute('role') || '', name: node.name, text: node.innerText, // we don't want to return hidden text, so use innerText rel: node.rel, target: node.target, id: node.getAttribute('id') || '', // @ts-expect-error - getNodeDetails put into scope via stringification node: getNodeDetails(node), }; } return { href: resolveURLOrEmpty(node.href.baseVal), rawHref: node.getAttribute('href') || '', onclick: getTruncatedOnclick(node), role: node.getAttribute('role') || '', text: node.textContent || '', rel: '', target: node.target.baseVal || '', id: node.getAttribute('id') || '', // @ts-expect-error - getNodeDetails put into scope via stringification node: getNodeDetails(node), }; }); } /* c8 ignore stop */ /** * @param {LH.Gatherer.ProtocolSession} session * @param {string} devtoolsNodePath * @return {Promise<Array<{type: string}>>} */ async function getEventListeners(session, devtoolsNodePath) { const objectId = await resolveDevtoolsNodePathToObjectId(session, devtoolsNodePath); if (!objectId) return []; const response = await session.sendCommand('DOMDebugger.getEventListeners', { objectId, }); return response.listeners.map(({type}) => ({type})); } class AnchorElements extends BaseGatherer { /** @type {LH.Gatherer.GathererMeta} */ meta = { supportedModes: ['snapshot', 'navigation'], }; /** * @param {LH.Gatherer.Context} passContext * @return {Promise<LH.Artifacts['AnchorElements']>} */ async getArtifact(passContext) { const session = passContext.driver.defaultSession; const anchors = await passContext.driver.executionContext.evaluate(collectAnchorElements, { args: [], useIsolation: true, deps: [ pageFunctions.getElementsInDocument, pageFunctions.getNodeDetails, ], }); 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 session.sendCommand('DOM.getDocument', {depth: -1, pierce: true}); const anchorsWithEventListeners = anchors.map(async anchor => { const listeners = await getEventListeners(session, anchor.node.devtoolsNodePath); return { ...anchor, listeners, }; }); const result = await Promise.all(anchorsWithEventListeners); await session.sendCommand('DOM.disable'); return result; } } export default AnchorElements;