AriaQueryHandler.ts (2640B)
1 /** 2 * @license 3 * Copyright 2020 Google Inc. 4 * SPDX-License-Identifier: Apache-2.0 5 */ 6 7 import type {ElementHandle} from '../api/ElementHandle.js'; 8 import {QueryHandler, type QuerySelector} from '../common/QueryHandler.js'; 9 import type {AwaitableIterable} from '../common/types.js'; 10 import {assert} from '../util/assert.js'; 11 import {AsyncIterableUtil} from '../util/AsyncIterableUtil.js'; 12 13 interface ARIASelector { 14 name?: string; 15 role?: string; 16 } 17 18 const isKnownAttribute = ( 19 attribute: string, 20 ): attribute is keyof ARIASelector => { 21 return ['name', 'role'].includes(attribute); 22 }; 23 24 /** 25 * The selectors consist of an accessible name to query for and optionally 26 * further aria attributes on the form `[<attribute>=<value>]`. 27 * Currently, we only support the `name` and `role` attribute. 28 * The following examples showcase how the syntax works wrt. querying: 29 * 30 * - 'title[role="heading"]' queries for elements with name 'title' and role 'heading'. 31 * - '[role="image"]' queries for elements with role 'image' and any name. 32 * - 'label' queries for elements with name 'label' and any role. 33 * - '[name=""][role="button"]' queries for elements with no name and role 'button'. 34 */ 35 const ATTRIBUTE_REGEXP = 36 /\[\s*(?<attribute>\w+)\s*=\s*(?<quote>"|')(?<value>\\.|.*?(?=\k<quote>))\k<quote>\s*\]/g; 37 const parseARIASelector = (selector: string): ARIASelector => { 38 if (selector.length > 10_000) { 39 throw new Error(`Selector ${selector} is too long`); 40 } 41 42 const queryOptions: ARIASelector = {}; 43 const defaultName = selector.replace( 44 ATTRIBUTE_REGEXP, 45 (_, attribute, __, value) => { 46 assert( 47 isKnownAttribute(attribute), 48 `Unknown aria attribute "${attribute}" in selector`, 49 ); 50 queryOptions[attribute] = value; 51 return ''; 52 }, 53 ); 54 if (defaultName && !queryOptions.name) { 55 queryOptions.name = defaultName; 56 } 57 return queryOptions; 58 }; 59 60 /** 61 * @internal 62 */ 63 export class ARIAQueryHandler extends QueryHandler { 64 static override querySelector: QuerySelector = async ( 65 node, 66 selector, 67 {ariaQuerySelector}, 68 ) => { 69 return await ariaQuerySelector(node, selector); 70 }; 71 72 static override async *queryAll( 73 element: ElementHandle<Node>, 74 selector: string, 75 ): AwaitableIterable<ElementHandle<Node>> { 76 const {name, role} = parseARIASelector(selector); 77 yield* element.queryAXTree(name, role); 78 } 79 80 static override queryOne = async ( 81 element: ElementHandle<Node>, 82 selector: string, 83 ): Promise<ElementHandle<Node> | null> => { 84 return ( 85 (await AsyncIterableUtil.first(this.queryAll(element, selector))) ?? null 86 ); 87 }; 88 }