chore(rpc): implement input, a11y, console (#2722)

This commit is contained in:
Pavel Feldman 2020-06-25 18:01:18 -07:00 committed by GitHub
parent ab6a6c9b82
commit 71618a9e2b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 375 additions and 74 deletions

View File

@ -14,48 +14,15 @@
* 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 dom from './dom'; import * as dom from './dom';
import * as types from './types';
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 { export interface AXNode {
isInteresting(insideControl: boolean): boolean; isInteresting(insideControl: boolean): boolean;
isLeafNode(): boolean; isLeafNode(): boolean;
isControl(): boolean; isControl(): boolean;
serialize(): SerializedAXNode; serialize(): types.SerializedAXNode;
children(): Iterable<AXNode>; children(): Iterable<AXNode>;
} }
@ -68,7 +35,7 @@ export class Accessibility {
async snapshot(options: { async snapshot(options: {
interestingOnly?: boolean; interestingOnly?: boolean;
root?: dom.ElementHandle; root?: dom.ElementHandle;
} = {}): Promise<SerializedAXNode | null> { } = {}): Promise<types.SerializedAXNode | null> {
const { const {
interestingOnly = true, interestingOnly = true,
root = null, root = null,
@ -98,8 +65,8 @@ function collectInterestingNodes(collection: Set<AXNode>, node: AXNode, insideCo
collectInterestingNodes(collection, child, insideControl); collectInterestingNodes(collection, child, insideControl);
} }
function serializeTree(node: AXNode, whitelistedNodes?: Set<AXNode>): SerializedAXNode[] { function serializeTree(node: AXNode, whitelistedNodes?: Set<AXNode>): types.SerializedAXNode[] {
const children: SerializedAXNode[] = []; const children: types.SerializedAXNode[] = [];
for (const child of node.children()) for (const child of node.children())
children.push(...serializeTree(child, whitelistedNodes)); children.push(...serializeTree(child, whitelistedNodes));

View File

@ -19,6 +19,7 @@ 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'; import * as accessibility from '../accessibility';
import * as types from '../types';
export async function getAccessibilityTree(client: CRSession, needle?: dom.ElementHandle): Promise<{tree: accessibility.AXNode, needle: accessibility.AXNode | null}> { export async function getAccessibilityTree(client: CRSession, needle?: dom.ElementHandle): Promise<{tree: accessibility.AXNode, needle: accessibility.AXNode | null}> {
const {nodes} = await client.send('Accessibility.getFullAXTree'); const {nodes} = await client.send('Accessibility.getFullAXTree');
@ -198,7 +199,7 @@ class CRAXNode implements accessibility.AXNode {
return this.isLeafNode() && !!this._name; return this.isLeafNode() && !!this._name;
} }
serialize(): accessibility.SerializedAXNode { serialize(): types.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);
@ -209,12 +210,12 @@ class CRAXNode implements accessibility.AXNode {
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 accessibility.SerializedAXNode]: any} = { const node: {[x in keyof types.SerializedAXNode]: any} = {
role: this._role, role: this._role,
name: this._payload.name ? (this._payload.name.value || '') : '' name: this._payload.name ? (this._payload.name.value || '') : ''
}; };
const userStringProperties: Array<keyof accessibility.SerializedAXNode> = [ const userStringProperties: Array<keyof types.SerializedAXNode> = [
'value', 'value',
'description', 'description',
'keyshortcuts', 'keyshortcuts',
@ -227,7 +228,7 @@ class CRAXNode implements accessibility.AXNode {
node[userStringProperty] = properties.get(userStringProperty); node[userStringProperty] = properties.get(userStringProperty);
} }
const booleanProperties: Array<keyof accessibility.SerializedAXNode> = [ const booleanProperties: Array<keyof types.SerializedAXNode> = [
'disabled', 'disabled',
'expanded', 'expanded',
'focused', 'focused',
@ -249,7 +250,7 @@ class CRAXNode implements accessibility.AXNode {
node[booleanProperty] = value; node[booleanProperty] = value;
} }
const tristateProperties: Array<keyof accessibility.SerializedAXNode> = [ const tristateProperties: Array<keyof types.SerializedAXNode> = [
'checked', 'checked',
'pressed', 'pressed',
]; ];
@ -259,7 +260,7 @@ class CRAXNode implements accessibility.AXNode {
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 accessibility.SerializedAXNode> = [ const numericalProperties: Array<keyof types.SerializedAXNode> = [
'level', 'level',
'valuemax', 'valuemax',
'valuemin', 'valuemin',
@ -269,7 +270,7 @@ class CRAXNode implements accessibility.AXNode {
continue; continue;
node[numericalProperty] = properties.get(numericalProperty); node[numericalProperty] = properties.get(numericalProperty);
} }
const tokenProperties: Array<keyof accessibility.SerializedAXNode> = [ const tokenProperties: Array<keyof types.SerializedAXNode> = [
'autocomplete', 'autocomplete',
'haspopup', 'haspopup',
'invalid', 'invalid',
@ -281,7 +282,7 @@ class CRAXNode implements accessibility.AXNode {
continue; continue;
node[tokenProperty] = value; node[tokenProperty] = value;
} }
return node as accessibility.SerializedAXNode; return node as types.SerializedAXNode;
} }
static createTree(client: CRSession, payloads: Protocol.Accessibility.AXNode[]): CRAXNode { static createTree(client: CRSession, payloads: Protocol.Accessibility.AXNode[]): CRAXNode {

View File

@ -16,12 +16,7 @@
import * as js from './javascript'; import * as js from './javascript';
import * as util from 'util'; import * as util from 'util';
import { ConsoleMessageLocation } from './types';
export type ConsoleMessageLocation = {
url?: string,
lineNumber?: number,
columnNumber?: number,
};
export class ConsoleMessage { export class ConsoleMessage {
private _type: string; private _type: string;

View File

@ -19,6 +19,7 @@ import * as accessibility from '../accessibility';
import { FFSession } from './ffConnection'; import { FFSession } from './ffConnection';
import { Protocol } from './protocol'; import { Protocol } from './protocol';
import * as dom from '../dom'; import * as dom from '../dom';
import * as types from '../types';
export async function getAccessibilityTree(session: FFSession, needle?: dom.ElementHandle): Promise<{tree: accessibility.AXNode, needle: accessibility.AXNode | null}> { export async function getAccessibilityTree(session: FFSession, needle?: dom.ElementHandle): Promise<{tree: accessibility.AXNode, needle: accessibility.AXNode | null}> {
const objectId = needle ? needle._objectId : undefined; const objectId = needle ? needle._objectId : undefined;
@ -198,12 +199,12 @@ class FFAXNode implements accessibility.AXNode {
return this.isLeafNode() && !!this._name.trim(); return this.isLeafNode() && !!this._name.trim();
} }
serialize(): accessibility.SerializedAXNode { serialize(): types.SerializedAXNode {
const node: {[x in keyof accessibility.SerializedAXNode]: any} = { const node: {[x in keyof types.SerializedAXNode]: any} = {
role: FFRoleToARIARole.get(this._role) || this._role, role: FFRoleToARIARole.get(this._role) || this._role,
name: this._name || '' name: this._name || ''
}; };
const userStringProperties: Array<keyof accessibility.SerializedAXNode & keyof Protocol.Accessibility.AXTree> = [ const userStringProperties: Array<keyof types.SerializedAXNode & keyof Protocol.Accessibility.AXTree> = [
'name', 'name',
'value', 'value',
'description', 'description',
@ -216,7 +217,7 @@ class FFAXNode implements accessibility.AXNode {
continue; continue;
node[userStringProperty] = this._payload[userStringProperty]; node[userStringProperty] = this._payload[userStringProperty];
} }
const booleanProperties: Array<keyof accessibility.SerializedAXNode & keyof Protocol.Accessibility.AXTree> = [ const booleanProperties: Array<keyof types.SerializedAXNode & keyof Protocol.Accessibility.AXTree> = [
'disabled', 'disabled',
'expanded', 'expanded',
'focused', 'focused',
@ -235,7 +236,7 @@ class FFAXNode implements accessibility.AXNode {
continue; continue;
node[booleanProperty] = value; node[booleanProperty] = value;
} }
const tristateProperties: Array<keyof accessibility.SerializedAXNode & keyof Protocol.Accessibility.AXTree> = [ const tristateProperties: Array<keyof types.SerializedAXNode & keyof Protocol.Accessibility.AXTree> = [
'checked', 'checked',
'pressed', 'pressed',
]; ];
@ -245,7 +246,7 @@ class FFAXNode implements accessibility.AXNode {
const value = this._payload[tristateProperty]; const value = this._payload[tristateProperty];
node[tristateProperty] = value; node[tristateProperty] = value;
} }
const numericalProperties: Array<keyof accessibility.SerializedAXNode & keyof Protocol.Accessibility.AXTree> = [ const numericalProperties: Array<keyof types.SerializedAXNode & keyof Protocol.Accessibility.AXTree> = [
'level' 'level'
]; ];
for (const numericalProperty of numericalProperties) { for (const numericalProperty of numericalProperties) {
@ -253,7 +254,7 @@ class FFAXNode implements accessibility.AXNode {
continue; continue;
node[numericalProperty] = this._payload[numericalProperty]; node[numericalProperty] = this._payload[numericalProperty];
} }
const tokenProperties: Array<keyof accessibility.SerializedAXNode & keyof Protocol.Accessibility.AXTree> = [ const tokenProperties: Array<keyof types.SerializedAXNode & keyof Protocol.Accessibility.AXTree> = [
'autocomplete', 'autocomplete',
'haspopup', 'haspopup',
'invalid', 'invalid',

View File

@ -26,7 +26,7 @@ import { TimeoutSettings } from './timeoutSettings';
import * as types from './types'; import * as types from './types';
import { Events } from './events'; import { Events } from './events';
import { BrowserContext, BrowserContextBase } from './browserContext'; import { BrowserContext, BrowserContextBase } from './browserContext';
import { ConsoleMessage, ConsoleMessageLocation } from './console'; import { ConsoleMessage } from './console';
import * as accessibility from './accessibility'; import * as accessibility from './accessibility';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { FileChooser } from './fileChooser'; import { FileChooser } from './fileChooser';
@ -281,7 +281,7 @@ export class Page extends EventEmitter {
await PageBinding.dispatch(this, payload, context); await PageBinding.dispatch(this, payload, context);
} }
_addConsoleMessage(type: string, args: js.JSHandle[], location: ConsoleMessageLocation, text?: string) { _addConsoleMessage(type: string, args: js.JSHandle[], location: types.ConsoleMessageLocation, text?: string) {
const message = new ConsoleMessage(type, text, args, location); const message = new ConsoleMessage(type, text, args, location);
const intercepted = this._frameManager.interceptConsoleMessage(message); const intercepted = this._frameManager.interceptConsoleMessage(message);
if (intercepted || !this.listenerCount(Events.Page.Console)) if (intercepted || !this.listenerCount(Events.Page.Console))

View File

@ -64,6 +64,7 @@ export interface PageChannel extends Channel {
on(event: 'requestFinished', callback: (params: RequestChannel) => void): this; on(event: 'requestFinished', callback: (params: RequestChannel) => void): this;
on(event: 'requestFailed', callback: (params: RequestChannel) => void): this; on(event: 'requestFailed', callback: (params: RequestChannel) => void): this;
on(event: 'close', callback: () => void): this; on(event: 'close', callback: () => void): this;
on(event: 'console', callback: (params: ConsoleMessageChannel) => void): this;
setDefaultNavigationTimeoutNoReply(params: { timeout: number }): void; setDefaultNavigationTimeoutNoReply(params: { timeout: number }): void;
setDefaultTimeoutNoReply(params: { timeout: number }): Promise<void>; setDefaultTimeoutNoReply(params: { timeout: number }): Promise<void>;
@ -82,6 +83,20 @@ export interface PageChannel extends Channel {
setNetworkInterceptionEnabled(params: { enabled: boolean }): Promise<void>; setNetworkInterceptionEnabled(params: { enabled: boolean }): Promise<void>;
screenshot(params: { options?: types.ScreenshotOptions }): Promise<Buffer>; screenshot(params: { options?: types.ScreenshotOptions }): Promise<Buffer>;
close(params: { options?: { runBeforeUnload?: boolean } }): Promise<void>; close(params: { options?: { runBeforeUnload?: boolean } }): Promise<void>;
// Input
keyboardDown(params: { key: string }): Promise<void>;
keyboardUp(params: { key: string }): Promise<void>;
keyboardInsertText(params: { text: string }): Promise<void>;
keyboardType(params: { text: string, options?: { delay?: number } }): Promise<void>;
keyboardPress(params: { key: string, options?: { delay?: number } }): Promise<void>;
mouseMove(params: { x: number, y: number, options?: { steps?: number } }): Promise<void>;
mouseDown(params: { options?: { button?: types.MouseButton, clickCount?: number } }): Promise<void>;
mouseUp(params: { options?: { button?: types.MouseButton, clickCount?: number } }): Promise<void>;
mouseClick(params: { x: number, y: number, options?: { delay?: number, button?: types.MouseButton, clickCount?: number } }): Promise<void>;
// A11Y
accessibilitySnapshot(params: { options: { interestingOnly?: boolean, root?: ElementHandleChannel } }): Promise<types.SerializedAXNode | null>;
} }
export interface FrameChannel extends Channel { export interface FrameChannel extends Channel {
@ -173,3 +188,5 @@ export interface ResponseChannel extends Channel {
finished(): Promise<Error | null>; finished(): Promise<Error | null>;
} }
export interface ConsoleMessageChannel extends Channel {
}

View File

@ -0,0 +1,33 @@
/**
* Copyright 2017 Google Inc. All rights reserved.
* Modifications copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { PageChannel } from '../channels';
import { ElementHandle } from './elementHandle';
import * as types from '../../types';
export class Accessibility {
private _channel: PageChannel;
constructor(channel: PageChannel) {
this._channel = channel;
}
snapshot(options: { interestingOnly?: boolean; root?: ElementHandle } = {}): Promise<types.SerializedAXNode | null> {
const root = options.root ? options.root._elementChannel : undefined;
return this._channel.accessibilitySnapshot({ options: { interestingOnly: options.interestingOnly, root } });
}
}

64
src/rpc/client/console.ts Normal file
View File

@ -0,0 +1,64 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as util from 'util';
import { ConsoleMessageLocation } from '../../types';
import { JSHandle } from './jsHandle';
import { ConsoleMessageChannel, JSHandleChannel } from '../channels';
import { ChannelOwner } from './channelOwner';
import { Connection } from '../connection';
export class ConsoleMessage extends ChannelOwner<ConsoleMessageChannel> {
private _type: string = '';
private _text: string = '';
private _args: JSHandle[] = [];
private _location: ConsoleMessageLocation = {};
static from(request: ConsoleMessageChannel): ConsoleMessage {
return request._object;
}
constructor(connection: Connection, channel: ConsoleMessageChannel) {
super(connection, channel);
}
_initialize(params: { type: string, text: string, args: JSHandleChannel[], location: ConsoleMessageLocation }) {
this._type = params.type;
this._text = params.text;
this._args = params.args.map(JSHandle.from);
this._location = params.location;
}
type(): string {
return this._type;
}
text(): string {
return this._text;
}
args(): JSHandle[] {
return this._args;
}
location(): ConsoleMessageLocation {
return this._location;
}
[util.inspect.custom]() {
return this.text();
}
}

View File

@ -21,7 +21,7 @@ import { FuncOn, JSHandle, convertArg } from './jsHandle';
import { Connection } from '../connection'; import { Connection } from '../connection';
export class ElementHandle<T extends Node = Node> extends JSHandle<T> { export class ElementHandle<T extends Node = Node> extends JSHandle<T> {
private _elementChannel: ElementHandleChannel; readonly _elementChannel: ElementHandleChannel;
static from(handle: ElementHandleChannel): ElementHandle { static from(handle: ElementHandleChannel): ElementHandle {
return handle._object; return handle._object;

75
src/rpc/client/input.ts Normal file
View File

@ -0,0 +1,75 @@
/**
* Copyright 2017 Google Inc. All rights reserved.
* Modifications copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as types from '../../types';
import { PageChannel } from '../channels';
export class Keyboard {
private _channel: PageChannel;
constructor(channel: PageChannel) {
this._channel = channel;
}
async down(key: string) {
await this._channel.keyboardDown({ key });
}
async up(key: string) {
await this._channel.keyboardUp({ key });
}
async insertText(text: string) {
await this._channel.keyboardInsertText({ text });
}
async type(text: string, options?: { delay?: number }) {
await this._channel.keyboardType({ text, options });
}
async press(key: string, options: { delay?: number } = {}) {
await this._channel.keyboardPress({ key, options });
}
}
export class Mouse {
private _channel: PageChannel;
constructor(channel: PageChannel) {
this._channel = channel;
}
async move(x: number, y: number, options: { steps?: number } = {}) {
await this._channel.mouseMove({ x, y, options });
}
async down(options: { button?: types.MouseButton, clickCount?: number } = {}) {
await this._channel.mouseDown({ options });
}
async up(options: { button?: types.MouseButton, clickCount?: number } = {}) {
await this._channel.mouseUp({ options });
}
async click(x: number, y: number, options: { delay?: number, button?: types.MouseButton, clickCount?: number } = {}) {
await this._channel.mouseClick({ x, y, options });
}
async dblclick(x: number, y: number, options: { delay?: number, button?: types.MouseButton } = {}) {
await this.click(x, y, { ...options, clickCount: 2 });
}
}

View File

@ -27,6 +27,9 @@ import { Frame, FunctionWithSource, GotoOptions } from './frame';
import { Func1, FuncOn, SmartHandle } from './jsHandle'; import { Func1, FuncOn, SmartHandle } from './jsHandle';
import { Request, Response, RouteHandler } from './network'; import { Request, Response, RouteHandler } from './network';
import { Connection } from '../connection'; import { Connection } from '../connection';
import { Keyboard, Mouse } from './input';
import { Accessibility } from './accessibility';
import { ConsoleMessage } from './console';
export class Page extends ChannelOwner<PageChannel> { export class Page extends ChannelOwner<PageChannel> {
readonly pdf: ((options?: types.PDFOptions) => Promise<Buffer>) | undefined; readonly pdf: ((options?: types.PDFOptions) => Promise<Buffer>) | undefined;
@ -39,6 +42,10 @@ export class Page extends ChannelOwner<PageChannel> {
private _viewportSize: types.Size | null = null; private _viewportSize: types.Size | null = null;
private _routes: { url: types.URLMatch, handler: RouteHandler }[] = []; private _routes: { url: types.URLMatch, handler: RouteHandler }[] = [];
readonly accessibility: Accessibility;
readonly keyboard: Keyboard;
readonly mouse: Mouse;
static from(page: PageChannel): Page { static from(page: PageChannel): Page {
return page._object; return page._object;
} }
@ -49,6 +56,9 @@ export class Page extends ChannelOwner<PageChannel> {
constructor(connection: Connection, channel: PageChannel) { constructor(connection: Connection, channel: PageChannel) {
super(connection, channel); super(connection, channel);
this.accessibility = new Accessibility(channel);
this.keyboard = new Keyboard(channel);
this.mouse = new Mouse(channel);
} }
_initialize(payload: { browserContext: BrowserContextChannel, mainFrame: FrameChannel, viewportSize: types.Size }) { _initialize(payload: { browserContext: BrowserContextChannel, mainFrame: FrameChannel, viewportSize: types.Size }) {
@ -64,6 +74,7 @@ export class Page extends ChannelOwner<PageChannel> {
this._channel.on('response', response => this.emit(Events.Page.Response, Response.from(response))); this._channel.on('response', response => this.emit(Events.Page.Response, Response.from(response)));
this._channel.on('requestFinished', request => this.emit(Events.Page.Request, Request.from(request))); this._channel.on('requestFinished', request => this.emit(Events.Page.Request, Request.from(request)));
this._channel.on('requestFailed', request => this.emit(Events.Page.Request, Request.from(request))); this._channel.on('requestFailed', request => this.emit(Events.Page.Request, Request.from(request)));
this._channel.on('console', message => this.emit(Events.Page.Console, ConsoleMessage.from(message)));
this._channel.on('close', () => this._onClose()); this._channel.on('close', () => this._onClose());
} }
@ -230,7 +241,9 @@ export class Page extends ChannelOwner<PageChannel> {
} }
async waitForEvent(event: string, optionsOrPredicate: types.WaitForEventOptions = {}): Promise<any> { async waitForEvent(event: string, optionsOrPredicate: types.WaitForEventOptions = {}): Promise<any> {
return await this._channel.waitForEvent({ event }); const result = await this._channel.waitForEvent({ event });
if (result._object)
return result._object;
} }
async goBack(options?: types.NavigateOptions): Promise<Response | null> { async goBack(options?: types.NavigateOptions): Promise<Response | null> {

View File

@ -26,6 +26,7 @@ import { Request, Response } from './client/network';
import { Page } from './client/page'; import { Page } from './client/page';
import debug = require('debug'); import debug = require('debug');
import { Channel } from './channels'; import { Channel } from './channels';
import { ConsoleMessage } from './client/console';
export class Connection { export class Connection {
private _channels = new Map<string, Channel>(); private _channels = new Map<string, Channel>();
@ -65,6 +66,9 @@ export class Connection {
case 'elementHandle': case 'elementHandle':
result = new ElementHandle(this, channel); result = new ElementHandle(this, channel);
break; break;
case 'consoleMessage':
result = new ConsoleMessage(this, channel);
break;
default: default:
throw new Error('Missing type ' + type); throw new Error('Missing type ' + type);
} }
@ -110,6 +114,8 @@ export class Connection {
return obj.emit; return obj.emit;
if (prop === 'on') if (prop === 'on')
return obj.on; return obj.on;
if (prop === 'once')
return obj.once;
if (prop === 'addEventListener') if (prop === 'addEventListener')
return obj.addListener; return obj.addListener;
if (prop === 'removeEventListener') if (prop === 'removeEventListener')

View File

@ -0,0 +1,38 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the 'License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ConsoleMessage } from '../../console';
import { ConsoleMessageChannel } from '../channels';
import { Dispatcher, DispatcherScope } from '../dispatcher';
import { ElementHandleDispatcher } from './elementHandlerDispatcher';
export class ConsoleMessageDispatcher extends Dispatcher implements ConsoleMessageChannel {
static from(scope: DispatcherScope, message: ConsoleMessage): ConsoleMessageDispatcher {
if ((message as any)[scope.dispatcherSymbol])
return (message as any)[scope.dispatcherSymbol];
return new ConsoleMessageDispatcher(scope, message);
}
constructor(scope: DispatcherScope, message: ConsoleMessage) {
super(scope, message, 'consoleMessage');
this._initialize({
type: message.type(),
text: message.text(),
args: message.args().map(a => ElementHandleDispatcher.from(this._scope, a)),
location: message.location(),
});
}
}

View File

@ -14,15 +14,17 @@
* limitations under the License. * limitations under the License.
*/ */
import { ConsoleMessage } from '../../console';
import { Events } from '../../events'; import { Events } from '../../events';
import { Frame } from '../../frames'; import { Frame } from '../../frames';
import { Page } from '../../page'; import { Page } from '../../page';
import * as types from '../../types'; import * as types from '../../types';
import { PageChannel, ResponseChannel } from '../channels'; import { ElementHandleChannel, PageChannel, ResponseChannel } from '../channels';
import { Dispatcher, DispatcherScope } from '../dispatcher'; import { Dispatcher, DispatcherScope } from '../dispatcher';
import { BrowserContextDispatcher } from './browserContextDispatcher'; import { BrowserContextDispatcher } from './browserContextDispatcher';
import { FrameDispatcher } from './frameDispatcher'; import { FrameDispatcher } from './frameDispatcher';
import { RequestDispatcher, ResponseDispatcher } from './networkDispatchers'; import { RequestDispatcher, ResponseDispatcher } from './networkDispatchers';
import { ConsoleMessageDispatcher } from './consoleMessageDispatcher';
export class PageDispatcher extends Dispatcher implements PageChannel { export class PageDispatcher extends Dispatcher implements PageChannel {
private _page: Page; private _page: Page;
@ -57,6 +59,7 @@ export class PageDispatcher extends Dispatcher implements PageChannel {
page.on(Events.Page.Response, response => this._dispatchEvent('response', ResponseDispatcher.from(this._scope, response))); page.on(Events.Page.Response, response => this._dispatchEvent('response', ResponseDispatcher.from(this._scope, response)));
page.on(Events.Page.RequestFinished, request => this._dispatchEvent('requestFinished', ResponseDispatcher.from(this._scope, request))); page.on(Events.Page.RequestFinished, request => this._dispatchEvent('requestFinished', ResponseDispatcher.from(this._scope, request)));
page.on(Events.Page.RequestFailed, request => this._dispatchEvent('requestFailed', ResponseDispatcher.from(this._scope, request))); page.on(Events.Page.RequestFailed, request => this._dispatchEvent('requestFailed', ResponseDispatcher.from(this._scope, request)));
page.on(Events.Page.Console, message => this._dispatchEvent('console', ConsoleMessageDispatcher.from(this._scope, message)));
} }
async setDefaultNavigationTimeoutNoReply(params: { timeout: number }) { async setDefaultNavigationTimeoutNoReply(params: { timeout: number }) {
@ -83,6 +86,9 @@ export class PageDispatcher extends Dispatcher implements PageChannel {
} }
async waitForEvent(params: { event: string }): Promise<any> { async waitForEvent(params: { event: string }): Promise<any> {
const result = await this._page.waitForEvent(params.event);
if (result instanceof ConsoleMessage)
return ConsoleMessageDispatcher.from(this._scope, result);
} }
async goBack(params: { options?: types.NavigateOptions }): Promise<ResponseChannel | null> { async goBack(params: { options?: types.NavigateOptions }): Promise<ResponseChannel | null> {
@ -123,6 +129,49 @@ export class PageDispatcher extends Dispatcher implements PageChannel {
return await this._page.title(); return await this._page.title();
} }
async keyboardDown(params: { key: string }): Promise<void> {
await this._page.keyboard.down(params.key);
}
async keyboardUp(params: { key: string }): Promise<void> {
await this._page.keyboard.up(params.key);
}
async keyboardInsertText(params: { text: string }): Promise<void> {
await this._page.keyboard.insertText(params.text);
}
async keyboardType(params: { text: string, options?: { delay?: number } }): Promise<void> {
await this._page.keyboard.type(params.text, params.options);
}
async keyboardPress(params: { key: string, options?: { delay?: number } }): Promise<void> {
await this._page.keyboard.press(params.key, params.options);
}
async mouseMove(params: { x: number, y: number, options?: { steps?: number } }): Promise<void> {
await this._page.mouse.move(params.x, params.y, params.options);
}
async mouseDown(params: { options?: { button?: types.MouseButton, clickCount?: number } }): Promise<void> {
await this._page.mouse.down(params.options);
}
async mouseUp(params: { options?: { button?: types.MouseButton, clickCount?: number } }): Promise<void> {
await this._page.mouse.up(params.options);
}
async mouseClick(params: { x: number, y: number, options?: { delay?: number, button?: types.MouseButton, clickCount?: number } }): Promise<void> {
await this._page.mouse.click(params.x, params.y, params.options);
}
async accessibilitySnapshot(params: { options: { interestingOnly?: boolean, root?: ElementHandleChannel } }): Promise<types.SerializedAXNode | null> {
return await this._page.accessibility.snapshot({
interestingOnly: params.options.interestingOnly,
root: params.options.root ? params.options.root._object : undefined
});
}
_onFrameAttached(frame: Frame) { _onFrameAttached(frame: Frame) {
this._dispatchEvent('frameAttached', FrameDispatcher.from(this._scope, frame)); this._dispatchEvent('frameAttached', FrameDispatcher.from(this._scope, frame));
} }

View File

@ -1,5 +1,6 @@
/** /**
* Copyright (c) Microsoft Corporation. * Copyright 2018 Google Inc. All rights reserved.
* Modifications copyright (c) Microsoft Corporation.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -263,3 +264,44 @@ export type ConnectOptions = {
slowMo?: number, slowMo?: number,
timeout?: number, timeout?: number,
}; };
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 type ConsoleMessageLocation = {
url?: string,
lineNumber?: number,
columnNumber?: number,
};

View File

@ -17,6 +17,7 @@ import * as accessibility from '../accessibility';
import { WKSession } from './wkConnection'; import { WKSession } from './wkConnection';
import { Protocol } from './protocol'; import { Protocol } from './protocol';
import * as dom from '../dom'; import * as dom from '../dom';
import * as types from '../types';
export async function getAccessibilityTree(session: WKSession, needle?: dom.ElementHandle) { export async function getAccessibilityTree(session: WKSession, needle?: dom.ElementHandle) {
const objectId = needle ? needle._objectId : undefined; const objectId = needle ? needle._objectId : undefined;
@ -166,8 +167,8 @@ class WKAXNode implements accessibility.AXNode {
return false; return false;
} }
serialize(): accessibility.SerializedAXNode { serialize(): types.SerializedAXNode {
const node: accessibility.SerializedAXNode = { const node: types.SerializedAXNode = {
role: WKRoleToARIARole.get(this._payload.role) || this._payload.role, role: WKRoleToARIARole.get(this._payload.role) || this._payload.role,
name: this._name(), name: this._name(),
}; };
@ -184,7 +185,7 @@ class WKAXNode implements accessibility.AXNode {
if ('value' in this._payload && this._payload.role !== 'text') if ('value' in this._payload && this._payload.role !== 'text')
node.value = this._payload.value; node.value = this._payload.value;
const userStringProperties: Array<keyof accessibility.SerializedAXNode & keyof Protocol.Page.AXNode> = [ const userStringProperties: Array<keyof types.SerializedAXNode & keyof Protocol.Page.AXNode> = [
'keyshortcuts', 'keyshortcuts',
'valuetext' 'valuetext'
]; ];
@ -194,7 +195,7 @@ class WKAXNode implements accessibility.AXNode {
(node as any)[userStringProperty] = this._payload[userStringProperty]; (node as any)[userStringProperty] = this._payload[userStringProperty];
} }
const booleanProperties: Array<keyof accessibility.SerializedAXNode & keyof Protocol.Page.AXNode> = [ const booleanProperties: Array<keyof types.SerializedAXNode & keyof Protocol.Page.AXNode> = [
'disabled', 'disabled',
'expanded', 'expanded',
'focused', 'focused',
@ -226,7 +227,7 @@ class WKAXNode implements accessibility.AXNode {
const value = this._payload[tristateProperty]; const value = this._payload[tristateProperty];
node[tristateProperty] = value === 'mixed' ? 'mixed' : value === 'true' ? true : false; node[tristateProperty] = value === 'mixed' ? 'mixed' : value === 'true' ? true : false;
} }
const numericalProperties: Array<keyof accessibility.SerializedAXNode & keyof Protocol.Page.AXNode> = [ const numericalProperties: Array<keyof types.SerializedAXNode & keyof Protocol.Page.AXNode> = [
'level', 'level',
'valuemax', 'valuemax',
'valuemin', 'valuemin',
@ -236,7 +237,7 @@ class WKAXNode implements accessibility.AXNode {
continue; continue;
(node as any)[numericalProperty] = (this._payload as any)[numericalProperty]; (node as any)[numericalProperty] = (this._payload as any)[numericalProperty];
} }
const tokenProperties: Array<keyof accessibility.SerializedAXNode & keyof Protocol.Page.AXNode> = [ const tokenProperties: Array<keyof types.SerializedAXNode & keyof Protocol.Page.AXNode> = [
'autocomplete', 'autocomplete',
'haspopup', 'haspopup',
'invalid', 'invalid',

View File

@ -36,7 +36,6 @@ import { WKBrowserContext } from './wkBrowser';
import { selectors } from '../selectors'; import { selectors } from '../selectors';
import * as jpeg from 'jpeg-js'; import * as jpeg from 'jpeg-js';
import * as png from 'pngjs'; import * as png from 'pngjs';
import { ConsoleMessageLocation } from '../console';
import { JSHandle } from '../javascript'; import { JSHandle } from '../javascript';
const UTILITY_WORLD_NAME = '__playwright_utility_world__'; const UTILITY_WORLD_NAME = '__playwright_utility_world__';
@ -63,7 +62,7 @@ export class WKPage implements PageDelegate {
private _firstNonInitialNavigationCommittedPromise: Promise<void>; private _firstNonInitialNavigationCommittedPromise: Promise<void>;
private _firstNonInitialNavigationCommittedFulfill = () => {}; private _firstNonInitialNavigationCommittedFulfill = () => {};
_firstNonInitialNavigationCommittedReject = (e: Error) => {}; _firstNonInitialNavigationCommittedReject = (e: Error) => {};
private _lastConsoleMessage: { derivedType: string, text: string, handles: JSHandle[]; count: number, location: ConsoleMessageLocation; } | null = null; private _lastConsoleMessage: { derivedType: string, text: string, handles: JSHandle[]; count: number, location: types.ConsoleMessageLocation; } | null = null;
// Holds window features for the next popup being opened via window.open, // Holds window features for the next popup being opened via window.open,
// until the popup page proxy arrives. // until the popup page proxy arrives.