feat(webkit): accessibility (#350)
* move accessibility out of features * feat(webkit): accessibility
This commit is contained in:
parent
fcd62b9611
commit
790e38a678
103
src/accessibility.ts
Normal file
103
src/accessibility.ts
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
// Copyright (c) Microsoft Corporation.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
import * as dom from './dom';
|
||||||
|
|
||||||
|
export type SerializedAXNode = {
|
||||||
|
role: string,
|
||||||
|
name: string,
|
||||||
|
value?: string|number,
|
||||||
|
description?: string,
|
||||||
|
|
||||||
|
keyshortcuts?: string,
|
||||||
|
roledescription?: string,
|
||||||
|
valuetext?: string,
|
||||||
|
|
||||||
|
disabled?: boolean,
|
||||||
|
expanded?: boolean,
|
||||||
|
focused?: boolean,
|
||||||
|
modal?: boolean,
|
||||||
|
multiline?: boolean,
|
||||||
|
multiselectable?: boolean,
|
||||||
|
readonly?: boolean,
|
||||||
|
required?: boolean,
|
||||||
|
selected?: boolean,
|
||||||
|
|
||||||
|
checked?: boolean|'mixed',
|
||||||
|
pressed?: boolean|'mixed',
|
||||||
|
|
||||||
|
level?: number,
|
||||||
|
valuemin?: number,
|
||||||
|
valuemax?: number,
|
||||||
|
|
||||||
|
autocomplete?: string,
|
||||||
|
haspopup?: string,
|
||||||
|
invalid?: string,
|
||||||
|
orientation?: string,
|
||||||
|
|
||||||
|
children?: SerializedAXNode[]
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface AXNode {
|
||||||
|
isInteresting(insideControl: boolean): boolean;
|
||||||
|
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>) {
|
||||||
|
this._getAXTree = getAXTree;
|
||||||
|
}
|
||||||
|
|
||||||
|
async snapshot(options: {
|
||||||
|
interestingOnly?: boolean;
|
||||||
|
root?: dom.ElementHandle | null;
|
||||||
|
} = {}): Promise<SerializedAXNode> {
|
||||||
|
const {
|
||||||
|
interestingOnly = true,
|
||||||
|
root = null,
|
||||||
|
} = options;
|
||||||
|
const defaultRoot = await this._getAXTree();
|
||||||
|
let needle = defaultRoot;
|
||||||
|
if (root) {
|
||||||
|
needle = await defaultRoot.findElement(root);
|
||||||
|
if (!needle)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!interestingOnly)
|
||||||
|
return serializeTree(needle)[0];
|
||||||
|
|
||||||
|
const interestingNodes: Set<AXNode> = new Set();
|
||||||
|
collectInterestingNodes(interestingNodes, defaultRoot, false);
|
||||||
|
if (root && !interestingNodes.has(needle))
|
||||||
|
return null;
|
||||||
|
return serializeTree(needle, interestingNodes)[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectInterestingNodes(collection: Set<AXNode>, node: AXNode, insideControl: boolean) {
|
||||||
|
if (node.isInteresting(insideControl))
|
||||||
|
collection.add(node);
|
||||||
|
if (node.isLeafNode())
|
||||||
|
return;
|
||||||
|
insideControl = insideControl || node.isControl();
|
||||||
|
for (const child of node.children())
|
||||||
|
collectInterestingNodes(collection, child, insideControl);
|
||||||
|
}
|
||||||
|
|
||||||
|
function serializeTree(node: AXNode, whitelistedNodes?: Set<AXNode>): SerializedAXNode[] {
|
||||||
|
const children: SerializedAXNode[] = [];
|
||||||
|
for (const child of node.children())
|
||||||
|
children.push(...serializeTree(child, whitelistedNodes));
|
||||||
|
|
||||||
|
if (whitelistedNodes && !whitelistedNodes.has(node))
|
||||||
|
return children;
|
||||||
|
|
||||||
|
const serializedNode = node.serialize();
|
||||||
|
if (children.length)
|
||||||
|
serializedNode.children = children;
|
||||||
|
return [serializedNode];
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,7 @@ export { Keyboard, Mouse } from './input';
|
||||||
export { JSHandle } from './javascript';
|
export { JSHandle } from './javascript';
|
||||||
export { Request, Response } from './network';
|
export { Request, Response } from './network';
|
||||||
export { Page, FileChooser } from './page';
|
export { Page, FileChooser } from './page';
|
||||||
|
export { Accessibility } from './accessibility';
|
||||||
|
|
||||||
export * from './chromium/crApi';
|
export * from './chromium/crApi';
|
||||||
export * from './firefox/ffApi';
|
export * from './firefox/ffApi';
|
||||||
|
|
|
||||||
|
|
@ -15,111 +15,17 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { CRSession } from '../crConnection';
|
import { CRSession } from './crConnection';
|
||||||
import { Protocol } from '../protocol';
|
import { Protocol } from './protocol';
|
||||||
import * as dom from '../../dom';
|
import * as dom from '../dom';
|
||||||
|
import * as accessibility from '../accessibility';
|
||||||
|
|
||||||
type SerializedAXNode = {
|
export async function getAccessibilityTree(client: CRSession) : Promise<accessibility.AXNode> {
|
||||||
role: string,
|
const {nodes} = await client.send('Accessibility.getFullAXTree');
|
||||||
name?: string,
|
return CRAXNode.createTree(client, nodes);
|
||||||
value?: string|number,
|
|
||||||
description?: string,
|
|
||||||
|
|
||||||
keyshortcuts?: string,
|
|
||||||
roledescription?: string,
|
|
||||||
valuetext?: string,
|
|
||||||
|
|
||||||
disabled?: boolean,
|
|
||||||
expanded?: boolean,
|
|
||||||
focused?: boolean,
|
|
||||||
modal?: boolean,
|
|
||||||
multiline?: boolean,
|
|
||||||
multiselectable?: boolean,
|
|
||||||
readonly?: boolean,
|
|
||||||
required?: boolean,
|
|
||||||
selected?: boolean,
|
|
||||||
|
|
||||||
checked?: boolean|'mixed',
|
|
||||||
pressed?: boolean|'mixed',
|
|
||||||
|
|
||||||
level?: number,
|
|
||||||
valuemin?: number,
|
|
||||||
valuemax?: number,
|
|
||||||
|
|
||||||
autocomplete?: string,
|
|
||||||
haspopup?: string,
|
|
||||||
invalid?: string,
|
|
||||||
orientation?: string,
|
|
||||||
|
|
||||||
children?: SerializedAXNode[]
|
|
||||||
};
|
|
||||||
|
|
||||||
export class CRAccessibility {
|
|
||||||
private _client: CRSession;
|
|
||||||
|
|
||||||
constructor(client: CRSession) {
|
|
||||||
this._client = client;
|
|
||||||
}
|
|
||||||
|
|
||||||
async snapshot(options: {
|
|
||||||
interestingOnly?: boolean;
|
|
||||||
root?: dom.ElementHandle | null;
|
|
||||||
} = {}): Promise<SerializedAXNode> {
|
|
||||||
const {
|
|
||||||
interestingOnly = true,
|
|
||||||
root = null,
|
|
||||||
} = options;
|
|
||||||
const {nodes} = await this._client.send('Accessibility.getFullAXTree');
|
|
||||||
let backendNodeId: number | null = null;
|
|
||||||
if (root) {
|
|
||||||
const remoteObject = root._remoteObject as Protocol.Runtime.RemoteObject;
|
|
||||||
const {node} = await this._client.send('DOM.describeNode', {objectId: remoteObject.objectId});
|
|
||||||
backendNodeId = node.backendNodeId;
|
|
||||||
}
|
|
||||||
const defaultRoot = CRAXNode.createTree(nodes);
|
|
||||||
let needle = defaultRoot;
|
|
||||||
if (backendNodeId) {
|
|
||||||
needle = defaultRoot.find(node => node._payload.backendDOMNodeId === backendNodeId);
|
|
||||||
if (!needle)
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (!interestingOnly)
|
|
||||||
return serializeTree(needle)[0];
|
|
||||||
|
|
||||||
const interestingNodes: Set<CRAXNode> = new Set();
|
|
||||||
collectInterestingNodes(interestingNodes, defaultRoot, false);
|
|
||||||
if (!interestingNodes.has(needle))
|
|
||||||
return null;
|
|
||||||
return serializeTree(needle, interestingNodes)[0];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectInterestingNodes(collection: Set<CRAXNode>, node: CRAXNode, insideControl: boolean) {
|
class CRAXNode implements accessibility.AXNode {
|
||||||
if (node.isInteresting(insideControl))
|
|
||||||
collection.add(node);
|
|
||||||
if (node.isLeafNode())
|
|
||||||
return;
|
|
||||||
insideControl = insideControl || node.isControl();
|
|
||||||
for (const child of node._children)
|
|
||||||
collectInterestingNodes(collection, child, insideControl);
|
|
||||||
}
|
|
||||||
|
|
||||||
function serializeTree(node: CRAXNode, whitelistedNodes?: Set<CRAXNode>): SerializedAXNode[] {
|
|
||||||
const children: SerializedAXNode[] = [];
|
|
||||||
for (const child of node._children)
|
|
||||||
children.push(...serializeTree(child, whitelistedNodes));
|
|
||||||
|
|
||||||
if (whitelistedNodes && !whitelistedNodes.has(node))
|
|
||||||
return children;
|
|
||||||
|
|
||||||
const serializedNode = node.serialize();
|
|
||||||
if (children.length)
|
|
||||||
serializedNode.children = children;
|
|
||||||
return [serializedNode];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class CRAXNode {
|
|
||||||
_payload: Protocol.Accessibility.AXNode;
|
_payload: Protocol.Accessibility.AXNode;
|
||||||
_children: CRAXNode[] = [];
|
_children: CRAXNode[] = [];
|
||||||
private _richlyEditable = false;
|
private _richlyEditable = false;
|
||||||
|
|
@ -130,8 +36,10 @@ class CRAXNode {
|
||||||
private _name: string;
|
private _name: string;
|
||||||
private _role: string;
|
private _role: string;
|
||||||
private _cachedHasFocusableChild: boolean | undefined;
|
private _cachedHasFocusableChild: boolean | undefined;
|
||||||
|
private _client: CRSession;
|
||||||
|
|
||||||
constructor(payload: Protocol.Accessibility.AXNode) {
|
constructor(client: CRSession, payload: Protocol.Accessibility.AXNode) {
|
||||||
|
this._client = client;
|
||||||
this._payload = payload;
|
this._payload = payload;
|
||||||
|
|
||||||
this._name = this._payload.name ? this._payload.name.value : '';
|
this._name = this._payload.name ? this._payload.name.value : '';
|
||||||
|
|
@ -178,6 +86,17 @@ class CRAXNode {
|
||||||
return this._cachedHasFocusableChild;
|
return this._cachedHasFocusableChild;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
children() {
|
||||||
|
return this._children;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
return needle || null;
|
||||||
|
}
|
||||||
|
|
||||||
find(predicate: (arg0: CRAXNode) => boolean): CRAXNode | null {
|
find(predicate: (arg0: CRAXNode) => boolean): CRAXNode | null {
|
||||||
if (predicate(this))
|
if (predicate(this))
|
||||||
return this;
|
return this;
|
||||||
|
|
@ -275,7 +194,7 @@ class CRAXNode {
|
||||||
return this.isLeafNode() && !!this._name;
|
return this.isLeafNode() && !!this._name;
|
||||||
}
|
}
|
||||||
|
|
||||||
serialize(): SerializedAXNode {
|
serialize(): accessibility.SerializedAXNode {
|
||||||
const properties: Map<string, number | string | boolean> = new Map();
|
const properties: Map<string, number | string | boolean> = new Map();
|
||||||
for (const property of this._payload.properties || [])
|
for (const property of this._payload.properties || [])
|
||||||
properties.set(property.name.toLowerCase(), property.value.value);
|
properties.set(property.name.toLowerCase(), property.value.value);
|
||||||
|
|
@ -286,12 +205,12 @@ class CRAXNode {
|
||||||
if (this._payload.description)
|
if (this._payload.description)
|
||||||
properties.set('description', this._payload.description.value);
|
properties.set('description', this._payload.description.value);
|
||||||
|
|
||||||
const node: {[x in keyof SerializedAXNode]: any} = {
|
const node: {[x in keyof accessibility.SerializedAXNode]: any} = {
|
||||||
role: this._role
|
role: this._role,
|
||||||
|
name: this._payload.name.value || ''
|
||||||
};
|
};
|
||||||
|
|
||||||
const userStringProperties: Array<keyof SerializedAXNode> = [
|
const userStringProperties: Array<keyof accessibility.SerializedAXNode> = [
|
||||||
'name',
|
|
||||||
'value',
|
'value',
|
||||||
'description',
|
'description',
|
||||||
'keyshortcuts',
|
'keyshortcuts',
|
||||||
|
|
@ -304,7 +223,7 @@ class CRAXNode {
|
||||||
node[userStringProperty] = properties.get(userStringProperty);
|
node[userStringProperty] = properties.get(userStringProperty);
|
||||||
}
|
}
|
||||||
|
|
||||||
const booleanProperties: Array<keyof SerializedAXNode> = [
|
const booleanProperties: Array<keyof accessibility.SerializedAXNode> = [
|
||||||
'disabled',
|
'disabled',
|
||||||
'expanded',
|
'expanded',
|
||||||
'focused',
|
'focused',
|
||||||
|
|
@ -326,7 +245,7 @@ class CRAXNode {
|
||||||
node[booleanProperty] = value;
|
node[booleanProperty] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tristateProperties: Array<keyof SerializedAXNode> = [
|
const tristateProperties: Array<keyof accessibility.SerializedAXNode> = [
|
||||||
'checked',
|
'checked',
|
||||||
'pressed',
|
'pressed',
|
||||||
];
|
];
|
||||||
|
|
@ -336,7 +255,7 @@ class CRAXNode {
|
||||||
const value = properties.get(tristateProperty);
|
const value = properties.get(tristateProperty);
|
||||||
node[tristateProperty] = value === 'mixed' ? 'mixed' : value === 'true' ? true : false;
|
node[tristateProperty] = value === 'mixed' ? 'mixed' : value === 'true' ? true : false;
|
||||||
}
|
}
|
||||||
const numericalProperties: Array<keyof SerializedAXNode> = [
|
const numericalProperties: Array<keyof accessibility.SerializedAXNode> = [
|
||||||
'level',
|
'level',
|
||||||
'valuemax',
|
'valuemax',
|
||||||
'valuemin',
|
'valuemin',
|
||||||
|
|
@ -346,7 +265,7 @@ class CRAXNode {
|
||||||
continue;
|
continue;
|
||||||
node[numericalProperty] = properties.get(numericalProperty);
|
node[numericalProperty] = properties.get(numericalProperty);
|
||||||
}
|
}
|
||||||
const tokenProperties: Array<keyof SerializedAXNode> = [
|
const tokenProperties: Array<keyof accessibility.SerializedAXNode> = [
|
||||||
'autocomplete',
|
'autocomplete',
|
||||||
'haspopup',
|
'haspopup',
|
||||||
'invalid',
|
'invalid',
|
||||||
|
|
@ -358,13 +277,13 @@ class CRAXNode {
|
||||||
continue;
|
continue;
|
||||||
node[tokenProperty] = value;
|
node[tokenProperty] = value;
|
||||||
}
|
}
|
||||||
return node as SerializedAXNode;
|
return node as accessibility.SerializedAXNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
static createTree(payloads: Protocol.Accessibility.AXNode[]): CRAXNode {
|
static createTree(client: CRSession, payloads: Protocol.Accessibility.AXNode[]): CRAXNode {
|
||||||
const nodeById: Map<string, CRAXNode> = new Map();
|
const nodeById: Map<string, CRAXNode> = new Map();
|
||||||
for (const payload of payloads)
|
for (const payload of payloads)
|
||||||
nodeById.set(payload.nodeId, new CRAXNode(payload));
|
nodeById.set(payload.nodeId, new CRAXNode(client, payload));
|
||||||
for (const node of nodeById.values()) {
|
for (const node of nodeById.values()) {
|
||||||
for (const childId of node._payload.childIds || [])
|
for (const childId of node._payload.childIds || [])
|
||||||
node._children.push(nodeById.get(childId));
|
node._children.push(nodeById.get(childId));
|
||||||
|
|
@ -6,6 +6,5 @@ export { CRSession as ChromiumSession } from './crConnection';
|
||||||
export { ChromiumPage } from './crPage';
|
export { ChromiumPage } from './crPage';
|
||||||
export { CRPlaywright as ChromiumPlaywright } from './crPlaywright';
|
export { CRPlaywright as ChromiumPlaywright } from './crPlaywright';
|
||||||
export { CRTarget as ChromiumTarget } from './crTarget';
|
export { CRTarget as ChromiumTarget } from './crTarget';
|
||||||
export { CRAccessibility as ChromiumAccessibility } from './features/crAccessibility';
|
|
||||||
export { CRCoverage as ChromiumCoverage } from './features/crCoverage';
|
export { CRCoverage as ChromiumCoverage } from './features/crCoverage';
|
||||||
export { CRWorker as ChromiumWorker } from './features/crWorkers';
|
export { CRWorker as ChromiumWorker } from './features/crWorkers';
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ import { toConsoleMessageLocation, exceptionToError, releaseObject } from './crP
|
||||||
import * as dialog from '../dialog';
|
import * as dialog from '../dialog';
|
||||||
import { PageDelegate } from '../page';
|
import { PageDelegate } from '../page';
|
||||||
import { RawMouseImpl, RawKeyboardImpl } from './crInput';
|
import { RawMouseImpl, RawKeyboardImpl } from './crInput';
|
||||||
import { CRAccessibility } from './features/crAccessibility';
|
import { getAccessibilityTree } from './crAccessibility';
|
||||||
import { CRCoverage } from './features/crCoverage';
|
import { CRCoverage } from './features/crCoverage';
|
||||||
import { CRPDF, PDFOptions } from './features/crPdf';
|
import { CRPDF, PDFOptions } from './features/crPdf';
|
||||||
import { CRWorkers, CRWorker } from './features/crWorkers';
|
import { CRWorkers, CRWorker } from './features/crWorkers';
|
||||||
|
|
@ -38,6 +38,7 @@ import { BrowserContext } from '../browserContext';
|
||||||
import * as types from '../types';
|
import * as types from '../types';
|
||||||
import * as input from '../input';
|
import * as input from '../input';
|
||||||
import { ConsoleMessage } from '../console';
|
import { ConsoleMessage } from '../console';
|
||||||
|
import * as accessibility from '../accessibility';
|
||||||
|
|
||||||
const UTILITY_WORLD_NAME = '__playwright_utility_world__';
|
const UTILITY_WORLD_NAME = '__playwright_utility_world__';
|
||||||
|
|
||||||
|
|
@ -478,10 +479,13 @@ export class CRPage implements PageDelegate {
|
||||||
throw new Error('Unable to adopt element handle from a different document');
|
throw new Error('Unable to adopt element handle from a different document');
|
||||||
return to._createHandle(result.object).asElement()!;
|
return to._createHandle(result.object).asElement()!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getAccessibilityTree(): Promise<accessibility.AXNode> {
|
||||||
|
return getAccessibilityTree(this._client);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ChromiumPage extends Page {
|
export class ChromiumPage extends Page {
|
||||||
readonly accessibility: CRAccessibility;
|
|
||||||
readonly coverage: CRCoverage;
|
readonly coverage: CRCoverage;
|
||||||
private _pdf: CRPDF;
|
private _pdf: CRPDF;
|
||||||
private _workers: CRWorkers;
|
private _workers: CRWorkers;
|
||||||
|
|
@ -489,7 +493,6 @@ export class ChromiumPage extends Page {
|
||||||
|
|
||||||
constructor(client: CRSession, delegate: CRPage, browserContext: BrowserContext) {
|
constructor(client: CRSession, delegate: CRPage, browserContext: BrowserContext) {
|
||||||
super(delegate, browserContext);
|
super(delegate, browserContext);
|
||||||
this.accessibility = new CRAccessibility(client);
|
|
||||||
this.coverage = new CRCoverage(client);
|
this.coverage = new CRCoverage(client);
|
||||||
this._pdf = new CRPDF(client);
|
this._pdf = new CRPDF(client);
|
||||||
this._workers = new CRWorkers(client, this, this._addConsoleMessage.bind(this), error => this.emit(Events.Page.PageError, error));
|
this._workers = new CRWorkers(client, this, this._addConsoleMessage.bind(this), error => this.emit(Events.Page.PageError, error));
|
||||||
|
|
|
||||||
|
|
@ -14,78 +14,16 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
import * as accessibility from '../accessibility';
|
||||||
|
import { FFSession } from './ffConnection';
|
||||||
|
|
||||||
interface SerializedAXNode {
|
export async function getAccessibilityTree(session: FFSession) : Promise<accessibility.AXNode> {
|
||||||
role: string;
|
const { tree } = await session.send('Accessibility.getFullAXTree');
|
||||||
|
return new FFAXNode(tree);
|
||||||
name?: string;
|
|
||||||
value?: string|number;
|
|
||||||
description?: string;
|
|
||||||
|
|
||||||
keyshortcuts?: string;
|
|
||||||
roledescription?: string;
|
|
||||||
valuetext?: string;
|
|
||||||
|
|
||||||
disabled?: boolean;
|
|
||||||
expanded?: boolean;
|
|
||||||
focused?: boolean;
|
|
||||||
modal?: boolean;
|
|
||||||
multiline?: boolean;
|
|
||||||
multiselectable?: boolean;
|
|
||||||
readonly?: boolean;
|
|
||||||
required?: boolean;
|
|
||||||
selected?: boolean;
|
|
||||||
|
|
||||||
checked?: boolean|'mixed';
|
|
||||||
pressed?: boolean|'mixed';
|
|
||||||
|
|
||||||
level?: number;
|
|
||||||
|
|
||||||
autocomplete?: string;
|
|
||||||
haspopup?: string;
|
|
||||||
invalid?: string;
|
|
||||||
orientation?: string;
|
|
||||||
|
|
||||||
children?: Array<SerializedAXNode>;
|
|
||||||
}
|
}
|
||||||
export class FFAccessibility {
|
|
||||||
_session: any;
|
class FFAXNode implements accessibility.AXNode {
|
||||||
constructor(session) {
|
_children: FFAXNode[];
|
||||||
this._session = session;
|
|
||||||
}
|
|
||||||
async snapshot(options: { interestingOnly?: boolean; } | undefined = {}): Promise<SerializedAXNode> {
|
|
||||||
const { interestingOnly = true } = options;
|
|
||||||
const { tree } = await this._session.send('Accessibility.getFullAXTree');
|
|
||||||
const root = new AXNode(tree);
|
|
||||||
if (!interestingOnly)
|
|
||||||
return serializeTree(root)[0];
|
|
||||||
const interestingNodes: Set<AXNode> = new Set();
|
|
||||||
collectInterestingNodes(interestingNodes, root, false);
|
|
||||||
return serializeTree(root, interestingNodes)[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function collectInterestingNodes(collection: Set<AXNode>, node: AXNode, insideControl: boolean) {
|
|
||||||
if (node.isInteresting(insideControl))
|
|
||||||
collection.add(node);
|
|
||||||
if (node.isLeafNode())
|
|
||||||
return;
|
|
||||||
insideControl = insideControl || node.isControl();
|
|
||||||
for (const child of node._children)
|
|
||||||
collectInterestingNodes(collection, child, insideControl);
|
|
||||||
}
|
|
||||||
function serializeTree(node: AXNode, whitelistedNodes?: Set<AXNode>): Array<SerializedAXNode> {
|
|
||||||
const children: Array<SerializedAXNode> = [];
|
|
||||||
for (const child of node._children)
|
|
||||||
children.push(...serializeTree(child, whitelistedNodes));
|
|
||||||
if (whitelistedNodes && !whitelistedNodes.has(node))
|
|
||||||
return children;
|
|
||||||
const serializedNode = node.serialize();
|
|
||||||
if (children.length)
|
|
||||||
serializedNode.children = children;
|
|
||||||
return [serializedNode];
|
|
||||||
}
|
|
||||||
class AXNode {
|
|
||||||
_children: AXNode[];
|
|
||||||
private _payload: any;
|
private _payload: any;
|
||||||
private _editable: boolean;
|
private _editable: boolean;
|
||||||
private _richlyEditable: boolean;
|
private _richlyEditable: boolean;
|
||||||
|
|
@ -97,7 +35,7 @@ class AXNode {
|
||||||
|
|
||||||
constructor(payload) {
|
constructor(payload) {
|
||||||
this._payload = payload;
|
this._payload = payload;
|
||||||
this._children = (payload.children || []).map(x => new AXNode(x));
|
this._children = (payload.children || []).map(x => new FFAXNode(x));
|
||||||
this._editable = payload.editable;
|
this._editable = payload.editable;
|
||||||
this._richlyEditable = this._editable && (payload.tag !== 'textarea' && payload.tag !== 'input');
|
this._richlyEditable = this._editable && (payload.tag !== 'textarea' && payload.tag !== 'input');
|
||||||
this._focusable = payload.focusable;
|
this._focusable = payload.focusable;
|
||||||
|
|
@ -133,6 +71,14 @@ class AXNode {
|
||||||
return this._cachedHasFocusableChild;
|
return this._cachedHasFocusableChild;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
children() {
|
||||||
|
return this._children;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findElement(): Promise<FFAXNode | null> {
|
||||||
|
throw new Error('Not implimented');
|
||||||
|
}
|
||||||
|
|
||||||
isLeafNode(): boolean {
|
isLeafNode(): boolean {
|
||||||
if (!this._children.length)
|
if (!this._children.length)
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -210,11 +156,12 @@ class AXNode {
|
||||||
return this.isLeafNode() && !!this._name.trim();
|
return this.isLeafNode() && !!this._name.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
serialize(): SerializedAXNode {
|
serialize(): accessibility.SerializedAXNode {
|
||||||
const node: {[x in keyof SerializedAXNode]: any} = {
|
const node: {[x in keyof accessibility.SerializedAXNode]: any} = {
|
||||||
role: this._role
|
role: this._role,
|
||||||
|
name: this._name || ''
|
||||||
};
|
};
|
||||||
const userStringProperties: Array<keyof SerializedAXNode> = [
|
const userStringProperties: Array<keyof accessibility.SerializedAXNode> = [
|
||||||
'name',
|
'name',
|
||||||
'value',
|
'value',
|
||||||
'description',
|
'description',
|
||||||
|
|
@ -227,7 +174,7 @@ class AXNode {
|
||||||
continue;
|
continue;
|
||||||
node[userStringProperty] = this._payload[userStringProperty];
|
node[userStringProperty] = this._payload[userStringProperty];
|
||||||
}
|
}
|
||||||
const booleanProperties: Array<keyof SerializedAXNode> = [
|
const booleanProperties: Array<keyof accessibility.SerializedAXNode> = [
|
||||||
'disabled',
|
'disabled',
|
||||||
'expanded',
|
'expanded',
|
||||||
'focused',
|
'focused',
|
||||||
|
|
@ -246,7 +193,7 @@ class AXNode {
|
||||||
continue;
|
continue;
|
||||||
node[booleanProperty] = value;
|
node[booleanProperty] = value;
|
||||||
}
|
}
|
||||||
const tristateProperties: Array<keyof SerializedAXNode> = [
|
const tristateProperties: Array<keyof accessibility.SerializedAXNode> = [
|
||||||
'checked',
|
'checked',
|
||||||
'pressed',
|
'pressed',
|
||||||
];
|
];
|
||||||
|
|
@ -256,7 +203,7 @@ class AXNode {
|
||||||
const value = this._payload[tristateProperty];
|
const value = this._payload[tristateProperty];
|
||||||
node[tristateProperty] = value;
|
node[tristateProperty] = value;
|
||||||
}
|
}
|
||||||
const numericalProperties: Array<keyof SerializedAXNode> = [
|
const numericalProperties: Array<keyof accessibility.SerializedAXNode> = [
|
||||||
'level'
|
'level'
|
||||||
];
|
];
|
||||||
for (const numericalProperty of numericalProperties) {
|
for (const numericalProperty of numericalProperties) {
|
||||||
|
|
@ -264,7 +211,7 @@ class AXNode {
|
||||||
continue;
|
continue;
|
||||||
node[numericalProperty] = this._payload[numericalProperty];
|
node[numericalProperty] = this._payload[numericalProperty];
|
||||||
}
|
}
|
||||||
const tokenProperties: Array<keyof SerializedAXNode> = [
|
const tokenProperties: Array<keyof accessibility.SerializedAXNode> = [
|
||||||
'autocomplete',
|
'autocomplete',
|
||||||
'haspopup',
|
'haspopup',
|
||||||
'invalid',
|
'invalid',
|
||||||
|
|
@ -28,9 +28,10 @@ import { Protocol } from './protocol';
|
||||||
import * as input from '../input';
|
import * as input from '../input';
|
||||||
import { RawMouseImpl, RawKeyboardImpl } from './ffInput';
|
import { RawMouseImpl, RawKeyboardImpl } from './ffInput';
|
||||||
import { BrowserContext } from '../browserContext';
|
import { BrowserContext } from '../browserContext';
|
||||||
import { FFAccessibility } from './features/ffAccessibility';
|
import { getAccessibilityTree } from './ffAccessibility';
|
||||||
import * as network from '../network';
|
import * as network from '../network';
|
||||||
import * as types from '../types';
|
import * as types from '../types';
|
||||||
|
import * as accessibility from '../accessibility';
|
||||||
|
|
||||||
export class FFPage implements PageDelegate {
|
export class FFPage implements PageDelegate {
|
||||||
readonly rawMouse: RawMouseImpl;
|
readonly rawMouse: RawMouseImpl;
|
||||||
|
|
@ -64,7 +65,6 @@ export class FFPage implements PageDelegate {
|
||||||
helper.addEventListener(this._session, 'Page.bindingCalled', this._onBindingCalled.bind(this)),
|
helper.addEventListener(this._session, 'Page.bindingCalled', this._onBindingCalled.bind(this)),
|
||||||
helper.addEventListener(this._session, 'Page.fileChooserOpened', this._onFileChooserOpened.bind(this)),
|
helper.addEventListener(this._session, 'Page.fileChooserOpened', this._onFileChooserOpened.bind(this)),
|
||||||
];
|
];
|
||||||
(this._page as any).accessibility = new FFAccessibility(session);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async _initialize() {
|
async _initialize() {
|
||||||
|
|
@ -347,6 +347,10 @@ export class FFPage implements PageDelegate {
|
||||||
assert(false, 'Multiple isolated worlds are not implemented');
|
assert(false, 'Multiple isolated worlds are not implemented');
|
||||||
return handle;
|
return handle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getAccessibilityTree() : Promise<accessibility.AXNode> {
|
||||||
|
return getAccessibilityTree(this._session);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeWaitUntil(waitUntil: frames.LifecycleEvent | frames.LifecycleEvent[]): frames.LifecycleEvent[] {
|
export function normalizeWaitUntil(waitUntil: frames.LifecycleEvent | frames.LifecycleEvent[]): frames.LifecycleEvent[] {
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ import { Events } from './events';
|
||||||
import { BrowserContext } from './browserContext';
|
import { BrowserContext } from './browserContext';
|
||||||
import { ConsoleMessage, ConsoleMessageLocation } from './console';
|
import { ConsoleMessage, ConsoleMessageLocation } from './console';
|
||||||
import Injected from './injected/injected';
|
import Injected from './injected/injected';
|
||||||
|
import * as accessibility from './accessibility';
|
||||||
|
|
||||||
export interface PageDelegate {
|
export interface PageDelegate {
|
||||||
readonly rawMouse: input.RawMouse;
|
readonly rawMouse: input.RawMouse;
|
||||||
|
|
@ -66,6 +67,8 @@ export interface PageDelegate {
|
||||||
layoutViewport(): Promise<{ width: number, height: number }>;
|
layoutViewport(): Promise<{ width: number, height: number }>;
|
||||||
setInputFiles(handle: dom.ElementHandle, files: input.FilePayload[]): Promise<void>;
|
setInputFiles(handle: dom.ElementHandle, files: input.FilePayload[]): Promise<void>;
|
||||||
getBoundingBox(handle: dom.ElementHandle): Promise<types.Rect | null>;
|
getBoundingBox(handle: dom.ElementHandle): Promise<types.Rect | null>;
|
||||||
|
|
||||||
|
getAccessibilityTree(): Promise<accessibility.AXNode>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type PageState = {
|
type PageState = {
|
||||||
|
|
@ -100,6 +103,7 @@ export class Page extends EventEmitter {
|
||||||
private _pageBindings = new Map<string, Function>();
|
private _pageBindings = new Map<string, Function>();
|
||||||
readonly _screenshotter: Screenshotter;
|
readonly _screenshotter: Screenshotter;
|
||||||
readonly _frameManager: frames.FrameManager;
|
readonly _frameManager: frames.FrameManager;
|
||||||
|
readonly accessibility: accessibility.Accessibility;
|
||||||
|
|
||||||
constructor(delegate: PageDelegate, browserContext: BrowserContext) {
|
constructor(delegate: PageDelegate, browserContext: BrowserContext) {
|
||||||
super();
|
super();
|
||||||
|
|
@ -117,6 +121,7 @@ export class Page extends EventEmitter {
|
||||||
offlineMode: null,
|
offlineMode: null,
|
||||||
credentials: null
|
credentials: null
|
||||||
};
|
};
|
||||||
|
this.accessibility = new accessibility.Accessibility(delegate.getAccessibilityTree.bind(delegate));
|
||||||
this.keyboard = new input.Keyboard(delegate.rawKeyboard);
|
this.keyboard = new input.Keyboard(delegate.rawKeyboard);
|
||||||
this.mouse = new input.Mouse(delegate.rawMouse, this.keyboard);
|
this.mouse = new input.Mouse(delegate.rawMouse, this.keyboard);
|
||||||
this._timeoutSettings = new TimeoutSettings();
|
this._timeoutSettings = new TimeoutSettings();
|
||||||
|
|
|
||||||
180
src/webkit/wkAccessibility.ts
Normal file
180
src/webkit/wkAccessibility.ts
Normal file
|
|
@ -0,0 +1,180 @@
|
||||||
|
// Copyright (c) Microsoft Corporation.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
import * as accessibility from '../accessibility';
|
||||||
|
import { WKTargetSession } from './wkConnection';
|
||||||
|
import { Protocol } from './protocol';
|
||||||
|
|
||||||
|
export async function getAccessibilityTree(sesssion: WKTargetSession) {
|
||||||
|
const {axNode} = await sesssion.send('Page.accessibilitySnapshot');
|
||||||
|
return new WKAXNode(axNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
class WKAXNode implements accessibility.AXNode {
|
||||||
|
private _payload: Protocol.Page.AXNode;
|
||||||
|
private _children: WKAXNode[];
|
||||||
|
|
||||||
|
constructor(payload : Protocol.Page.AXNode) {
|
||||||
|
this._payload = payload;
|
||||||
|
|
||||||
|
this._children = [];
|
||||||
|
for (const payload of this._payload.children || [])
|
||||||
|
this._children.push(new WKAXNode(payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
children() {
|
||||||
|
return this._children;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findElement() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
isControl() : boolean {
|
||||||
|
switch (this._payload.role) {
|
||||||
|
case 'button':
|
||||||
|
case 'checkbox':
|
||||||
|
case 'ColorWell':
|
||||||
|
case 'combobox':
|
||||||
|
case 'DisclosureTriangle':
|
||||||
|
case 'listbox':
|
||||||
|
case 'menu':
|
||||||
|
case 'menubar':
|
||||||
|
case 'menuitem':
|
||||||
|
case 'menuitemcheckbox':
|
||||||
|
case 'menuitemradio':
|
||||||
|
case 'radio':
|
||||||
|
case 'scrollbar':
|
||||||
|
case 'searchbox':
|
||||||
|
case 'slider':
|
||||||
|
case 'spinbutton':
|
||||||
|
case 'switch':
|
||||||
|
case 'tab':
|
||||||
|
case 'textbox':
|
||||||
|
case 'TextField':
|
||||||
|
case 'tree':
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isInteresting(insideControl: boolean) : boolean {
|
||||||
|
const {role, focusable, name} = this._payload;
|
||||||
|
if (role === 'ScrollArea')
|
||||||
|
return false;
|
||||||
|
if (role === 'WebArea')
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (focusable || role === 'MenuListOption')
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// If it's not focusable but has a control role, then it's interesting.
|
||||||
|
if (this.isControl())
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// A non focusable child of a control is not interesting
|
||||||
|
if (insideControl)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return this.isLeafNode() && !!name;
|
||||||
|
}
|
||||||
|
|
||||||
|
isLeafNode() : boolean {
|
||||||
|
return !this._children.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
serialize(): accessibility.SerializedAXNode {
|
||||||
|
const node : accessibility.SerializedAXNode = {
|
||||||
|
role: this._payload.role,
|
||||||
|
name: this._payload.name || '',
|
||||||
|
};
|
||||||
|
type AXPropertyOfType<Type> = {
|
||||||
|
[Key in keyof Protocol.Page.AXNode]:
|
||||||
|
Protocol.Page.AXNode[Key] extends Type ? Key : never
|
||||||
|
}[keyof Protocol.Page.AXNode];
|
||||||
|
|
||||||
|
const userStringProperties: AXPropertyOfType<string>[] = [
|
||||||
|
'value',
|
||||||
|
'description',
|
||||||
|
'keyshortcuts',
|
||||||
|
'roledescription',
|
||||||
|
'valuetext'
|
||||||
|
];
|
||||||
|
for (const userStringProperty of userStringProperties) {
|
||||||
|
if (!(userStringProperty in this._payload))
|
||||||
|
continue;
|
||||||
|
node[userStringProperty] = this._payload[userStringProperty];
|
||||||
|
}
|
||||||
|
|
||||||
|
const booleanProperties: AXPropertyOfType<boolean>[] = [
|
||||||
|
'disabled',
|
||||||
|
'expanded',
|
||||||
|
'focused',
|
||||||
|
'modal',
|
||||||
|
'multiline',
|
||||||
|
'multiselectable',
|
||||||
|
'readonly',
|
||||||
|
'required',
|
||||||
|
'selected',
|
||||||
|
];
|
||||||
|
for (const booleanProperty of booleanProperties) {
|
||||||
|
// WebArea and ScorllArea treat focus differently than other nodes. They report whether their frame has focus,
|
||||||
|
// 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[booleanProperty];
|
||||||
|
if (!value)
|
||||||
|
continue;
|
||||||
|
node[booleanProperty] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tristateProperties: ('checked'|'pressed')[] = [
|
||||||
|
'checked',
|
||||||
|
'pressed',
|
||||||
|
];
|
||||||
|
for (const tristateProperty of tristateProperties) {
|
||||||
|
if (!(tristateProperty in this._payload))
|
||||||
|
continue;
|
||||||
|
const value = this._payload[tristateProperty];
|
||||||
|
node[tristateProperty] = value === 'mixed' ? 'mixed' : value === 'true' ? true : false;
|
||||||
|
}
|
||||||
|
const numericalProperties: AXPropertyOfType<number>[] = [
|
||||||
|
'level',
|
||||||
|
'valuemax',
|
||||||
|
'valuemin',
|
||||||
|
];
|
||||||
|
for (const numericalProperty of numericalProperties) {
|
||||||
|
if (!(numericalProperty in this._payload))
|
||||||
|
continue;
|
||||||
|
node[numericalProperty] = this._payload[numericalProperty];
|
||||||
|
}
|
||||||
|
const tokenProperties: AXPropertyOfType<string>[] = [
|
||||||
|
'autocomplete',
|
||||||
|
'haspopup',
|
||||||
|
'invalid',
|
||||||
|
];
|
||||||
|
for (const tokenProperty of tokenProperties) {
|
||||||
|
const value = this._payload[tokenProperty];
|
||||||
|
if (!value || value === 'false')
|
||||||
|
continue;
|
||||||
|
node[tokenProperty] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const orientationIsApplicable = new Set([
|
||||||
|
'ScrollArea',
|
||||||
|
'scrollbar',
|
||||||
|
'listbox',
|
||||||
|
'combobox',
|
||||||
|
'menu',
|
||||||
|
'tree',
|
||||||
|
'separator',
|
||||||
|
'slider',
|
||||||
|
'tablist',
|
||||||
|
'toolbar',
|
||||||
|
]);
|
||||||
|
if (this._payload.orientation && orientationIsApplicable.has(this._payload.role))
|
||||||
|
node.orientation = this._payload.orientation;
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -33,6 +33,8 @@ import * as input from '../input';
|
||||||
import * as types from '../types';
|
import * as types from '../types';
|
||||||
import * as jpeg from 'jpeg-js';
|
import * as jpeg from 'jpeg-js';
|
||||||
import { PNG } from 'pngjs';
|
import { PNG } from 'pngjs';
|
||||||
|
import * as accessibility from '../accessibility';
|
||||||
|
import { getAccessibilityTree } from './wkAccessibility';
|
||||||
|
|
||||||
const UTILITY_WORLD_NAME = '__playwright_utility_world__';
|
const UTILITY_WORLD_NAME = '__playwright_utility_world__';
|
||||||
const BINDING_CALL_MESSAGE = '__playwright_binding_call__';
|
const BINDING_CALL_MESSAGE = '__playwright_binding_call__';
|
||||||
|
|
@ -480,6 +482,10 @@ export class WKPage implements PageDelegate {
|
||||||
throw new Error('Unable to adopt element handle from a different document');
|
throw new Error('Unable to adopt element handle from a different document');
|
||||||
return to._createHandle(result.object) as dom.ElementHandle<T>;
|
return to._createHandle(result.object) as dom.ElementHandle<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getAccessibilityTree() : Promise<accessibility.AXNode> {
|
||||||
|
return getAccessibilityTree(this._session);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toRemoteObject(handle: dom.ElementHandle): Protocol.Runtime.RemoteObject {
|
function toRemoteObject(handle: dom.ElementHandle): Protocol.Runtime.RemoteObject {
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROME, WEBKIT}) {
|
||||||
const {it, fit, xit, dit} = testRunner;
|
const {it, fit, xit, dit} = testRunner;
|
||||||
const {beforeAll, beforeEach, afterAll, afterEach} = testRunner;
|
const {beforeAll, beforeEach, afterAll, afterEach} = testRunner;
|
||||||
|
|
||||||
describe.skip(WEBKIT)('Accessibility', function() {
|
describe('Accessibility', function() {
|
||||||
it('should work', async function({page}) {
|
it('should work', async function({page}) {
|
||||||
await page.setContent(`
|
await page.setContent(`
|
||||||
<head>
|
<head>
|
||||||
|
|
@ -62,7 +62,7 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROME, WEBKIT}) {
|
||||||
{role: 'combobox option', name: 'First Option', selected: true},
|
{role: 'combobox option', name: 'First Option', selected: true},
|
||||||
{role: 'combobox option', name: 'Second Option'}]
|
{role: 'combobox option', name: 'Second Option'}]
|
||||||
}]
|
}]
|
||||||
} : {
|
} : CHROME ? {
|
||||||
role: 'WebArea',
|
role: 'WebArea',
|
||||||
name: 'Accessibility Test',
|
name: 'Accessibility Test',
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -79,6 +79,23 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROME, WEBKIT}) {
|
||||||
{role: 'menuitem', name: 'First Option', selected: true},
|
{role: 'menuitem', name: 'First Option', selected: true},
|
||||||
{role: 'menuitem', name: 'Second Option'}]
|
{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' }]
|
||||||
|
}
|
||||||
|
]
|
||||||
};
|
};
|
||||||
expect(await page.accessibility.snapshot()).toEqual(golden);
|
expect(await page.accessibility.snapshot()).toEqual(golden);
|
||||||
});
|
});
|
||||||
|
|
@ -96,7 +113,7 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROME, WEBKIT}) {
|
||||||
role: 'text leaf',
|
role: 'text leaf',
|
||||||
name: 'hi'
|
name: 'hi'
|
||||||
}]
|
}]
|
||||||
} : {
|
} : CHROME ? {
|
||||||
role: 'textbox',
|
role: 'textbox',
|
||||||
name: '',
|
name: '',
|
||||||
value: 'hi',
|
value: 'hi',
|
||||||
|
|
@ -109,10 +126,16 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROME, WEBKIT}) {
|
||||||
role: 'text', name: 'hi'
|
role: 'text', name: 'hi'
|
||||||
}]
|
}]
|
||||||
}]
|
}]
|
||||||
|
} : {
|
||||||
|
role: 'textbox',
|
||||||
|
name: '',
|
||||||
|
value: 'hi',
|
||||||
|
focused: true,
|
||||||
|
multiline: true
|
||||||
};
|
};
|
||||||
expect(findFocusedNode(await page.accessibility.snapshot({interestingOnly: false}))).toEqual(golden);
|
expect(findFocusedNode(await page.accessibility.snapshot({interestingOnly: false}))).toEqual(golden);
|
||||||
});
|
});
|
||||||
it('roledescription', async({page}) => {
|
it.skip(WEBKIT)('roledescription', async({page}) => {
|
||||||
await page.setContent('<div tabIndex=-1 aria-roledescription="foo">Hi</div>');
|
await page.setContent('<div tabIndex=-1 aria-roledescription="foo">Hi</div>');
|
||||||
const snapshot = await page.accessibility.snapshot();
|
const snapshot = await page.accessibility.snapshot();
|
||||||
expect(snapshot.children[0].roledescription).toEqual('foo');
|
expect(snapshot.children[0].roledescription).toEqual('foo');
|
||||||
|
|
@ -122,7 +145,7 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROME, WEBKIT}) {
|
||||||
const snapshot = await page.accessibility.snapshot();
|
const snapshot = await page.accessibility.snapshot();
|
||||||
expect(snapshot.children[0].orientation).toEqual('vertical');
|
expect(snapshot.children[0].orientation).toEqual('vertical');
|
||||||
});
|
});
|
||||||
it.skip(FFOX)('autocomplete', async({page}) => {
|
it.skip(FFOX || WEBKIT)('autocomplete', async({page}) => {
|
||||||
await page.setContent('<input type="number" aria-autocomplete="list" />');
|
await page.setContent('<input type="number" aria-autocomplete="list" />');
|
||||||
const snapshot = await page.accessibility.snapshot();
|
const snapshot = await page.accessibility.snapshot();
|
||||||
expect(snapshot.children[0].autocomplete).toEqual('list');
|
expect(snapshot.children[0].autocomplete).toEqual('list');
|
||||||
|
|
@ -169,7 +192,8 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROME, WEBKIT}) {
|
||||||
};
|
};
|
||||||
expect(await page.accessibility.snapshot()).toEqual(golden);
|
expect(await page.accessibility.snapshot()).toEqual(golden);
|
||||||
});
|
});
|
||||||
it('rich text editable fields should have children', async function({page}) {
|
// WebKit rich text accessibility is iffy
|
||||||
|
!WEBKIT && fit('rich text editable fields should have children', async function({page}) {
|
||||||
await page.setContent(`
|
await page.setContent(`
|
||||||
<div contenteditable="true">
|
<div contenteditable="true">
|
||||||
Edit this image: <img src="fakeimage.png" alt="my fake image">
|
Edit this image: <img src="fakeimage.png" alt="my fake image">
|
||||||
|
|
@ -199,7 +223,8 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROME, WEBKIT}) {
|
||||||
const snapshot = await page.accessibility.snapshot();
|
const snapshot = await page.accessibility.snapshot();
|
||||||
expect(snapshot.children[0]).toEqual(golden);
|
expect(snapshot.children[0]).toEqual(golden);
|
||||||
});
|
});
|
||||||
it('rich text editable fields with role should have children', async function({page}) {
|
// WebKit rich text accessibility is iffy
|
||||||
|
!WEBKIT && it('rich text editable fields with role should have children', async function({page}) {
|
||||||
await page.setContent(`
|
await page.setContent(`
|
||||||
<div contenteditable="true" role='textbox'>
|
<div contenteditable="true" role='textbox'>
|
||||||
Edit this image: <img src="fakeimage.png" alt="my fake image">
|
Edit this image: <img src="fakeimage.png" alt="my fake image">
|
||||||
|
|
@ -228,7 +253,8 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROME, WEBKIT}) {
|
||||||
expect(snapshot.children[0]).toEqual(golden);
|
expect(snapshot.children[0]).toEqual(golden);
|
||||||
});
|
});
|
||||||
// Firefox does not support contenteditable="plaintext-only".
|
// Firefox does not support contenteditable="plaintext-only".
|
||||||
!FFOX && describe('plaintext contenteditable', function() {
|
// WebKit rich text accessibility is iffy
|
||||||
|
!FFOX && !WEBKIT && describe('plaintext contenteditable', function() {
|
||||||
it('plain text field with role should not have children', async function({page}) {
|
it('plain text field with role should not have children', async function({page}) {
|
||||||
await page.setContent(`
|
await page.setContent(`
|
||||||
<div contenteditable="plaintext-only" role='textbox'>Edit this image:<img src="fakeimage.png" alt="my fake image"></div>`);
|
<div contenteditable="plaintext-only" role='textbox'>Edit this image:<img src="fakeimage.png" alt="my fake image"></div>`);
|
||||||
|
|
@ -268,10 +294,15 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROME, WEBKIT}) {
|
||||||
role: 'entry',
|
role: 'entry',
|
||||||
name: 'my favorite textbox',
|
name: 'my favorite textbox',
|
||||||
value: 'this is the inner content yo'
|
value: 'this is the inner content yo'
|
||||||
} : {
|
} : CHROME ? {
|
||||||
role: 'textbox',
|
role: 'textbox',
|
||||||
name: 'my favorite textbox',
|
name: 'my favorite textbox',
|
||||||
value: 'this is the inner content '
|
value: 'this is the inner content '
|
||||||
|
} : {
|
||||||
|
role: 'TextField',
|
||||||
|
name: 'my favorite textbox',
|
||||||
|
value: 'this is the inner content ',
|
||||||
|
description: 'my favorite textbox'
|
||||||
};
|
};
|
||||||
const snapshot = await page.accessibility.snapshot();
|
const snapshot = await page.accessibility.snapshot();
|
||||||
expect(snapshot.children[0]).toEqual(golden);
|
expect(snapshot.children[0]).toEqual(golden);
|
||||||
|
|
@ -286,9 +317,14 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROME, WEBKIT}) {
|
||||||
role: 'checkbutton',
|
role: 'checkbutton',
|
||||||
name: 'my favorite checkbox',
|
name: 'my favorite checkbox',
|
||||||
checked: true
|
checked: true
|
||||||
|
} : CHROME ? {
|
||||||
|
role: 'checkbox',
|
||||||
|
name: 'my favorite checkbox',
|
||||||
|
checked: true
|
||||||
} : {
|
} : {
|
||||||
role: 'checkbox',
|
role: 'checkbox',
|
||||||
name: 'my favorite checkbox',
|
name: 'my favorite checkbox',
|
||||||
|
description: "my favorite checkbox",
|
||||||
checked: true
|
checked: true
|
||||||
};
|
};
|
||||||
const snapshot = await page.accessibility.snapshot();
|
const snapshot = await page.accessibility.snapshot();
|
||||||
|
|
@ -313,7 +349,7 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROME, WEBKIT}) {
|
||||||
expect(snapshot.children[0]).toEqual(golden);
|
expect(snapshot.children[0]).toEqual(golden);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe.skip(FFOX)('root option', function() {
|
describe.skip(FFOX || WEBKIT)('root option', function() {
|
||||||
it('should work a button', async({page}) => {
|
it('should work a button', async({page}) => {
|
||||||
await page.setContent(`<button>My Button</button>`);
|
await page.setContent(`<button>My Button</button>`);
|
||||||
|
|
||||||
|
|
@ -148,6 +148,7 @@ module.exports.describe = ({testRunner, product, playwrightPath}) => {
|
||||||
|
|
||||||
// Page-level tests that are given a browser, a context and a page.
|
// Page-level tests that are given a browser, a context and a page.
|
||||||
// Each test is launched in a new browser context.
|
// Each test is launched in a new browser context.
|
||||||
|
testRunner.loadTests(require('./accessibility.spec.js'), testOptions);
|
||||||
testRunner.loadTests(require('./click.spec.js'), testOptions);
|
testRunner.loadTests(require('./click.spec.js'), testOptions);
|
||||||
testRunner.loadTests(require('./cookies.spec.js'), testOptions);
|
testRunner.loadTests(require('./cookies.spec.js'), testOptions);
|
||||||
testRunner.loadTests(require('./dialog.spec.js'), testOptions);
|
testRunner.loadTests(require('./dialog.spec.js'), testOptions);
|
||||||
|
|
@ -177,7 +178,6 @@ module.exports.describe = ({testRunner, product, playwrightPath}) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CHROME || FFOX) {
|
if (CHROME || FFOX) {
|
||||||
testRunner.loadTests(require('./features/accessibility.spec.js'), testOptions);
|
|
||||||
testRunner.loadTests(require('./features/permissions.spec.js'), testOptions);
|
testRunner.loadTests(require('./features/permissions.spec.js'), testOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue