UNPKG

chrome-devtools-frontend

Version:
369 lines (310 loc) • 11.7 kB
// Copyright (c) 2014 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. import type * as Protocol from '../../generated/protocol.js'; import type * as ProtocolProxyApi from '../../generated/protocol-proxy-api.js'; import {DeferredDOMNode, type DOMNode} from './DOMModel.js'; import {Capability, type Target} from './Target.js'; import {SDKModel} from './SDKModel.js'; // TODO(crbug.com/1167717): Make this a const enum again // eslint-disable-next-line rulesdir/const_enum export enum CoreAxPropertyName { Name = 'name', Description = 'description', Value = 'value', Role = 'role', } export interface CoreOrProtocolAxProperty { name: CoreAxPropertyName|Protocol.Accessibility.AXPropertyName; value: Protocol.Accessibility.AXValue; } export class AccessibilityNode { readonly #accessibilityModelInternal: AccessibilityModel; readonly #idInternal: Protocol.Accessibility.AXNodeId; readonly #backendDOMNodeIdInternal: Protocol.DOM.BackendNodeId|null; readonly #deferredDOMNodeInternal: DeferredDOMNode|null; readonly #ignoredInternal: boolean; readonly #ignoredReasonsInternal: Protocol.Accessibility.AXProperty[]|undefined; readonly #roleInternal: Protocol.Accessibility.AXValue|null; readonly #nameInternal: Protocol.Accessibility.AXValue|null; readonly #descriptionInternal: Protocol.Accessibility.AXValue|null; readonly #valueInternal: Protocol.Accessibility.AXValue|null; readonly #propertiesInternal: Protocol.Accessibility.AXProperty[]|null; readonly #parentId: Protocol.Accessibility.AXNodeId|null; readonly #frameId: Protocol.Page.FrameId|null; readonly #childIds: Protocol.Accessibility.AXNodeId[]|null; constructor(accessibilityModel: AccessibilityModel, payload: Protocol.Accessibility.AXNode) { this.#accessibilityModelInternal = accessibilityModel; this.#idInternal = payload.nodeId; accessibilityModel.setAXNodeForAXId(this.#idInternal, this); if (payload.backendDOMNodeId) { accessibilityModel.setAXNodeForBackendDOMNodeId(payload.backendDOMNodeId, this); this.#backendDOMNodeIdInternal = payload.backendDOMNodeId; this.#deferredDOMNodeInternal = new DeferredDOMNode(accessibilityModel.target(), payload.backendDOMNodeId); } else { this.#backendDOMNodeIdInternal = null; this.#deferredDOMNodeInternal = null; } this.#ignoredInternal = payload.ignored; if (this.#ignoredInternal && 'ignoredReasons' in payload) { this.#ignoredReasonsInternal = payload.ignoredReasons; } this.#roleInternal = payload.role || null; this.#nameInternal = payload.name || null; this.#descriptionInternal = payload.description || null; this.#valueInternal = payload.value || null; this.#propertiesInternal = payload.properties || null; this.#childIds = payload.childIds || null; this.#parentId = payload.parentId || null; if (payload.frameId && !payload.parentId) { this.#frameId = payload.frameId; accessibilityModel.setRootAXNodeForFrameId(payload.frameId, this); } else { this.#frameId = null; } } id(): Protocol.Accessibility.AXNodeId { return this.#idInternal; } accessibilityModel(): AccessibilityModel { return this.#accessibilityModelInternal; } ignored(): boolean { return this.#ignoredInternal; } ignoredReasons(): Protocol.Accessibility.AXProperty[]|null { return this.#ignoredReasonsInternal || null; } role(): Protocol.Accessibility.AXValue|null { return this.#roleInternal || null; } coreProperties(): CoreOrProtocolAxProperty[] { const properties: CoreOrProtocolAxProperty[] = []; if (this.#nameInternal) { properties.push({name: CoreAxPropertyName.Name, value: this.#nameInternal}); } if (this.#descriptionInternal) { properties.push({name: CoreAxPropertyName.Description, value: this.#descriptionInternal}); } if (this.#valueInternal) { properties.push({name: CoreAxPropertyName.Value, value: this.#valueInternal}); } return properties; } name(): Protocol.Accessibility.AXValue|null { return this.#nameInternal || null; } description(): Protocol.Accessibility.AXValue|null { return this.#descriptionInternal || null; } value(): Protocol.Accessibility.AXValue|null { return this.#valueInternal || null; } properties(): Protocol.Accessibility.AXProperty[]|null { return this.#propertiesInternal || null; } parentNode(): AccessibilityNode|null { if (this.#parentId) { return this.#accessibilityModelInternal.axNodeForId(this.#parentId); } return null; } isDOMNode(): boolean { return Boolean(this.#backendDOMNodeIdInternal); } backendDOMNodeId(): Protocol.DOM.BackendNodeId|null { return this.#backendDOMNodeIdInternal; } deferredDOMNode(): DeferredDOMNode|null { return this.#deferredDOMNodeInternal; } highlightDOMNode(): void { const deferredNode = this.deferredDOMNode(); if (!deferredNode) { return; } // Highlight node in page. deferredNode.highlight(); } children(): AccessibilityNode[] { if (!this.#childIds) { return []; } const children = []; for (const childId of this.#childIds) { const child = this.#accessibilityModelInternal.axNodeForId(childId); if (child) { children.push(child); } } return children; } numChildren(): number { if (!this.#childIds) { return 0; } return this.#childIds.length; } hasOnlyUnloadedChildren(): boolean { if (!this.#childIds || !this.#childIds.length) { return false; } return this.#childIds.every(id => this.#accessibilityModelInternal.axNodeForId(id) === null); } hasUnloadedChildren(): boolean { if (!this.#childIds || !this.#childIds.length) { return false; } return this.#childIds.some(id => this.#accessibilityModelInternal.axNodeForId(id) === null); } // Only the root node gets a frameId, so nodes have to walk up the tree to find their frameId. getFrameId(): Protocol.Page.FrameId|null { return this.#frameId || this.parentNode()?.getFrameId() || null; } } // TODO(crbug.com/1167717): Make this a const enum again // eslint-disable-next-line rulesdir/const_enum export enum Events { TreeUpdated = 'TreeUpdated', } export type EventTypes = { [Events.TreeUpdated]: {root?: AccessibilityNode}, }; export class AccessibilityModel extends SDKModel<EventTypes> implements ProtocolProxyApi.AccessibilityDispatcher { agent: ProtocolProxyApi.AccessibilityApi; #axIdToAXNode: Map<string, AccessibilityNode>; #backendDOMNodeIdToAXNode: Map<Protocol.DOM.BackendNodeId, AccessibilityNode>; #frameIdToAXNode: Map<Protocol.Page.FrameId, AccessibilityNode>; #pendingChildRequests: Map<string, Promise<Protocol.Accessibility.GetChildAXNodesResponse>>; #root: AccessibilityNode|null; constructor(target: Target) { super(target); target.registerAccessibilityDispatcher(this); this.agent = target.accessibilityAgent(); void this.resumeModel(); this.#axIdToAXNode = new Map(); this.#backendDOMNodeIdToAXNode = new Map(); this.#frameIdToAXNode = new Map(); this.#pendingChildRequests = new Map(); this.#root = null; } clear(): void { this.#root = null; this.#axIdToAXNode.clear(); this.#backendDOMNodeIdToAXNode.clear(); this.#frameIdToAXNode.clear(); } override async resumeModel(): Promise<void> { await this.agent.invoke_enable(); } override async suspendModel(): Promise<void> { await this.agent.invoke_disable(); } async requestPartialAXTree(node: DOMNode): Promise<void> { const {nodes} = await this.agent.invoke_getPartialAXTree({nodeId: node.id, fetchRelatives: true}); if (!nodes) { return; } const axNodes = []; for (const payload of nodes) { axNodes.push(new AccessibilityNode(this, payload)); } } loadComplete({root}: Protocol.Accessibility.LoadCompleteEvent): void { this.clear(); this.#root = new AccessibilityNode(this, root); this.dispatchEventToListeners(Events.TreeUpdated, {root: this.#root}); } nodesUpdated({nodes}: Protocol.Accessibility.NodesUpdatedEvent): void { this.createNodesFromPayload(nodes); this.dispatchEventToListeners(Events.TreeUpdated, {}); return; } private createNodesFromPayload(payloadNodes: Protocol.Accessibility.AXNode[]): AccessibilityNode[] { const accessibilityNodes = payloadNodes.map(node => { const sdkNode = new AccessibilityNode(this, node); return sdkNode; }); return accessibilityNodes; } async requestRootNode(frameId?: Protocol.Page.FrameId): Promise<AccessibilityNode|undefined> { if (frameId && this.#frameIdToAXNode.has(frameId)) { return this.#frameIdToAXNode.get(frameId); } if (!frameId && this.#root) { return this.#root; } const {node} = await this.agent.invoke_getRootAXNode({frameId}); if (!node) { return; } return this.createNodesFromPayload([node])[0]; } async requestAXChildren(nodeId: Protocol.Accessibility.AXNodeId, frameId?: Protocol.Page.FrameId): Promise<AccessibilityNode[]> { const parent = this.#axIdToAXNode.get(nodeId); if (!parent) { throw Error('Cannot request children before parent'); } if (!parent.hasUnloadedChildren()) { return parent.children(); } const request = this.#pendingChildRequests.get(nodeId); if (request) { await request; } else { const request = this.agent.invoke_getChildAXNodes({id: nodeId, frameId}); this.#pendingChildRequests.set(nodeId, request); const result = await request; if (!result.getError()) { this.createNodesFromPayload(result.nodes); this.#pendingChildRequests.delete(nodeId); } } return parent.children(); } async requestAndLoadSubTreeToNode(node: DOMNode): Promise<AccessibilityNode[]|null> { // Node may have already been loaded, so don't bother requesting it again. const result = []; let ancestor = this.axNodeForDOMNode(node); while (ancestor) { result.push(ancestor); const parent = ancestor.parentNode(); if (!parent) { return result; } ancestor = parent; } const {nodes} = await this.agent.invoke_getAXNodeAndAncestors({backendNodeId: node.backendNodeId()}); if (!nodes) { return null; } const ancestors = this.createNodesFromPayload(nodes); return ancestors; } axNodeForId(axId: Protocol.Accessibility.AXNodeId): AccessibilityNode|null { return this.#axIdToAXNode.get(axId) || null; } setRootAXNodeForFrameId(frameId: Protocol.Page.FrameId, axNode: AccessibilityNode): void { this.#frameIdToAXNode.set(frameId, axNode); } axNodeForFrameId(frameId: Protocol.Page.FrameId): AccessibilityNode|null { return this.#frameIdToAXNode.get(frameId) ?? null; } setAXNodeForAXId(axId: string, axNode: AccessibilityNode): void { this.#axIdToAXNode.set(axId, axNode); } axNodeForDOMNode(domNode: DOMNode|null): AccessibilityNode|null { if (!domNode) { return null; } return this.#backendDOMNodeIdToAXNode.get(domNode.backendNodeId()) ?? null; } setAXNodeForBackendDOMNodeId(backendDOMNodeId: Protocol.DOM.BackendNodeId, axNode: AccessibilityNode): void { this.#backendDOMNodeIdToAXNode.set(backendDOMNodeId, axNode); } getAgent(): ProtocolProxyApi.AccessibilityApi { return this.agent; } } SDKModel.register(AccessibilityModel, {capabilities: Capability.DOM, autostart: false});