chrome-devtools-frontend
Version:
Chrome DevTools UI
354 lines (298 loc) • 10.8 kB
text/typescript
// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import type * as ProtocolProxyApi from '../../generated/protocol-proxy-api.js';
import type * as Protocol from '../../generated/protocol.js';
import {DeferredDOMNode, type DOMNode} from './DOMModel.js';
import {SDKModel} from './SDKModel.js';
import {Capability, type Target} from './Target.js';
export const 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 #accessibilityModel: AccessibilityModel;
readonly #id: Protocol.Accessibility.AXNodeId;
readonly #backendDOMNodeId: Protocol.DOM.BackendNodeId|null;
readonly #deferredDOMNode: DeferredDOMNode|null;
readonly #ignored: boolean;
readonly #ignoredReasons: Protocol.Accessibility.AXProperty[]|undefined;
readonly #role: Protocol.Accessibility.AXValue|null;
readonly #name: Protocol.Accessibility.AXValue|null;
readonly #description: Protocol.Accessibility.AXValue|null;
readonly #value: Protocol.Accessibility.AXValue|null;
readonly #properties: 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.#accessibilityModel = accessibilityModel;
this.#id = payload.nodeId;
accessibilityModel.setAXNodeForAXId(this.#id, this);
if (payload.backendDOMNodeId) {
accessibilityModel.setAXNodeForBackendDOMNodeId(payload.backendDOMNodeId, this);
this.#backendDOMNodeId = payload.backendDOMNodeId;
this.#deferredDOMNode = new DeferredDOMNode(accessibilityModel.target(), payload.backendDOMNodeId);
} else {
this.#backendDOMNodeId = null;
this.#deferredDOMNode = null;
}
this.#ignored = payload.ignored;
if (this.#ignored && 'ignoredReasons' in payload) {
this.#ignoredReasons = payload.ignoredReasons;
}
this.#role = payload.role || null;
this.#name = payload.name || null;
this.#description = payload.description || null;
this.#value = payload.value || null;
this.#properties = payload.properties || null;
this.#childIds = [...new Set(payload.childIds)];
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.#id;
}
accessibilityModel(): AccessibilityModel {
return this.#accessibilityModel;
}
ignored(): boolean {
return this.#ignored;
}
ignoredReasons(): Protocol.Accessibility.AXProperty[]|null {
return this.#ignoredReasons || null;
}
role(): Protocol.Accessibility.AXValue|null {
return this.#role || null;
}
coreProperties(): CoreOrProtocolAxProperty[] {
const properties: CoreOrProtocolAxProperty[] = [];
if (this.#name) {
properties.push({name: CoreAxPropertyName.NAME, value: this.#name});
}
if (this.#description) {
properties.push({name: CoreAxPropertyName.DESCRIPTION, value: this.#description});
}
if (this.#value) {
properties.push({name: CoreAxPropertyName.VALUE, value: this.#value});
}
return properties;
}
name(): Protocol.Accessibility.AXValue|null {
return this.#name || null;
}
description(): Protocol.Accessibility.AXValue|null {
return this.#description || null;
}
value(): Protocol.Accessibility.AXValue|null {
return this.#value || null;
}
properties(): Protocol.Accessibility.AXProperty[]|null {
return this.#properties || null;
}
parentNode(): AccessibilityNode|null {
if (this.#parentId) {
return this.#accessibilityModel.axNodeForId(this.#parentId);
}
return null;
}
isDOMNode(): boolean {
return Boolean(this.#backendDOMNodeId);
}
backendDOMNodeId(): Protocol.DOM.BackendNodeId|null {
return this.#backendDOMNodeId;
}
deferredDOMNode(): DeferredDOMNode|null {
return this.#deferredDOMNode;
}
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.#accessibilityModel.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.#accessibilityModel.axNodeForId(id) === null);
}
hasUnloadedChildren(): boolean {
if (!this.#childIds || !this.#childIds.length) {
return false;
}
return this.#childIds.some(id => this.#accessibilityModel.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;
}
}
export const enum Events {
TREE_UPDATED = 'TreeUpdated',
}
export interface EventTypes {
[Events.TREE_UPDATED]: {root?: AccessibilityNode};
}
export class AccessibilityModel extends SDKModel<EventTypes> implements ProtocolProxyApi.AccessibilityDispatcher {
agent: ProtocolProxyApi.AccessibilityApi;
#axIdToAXNode = new Map<string, AccessibilityNode>();
#backendDOMNodeIdToAXNode = new Map<Protocol.DOM.BackendNodeId, AccessibilityNode>();
#frameIdToAXNode = new Map<Protocol.Page.FrameId, AccessibilityNode>();
#pendingChildRequests = new Map<string, Promise<Protocol.Accessibility.GetChildAXNodesResponse>>();
#root: AccessibilityNode|null = null;
constructor(target: Target) {
super(target);
target.registerAccessibilityDispatcher(this);
this.agent = target.accessibilityAgent();
void this.resumeModel();
}
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.TREE_UPDATED, {root: this.#root});
}
nodesUpdated({nodes}: Protocol.Accessibility.NodesUpdatedEvent): void {
this.createNodesFromPayload(nodes);
this.dispatchEventToListeners(Events.TREE_UPDATED, {});
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 new 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);
}
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});