feat(firefox&webkit): support root in accessibility.snapshot (#495)
This adds support for `root` in accessibility.snapshot firefox role names are now normalized to aria roles where they match webkit roledescriptions are less noisey on mac webkit mac/linux results are further defined interestingOnly tests are replaced by one that doesn't rely on undefined behavior the main accessibility test was split up a bit for more refined testing.
This commit is contained in:
		
							parent
							
								
									92bd854d8f
								
							
						
					
					
						commit
						aaa1c9203e
					
				| 
						 | 
				
			
			@ -55,13 +55,12 @@ export interface AXNode {
 | 
			
		|||
    isLeafNode(): boolean;
 | 
			
		||||
    isControl(): boolean;
 | 
			
		||||
    serialize(): SerializedAXNode;
 | 
			
		||||
    findElement(element: dom.ElementHandle): Promise<AXNode|null>;
 | 
			
		||||
    children(): Iterable<AXNode>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class Accessibility {
 | 
			
		||||
  private _getAXTree:  () => Promise<AXNode>;
 | 
			
		||||
  constructor(getAXTree: () => Promise<AXNode>) {
 | 
			
		||||
  private _getAXTree:  (needle?: dom.ElementHandle) => Promise<{tree: AXNode, needle: AXNode | null}>;
 | 
			
		||||
  constructor(getAXTree: (needle?: dom.ElementHandle) => Promise<{tree: AXNode, needle: AXNode | null}>) {
 | 
			
		||||
    this._getAXTree = getAXTree;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -73,21 +72,18 @@ export class Accessibility {
 | 
			
		|||
      interestingOnly = true,
 | 
			
		||||
      root = null,
 | 
			
		||||
    } = options;
 | 
			
		||||
    const defaultRoot = await this._getAXTree();
 | 
			
		||||
    let needle: AXNode | null = defaultRoot;
 | 
			
		||||
    if (root) {
 | 
			
		||||
      needle = await defaultRoot.findElement(root);
 | 
			
		||||
      if (!needle)
 | 
			
		||||
        return null;
 | 
			
		||||
    const {tree, needle} = await this._getAXTree(root || undefined);
 | 
			
		||||
    if (!interestingOnly) {
 | 
			
		||||
      if (root)
 | 
			
		||||
        return needle && serializeTree(needle)[0];
 | 
			
		||||
      return serializeTree(tree)[0];
 | 
			
		||||
    }
 | 
			
		||||
    if (!interestingOnly)
 | 
			
		||||
      return serializeTree(needle)[0];
 | 
			
		||||
 | 
			
		||||
    const interestingNodes: Set<AXNode> = new Set();
 | 
			
		||||
    collectInterestingNodes(interestingNodes, defaultRoot, false);
 | 
			
		||||
    if (root && !interestingNodes.has(needle))
 | 
			
		||||
    collectInterestingNodes(interestingNodes, tree, false);
 | 
			
		||||
    if (root && (!needle || !interestingNodes.has(needle)))
 | 
			
		||||
      return null;
 | 
			
		||||
    return serializeTree(needle, interestingNodes)[0];
 | 
			
		||||
    return serializeTree(needle || tree, interestingNodes)[0];
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,9 +20,13 @@ import { Protocol } from './protocol';
 | 
			
		|||
import * as dom from '../dom';
 | 
			
		||||
import * as accessibility from '../accessibility';
 | 
			
		||||
 | 
			
		||||
export async function getAccessibilityTree(client: CRSession) : Promise<accessibility.AXNode> {
 | 
			
		||||
export async function getAccessibilityTree(client: CRSession, needle?: dom.ElementHandle) : Promise<{tree: accessibility.AXNode, needle: accessibility.AXNode | null}> {
 | 
			
		||||
  const {nodes} = await client.send('Accessibility.getFullAXTree');
 | 
			
		||||
  return CRAXNode.createTree(client, nodes);
 | 
			
		||||
  const tree = CRAXNode.createTree(client, nodes);
 | 
			
		||||
  return {
 | 
			
		||||
    tree,
 | 
			
		||||
    needle: needle ? await tree._findElement(needle) : null
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class CRAXNode implements accessibility.AXNode {
 | 
			
		||||
| 
						 | 
				
			
			@ -90,7 +94,7 @@ class CRAXNode implements accessibility.AXNode {
 | 
			
		|||
    return this._children;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async findElement(element: dom.ElementHandle): Promise<CRAXNode | null> {
 | 
			
		||||
  async _findElement(element: dom.ElementHandle): Promise<CRAXNode | null> {
 | 
			
		||||
    const remoteObject = element._remoteObject as Protocol.Runtime.RemoteObject;
 | 
			
		||||
    const {node: {backendNodeId}} = await this._client.send('DOM.describeNode', {objectId: remoteObject.objectId});
 | 
			
		||||
    const needle = this.find(node => node._payload.backendDOMNodeId === backendNodeId);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -480,8 +480,8 @@ export class CRPage implements PageDelegate {
 | 
			
		|||
    return to._createHandle(result.object).asElement()!;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async getAccessibilityTree(): Promise<accessibility.AXNode> {
 | 
			
		||||
    return getAccessibilityTree(this._client);
 | 
			
		||||
  async getAccessibilityTree(needle?: dom.ElementHandle) {
 | 
			
		||||
    return getAccessibilityTree(this._client, needle);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async pdf(options?: types.PDFOptions): Promise<platform.BufferType> {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,15 +18,48 @@
 | 
			
		|||
import * as accessibility from '../accessibility';
 | 
			
		||||
import { FFSession } from './ffConnection';
 | 
			
		||||
import { Protocol } from './protocol';
 | 
			
		||||
import * as dom from '../dom';
 | 
			
		||||
 | 
			
		||||
export async function getAccessibilityTree(session: FFSession) : Promise<accessibility.AXNode> {
 | 
			
		||||
  const { tree } = await session.send('Accessibility.getFullAXTree');
 | 
			
		||||
  return new FFAXNode(tree);
 | 
			
		||||
export async function getAccessibilityTree(session: FFSession, needle?: dom.ElementHandle) : Promise<{tree: accessibility.AXNode, needle: accessibility.AXNode | null}> {
 | 
			
		||||
  const objectId = needle ? needle._remoteObject.objectId : undefined;
 | 
			
		||||
  const { tree } = await session.send('Accessibility.getFullAXTree', { objectId });
 | 
			
		||||
  const axNode = new FFAXNode(tree);
 | 
			
		||||
  return {
 | 
			
		||||
    tree: axNode,
 | 
			
		||||
    needle: needle ? axNode._findNeedle() : null
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const FFRoleToARIARole = new Map(Object.entries({
 | 
			
		||||
  'pushbutton': 'button',
 | 
			
		||||
  'checkbutton': 'checkbox',
 | 
			
		||||
  'editcombobox': 'combobox',
 | 
			
		||||
  'content deletion': 'deletion',
 | 
			
		||||
  'footnote': 'doc-footnote',
 | 
			
		||||
  'non-native document': 'document',
 | 
			
		||||
  'grouping': 'group',
 | 
			
		||||
  'graphic': 'img',
 | 
			
		||||
  'content insertion': 'insertion',
 | 
			
		||||
  'animation': 'marquee',
 | 
			
		||||
  'flat equation': 'math',
 | 
			
		||||
  'menupopup': 'menu',
 | 
			
		||||
  'check menu item': 'menuitemcheckbox',
 | 
			
		||||
  'radio menu item': 'menuitemradio',
 | 
			
		||||
  'listbox option': 'option',
 | 
			
		||||
  'radiobutton': 'radio',
 | 
			
		||||
  'statusbar': 'status',
 | 
			
		||||
  'pagetab': 'tab',
 | 
			
		||||
  'pagetablist': 'tablist',
 | 
			
		||||
  'propertypage': 'tabpanel',
 | 
			
		||||
  'entry': 'textbox',
 | 
			
		||||
  'outline': 'tree',
 | 
			
		||||
  'tree table': 'treegrid',
 | 
			
		||||
  'outlineitem': 'treeitem',
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
class FFAXNode implements accessibility.AXNode {
 | 
			
		||||
  _children: FFAXNode[];
 | 
			
		||||
  private _payload: any;
 | 
			
		||||
  private _payload: Protocol.Accessibility.AXTree;
 | 
			
		||||
  private _editable: boolean;
 | 
			
		||||
  private _richlyEditable: boolean;
 | 
			
		||||
  private _focusable: boolean;
 | 
			
		||||
| 
						 | 
				
			
			@ -77,8 +110,15 @@ class FFAXNode implements accessibility.AXNode {
 | 
			
		|||
    return this._children;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async findElement(): Promise<FFAXNode | null> {
 | 
			
		||||
    throw new Error('Not implimented');
 | 
			
		||||
  _findNeedle(): FFAXNode | null {
 | 
			
		||||
    if (this._payload.foundObject)
 | 
			
		||||
      return this;
 | 
			
		||||
    for (const child of this._children) {
 | 
			
		||||
      const found = child._findNeedle();
 | 
			
		||||
      if (found)
 | 
			
		||||
        return found;
 | 
			
		||||
    }
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  isLeafNode(): boolean {
 | 
			
		||||
| 
						 | 
				
			
			@ -160,10 +200,10 @@ class FFAXNode implements accessibility.AXNode {
 | 
			
		|||
 | 
			
		||||
  serialize(): accessibility.SerializedAXNode {
 | 
			
		||||
    const node: {[x in keyof accessibility.SerializedAXNode]: any} = {
 | 
			
		||||
      role: this._role,
 | 
			
		||||
      role: FFRoleToARIARole.get(this._role) || this._role,
 | 
			
		||||
      name: this._name || ''
 | 
			
		||||
    };
 | 
			
		||||
    const userStringProperties: Array<keyof accessibility.SerializedAXNode> = [
 | 
			
		||||
    const userStringProperties: Array<keyof accessibility.SerializedAXNode & keyof Protocol.Accessibility.AXTree> = [
 | 
			
		||||
      'name',
 | 
			
		||||
      'value',
 | 
			
		||||
      'description',
 | 
			
		||||
| 
						 | 
				
			
			@ -176,7 +216,7 @@ class FFAXNode implements accessibility.AXNode {
 | 
			
		|||
        continue;
 | 
			
		||||
      node[userStringProperty] = this._payload[userStringProperty];
 | 
			
		||||
    }
 | 
			
		||||
    const booleanProperties: Array<keyof accessibility.SerializedAXNode> = [
 | 
			
		||||
    const booleanProperties: Array<keyof accessibility.SerializedAXNode & keyof Protocol.Accessibility.AXTree> = [
 | 
			
		||||
      'disabled',
 | 
			
		||||
      'expanded',
 | 
			
		||||
      'focused',
 | 
			
		||||
| 
						 | 
				
			
			@ -195,7 +235,7 @@ class FFAXNode implements accessibility.AXNode {
 | 
			
		|||
        continue;
 | 
			
		||||
      node[booleanProperty] = value;
 | 
			
		||||
    }
 | 
			
		||||
    const tristateProperties: Array<keyof accessibility.SerializedAXNode> = [
 | 
			
		||||
    const tristateProperties: Array<keyof accessibility.SerializedAXNode & keyof Protocol.Accessibility.AXTree> = [
 | 
			
		||||
      'checked',
 | 
			
		||||
      'pressed',
 | 
			
		||||
    ];
 | 
			
		||||
| 
						 | 
				
			
			@ -205,7 +245,7 @@ class FFAXNode implements accessibility.AXNode {
 | 
			
		|||
      const value = this._payload[tristateProperty];
 | 
			
		||||
      node[tristateProperty] = value;
 | 
			
		||||
    }
 | 
			
		||||
    const numericalProperties: Array<keyof accessibility.SerializedAXNode> = [
 | 
			
		||||
    const numericalProperties: Array<keyof accessibility.SerializedAXNode & keyof Protocol.Accessibility.AXTree> = [
 | 
			
		||||
      'level'
 | 
			
		||||
    ];
 | 
			
		||||
    for (const numericalProperty of numericalProperties) {
 | 
			
		||||
| 
						 | 
				
			
			@ -213,7 +253,7 @@ class FFAXNode implements accessibility.AXNode {
 | 
			
		|||
        continue;
 | 
			
		||||
      node[numericalProperty] = this._payload[numericalProperty];
 | 
			
		||||
    }
 | 
			
		||||
    const tokenProperties: Array<keyof accessibility.SerializedAXNode> = [
 | 
			
		||||
    const tokenProperties: Array<keyof accessibility.SerializedAXNode & keyof Protocol.Accessibility.AXTree> = [
 | 
			
		||||
      'autocomplete',
 | 
			
		||||
      'haspopup',
 | 
			
		||||
      'invalid',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -355,8 +355,8 @@ export class FFPage implements PageDelegate {
 | 
			
		|||
    return handle;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async getAccessibilityTree() : Promise<accessibility.AXNode> {
 | 
			
		||||
    return getAccessibilityTree(this._session);
 | 
			
		||||
  async getAccessibilityTree(needle?: dom.ElementHandle) {
 | 
			
		||||
    return getAccessibilityTree(this._session, needle);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  coverage(): Coverage | undefined {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -68,7 +68,7 @@ export interface PageDelegate {
 | 
			
		|||
  setInputFiles(handle: dom.ElementHandle<HTMLInputElement>, files: types.FilePayload[]): Promise<void>;
 | 
			
		||||
  getBoundingBox(handle: dom.ElementHandle): Promise<types.Rect | null>;
 | 
			
		||||
 | 
			
		||||
  getAccessibilityTree(): Promise<accessibility.AXNode>;
 | 
			
		||||
  getAccessibilityTree(needle?: dom.ElementHandle): Promise<{tree: accessibility.AXNode, needle: accessibility.AXNode | null}>;
 | 
			
		||||
  pdf?: (options?: types.PDFOptions) => Promise<platform.BufferType>;
 | 
			
		||||
  coverage(): Coverage | undefined;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,12 +16,42 @@
 | 
			
		|||
import * as accessibility from '../accessibility';
 | 
			
		||||
import { WKSession } from './wkConnection';
 | 
			
		||||
import { Protocol } from './protocol';
 | 
			
		||||
import * as dom from '../dom';
 | 
			
		||||
 | 
			
		||||
export async function getAccessibilityTree(session: WKSession) {
 | 
			
		||||
  const {axNode} = await session.send('Page.accessibilitySnapshot');
 | 
			
		||||
  return new WKAXNode(axNode);
 | 
			
		||||
export async function getAccessibilityTree(session: WKSession, needle?: dom.ElementHandle) {
 | 
			
		||||
  const objectId = needle ? needle._remoteObject.objectId : undefined;
 | 
			
		||||
  const {axNode} = await session.send('Page.accessibilitySnapshot', { objectId });
 | 
			
		||||
  const tree = new WKAXNode(axNode);
 | 
			
		||||
  return {
 | 
			
		||||
    tree,
 | 
			
		||||
    needle: needle ? tree._findNeedle() : null
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const WKRoleToARIARole = new Map(Object.entries({
 | 
			
		||||
  'TextField': 'textbox',
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
// WebKit localizes role descriptions on mac, but the english versions only add noise.
 | 
			
		||||
const WKUnhelpfulRoleDescriptions = new Map(Object.entries({
 | 
			
		||||
  'WebArea': 'HTML content',
 | 
			
		||||
  'Summary': 'summary',
 | 
			
		||||
  'DescriptionList': 'description list',
 | 
			
		||||
  'ImageMap': 'image map',
 | 
			
		||||
  'ListMarker': 'list marker',
 | 
			
		||||
  'Video': 'video playback',
 | 
			
		||||
  'Mark': 'highlighted',
 | 
			
		||||
  'contentinfo': 'content information',
 | 
			
		||||
  'Details': 'details',
 | 
			
		||||
  'DescriptionListDetail': 'description',
 | 
			
		||||
  'DescriptionListTerm': 'term',
 | 
			
		||||
  'alertdialog': 'web alert dialog',
 | 
			
		||||
  'dialog': 'web dialog',
 | 
			
		||||
  'status': 'application status',
 | 
			
		||||
  'tabpanel': 'tab panel',
 | 
			
		||||
  'application': 'web application',
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
class WKAXNode implements accessibility.AXNode {
 | 
			
		||||
    private _payload: Protocol.Page.AXNode;
 | 
			
		||||
    private _children: WKAXNode[];
 | 
			
		||||
| 
						 | 
				
			
			@ -38,7 +68,14 @@ class WKAXNode implements accessibility.AXNode {
 | 
			
		|||
      return this._children;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async findElement() {
 | 
			
		||||
    _findNeedle() : WKAXNode | null {
 | 
			
		||||
      if (this._payload.found)
 | 
			
		||||
        return this;
 | 
			
		||||
      for (const child of this._children) {
 | 
			
		||||
        const found = child._findNeedle();
 | 
			
		||||
        if (found)
 | 
			
		||||
          return found;
 | 
			
		||||
      }
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -71,8 +108,26 @@ class WKAXNode implements accessibility.AXNode {
 | 
			
		|||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _isTextControl() : boolean {
 | 
			
		||||
      switch (this._payload.role) {
 | 
			
		||||
        case 'combobox':
 | 
			
		||||
        case 'searchfield':
 | 
			
		||||
        case 'textbox':
 | 
			
		||||
        case 'TextField':
 | 
			
		||||
          return true;
 | 
			
		||||
      }
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _name() : string {
 | 
			
		||||
      if (this._payload.role === 'text')
 | 
			
		||||
        return this._payload.value || '';
 | 
			
		||||
      return this._payload.name || '';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    isInteresting(insideControl: boolean) : boolean {
 | 
			
		||||
      const {role, focusable, name} = this._payload;
 | 
			
		||||
      const {role, focusable} = this._payload;
 | 
			
		||||
      const name = this._name();
 | 
			
		||||
      if (role === 'ScrollArea')
 | 
			
		||||
        return false;
 | 
			
		||||
      if (role === 'WebArea')
 | 
			
		||||
| 
						 | 
				
			
			@ -92,30 +147,54 @@ class WKAXNode implements accessibility.AXNode {
 | 
			
		|||
      return this.isLeafNode() && !!name;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _hasRendundantTextChild() {
 | 
			
		||||
      if (this._children.length !== 1)
 | 
			
		||||
        return false;
 | 
			
		||||
      const child = this._children[0];
 | 
			
		||||
      return child._payload.role === 'text' && this._payload.name === child._payload.value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    isLeafNode() : boolean {
 | 
			
		||||
      return !this._children.length;
 | 
			
		||||
      if (!this._children.length)
 | 
			
		||||
        return true;
 | 
			
		||||
      // WebKit on Linux ignores everything inside text controls, normalize this behavior
 | 
			
		||||
      if (this._isTextControl())
 | 
			
		||||
        return true;
 | 
			
		||||
      // WebKit for mac has text nodes inside heading, li, menuitem, a, and p nodes
 | 
			
		||||
      if (this._hasRendundantTextChild())
 | 
			
		||||
        return true;
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    serialize(): accessibility.SerializedAXNode {
 | 
			
		||||
      const node : accessibility.SerializedAXNode = {
 | 
			
		||||
        role: this._payload.role,
 | 
			
		||||
        name: this._payload.name || '',
 | 
			
		||||
        role: WKRoleToARIARole.get(this._payload.role) || this._payload.role,
 | 
			
		||||
        name: this._name(),
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      const userStringProperties: string[] = [
 | 
			
		||||
        'value',
 | 
			
		||||
        'description',
 | 
			
		||||
      if ('description' in this._payload && this._payload.description !== node.name)
 | 
			
		||||
        node.description = this._payload.description;
 | 
			
		||||
 | 
			
		||||
      if ('roledescription' in this._payload) {
 | 
			
		||||
        const roledescription = this._payload.roledescription;
 | 
			
		||||
        if (roledescription !== this._payload.role && WKUnhelpfulRoleDescriptions.get(this._payload.role) !== roledescription)
 | 
			
		||||
          node.roledescription = roledescription;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if ('value' in this._payload && this._payload.role !== 'text')
 | 
			
		||||
        node.value = this._payload.value;
 | 
			
		||||
 | 
			
		||||
      const userStringProperties: Array<keyof accessibility.SerializedAXNode & keyof Protocol.Page.AXNode> = [
 | 
			
		||||
        'keyshortcuts',
 | 
			
		||||
        'roledescription',
 | 
			
		||||
        'valuetext'
 | 
			
		||||
      ];
 | 
			
		||||
      for (const userStringProperty of userStringProperties) {
 | 
			
		||||
        if (!(userStringProperty in this._payload))
 | 
			
		||||
          continue;
 | 
			
		||||
        (node as any)[userStringProperty] = (this._payload as any)[userStringProperty];
 | 
			
		||||
        (node as any)[userStringProperty] = this._payload[userStringProperty];
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const booleanProperties: string[] = [
 | 
			
		||||
      const booleanProperties: Array<keyof accessibility.SerializedAXNode & keyof Protocol.Page.AXNode> = [
 | 
			
		||||
        'disabled',
 | 
			
		||||
        'expanded',
 | 
			
		||||
        'focused',
 | 
			
		||||
| 
						 | 
				
			
			@ -131,7 +210,7 @@ class WKAXNode implements accessibility.AXNode {
 | 
			
		|||
        // not whether focus is specifically on the root node.
 | 
			
		||||
        if (booleanProperty === 'focused' && (this._payload.role === 'WebArea' || this._payload.role === 'ScrollArea'))
 | 
			
		||||
          continue;
 | 
			
		||||
        const value = (this._payload as any)[booleanProperty];
 | 
			
		||||
        const value = this._payload[booleanProperty];
 | 
			
		||||
        if (!value)
 | 
			
		||||
          continue;
 | 
			
		||||
        (node as any)[booleanProperty] = value;
 | 
			
		||||
| 
						 | 
				
			
			@ -147,7 +226,7 @@ class WKAXNode implements accessibility.AXNode {
 | 
			
		|||
        const value = this._payload[tristateProperty];
 | 
			
		||||
        node[tristateProperty] = value === 'mixed' ? 'mixed' : value === 'true' ? true : false;
 | 
			
		||||
      }
 | 
			
		||||
      const numericalProperties: string[] = [
 | 
			
		||||
      const numericalProperties: Array<keyof accessibility.SerializedAXNode & keyof Protocol.Page.AXNode> = [
 | 
			
		||||
        'level',
 | 
			
		||||
        'valuemax',
 | 
			
		||||
        'valuemin',
 | 
			
		||||
| 
						 | 
				
			
			@ -157,7 +236,7 @@ class WKAXNode implements accessibility.AXNode {
 | 
			
		|||
          continue;
 | 
			
		||||
        (node as any)[numericalProperty] = (this._payload as any)[numericalProperty];
 | 
			
		||||
      }
 | 
			
		||||
      const tokenProperties: string[] = [
 | 
			
		||||
      const tokenProperties: Array<keyof accessibility.SerializedAXNode & keyof Protocol.Page.AXNode> = [
 | 
			
		||||
        'autocomplete',
 | 
			
		||||
        'haspopup',
 | 
			
		||||
        'invalid',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -497,8 +497,8 @@ export class WKPage implements PageDelegate {
 | 
			
		|||
    return to._createHandle(result.object) as dom.ElementHandle<T>;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async getAccessibilityTree() : Promise<accessibility.AXNode> {
 | 
			
		||||
    return getAccessibilityTree(this._session);
 | 
			
		||||
  async getAccessibilityTree(needle?: dom.ElementHandle) : Promise<{tree: accessibility.AXNode, needle: accessibility.AXNode | null}> {
 | 
			
		||||
    return getAccessibilityTree(this._session, needle);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  coverage(): Coverage | undefined {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,19 +15,18 @@
 | 
			
		|||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT}) {
 | 
			
		||||
module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT, MAC}) {
 | 
			
		||||
  const {describe, xdescribe, fdescribe} = testRunner;
 | 
			
		||||
  const {it, fit, xit, dit} = testRunner;
 | 
			
		||||
  const {beforeAll, beforeEach, afterAll, afterEach} = testRunner;
 | 
			
		||||
 | 
			
		||||
  describe('Accessibility', function() {
 | 
			
		||||
    it.skip(WEBKIT)('should work', async function({page}) {
 | 
			
		||||
    it('should work', async function({page}) {
 | 
			
		||||
      await page.setContent(`
 | 
			
		||||
      <head>
 | 
			
		||||
        <title>Accessibility Test</title>
 | 
			
		||||
      </head>
 | 
			
		||||
      <body>
 | 
			
		||||
        <div>Hello World</div>
 | 
			
		||||
        <h1>Inputs</h1>
 | 
			
		||||
        <input placeholder="Empty input" autofocus />
 | 
			
		||||
        <input placeholder="readonly input" readonly />
 | 
			
		||||
| 
						 | 
				
			
			@ -37,10 +36,6 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
 | 
			
		|||
        <input aria-placeholder="placeholder" value="and a value" />
 | 
			
		||||
        <div aria-hidden="true" id="desc">This is a description!</div>
 | 
			
		||||
        <input aria-placeholder="placeholder" value="and a value" aria-describedby="desc" />
 | 
			
		||||
        <select>
 | 
			
		||||
          <option>First Option</option>
 | 
			
		||||
          <option>Second Option</option>
 | 
			
		||||
        </select>
 | 
			
		||||
      </body>`);
 | 
			
		||||
      // autofocus happens after a delay in chrome these days
 | 
			
		||||
      await page.waitForFunction(() => document.activeElement.hasAttribute('autofocus'));
 | 
			
		||||
| 
						 | 
				
			
			@ -49,24 +44,19 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
 | 
			
		|||
        role: 'document',
 | 
			
		||||
        name: 'Accessibility Test',
 | 
			
		||||
        children: [
 | 
			
		||||
          {role: 'text leaf', name: 'Hello World'},
 | 
			
		||||
          {role: 'heading', name: 'Inputs', level: 1},
 | 
			
		||||
          {role: 'entry', name: 'Empty input', focused: true},
 | 
			
		||||
          {role: 'entry', name: 'readonly input', readonly: true},
 | 
			
		||||
          {role: 'entry', name: 'disabled input', disabled: true},
 | 
			
		||||
          {role: 'entry', name: 'Input with whitespace', value: '  '},
 | 
			
		||||
          {role: 'entry', name: '', value: 'value only'},
 | 
			
		||||
          {role: 'entry', name: '', value: 'and a value'}, // firefox doesn't use aria-placeholder for the name
 | 
			
		||||
          {role: 'entry', name: '', value: 'and a value', description: 'This is a description!'}, // and here
 | 
			
		||||
          {role: 'combobox', name: '', value: 'First Option', haspopup: true, children: [
 | 
			
		||||
            {role: 'combobox option', name: 'First Option', selected: true},
 | 
			
		||||
            {role: 'combobox option', name: 'Second Option'}]
 | 
			
		||||
          }]
 | 
			
		||||
          {role: 'textbox', name: 'Empty input', focused: true},
 | 
			
		||||
          {role: 'textbox', name: 'readonly input', readonly: true},
 | 
			
		||||
          {role: 'textbox', name: 'disabled input', disabled: true},
 | 
			
		||||
          {role: 'textbox', name: 'Input with whitespace', value: '  '},
 | 
			
		||||
          {role: 'textbox', name: '', value: 'value only'},
 | 
			
		||||
          {role: 'textbox', name: '', value: 'and a value'}, // firefox doesn't use aria-placeholder for the name
 | 
			
		||||
          {role: 'textbox', name: '', value: 'and a value', description: 'This is a description!'}, // and here
 | 
			
		||||
        ]
 | 
			
		||||
      } : CHROMIUM ? {
 | 
			
		||||
        role: 'WebArea',
 | 
			
		||||
        name: 'Accessibility Test',
 | 
			
		||||
        children: [
 | 
			
		||||
          {role: 'text', name: 'Hello World'},
 | 
			
		||||
          {role: 'heading', name: 'Inputs', level: 1},
 | 
			
		||||
          {role: 'textbox', name: 'Empty input', focused: true},
 | 
			
		||||
          {role: 'textbox', name: 'readonly input', readonly: true},
 | 
			
		||||
| 
						 | 
				
			
			@ -75,65 +65,30 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
 | 
			
		|||
          {role: 'textbox', name: '', value: 'value only'},
 | 
			
		||||
          {role: 'textbox', name: 'placeholder', value: 'and a value'},
 | 
			
		||||
          {role: 'textbox', name: 'placeholder', value: 'and a value', description: 'This is a description!'},
 | 
			
		||||
          {role: 'combobox', name: '', value: 'First Option', children: [
 | 
			
		||||
            {role: 'menuitem', name: 'First Option', selected: true},
 | 
			
		||||
            {role: 'menuitem', name: 'Second Option'}]
 | 
			
		||||
          }]
 | 
			
		||||
        ]
 | 
			
		||||
      } : {
 | 
			
		||||
        role: 'WebArea',
 | 
			
		||||
        name: 'Accessibility Test',
 | 
			
		||||
        children: [
 | 
			
		||||
          {role: 'heading', name: 'Inputs', level: 1 },
 | 
			
		||||
          {role: 'TextField', name: 'Empty input', focused: true, readonly: true},
 | 
			
		||||
          {role: 'TextField', name: 'readonly input', readonly: true },
 | 
			
		||||
          {role: 'TextField', name: 'disabled input', disabled: true, readonly: true},
 | 
			
		||||
          {role: 'TextField', name: 'Input with whitespace', value: '  ', description: 'Input with whitespace', readonly: true},
 | 
			
		||||
          {role: 'TextField', name: '', value: 'value only', readonly: true },
 | 
			
		||||
          {role: 'TextField', name: 'placeholder',value: 'and a value',readonly: true},
 | 
			
		||||
          {role: 'TextField', name: 'This is a description!',value: 'and a value',readonly: true},
 | 
			
		||||
          {role: 'button', name: '', value: 'First Option', children: [
 | 
			
		||||
            { role: 'MenuListOption', name: '', value: 'First Option', selected: true },
 | 
			
		||||
            { role: 'MenuListOption', name: '', value: 'Second Option' }]
 | 
			
		||||
          }
 | 
			
		||||
          {role: 'heading', name: 'Inputs', level: 1},
 | 
			
		||||
          {role: 'textbox', name: 'Empty input', focused: true},
 | 
			
		||||
          {role: 'textbox', name: 'readonly input', readonly: true},
 | 
			
		||||
          {role: 'textbox', name: 'disabled input', disabled: true},
 | 
			
		||||
          {role: 'textbox', name: 'Input with whitespace', value: '  ' },
 | 
			
		||||
          {role: 'textbox', name: '', value: 'value only' },
 | 
			
		||||
          {role: 'textbox', name: 'placeholder', value: 'and a value'},
 | 
			
		||||
          {role: 'textbox', name: 'This is a description!',value: 'and a value'}, // webkit uses the description over placeholder for the name
 | 
			
		||||
        ]
 | 
			
		||||
      };
 | 
			
		||||
      expect(await page.accessibility.snapshot()).toEqual(golden);
 | 
			
		||||
    });
 | 
			
		||||
    it.skip(WEBKIT)('should report uninteresting nodes', async function({page}) {
 | 
			
		||||
      await page.setContent(`<textarea autofocus>hi</textarea>`);
 | 
			
		||||
      // autofocus happens after a delay in chrome these days
 | 
			
		||||
      await page.waitForFunction(() => document.activeElement.hasAttribute('autofocus'));
 | 
			
		||||
      const golden = FFOX ? {
 | 
			
		||||
        role: 'entry',
 | 
			
		||||
        name: '',
 | 
			
		||||
        value: 'hi',
 | 
			
		||||
        focused: true,
 | 
			
		||||
        multiline: true,
 | 
			
		||||
        children: [{
 | 
			
		||||
          role: 'text leaf',
 | 
			
		||||
          name: 'hi'
 | 
			
		||||
        }]
 | 
			
		||||
      } : CHROMIUM ? {
 | 
			
		||||
        role: 'textbox',
 | 
			
		||||
        name: '',
 | 
			
		||||
        value: 'hi',
 | 
			
		||||
        focused: true,
 | 
			
		||||
        multiline: true,
 | 
			
		||||
        children: [{
 | 
			
		||||
          role: 'generic',
 | 
			
		||||
          name: '',
 | 
			
		||||
          children: [{
 | 
			
		||||
            role: 'text', name: 'hi'
 | 
			
		||||
          }]
 | 
			
		||||
        }]
 | 
			
		||||
      } : {
 | 
			
		||||
        role: 'textbox',
 | 
			
		||||
        name: '',
 | 
			
		||||
        value: 'hi',
 | 
			
		||||
        focused: true,
 | 
			
		||||
        multiline: true
 | 
			
		||||
      };
 | 
			
		||||
      expect(findFocusedNode(await page.accessibility.snapshot({interestingOnly: false}))).toEqual(golden);
 | 
			
		||||
    it.skip(WEBKIT && !MAC)('should work with regular text', async({page}) => {
 | 
			
		||||
      await page.setContent(`<div>Hello World</div>`);
 | 
			
		||||
      const snapshot = await page.accessibility.snapshot();
 | 
			
		||||
      expect(snapshot.children[0]).toEqual({
 | 
			
		||||
        role: FFOX ? 'text leaf' : 'text',
 | 
			
		||||
        name: 'Hello World',
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    it('roledescription', async({page}) => {
 | 
			
		||||
      await page.setContent('<div tabIndex=-1 aria-roledescription="foo">Hi</div>');
 | 
			
		||||
| 
						 | 
				
			
			@ -145,8 +100,8 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
 | 
			
		|||
      const snapshot = await page.accessibility.snapshot();
 | 
			
		||||
      expect(snapshot.children[0].orientation).toEqual('vertical');
 | 
			
		||||
    });
 | 
			
		||||
    it.skip(FFOX || WEBKIT)('autocomplete', async({page}) => {
 | 
			
		||||
      await page.setContent('<input type="number" aria-autocomplete="list" />');
 | 
			
		||||
    it('autocomplete', async({page}) => {
 | 
			
		||||
      await page.setContent('<div role="textbox" aria-autocomplete="list">hi</div>');
 | 
			
		||||
      const snapshot = await page.accessibility.snapshot();
 | 
			
		||||
      expect(snapshot.children[0].autocomplete).toEqual('list');
 | 
			
		||||
    });
 | 
			
		||||
| 
						 | 
				
			
			@ -167,33 +122,8 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
 | 
			
		|||
          <div role="tab" aria-selected="true"><b>Tab1</b></div>
 | 
			
		||||
          <div role="tab">Tab2</div>
 | 
			
		||||
        </div>`);
 | 
			
		||||
        const golden = FFOX ? {
 | 
			
		||||
          role: 'document',
 | 
			
		||||
          name: '',
 | 
			
		||||
          children: [{
 | 
			
		||||
            role: 'pagetab',
 | 
			
		||||
            name: 'Tab1',
 | 
			
		||||
            selected: true
 | 
			
		||||
          }, {
 | 
			
		||||
            role: 'pagetab',
 | 
			
		||||
            name: 'Tab2'
 | 
			
		||||
          }]
 | 
			
		||||
        } : WEBKIT ? {
 | 
			
		||||
          role: 'WebArea',
 | 
			
		||||
          name: '',
 | 
			
		||||
          roledescription: 'HTML content',
 | 
			
		||||
          children: [{
 | 
			
		||||
            role: 'tab',
 | 
			
		||||
            name: 'Tab1',
 | 
			
		||||
            roledescription: 'tab',
 | 
			
		||||
            selected: true
 | 
			
		||||
          }, {
 | 
			
		||||
            role: 'tab',
 | 
			
		||||
            name: 'Tab2',
 | 
			
		||||
            roledescription: 'tab',
 | 
			
		||||
          }]
 | 
			
		||||
        } : {
 | 
			
		||||
          role: 'WebArea',
 | 
			
		||||
        const golden = {
 | 
			
		||||
          role: FFOX ? 'document' : 'WebArea',
 | 
			
		||||
          name: '',
 | 
			
		||||
          children: [{
 | 
			
		||||
            role: 'tab',
 | 
			
		||||
| 
						 | 
				
			
			@ -244,7 +174,7 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
 | 
			
		|||
          Edit this image: <img src="fakeimage.png" alt="my fake image">
 | 
			
		||||
        </div>`);
 | 
			
		||||
        const golden = FFOX ? {
 | 
			
		||||
          role: 'entry',
 | 
			
		||||
          role: 'textbox',
 | 
			
		||||
          name: '',
 | 
			
		||||
          value: 'Edit this image: my fake image',
 | 
			
		||||
          children: [{
 | 
			
		||||
| 
						 | 
				
			
			@ -305,7 +235,7 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
 | 
			
		|||
          <img alt="yo" src="fakeimg.png">
 | 
			
		||||
        </div>`);
 | 
			
		||||
        const golden = FFOX ? {
 | 
			
		||||
          role: 'entry',
 | 
			
		||||
          role: 'textbox',
 | 
			
		||||
          name: 'my favorite textbox',
 | 
			
		||||
          value: 'this is the inner content yo'
 | 
			
		||||
        } : CHROMIUM ? {
 | 
			
		||||
| 
						 | 
				
			
			@ -313,10 +243,9 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
 | 
			
		|||
          name: 'my favorite textbox',
 | 
			
		||||
          value: 'this is the inner content '
 | 
			
		||||
        } : {
 | 
			
		||||
          role: 'TextField',
 | 
			
		||||
          role: 'textbox',
 | 
			
		||||
          name: 'my favorite textbox',
 | 
			
		||||
          value: 'this is the inner content  ',
 | 
			
		||||
          description: 'my favorite textbox'
 | 
			
		||||
        };
 | 
			
		||||
        const snapshot = await page.accessibility.snapshot();
 | 
			
		||||
        expect(snapshot.children[0]).toEqual(golden);
 | 
			
		||||
| 
						 | 
				
			
			@ -327,19 +256,10 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
 | 
			
		|||
          this is the inner content
 | 
			
		||||
          <img alt="yo" src="fakeimg.png">
 | 
			
		||||
        </div>`);
 | 
			
		||||
        const golden = FFOX ? {
 | 
			
		||||
          role: 'checkbutton',
 | 
			
		||||
          name: 'my favorite checkbox',
 | 
			
		||||
          checked: true
 | 
			
		||||
        } : CHROMIUM ? {
 | 
			
		||||
        const golden = {
 | 
			
		||||
          role: 'checkbox',
 | 
			
		||||
          name: 'my favorite checkbox',
 | 
			
		||||
          checked: true
 | 
			
		||||
        } : {
 | 
			
		||||
          role: 'checkbox',
 | 
			
		||||
          name: 'my favorite checkbox',
 | 
			
		||||
          description: "my favorite checkbox",
 | 
			
		||||
          checked: true
 | 
			
		||||
        };
 | 
			
		||||
        const snapshot = await page.accessibility.snapshot();
 | 
			
		||||
        expect(snapshot.children[0]).toEqual(golden);
 | 
			
		||||
| 
						 | 
				
			
			@ -351,7 +271,7 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
 | 
			
		|||
          <img alt="yo" src="fakeimg.png">
 | 
			
		||||
        </div>`);
 | 
			
		||||
        const golden = FFOX ? {
 | 
			
		||||
          role: 'checkbutton',
 | 
			
		||||
          role: 'checkbox',
 | 
			
		||||
          name: 'this is the inner content yo',
 | 
			
		||||
          checked: true
 | 
			
		||||
        } : {
 | 
			
		||||
| 
						 | 
				
			
			@ -363,7 +283,7 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
 | 
			
		|||
        expect(snapshot.children[0]).toEqual(golden);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      describe.skip(FFOX || WEBKIT)('root option', function() {
 | 
			
		||||
      describe('root option', function() {
 | 
			
		||||
        it('should work a button', async({page}) => {
 | 
			
		||||
          await page.setContent(`<button>My Button</button>`);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -383,7 +303,7 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
 | 
			
		|||
            value: 'My Value'
 | 
			
		||||
          });
 | 
			
		||||
        });
 | 
			
		||||
        it('should work a menu', async({page}) => {
 | 
			
		||||
        it('should work on a menu', async({page}) => {
 | 
			
		||||
          await page.setContent(`
 | 
			
		||||
            <div role="menu" title="My Menu">
 | 
			
		||||
              <div role="menuitem">First Item</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -399,7 +319,8 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
 | 
			
		|||
            children:
 | 
			
		||||
            [ { role: 'menuitem', name: 'First Item' },
 | 
			
		||||
              { role: 'menuitem', name: 'Second Item' },
 | 
			
		||||
              { role: 'menuitem', name: 'Third Item' } ]
 | 
			
		||||
              { role: 'menuitem', name: 'Third Item' } ],
 | 
			
		||||
            orientation: WEBKIT ? 'vertical' : undefined
 | 
			
		||||
          });
 | 
			
		||||
        });
 | 
			
		||||
        it('should return null when the element is no longer in DOM', async({page}) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -408,38 +329,26 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
 | 
			
		|||
          await page.$eval('button', button => button.remove());
 | 
			
		||||
          expect(await page.accessibility.snapshot({root: button})).toEqual(null);
 | 
			
		||||
        });
 | 
			
		||||
        it('should support the interestingOnly option', async({page}) => {
 | 
			
		||||
          await page.setContent(`<div><button>My Button</button></div>`);
 | 
			
		||||
          const div = await page.$('div');
 | 
			
		||||
          expect(await page.accessibility.snapshot({root: div})).toEqual(null);
 | 
			
		||||
          expect(await page.accessibility.snapshot({root: div, interestingOnly: false})).toEqual({
 | 
			
		||||
            role: 'generic',
 | 
			
		||||
            name: '',
 | 
			
		||||
            children: [
 | 
			
		||||
              {
 | 
			
		||||
                role: 'button',
 | 
			
		||||
                name: 'My Button',
 | 
			
		||||
                children: [
 | 
			
		||||
                  {
 | 
			
		||||
                    role: "text",
 | 
			
		||||
                    name: "My Button"
 | 
			
		||||
                  }
 | 
			
		||||
                ],
 | 
			
		||||
              }
 | 
			
		||||
            ]
 | 
			
		||||
          });
 | 
			
		||||
        it('should show uninteresting nodes', async({page}) => {
 | 
			
		||||
          await page.setContent(`
 | 
			
		||||
            <div id="root" role="textbox">
 | 
			
		||||
              <div>
 | 
			
		||||
                hello
 | 
			
		||||
                <div>
 | 
			
		||||
                  world
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          `);
 | 
			
		||||
 | 
			
		||||
          const root = await page.$('#root');
 | 
			
		||||
          const snapshot = await page.accessibility.snapshot({root, interestingOnly: false});
 | 
			
		||||
          expect(snapshot.role).toBe('textbox');
 | 
			
		||||
          expect(snapshot.value).toContain('hello');
 | 
			
		||||
          expect(snapshot.value).toContain('world');
 | 
			
		||||
          expect(!!snapshot.children).toBe(true);
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    function findFocusedNode(node) {
 | 
			
		||||
      if (node.focused)
 | 
			
		||||
        return node;
 | 
			
		||||
      for (const child of node.children || []) {
 | 
			
		||||
        const focusedChild = findFocusedNode(child);
 | 
			
		||||
        if (focusedChild)
 | 
			
		||||
          return focusedChild;
 | 
			
		||||
      }
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue