UNPKG

chrome-devtools-frontend

Version:
151 lines (135 loc) 5.26 kB
// Copyright 2023 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /* eslint-disable rulesdir/es_modules_import */ import {type SelectorType} from '../../../third_party/puppeteer-replay/puppeteer-replay.js'; import {type Logger} from './Logger.js'; import {MonotonicArray} from './MonotonicArray.js'; import { computeARIASelector, type AccessibilityBindings, } from './selectors/ARIASelector.js'; import {computeCSSSelector} from './selectors/CSSSelector.js'; import {computePierceSelector} from './selectors/PierceSelector.js'; import {type Selector} from './selectors/Selector.js'; import {computeTextSelector} from './selectors/TextSelector.js'; import {computeXPath} from './selectors/XPath.js'; const prefixSelector = ( selector: Selector|undefined, prefix: string, ): Selector|undefined => { if (selector === undefined) { return; } if (typeof selector === 'string') { return `${prefix}/${selector}`; } return selector.map(selector => `${prefix}/${selector}`); }; export class SelectorComputer { #customAttributes = [ // Most common attributes first. 'data-testid', 'data-test', 'data-qa', 'data-cy', 'data-test-id', 'data-qa-id', 'data-testing', ]; #bindings: AccessibilityBindings; #logger: Logger; #nodes = new MonotonicArray<Node>(); #selectorFunctionsInOrder: Array<(node: Node) => Selector | undefined>; constructor( bindings: AccessibilityBindings, logger: Logger, customAttribute = '', selectorTypesToRecord?: SelectorType[], ) { this.#bindings = bindings; this.#logger = logger; let selectorOrder = [ 'aria', 'css', 'xpath', 'pierce', 'text', ] as SelectorType[]; if (customAttribute) { // Custom DOM attributes indicate a preference for CSS/XPath selectors. this.#customAttributes.unshift(customAttribute); selectorOrder = [ 'css', 'xpath', 'pierce', 'aria', 'text', ] as SelectorType[]; } this.#selectorFunctionsInOrder = selectorOrder .filter(type => { if (selectorTypesToRecord) { return selectorTypesToRecord.includes(type); } return true; }) .map(selectorType => { switch (selectorType) { case 'css': return this.getCSSSelector.bind(this); case 'xpath': return this.getXPathSelector.bind(this); case 'pierce': return this.getPierceSelector.bind(this); case 'aria': return this.getARIASelector.bind(this); case 'text': return this.getTextSelector.bind(this); default: throw new Error('Unknown selector type: ' + selectorType); } }); } getSelectors(node: Node): Selector[] { const selectors: Selector[] = []; for (const getSelector of this.#selectorFunctionsInOrder) { const selector = getSelector(node); if (selector) { selectors.push(selector); } } return selectors; } getCSSSelector(node: Node): Selector|undefined { return this.#logger.timed(`getCSSSelector: ${this.#nodes.getOrInsert(node)} ${node.nodeName}`, () => { return computeCSSSelector(node, this.#customAttributes); }); } getTextSelector(node: Node): Selector|undefined { return this.#logger.timed(`getTextSelector: ${this.#nodes.getOrInsert(node)} ${node.nodeName}`, () => { return prefixSelector(computeTextSelector(node), 'text'); }); } getXPathSelector(node: Node): Selector|undefined { return this.#logger.timed(`getXPathSelector: ${this.#nodes.getOrInsert(node)} ${node.nodeName}`, () => { return prefixSelector( computeXPath(node, true, this.#customAttributes), 'xpath', ); }); } getPierceSelector(node: Node): Selector|undefined { return this.#logger.timed(`getPierceSelector: ${this.#nodes.getOrInsert(node)} ${node.nodeName}`, () => { return prefixSelector( computePierceSelector(node, this.#customAttributes), 'pierce', ); }); } getARIASelector(node: Node): Selector|undefined { return this.#logger.timed(`getARIASelector: ${this.#nodes.getOrInsert(node)} ${node.nodeName}`, () => { return prefixSelector(computeARIASelector(node, this.#bindings), 'aria'); }); } }