chore: expose adb devices and actions (#4647)
This commit is contained in:
parent
ab44d682ca
commit
aacd8e633c
128
android-types-internal.d.ts
vendored
Normal file
128
android-types-internal.d.ts
vendored
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export type AndroidElementInfo = {
|
||||
clazz: string;
|
||||
desc: string;
|
||||
res: string;
|
||||
pkg: string;
|
||||
text: string;
|
||||
bounds: { x: number, y: number, width: number, height: number };
|
||||
checkable: boolean;
|
||||
checked: boolean;
|
||||
clickable: boolean;
|
||||
enabled: boolean;
|
||||
focusable: boolean;
|
||||
focused: boolean;
|
||||
longClickable: boolean;
|
||||
scrollable: boolean;
|
||||
selected: boolean;
|
||||
};
|
||||
|
||||
export type AndroidSelector = {
|
||||
checkable?: boolean,
|
||||
checked?: boolean,
|
||||
clazz?: string | RegExp,
|
||||
clickable?: boolean,
|
||||
depth?: number,
|
||||
desc?: string | RegExp,
|
||||
enabled?: boolean,
|
||||
focusable?: boolean,
|
||||
focused?: boolean,
|
||||
hasChild?: { selector: AndroidSelector },
|
||||
hasDescendant?: { selector: AndroidSelector, maxDepth?: number },
|
||||
longClickable?: boolean,
|
||||
pkg?: string | RegExp,
|
||||
res?: string | RegExp,
|
||||
scrollable?: boolean,
|
||||
selected?: boolean,
|
||||
text?: string | RegExp,
|
||||
};
|
||||
|
||||
export interface AndroidDevice<BrowserContextOptions, BrowserContext> {
|
||||
input: AndroidInput;
|
||||
|
||||
serial(): string;
|
||||
model(): string;
|
||||
shell(command: string): Promise<string>;
|
||||
launchBrowser(options?: BrowserContextOptions & { packageName?: string }): Promise<BrowserContext>;
|
||||
close(): Promise<void>;
|
||||
|
||||
wait(selector: AndroidSelector, options?: { state?: 'gone' } & { timeout?: number }): Promise<void>;
|
||||
fill(selector: AndroidSelector, text: string, options?: { timeout?: number }): Promise<void>;
|
||||
tap(selector: AndroidSelector, options?: { duration?: number } & { timeout?: number }): Promise<void>;
|
||||
drag(selector: AndroidSelector, dest: { x: number, y: number }, options?: { speed?: number } & { timeout?: number }): Promise<void>;
|
||||
fling(selector: AndroidSelector, direction: 'down' | 'up' | 'left' | 'right', options?: { speed?: number } & { timeout?: number }): Promise<void>;
|
||||
longTap(selector: AndroidSelector, options?: { timeout?: number }): Promise<void>;
|
||||
pinchClose(selector: AndroidSelector, percent: number, options?: { speed?: number } & { timeout?: number }): Promise<void>;
|
||||
pinchOpen(selector: AndroidSelector, percent: number, options?: { speed?: number } & { timeout?: number }): Promise<void>;
|
||||
scroll(selector: AndroidSelector, direction: 'down' | 'up' | 'left' | 'right', percent: number, options?: { speed?: number } & { timeout?: number }): Promise<void>;
|
||||
swipe(selector: AndroidSelector, direction: 'down' | 'up' | 'left' | 'right', percent: number, options?: { speed?: number } & { timeout?: number }): Promise<void>;
|
||||
|
||||
info(selector: AndroidSelector): Promise<AndroidElementInfo>;
|
||||
}
|
||||
|
||||
export interface AndroidInput {
|
||||
type(text: string): Promise<void>;
|
||||
press(key: AndroidKey): Promise<void>;
|
||||
tap(point: { x: number, y: number }): Promise<void>;
|
||||
swipe(from: { x: number, y: number }, segments: { x: number, y: number }[], steps: number): Promise<void>;
|
||||
drag(from: { x: number, y: number }, to: { x: number, y: number }, steps: number): Promise<void>;
|
||||
}
|
||||
|
||||
export type AndroidKey =
|
||||
'Unknown' |
|
||||
'SoftLeft' | 'SoftRight' |
|
||||
'Home' |
|
||||
'Back' |
|
||||
'Call' | 'EndCall' |
|
||||
'0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' |
|
||||
'Star' | 'Pound' | '*' | '#' |
|
||||
'DialUp' | 'DialDown' | 'DialLeft' | 'DialRight' | 'DialCenter' |
|
||||
'VolumeUp' | 'VolumeDown' |
|
||||
'Power' |
|
||||
'Camera' |
|
||||
'Clear' |
|
||||
'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' |
|
||||
'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z' |
|
||||
'Comma' | ',' |
|
||||
'Period' | '.' |
|
||||
'AltLeft' | 'AltRight' |
|
||||
'ShiftLeft' | 'ShiftRight' |
|
||||
'Tab' | '\t' |
|
||||
'Space' | ' ' |
|
||||
'Sym' |
|
||||
'Explorer' |
|
||||
'Envelop' |
|
||||
'Enter' | '\n' |
|
||||
'Del' |
|
||||
'Grave' |
|
||||
'Minus' | '-' |
|
||||
'Equals' | '=' |
|
||||
'LeftBracket' | '(' |
|
||||
'RightBracket' | ')' |
|
||||
'Backslash' | '\\' |
|
||||
'Semicolon' | ';' |
|
||||
'Apostrophe' | '`' |
|
||||
'Slash' | '/' |
|
||||
'At' |
|
||||
'Num' |
|
||||
'HeadsetHook' |
|
||||
'Focus' |
|
||||
'Plus' | '+' |
|
||||
'Menu' |
|
||||
'Notification' |
|
||||
'Search';
|
||||
21
android-types.d.ts
vendored
Normal file
21
android-types.d.ts
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
/**
|
||||
* 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 { BrowserContext, BrowserContextOptions } from './types/types';
|
||||
import * as apiInternal from './android-types-internal';
|
||||
|
||||
export * from './android-types-internal';
|
||||
export type AndroidDevice = apiInternal.AndroidDevice<BrowserContext, BrowserContextOptions>;
|
||||
BIN
bin/android-driver-target.apk
Normal file
BIN
bin/android-driver-target.apk
Normal file
Binary file not shown.
BIN
bin/android-driver.apk
Normal file
BIN
bin/android-driver.apk
Normal file
Binary file not shown.
2
index.js
2
index.js
|
|
@ -18,10 +18,8 @@ const { setUnderTest } = require('./lib/utils/utils');
|
|||
setUnderTest(); // Note: we must call setUnderTest before initializing.
|
||||
|
||||
const { Playwright } = require('./lib/server/playwright');
|
||||
const { Electron } = require('./lib/server/electron/electron');
|
||||
const { setupInProcess } = require('./lib/inprocess');
|
||||
const path = require('path');
|
||||
|
||||
const playwright = new Playwright(__dirname, require(path.join(__dirname, 'browsers.json'))['browsers']);
|
||||
playwright.electron = new Electron();
|
||||
module.exports = setupInProcess(playwright);
|
||||
|
|
|
|||
|
|
@ -64,6 +64,12 @@ const PACKAGES = {
|
|||
browsers: [],
|
||||
files: [...PLAYWRIGHT_CORE_FILES, ...FFMPEG_FILES, 'electron-types.d.ts'],
|
||||
},
|
||||
'playwright-android': {
|
||||
version: '0.0.2', // Manually manage playwright-android version.
|
||||
description: 'A high-level API to automate Chrome for Android',
|
||||
browsers: [],
|
||||
files: [...PLAYWRIGHT_CORE_FILES, ...FFMPEG_FILES, 'android-types.d.ts', 'bin/android-driver.apk', 'bin/android-driver-target.apk'],
|
||||
},
|
||||
};
|
||||
|
||||
// 1. Parse CLI arguments
|
||||
|
|
|
|||
43
packages/playwright-android/README.md
Normal file
43
packages/playwright-android/README.md
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# playwright-android
|
||||
This package contains the [Android](https://www.android.com/) flavor of [Playwright](http://github.com/microsoft/playwright).
|
||||
|
||||
## Requirements
|
||||
|
||||
- Android device or AVD Emulator.
|
||||
- [ADB daemon](https://developer.android.com/studio/command-line/adb) running and authenticated with your device. Typically running `adb devices` is all you need to do.
|
||||
- [Chrome 87](https://play.google.com/store/apps/details?id=com.android.chrome) or newer installed on the device
|
||||
- "Enable command line on non-rooted devices" enabled in `chrome://flags`.
|
||||
|
||||
## How to demo
|
||||
|
||||
```js
|
||||
const { android } = require('playwright-android');
|
||||
|
||||
(async () => {
|
||||
const [device] = await android.devices();
|
||||
|
||||
// Android automation.
|
||||
console.log(`Model: ${device.model()}`);
|
||||
console.log(`Serial: ${device.serial()}`);
|
||||
|
||||
await device.tap({ desc: 'Home' });
|
||||
console.log(await device.info({ text: 'Chrome' }));
|
||||
await device.tap({ text: 'Chrome' });
|
||||
await device.fill({ res: 'com.android.chrome:id/url_bar' }, 'www.chromium.org');
|
||||
await device.input.press('Enter');
|
||||
await new Promise(f => setTimeout(f, 1000));
|
||||
|
||||
await device.tap({ res: 'com.android.chrome:id/tab_switcher_button' });
|
||||
await device.tap({ desc: 'More options' });
|
||||
await device.tap({ desc: 'Close all tabs' });
|
||||
|
||||
// Browser automation.
|
||||
const context = await device.launchBrowser();
|
||||
const [page] = context.pages();
|
||||
await page.goto('https://webkit.org/');
|
||||
console.log(await page.evaluate(() => window.location.href));
|
||||
await context.close();
|
||||
|
||||
await device.close();
|
||||
})();
|
||||
```
|
||||
20
packages/playwright-android/index.d.ts
vendored
Normal file
20
packages/playwright-android/index.d.ts
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
* 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 { Android } from './android-types';
|
||||
export * from './types/types';
|
||||
export * from './android-types';
|
||||
export const android: Android;
|
||||
23
packages/playwright-android/index.js
Normal file
23
packages/playwright-android/index.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const { Playwright } = require('./lib/server/playwright');
|
||||
const { setupInProcess } = require('./lib/inprocess');
|
||||
|
||||
const playwrightServer = new Playwright(__dirname, require('./browsers.json')['browsers']);
|
||||
const playwright = setupInProcess(playwrightServer);
|
||||
playwright.android = playwright._android;
|
||||
module.exports = playwright;
|
||||
23
packages/playwright-android/index.mjs
Normal file
23
packages/playwright-android/index.mjs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
/**
|
||||
* 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 playwright from './index.js';
|
||||
|
||||
export const android = playwright.android;
|
||||
export const selectors = playwright.selectors;
|
||||
export const devices = playwright.devices;
|
||||
export const errors = playwright.errors;
|
||||
export default playwright;
|
||||
17
packages/playwright-android/install.js
Normal file
17
packages/playwright-android/install.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/* NOTE: playwright-android does not install browsers by design. */
|
||||
|
|
@ -15,9 +15,9 @@
|
|||
*/
|
||||
|
||||
const { Playwright } = require('./lib/server/playwright');
|
||||
const { Electron } = require('./lib/server/electron/electron');
|
||||
const { setupInProcess } = require('./lib/inprocess');
|
||||
|
||||
const playwright = new Playwright(__dirname, require('./browsers.json')['browsers']);
|
||||
playwright.electron = new Electron();
|
||||
module.exports = setupInProcess(playwright);
|
||||
const playwrightServer = new Playwright(__dirname, require('./browsers.json')['browsers']);
|
||||
const playwright = setupInProcess(playwrightServer);
|
||||
playwright.electron = playwright._electron;
|
||||
module.exports = playwright;
|
||||
|
|
|
|||
237
src/client/android.ts
Normal file
237
src/client/android.ts
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
/**
|
||||
* 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 channels from '../protocol/channels';
|
||||
import { BrowserContext, validateBrowserContextOptions } from './browserContext';
|
||||
import { ChannelOwner } from './channelOwner';
|
||||
import * as apiInternal from '../../android-types-internal';
|
||||
import * as types from './types';
|
||||
|
||||
type Direction = 'down' | 'up' | 'left' | 'right';
|
||||
type SpeedOptions = { speed?: number };
|
||||
|
||||
export class Android extends ChannelOwner<channels.AndroidChannel, channels.AndroidInitializer> {
|
||||
static from(android: channels.AndroidChannel): Android {
|
||||
return (android as any)._object;
|
||||
}
|
||||
|
||||
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.AndroidInitializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
}
|
||||
|
||||
async devices(): Promise<AndroidDevice[]> {
|
||||
return this._wrapApiCall('android.devices', async () => {
|
||||
const { devices } = await this._channel.devices();
|
||||
return devices.map(d => AndroidDevice.from(d));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class AndroidDevice extends ChannelOwner<channels.AndroidDeviceChannel, channels.AndroidDeviceInitializer> {
|
||||
static from(androidDevice: channels.AndroidDeviceChannel): AndroidDevice {
|
||||
return (androidDevice as any)._object;
|
||||
}
|
||||
|
||||
input: Input;
|
||||
|
||||
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.AndroidDeviceInitializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this.input = new Input(this);
|
||||
}
|
||||
|
||||
serial(): string {
|
||||
return this._initializer.serial;
|
||||
}
|
||||
|
||||
model(): string {
|
||||
return this._initializer.model;
|
||||
}
|
||||
|
||||
async wait(selector: apiInternal.AndroidSelector, options?: { state?: 'gone' } & types.TimeoutOptions) {
|
||||
await this._wrapApiCall('androidDevice.wait', async () => {
|
||||
await this._channel.wait({ selector: toSelectorChannel(selector), ...options });
|
||||
});
|
||||
}
|
||||
|
||||
async fill(selector: apiInternal.AndroidSelector, text: string, options?: types.TimeoutOptions) {
|
||||
await this._wrapApiCall('androidDevice.fill', async () => {
|
||||
await this._channel.fill({ selector: toSelectorChannel(selector), text, ...options });
|
||||
});
|
||||
}
|
||||
|
||||
async tap(selector: apiInternal.AndroidSelector, options?: { duration?: number } & types.TimeoutOptions) {
|
||||
await this._wrapApiCall('androidDevice.tap', async () => {
|
||||
await this._channel.tap({ selector: toSelectorChannel(selector), ...options });
|
||||
});
|
||||
}
|
||||
|
||||
async drag(selector: apiInternal.AndroidSelector, dest: types.Point, options?: SpeedOptions & types.TimeoutOptions) {
|
||||
await this._wrapApiCall('androidDevice.drag', async () => {
|
||||
await this._channel.drag({ selector: toSelectorChannel(selector), dest, ...options });
|
||||
});
|
||||
}
|
||||
|
||||
async fling(selector: apiInternal.AndroidSelector, direction: Direction, options?: SpeedOptions & types.TimeoutOptions) {
|
||||
await this._wrapApiCall('androidDevice.fling', async () => {
|
||||
await this._channel.fling({ selector: toSelectorChannel(selector), direction, ...options });
|
||||
});
|
||||
}
|
||||
|
||||
async longTap(selector: apiInternal.AndroidSelector, options?: types.TimeoutOptions) {
|
||||
await this._wrapApiCall('androidDevice.longTap', async () => {
|
||||
await this._channel.longTap({ selector: toSelectorChannel(selector), ...options });
|
||||
});
|
||||
}
|
||||
|
||||
async pinchClose(selector: apiInternal.AndroidSelector, percent: number, options?: SpeedOptions & types.TimeoutOptions) {
|
||||
await this._wrapApiCall('androidDevice.pinchClose', async () => {
|
||||
await this._channel.pinchClose({ selector: toSelectorChannel(selector), percent, ...options });
|
||||
});
|
||||
}
|
||||
|
||||
async pinchOpen(selector: apiInternal.AndroidSelector, percent: number, options?: SpeedOptions & types.TimeoutOptions) {
|
||||
await this._wrapApiCall('androidDevice.pinchOpen', async () => {
|
||||
await this._channel.pinchOpen({ selector: toSelectorChannel(selector), percent, ...options });
|
||||
});
|
||||
}
|
||||
|
||||
async scroll(selector: apiInternal.AndroidSelector, direction: Direction, percent: number, options?: SpeedOptions & types.TimeoutOptions) {
|
||||
await this._wrapApiCall('androidDevice.scroll', async () => {
|
||||
await this._channel.scroll({ selector: toSelectorChannel(selector), direction, percent, ...options });
|
||||
});
|
||||
}
|
||||
|
||||
async swipe(selector: apiInternal.AndroidSelector, direction: Direction, percent: number, options?: SpeedOptions & types.TimeoutOptions) {
|
||||
await this._wrapApiCall('androidDevice.swipe', async () => {
|
||||
await this._channel.swipe({ selector: toSelectorChannel(selector), direction, percent, ...options });
|
||||
});
|
||||
}
|
||||
|
||||
async info(selector: apiInternal.AndroidSelector): Promise<apiInternal.AndroidElementInfo> {
|
||||
return await this._wrapApiCall('androidDevice.info', async () => {
|
||||
return (await this._channel.info({ selector: toSelectorChannel(selector) })).info;
|
||||
});
|
||||
}
|
||||
|
||||
async close() {
|
||||
return this._wrapApiCall('androidDevice.close', async () => {
|
||||
await this._channel.close();
|
||||
});
|
||||
}
|
||||
|
||||
async shell(command: string): Promise<string> {
|
||||
return this._wrapApiCall('androidDevice.shell', async () => {
|
||||
const { result } = await this._channel.shell({ command });
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
async launchBrowser(options: types.BrowserContextOptions & { packageName?: string } = {}): Promise<BrowserContext> {
|
||||
return this._wrapApiCall('androidDevice.launchBrowser', async () => {
|
||||
const contextOptions = validateBrowserContextOptions(options);
|
||||
const { context } = await this._channel.launchBrowser(contextOptions);
|
||||
return BrowserContext.from(context);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class Input implements apiInternal.AndroidInput {
|
||||
private _device: AndroidDevice;
|
||||
|
||||
constructor(device: AndroidDevice) {
|
||||
this._device = device;
|
||||
}
|
||||
|
||||
async type(text: string) {
|
||||
return this._device._wrapApiCall('androidDevice.inputType', async () => {
|
||||
await this._device._channel.inputType({ text });
|
||||
});
|
||||
}
|
||||
|
||||
async press(key: apiInternal.AndroidKey) {
|
||||
return this._device._wrapApiCall('androidDevice.inputPress', async () => {
|
||||
await this._device._channel.inputPress({ key });
|
||||
});
|
||||
}
|
||||
|
||||
async tap(point: types.Point) {
|
||||
return this._device._wrapApiCall('androidDevice.inputTap', async () => {
|
||||
await this._device._channel.inputTap({ point });
|
||||
});
|
||||
}
|
||||
|
||||
async swipe(from: types.Point, segments: types.Point[], steps: number) {
|
||||
return this._device._wrapApiCall('androidDevice.inputSwipe', async () => {
|
||||
await this._device._channel.inputSwipe({ segments, steps });
|
||||
});
|
||||
}
|
||||
|
||||
async drag(from: types.Point, to: types.Point, steps: number) {
|
||||
return this._device._wrapApiCall('androidDevice.inputDragAndDrop', async () => {
|
||||
await this._device._channel.inputDrag({ from, to, steps });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function toSelectorChannel(selector: apiInternal.AndroidSelector): channels.AndroidSelector {
|
||||
const {
|
||||
checkable,
|
||||
checked,
|
||||
clazz,
|
||||
clickable,
|
||||
depth,
|
||||
desc,
|
||||
enabled,
|
||||
focusable,
|
||||
focused,
|
||||
hasChild,
|
||||
hasDescendant,
|
||||
longClickable,
|
||||
pkg,
|
||||
res,
|
||||
scrollable,
|
||||
selected,
|
||||
text,
|
||||
} = selector;
|
||||
|
||||
const toRegex = (value: RegExp | string | undefined): string | undefined => {
|
||||
if (value === undefined)
|
||||
return undefined;
|
||||
if (value instanceof RegExp)
|
||||
return value.source;
|
||||
return '^' + value.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&').replace(/-/g, '\\x2d') + '$';
|
||||
};
|
||||
|
||||
return {
|
||||
checkable,
|
||||
checked,
|
||||
clazz: toRegex(clazz),
|
||||
pkg: toRegex(pkg),
|
||||
desc: toRegex(desc),
|
||||
res: toRegex(res),
|
||||
text: toRegex(text),
|
||||
clickable,
|
||||
depth,
|
||||
enabled,
|
||||
focusable,
|
||||
focused,
|
||||
hasChild: hasChild ? { selector: toSelectorChannel(hasChild.selector) } : undefined,
|
||||
hasDescendant: hasDescendant ? { selector: toSelectorChannel(hasDescendant.selector), maxDepth: hasDescendant.maxDepth} : undefined,
|
||||
longClickable,
|
||||
scrollable,
|
||||
selected,
|
||||
};
|
||||
}
|
||||
|
|
@ -82,7 +82,7 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
|
|||
};
|
||||
}
|
||||
|
||||
protected async _wrapApiCall<T>(apiName: string, func: () => Promise<T>, logger?: Logger): Promise<T> {
|
||||
async _wrapApiCall<T>(apiName: string, func: () => Promise<T>, logger?: Logger): Promise<T> {
|
||||
logger = logger || this._logger;
|
||||
try {
|
||||
logApiCall(logger, `=> ${apiName} started`);
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ import { FirefoxBrowser } from './firefoxBrowser';
|
|||
import { debugLogger } from '../utils/debugLogger';
|
||||
import { SelectorsOwner } from './selectors';
|
||||
import { isUnderTest } from '../utils/utils';
|
||||
import { Android, AndroidDevice } from './android';
|
||||
|
||||
class Root extends ChannelOwner<channels.Channel, {}> {
|
||||
constructor(connection: Connection) {
|
||||
|
|
@ -147,6 +148,12 @@ export class Connection {
|
|||
let result: ChannelOwner<any, any>;
|
||||
initializer = this._replaceGuidsWithChannels(initializer);
|
||||
switch (type) {
|
||||
case 'Android':
|
||||
result = new Android(parent, type, guid, initializer);
|
||||
break;
|
||||
case 'AndroidDevice':
|
||||
result = new AndroidDevice(parent, type, guid, initializer);
|
||||
break;
|
||||
case 'BindingCall':
|
||||
result = new BindingCall(parent, type, guid, initializer);
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import { Selectors, SelectorsOwner, sharedSelectors } from './selectors';
|
|||
import { Electron } from './electron';
|
||||
import { TimeoutError } from '../utils/errors';
|
||||
import { Size } from './types';
|
||||
import { Android } from './android';
|
||||
|
||||
type DeviceDescriptor = {
|
||||
userAgent: string,
|
||||
|
|
@ -33,10 +34,11 @@ type DeviceDescriptor = {
|
|||
type Devices = { [name: string]: DeviceDescriptor };
|
||||
|
||||
export class Playwright extends ChannelOwner<channels.PlaywrightChannel, channels.PlaywrightInitializer> {
|
||||
readonly _android: Android;
|
||||
readonly _electron: Electron;
|
||||
readonly chromium: BrowserType;
|
||||
readonly firefox: BrowserType;
|
||||
readonly webkit: BrowserType;
|
||||
readonly _clank: BrowserType;
|
||||
readonly devices: Devices;
|
||||
readonly selectors: Selectors;
|
||||
readonly errors: { TimeoutError: typeof TimeoutError };
|
||||
|
|
@ -46,9 +48,8 @@ export class Playwright extends ChannelOwner<channels.PlaywrightChannel, channel
|
|||
this.chromium = BrowserType.from(initializer.chromium);
|
||||
this.firefox = BrowserType.from(initializer.firefox);
|
||||
this.webkit = BrowserType.from(initializer.webkit);
|
||||
this._clank = BrowserType.from(initializer.clank);
|
||||
if (initializer.electron)
|
||||
(this as any).electron = Electron.from(initializer.electron);
|
||||
this._android = Android.from(initializer.android);
|
||||
this._electron = Electron.from(initializer.electron);
|
||||
this.devices = {};
|
||||
for (const { name, descriptor } of initializer.deviceDescriptors)
|
||||
this.devices[name] = descriptor;
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ export type Headers = { [key: string]: string };
|
|||
export type Env = { [key: string]: string | number | boolean | undefined };
|
||||
export type URLMatch = string | RegExp | ((url: URL) => boolean);
|
||||
|
||||
export type TimeoutOptions = { timeout?: number };
|
||||
export type WaitForEventOptions = Function | { predicate?: Function, timeout?: number };
|
||||
export type WaitForFunctionOptions = { timeout?: number, polling?: 'raf' | number };
|
||||
|
||||
|
|
|
|||
234
src/dispatchers/androidDispatcher.ts
Normal file
234
src/dispatchers/androidDispatcher.ts
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
/**
|
||||
* 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 { Dispatcher, DispatcherScope } from './dispatcher';
|
||||
import { Android, AndroidDevice } from '../server/android/android';
|
||||
import * as channels from '../protocol/channels';
|
||||
import { BrowserContextDispatcher } from './browserContextDispatcher';
|
||||
|
||||
export class AndroidDispatcher extends Dispatcher<Android, channels.AndroidInitializer> implements channels.AndroidChannel {
|
||||
constructor(scope: DispatcherScope, electron: Android) {
|
||||
super(scope, electron, 'Android', {}, true);
|
||||
}
|
||||
|
||||
async devices(params: channels.AndroidDevicesParams): Promise<channels.AndroidDevicesResult> {
|
||||
const devices = await this._object.devices();
|
||||
return {
|
||||
devices: devices.map(d => new AndroidDeviceDispatcher(this._scope, d))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class AndroidDeviceDispatcher extends Dispatcher<AndroidDevice, channels.AndroidDeviceInitializer> implements channels.AndroidDeviceChannel {
|
||||
constructor(scope: DispatcherScope, device: AndroidDevice) {
|
||||
super(scope, device, 'AndroidDevice', {
|
||||
model: device.model,
|
||||
serial: device.serial
|
||||
}, true);
|
||||
}
|
||||
|
||||
async wait(params: channels.AndroidDeviceWaitParams) {
|
||||
await this._object.send('wait', params);
|
||||
}
|
||||
|
||||
async fill(params: channels.AndroidDeviceFillParams) {
|
||||
await Promise.all([
|
||||
this._object.send('click', { selector: params.selector }),
|
||||
this._object.send('fill', params)
|
||||
]);
|
||||
}
|
||||
|
||||
async tap(params: channels.AndroidDeviceTapParams) {
|
||||
await this._object.send('click', params);
|
||||
}
|
||||
|
||||
async drag(params: channels.AndroidDeviceDragParams) {
|
||||
await this._object.send('drag', params);
|
||||
}
|
||||
|
||||
async fling(params: channels.AndroidDeviceFlingParams) {
|
||||
await this._object.send('fling', params);
|
||||
}
|
||||
|
||||
async longTap(params: channels.AndroidDeviceLongTapParams) {
|
||||
await this._object.send('longClick', params);
|
||||
}
|
||||
|
||||
async pinchClose(params: channels.AndroidDevicePinchCloseParams) {
|
||||
await this._object.send('pinchClose', params);
|
||||
}
|
||||
|
||||
async pinchOpen(params: channels.AndroidDevicePinchOpenParams) {
|
||||
await this._object.send('pinchOpen', params);
|
||||
}
|
||||
|
||||
async scroll(params: channels.AndroidDeviceScrollParams) {
|
||||
await this._object.send('scroll', params);
|
||||
}
|
||||
|
||||
async swipe(params: channels.AndroidDeviceSwipeParams) {
|
||||
await this._object.send('swipe', params);
|
||||
}
|
||||
|
||||
async info(params: channels.AndroidDeviceTapParams): Promise<channels.AndroidDeviceInfoResult> {
|
||||
return { info: await this._object.send('info', params) };
|
||||
}
|
||||
|
||||
async inputType(params: channels.AndroidDeviceInputTypeParams) {
|
||||
const text = params.text;
|
||||
const keyCodes: number[] = [];
|
||||
for (let i = 0; i < text.length; ++i) {
|
||||
const code = keyMap.get(text[i].toUpperCase());
|
||||
if (code === undefined)
|
||||
throw new Error('No mapping for ' + text[i] + ' found');
|
||||
keyCodes.push(code);
|
||||
}
|
||||
await Promise.all(keyCodes.map(keyCode => this._object.send('inputPress', { keyCode })));
|
||||
}
|
||||
|
||||
async inputPress(params: channels.AndroidDeviceInputPressParams) {
|
||||
if (!keyMap.has(params.key))
|
||||
throw new Error('Unknown key: ' + params.key);
|
||||
await this._object.send('inputPress', { keyCode: keyMap.get(params.key) });
|
||||
}
|
||||
|
||||
async inputTap(params: channels.AndroidDeviceInputTapParams) {
|
||||
await this._object.send('inputClick', params);
|
||||
}
|
||||
|
||||
async inputSwipe(params: channels.AndroidDeviceInputSwipeParams) {
|
||||
await this._object.send('inputSwipe', params);
|
||||
}
|
||||
|
||||
async inputDrag(params: channels.AndroidDeviceInputDragParams) {
|
||||
await this._object.send('inputDrag', params);
|
||||
}
|
||||
|
||||
async shell(params: channels.AndroidDeviceShellParams) {
|
||||
return { result: await this._object.shell(params.command) };
|
||||
}
|
||||
|
||||
async launchBrowser(params: channels.AndroidDeviceLaunchBrowserParams): Promise<channels.AndroidDeviceLaunchBrowserResult> {
|
||||
const context = await this._object.launchBrowser(params.packageName, params);
|
||||
return { context: new BrowserContextDispatcher(this._scope, context) };
|
||||
}
|
||||
|
||||
async close(params: channels.AndroidDeviceCloseParams) {
|
||||
await this._object.close();
|
||||
}
|
||||
}
|
||||
|
||||
const keyMap = new Map<string, number>([
|
||||
['Unknown', 0],
|
||||
['SoftLeft', 1],
|
||||
['SoftRight', 2],
|
||||
['Home', 3],
|
||||
['Back', 4],
|
||||
['Call', 5],
|
||||
['EndCall', 6],
|
||||
['0', 7],
|
||||
['1', 8],
|
||||
['2', 9],
|
||||
['3', 10],
|
||||
['4', 11],
|
||||
['5', 12],
|
||||
['6', 13],
|
||||
['7', 14],
|
||||
['8', 15],
|
||||
['9', 16],
|
||||
['Star', 17],
|
||||
['*', 17],
|
||||
['Pound', 18],
|
||||
['#', 18],
|
||||
['DialUp', 19],
|
||||
['DialDown', 20],
|
||||
['DialLeft', 21],
|
||||
['DialRight', 22],
|
||||
['DialCenter', 23],
|
||||
['VolumeUp', 24],
|
||||
['VolumeDown', 25],
|
||||
['Power', 26],
|
||||
['Camera', 27],
|
||||
['Clear', 28],
|
||||
['A', 29],
|
||||
['B', 30],
|
||||
['C', 31],
|
||||
['D', 32],
|
||||
['E', 33],
|
||||
['F', 34],
|
||||
['G', 35],
|
||||
['H', 36],
|
||||
['I', 37],
|
||||
['J', 38],
|
||||
['K', 39],
|
||||
['L', 40],
|
||||
['M', 41],
|
||||
['N', 42],
|
||||
['O', 43],
|
||||
['P', 44],
|
||||
['Q', 45],
|
||||
['R', 46],
|
||||
['S', 47],
|
||||
['T', 48],
|
||||
['U', 49],
|
||||
['V', 50],
|
||||
['W', 51],
|
||||
['X', 52],
|
||||
['Y', 53],
|
||||
['Z', 54],
|
||||
['Comma', 55],
|
||||
[',', 55],
|
||||
['Period', 56],
|
||||
['.', 56],
|
||||
['AltLeft', 57],
|
||||
['AltRight', 58],
|
||||
['ShiftLeft', 59],
|
||||
['ShiftRight', 60],
|
||||
['Tab', 61],
|
||||
['\t', 61],
|
||||
['Space', 62],
|
||||
[' ', 62],
|
||||
['Sym', 63],
|
||||
['Explorer', 64],
|
||||
['Envelop', 65],
|
||||
['Enter', 66],
|
||||
['Del', 67],
|
||||
['Grave', 68],
|
||||
['Minus', 69],
|
||||
['-', 69],
|
||||
['Equals', 70],
|
||||
['=', 70],
|
||||
['LeftBracket', 71],
|
||||
['(', 71],
|
||||
['RightBracket', 72],
|
||||
[')', 72],
|
||||
['Backslash', 73],
|
||||
['\\', 73],
|
||||
['Semicolon', 74],
|
||||
[';', 74],
|
||||
['Apostrophe', 75],
|
||||
['`', 75],
|
||||
['Slash', 76],
|
||||
['/', 76],
|
||||
['At', 77],
|
||||
['Num', 78],
|
||||
['HeadsetHook', 79],
|
||||
['Focus', 80],
|
||||
['Plus', 81],
|
||||
['Menu', 82],
|
||||
['Notification', 83],
|
||||
['Search', 84],
|
||||
]);
|
||||
|
|
@ -14,26 +14,25 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Playwright } from '../server/playwright';
|
||||
import * as channels from '../protocol/channels';
|
||||
import { DeviceDescriptors } from '../server/deviceDescriptors';
|
||||
import { Playwright } from '../server/playwright';
|
||||
import { AndroidDispatcher } from './androidDispatcher';
|
||||
import { BrowserTypeDispatcher } from './browserTypeDispatcher';
|
||||
import { Dispatcher, DispatcherScope } from './dispatcher';
|
||||
import { Electron } from '../server/electron/electron';
|
||||
import { ElectronDispatcher } from './electronDispatcher';
|
||||
import { DeviceDescriptors } from '../server/deviceDescriptors';
|
||||
import { SelectorsDispatcher } from './selectorsDispatcher';
|
||||
|
||||
export class PlaywrightDispatcher extends Dispatcher<Playwright, channels.PlaywrightInitializer> implements channels.PlaywrightChannel {
|
||||
constructor(scope: DispatcherScope, playwright: Playwright) {
|
||||
const electron = (playwright as any).electron as (Electron | undefined);
|
||||
const deviceDescriptors = Object.entries(DeviceDescriptors)
|
||||
.map(([name, descriptor]) => ({ name, descriptor }));
|
||||
super(scope, playwright, 'Playwright', {
|
||||
chromium: new BrowserTypeDispatcher(scope, playwright.chromium),
|
||||
clank: new BrowserTypeDispatcher(scope, playwright.clank),
|
||||
firefox: new BrowserTypeDispatcher(scope, playwright.firefox),
|
||||
webkit: new BrowserTypeDispatcher(scope, playwright.webkit),
|
||||
electron: electron ? new ElectronDispatcher(scope, electron) : undefined,
|
||||
android: new AndroidDispatcher(scope, playwright.android),
|
||||
electron: new ElectronDispatcher(scope, playwright.electron),
|
||||
deviceDescriptors,
|
||||
selectors: new SelectorsDispatcher(scope, playwright.selectors),
|
||||
}, false, 'Playwright');
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import { DispatcherConnection } from './dispatchers/dispatcher';
|
|||
import { PlaywrightDispatcher } from './dispatchers/playwrightDispatcher';
|
||||
import { installBrowsersWithProgressBar } from './install/installer';
|
||||
import { Transport } from './protocol/transport';
|
||||
import { Electron } from './server/electron/electron';
|
||||
import { Playwright } from './server/playwright';
|
||||
import { gracefullyCloseAll } from './server/processLauncher';
|
||||
import { installHarTracer } from './trace/harTracer';
|
||||
|
|
@ -62,7 +61,6 @@ export function runServer() {
|
|||
dispatcherConnection.onmessage = message => transport.send(JSON.stringify(message));
|
||||
|
||||
const playwright = new Playwright(__dirname, require('../browsers.json')['browsers']);
|
||||
(playwright as any).electron = new Electron();
|
||||
new PlaywrightDispatcher(dispatcherConnection.rootDispatcher(), playwright);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,18 @@ export type Metadata = {
|
|||
stack?: string,
|
||||
};
|
||||
|
||||
export type Point = {
|
||||
x: number,
|
||||
y: number,
|
||||
};
|
||||
|
||||
export type Rect = {
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
};
|
||||
|
||||
export type SerializedValue = {
|
||||
n?: number,
|
||||
b?: boolean,
|
||||
|
|
@ -125,10 +137,10 @@ export type SerializedError = {
|
|||
// ----------- Playwright -----------
|
||||
export type PlaywrightInitializer = {
|
||||
chromium: BrowserTypeChannel,
|
||||
clank: BrowserTypeChannel,
|
||||
firefox: BrowserTypeChannel,
|
||||
webkit: BrowserTypeChannel,
|
||||
electron?: ElectronChannel,
|
||||
android: AndroidChannel,
|
||||
electron: ElectronChannel,
|
||||
deviceDescriptors: {
|
||||
name: string,
|
||||
descriptor: {
|
||||
|
|
@ -916,12 +928,7 @@ export type PageScreenshotParams = {
|
|||
quality?: number,
|
||||
omitBackground?: boolean,
|
||||
fullPage?: boolean,
|
||||
clip?: {
|
||||
width: number,
|
||||
height: number,
|
||||
x: number,
|
||||
y: number,
|
||||
},
|
||||
clip?: Rect,
|
||||
};
|
||||
export type PageScreenshotOptions = {
|
||||
timeout?: number,
|
||||
|
|
@ -929,12 +936,7 @@ export type PageScreenshotOptions = {
|
|||
quality?: number,
|
||||
omitBackground?: boolean,
|
||||
fullPage?: boolean,
|
||||
clip?: {
|
||||
width: number,
|
||||
height: number,
|
||||
x: number,
|
||||
y: number,
|
||||
},
|
||||
clip?: Rect,
|
||||
};
|
||||
export type PageScreenshotResult = {
|
||||
binary: Binary,
|
||||
|
|
@ -1271,10 +1273,7 @@ export type FrameClickParams = {
|
|||
force?: boolean,
|
||||
noWaitAfter?: boolean,
|
||||
modifiers?: ('Alt' | 'Control' | 'Meta' | 'Shift')[],
|
||||
position?: {
|
||||
x: number,
|
||||
y: number,
|
||||
},
|
||||
position?: Point,
|
||||
delay?: number,
|
||||
button?: 'left' | 'right' | 'middle',
|
||||
clickCount?: number,
|
||||
|
|
@ -1284,10 +1283,7 @@ export type FrameClickOptions = {
|
|||
force?: boolean,
|
||||
noWaitAfter?: boolean,
|
||||
modifiers?: ('Alt' | 'Control' | 'Meta' | 'Shift')[],
|
||||
position?: {
|
||||
x: number,
|
||||
y: number,
|
||||
},
|
||||
position?: Point,
|
||||
delay?: number,
|
||||
button?: 'left' | 'right' | 'middle',
|
||||
clickCount?: number,
|
||||
|
|
@ -1304,10 +1300,7 @@ export type FrameDblclickParams = {
|
|||
force?: boolean,
|
||||
noWaitAfter?: boolean,
|
||||
modifiers?: ('Alt' | 'Control' | 'Meta' | 'Shift')[],
|
||||
position?: {
|
||||
x: number,
|
||||
y: number,
|
||||
},
|
||||
position?: Point,
|
||||
delay?: number,
|
||||
button?: 'left' | 'right' | 'middle',
|
||||
timeout?: number,
|
||||
|
|
@ -1316,10 +1309,7 @@ export type FrameDblclickOptions = {
|
|||
force?: boolean,
|
||||
noWaitAfter?: boolean,
|
||||
modifiers?: ('Alt' | 'Control' | 'Meta' | 'Shift')[],
|
||||
position?: {
|
||||
x: number,
|
||||
y: number,
|
||||
},
|
||||
position?: Point,
|
||||
delay?: number,
|
||||
button?: 'left' | 'right' | 'middle',
|
||||
timeout?: number,
|
||||
|
|
@ -1412,19 +1402,13 @@ export type FrameHoverParams = {
|
|||
selector: string,
|
||||
force?: boolean,
|
||||
modifiers?: ('Alt' | 'Control' | 'Meta' | 'Shift')[],
|
||||
position?: {
|
||||
x: number,
|
||||
y: number,
|
||||
},
|
||||
position?: Point,
|
||||
timeout?: number,
|
||||
};
|
||||
export type FrameHoverOptions = {
|
||||
force?: boolean,
|
||||
modifiers?: ('Alt' | 'Control' | 'Meta' | 'Shift')[],
|
||||
position?: {
|
||||
x: number,
|
||||
y: number,
|
||||
},
|
||||
position?: Point,
|
||||
timeout?: number,
|
||||
};
|
||||
export type FrameHoverResult = void;
|
||||
|
|
@ -1533,20 +1517,14 @@ export type FrameTapParams = {
|
|||
force?: boolean,
|
||||
noWaitAfter?: boolean,
|
||||
modifiers?: ('Alt' | 'Control' | 'Meta' | 'Shift')[],
|
||||
position?: {
|
||||
x: number,
|
||||
y: number,
|
||||
},
|
||||
position?: Point,
|
||||
timeout?: number,
|
||||
};
|
||||
export type FrameTapOptions = {
|
||||
force?: boolean,
|
||||
noWaitAfter?: boolean,
|
||||
modifiers?: ('Alt' | 'Control' | 'Meta' | 'Shift')[],
|
||||
position?: {
|
||||
x: number,
|
||||
y: number,
|
||||
},
|
||||
position?: Point,
|
||||
timeout?: number,
|
||||
};
|
||||
export type FrameTapResult = void;
|
||||
|
|
@ -1784,12 +1762,7 @@ export type ElementHandleEvalOnSelectorAllResult = {
|
|||
export type ElementHandleBoundingBoxParams = {};
|
||||
export type ElementHandleBoundingBoxOptions = {};
|
||||
export type ElementHandleBoundingBoxResult = {
|
||||
value?: {
|
||||
width: number,
|
||||
height: number,
|
||||
x: number,
|
||||
y: number,
|
||||
},
|
||||
value?: Rect,
|
||||
};
|
||||
export type ElementHandleCheckParams = {
|
||||
force?: boolean,
|
||||
|
|
@ -1806,10 +1779,7 @@ export type ElementHandleClickParams = {
|
|||
force?: boolean,
|
||||
noWaitAfter?: boolean,
|
||||
modifiers?: ('Alt' | 'Control' | 'Meta' | 'Shift')[],
|
||||
position?: {
|
||||
x: number,
|
||||
y: number,
|
||||
},
|
||||
position?: Point,
|
||||
delay?: number,
|
||||
button?: 'left' | 'right' | 'middle',
|
||||
clickCount?: number,
|
||||
|
|
@ -1819,10 +1789,7 @@ export type ElementHandleClickOptions = {
|
|||
force?: boolean,
|
||||
noWaitAfter?: boolean,
|
||||
modifiers?: ('Alt' | 'Control' | 'Meta' | 'Shift')[],
|
||||
position?: {
|
||||
x: number,
|
||||
y: number,
|
||||
},
|
||||
position?: Point,
|
||||
delay?: number,
|
||||
button?: 'left' | 'right' | 'middle',
|
||||
clickCount?: number,
|
||||
|
|
@ -1838,10 +1805,7 @@ export type ElementHandleDblclickParams = {
|
|||
force?: boolean,
|
||||
noWaitAfter?: boolean,
|
||||
modifiers?: ('Alt' | 'Control' | 'Meta' | 'Shift')[],
|
||||
position?: {
|
||||
x: number,
|
||||
y: number,
|
||||
},
|
||||
position?: Point,
|
||||
delay?: number,
|
||||
button?: 'left' | 'right' | 'middle',
|
||||
timeout?: number,
|
||||
|
|
@ -1850,10 +1814,7 @@ export type ElementHandleDblclickOptions = {
|
|||
force?: boolean,
|
||||
noWaitAfter?: boolean,
|
||||
modifiers?: ('Alt' | 'Control' | 'Meta' | 'Shift')[],
|
||||
position?: {
|
||||
x: number,
|
||||
y: number,
|
||||
},
|
||||
position?: Point,
|
||||
delay?: number,
|
||||
button?: 'left' | 'right' | 'middle',
|
||||
timeout?: number,
|
||||
|
|
@ -1892,19 +1853,13 @@ export type ElementHandleGetAttributeResult = {
|
|||
export type ElementHandleHoverParams = {
|
||||
force?: boolean,
|
||||
modifiers?: ('Alt' | 'Control' | 'Meta' | 'Shift')[],
|
||||
position?: {
|
||||
x: number,
|
||||
y: number,
|
||||
},
|
||||
position?: Point,
|
||||
timeout?: number,
|
||||
};
|
||||
export type ElementHandleHoverOptions = {
|
||||
force?: boolean,
|
||||
modifiers?: ('Alt' | 'Control' | 'Meta' | 'Shift')[],
|
||||
position?: {
|
||||
x: number,
|
||||
y: number,
|
||||
},
|
||||
position?: Point,
|
||||
timeout?: number,
|
||||
};
|
||||
export type ElementHandleHoverResult = void;
|
||||
|
|
@ -2023,20 +1978,14 @@ export type ElementHandleTapParams = {
|
|||
force?: boolean,
|
||||
noWaitAfter?: boolean,
|
||||
modifiers?: ('Alt' | 'Control' | 'Meta' | 'Shift')[],
|
||||
position?: {
|
||||
x: number,
|
||||
y: number,
|
||||
},
|
||||
position?: Point,
|
||||
timeout?: number,
|
||||
};
|
||||
export type ElementHandleTapOptions = {
|
||||
force?: boolean,
|
||||
noWaitAfter?: boolean,
|
||||
modifiers?: ('Alt' | 'Control' | 'Meta' | 'Shift')[],
|
||||
position?: {
|
||||
x: number,
|
||||
y: number,
|
||||
},
|
||||
position?: Point,
|
||||
timeout?: number,
|
||||
};
|
||||
export type ElementHandleTapResult = void;
|
||||
|
|
@ -2452,3 +2401,341 @@ export type ElectronApplicationEvaluateExpressionHandleResult = {
|
|||
export type ElectronApplicationCloseParams = {};
|
||||
export type ElectronApplicationCloseOptions = {};
|
||||
export type ElectronApplicationCloseResult = void;
|
||||
|
||||
// ----------- Android -----------
|
||||
export type AndroidInitializer = {};
|
||||
export interface AndroidChannel extends Channel {
|
||||
devices(params?: AndroidDevicesParams, metadata?: Metadata): Promise<AndroidDevicesResult>;
|
||||
}
|
||||
export type AndroidDevicesParams = {};
|
||||
export type AndroidDevicesOptions = {};
|
||||
export type AndroidDevicesResult = {
|
||||
devices: AndroidDeviceChannel[],
|
||||
};
|
||||
|
||||
export type AndroidSelector = {
|
||||
checkable?: boolean,
|
||||
checked?: boolean,
|
||||
clazz?: string,
|
||||
clickable?: boolean,
|
||||
depth?: number,
|
||||
desc?: string,
|
||||
enabled?: boolean,
|
||||
focusable?: boolean,
|
||||
focused?: boolean,
|
||||
hasChild?: {
|
||||
selector: AndroidSelector,
|
||||
},
|
||||
hasDescendant?: {
|
||||
selector: AndroidSelector,
|
||||
maxDepth?: number,
|
||||
},
|
||||
longClickable?: boolean,
|
||||
pkg?: string,
|
||||
res?: string,
|
||||
scrollable?: boolean,
|
||||
selected?: boolean,
|
||||
text?: string,
|
||||
};
|
||||
|
||||
export type AndroidElementInfo = {
|
||||
clazz: string,
|
||||
desc: string,
|
||||
res: string,
|
||||
pkg: string,
|
||||
text: string,
|
||||
bounds: Rect,
|
||||
checkable: boolean,
|
||||
checked: boolean,
|
||||
clickable: boolean,
|
||||
enabled: boolean,
|
||||
focusable: boolean,
|
||||
focused: boolean,
|
||||
longClickable: boolean,
|
||||
scrollable: boolean,
|
||||
selected: boolean,
|
||||
};
|
||||
|
||||
// ----------- AndroidDevice -----------
|
||||
export type AndroidDeviceInitializer = {
|
||||
model: string,
|
||||
serial: string,
|
||||
};
|
||||
export interface AndroidDeviceChannel extends Channel {
|
||||
wait(params: AndroidDeviceWaitParams, metadata?: Metadata): Promise<AndroidDeviceWaitResult>;
|
||||
fill(params: AndroidDeviceFillParams, metadata?: Metadata): Promise<AndroidDeviceFillResult>;
|
||||
tap(params: AndroidDeviceTapParams, metadata?: Metadata): Promise<AndroidDeviceTapResult>;
|
||||
drag(params: AndroidDeviceDragParams, metadata?: Metadata): Promise<AndroidDeviceDragResult>;
|
||||
fling(params: AndroidDeviceFlingParams, metadata?: Metadata): Promise<AndroidDeviceFlingResult>;
|
||||
longTap(params: AndroidDeviceLongTapParams, metadata?: Metadata): Promise<AndroidDeviceLongTapResult>;
|
||||
pinchClose(params: AndroidDevicePinchCloseParams, metadata?: Metadata): Promise<AndroidDevicePinchCloseResult>;
|
||||
pinchOpen(params: AndroidDevicePinchOpenParams, metadata?: Metadata): Promise<AndroidDevicePinchOpenResult>;
|
||||
scroll(params: AndroidDeviceScrollParams, metadata?: Metadata): Promise<AndroidDeviceScrollResult>;
|
||||
swipe(params: AndroidDeviceSwipeParams, metadata?: Metadata): Promise<AndroidDeviceSwipeResult>;
|
||||
info(params: AndroidDeviceInfoParams, metadata?: Metadata): Promise<AndroidDeviceInfoResult>;
|
||||
inputType(params: AndroidDeviceInputTypeParams, metadata?: Metadata): Promise<AndroidDeviceInputTypeResult>;
|
||||
inputPress(params: AndroidDeviceInputPressParams, metadata?: Metadata): Promise<AndroidDeviceInputPressResult>;
|
||||
inputTap(params: AndroidDeviceInputTapParams, metadata?: Metadata): Promise<AndroidDeviceInputTapResult>;
|
||||
inputSwipe(params: AndroidDeviceInputSwipeParams, metadata?: Metadata): Promise<AndroidDeviceInputSwipeResult>;
|
||||
inputDrag(params: AndroidDeviceInputDragParams, metadata?: Metadata): Promise<AndroidDeviceInputDragResult>;
|
||||
launchBrowser(params: AndroidDeviceLaunchBrowserParams, metadata?: Metadata): Promise<AndroidDeviceLaunchBrowserResult>;
|
||||
shell(params: AndroidDeviceShellParams, metadata?: Metadata): Promise<AndroidDeviceShellResult>;
|
||||
close(params?: AndroidDeviceCloseParams, metadata?: Metadata): Promise<AndroidDeviceCloseResult>;
|
||||
}
|
||||
export type AndroidDeviceWaitParams = {
|
||||
selector: AndroidSelector,
|
||||
state?: 'gone',
|
||||
timeout?: number,
|
||||
};
|
||||
export type AndroidDeviceWaitOptions = {
|
||||
state?: 'gone',
|
||||
timeout?: number,
|
||||
};
|
||||
export type AndroidDeviceWaitResult = void;
|
||||
export type AndroidDeviceFillParams = {
|
||||
selector: AndroidSelector,
|
||||
text: string,
|
||||
timeout?: number,
|
||||
};
|
||||
export type AndroidDeviceFillOptions = {
|
||||
timeout?: number,
|
||||
};
|
||||
export type AndroidDeviceFillResult = void;
|
||||
export type AndroidDeviceTapParams = {
|
||||
selector: AndroidSelector,
|
||||
duration?: number,
|
||||
timeout?: number,
|
||||
};
|
||||
export type AndroidDeviceTapOptions = {
|
||||
duration?: number,
|
||||
timeout?: number,
|
||||
};
|
||||
export type AndroidDeviceTapResult = void;
|
||||
export type AndroidDeviceDragParams = {
|
||||
selector: AndroidSelector,
|
||||
dest: Point,
|
||||
speed?: number,
|
||||
timeout?: number,
|
||||
};
|
||||
export type AndroidDeviceDragOptions = {
|
||||
speed?: number,
|
||||
timeout?: number,
|
||||
};
|
||||
export type AndroidDeviceDragResult = void;
|
||||
export type AndroidDeviceFlingParams = {
|
||||
selector: AndroidSelector,
|
||||
direction: 'up' | 'down' | 'left' | 'right',
|
||||
speed?: number,
|
||||
timeout?: number,
|
||||
};
|
||||
export type AndroidDeviceFlingOptions = {
|
||||
speed?: number,
|
||||
timeout?: number,
|
||||
};
|
||||
export type AndroidDeviceFlingResult = void;
|
||||
export type AndroidDeviceLongTapParams = {
|
||||
selector: AndroidSelector,
|
||||
timeout?: number,
|
||||
};
|
||||
export type AndroidDeviceLongTapOptions = {
|
||||
timeout?: number,
|
||||
};
|
||||
export type AndroidDeviceLongTapResult = void;
|
||||
export type AndroidDevicePinchCloseParams = {
|
||||
selector: AndroidSelector,
|
||||
percent: number,
|
||||
speed?: number,
|
||||
timeout?: number,
|
||||
};
|
||||
export type AndroidDevicePinchCloseOptions = {
|
||||
speed?: number,
|
||||
timeout?: number,
|
||||
};
|
||||
export type AndroidDevicePinchCloseResult = void;
|
||||
export type AndroidDevicePinchOpenParams = {
|
||||
selector: AndroidSelector,
|
||||
percent: number,
|
||||
speed?: number,
|
||||
timeout?: number,
|
||||
};
|
||||
export type AndroidDevicePinchOpenOptions = {
|
||||
speed?: number,
|
||||
timeout?: number,
|
||||
};
|
||||
export type AndroidDevicePinchOpenResult = void;
|
||||
export type AndroidDeviceScrollParams = {
|
||||
selector: AndroidSelector,
|
||||
direction: 'up' | 'down' | 'left' | 'right',
|
||||
percent: number,
|
||||
speed?: number,
|
||||
timeout?: number,
|
||||
};
|
||||
export type AndroidDeviceScrollOptions = {
|
||||
speed?: number,
|
||||
timeout?: number,
|
||||
};
|
||||
export type AndroidDeviceScrollResult = void;
|
||||
export type AndroidDeviceSwipeParams = {
|
||||
selector: AndroidSelector,
|
||||
direction: 'up' | 'down' | 'left' | 'right',
|
||||
percent: number,
|
||||
speed?: number,
|
||||
timeout?: number,
|
||||
};
|
||||
export type AndroidDeviceSwipeOptions = {
|
||||
speed?: number,
|
||||
timeout?: number,
|
||||
};
|
||||
export type AndroidDeviceSwipeResult = void;
|
||||
export type AndroidDeviceInfoParams = {
|
||||
selector: AndroidSelector,
|
||||
};
|
||||
export type AndroidDeviceInfoOptions = {
|
||||
|
||||
};
|
||||
export type AndroidDeviceInfoResult = {
|
||||
info: AndroidElementInfo,
|
||||
};
|
||||
export type AndroidDeviceInputTypeParams = {
|
||||
text: string,
|
||||
};
|
||||
export type AndroidDeviceInputTypeOptions = {
|
||||
|
||||
};
|
||||
export type AndroidDeviceInputTypeResult = void;
|
||||
export type AndroidDeviceInputPressParams = {
|
||||
key: string,
|
||||
};
|
||||
export type AndroidDeviceInputPressOptions = {
|
||||
|
||||
};
|
||||
export type AndroidDeviceInputPressResult = void;
|
||||
export type AndroidDeviceInputTapParams = {
|
||||
point: Point,
|
||||
};
|
||||
export type AndroidDeviceInputTapOptions = {
|
||||
|
||||
};
|
||||
export type AndroidDeviceInputTapResult = void;
|
||||
export type AndroidDeviceInputSwipeParams = {
|
||||
segments: Point[],
|
||||
steps: number,
|
||||
};
|
||||
export type AndroidDeviceInputSwipeOptions = {
|
||||
|
||||
};
|
||||
export type AndroidDeviceInputSwipeResult = void;
|
||||
export type AndroidDeviceInputDragParams = {
|
||||
from: Point,
|
||||
to: Point,
|
||||
steps: number,
|
||||
};
|
||||
export type AndroidDeviceInputDragOptions = {
|
||||
|
||||
};
|
||||
export type AndroidDeviceInputDragResult = void;
|
||||
export type AndroidDeviceLaunchBrowserParams = {
|
||||
packageName?: string,
|
||||
ignoreHTTPSErrors?: boolean,
|
||||
javaScriptEnabled?: boolean,
|
||||
bypassCSP?: boolean,
|
||||
userAgent?: string,
|
||||
locale?: string,
|
||||
timezoneId?: string,
|
||||
geolocation?: {
|
||||
longitude: number,
|
||||
latitude: number,
|
||||
accuracy?: number,
|
||||
},
|
||||
permissions?: string[],
|
||||
extraHTTPHeaders?: NameValue[],
|
||||
offline?: boolean,
|
||||
httpCredentials?: {
|
||||
username: string,
|
||||
password: string,
|
||||
},
|
||||
deviceScaleFactor?: number,
|
||||
isMobile?: boolean,
|
||||
hasTouch?: boolean,
|
||||
colorScheme?: 'dark' | 'light' | 'no-preference',
|
||||
acceptDownloads?: boolean,
|
||||
_traceResourcesPath?: string,
|
||||
_tracePath?: string,
|
||||
recordVideo?: {
|
||||
dir: string,
|
||||
size?: {
|
||||
width: number,
|
||||
height: number,
|
||||
},
|
||||
},
|
||||
recordHar?: {
|
||||
omitContent?: boolean,
|
||||
path: string,
|
||||
},
|
||||
proxy?: {
|
||||
server: string,
|
||||
bypass?: string,
|
||||
username?: string,
|
||||
password?: string,
|
||||
},
|
||||
};
|
||||
export type AndroidDeviceLaunchBrowserOptions = {
|
||||
packageName?: string,
|
||||
ignoreHTTPSErrors?: boolean,
|
||||
javaScriptEnabled?: boolean,
|
||||
bypassCSP?: boolean,
|
||||
userAgent?: string,
|
||||
locale?: string,
|
||||
timezoneId?: string,
|
||||
geolocation?: {
|
||||
longitude: number,
|
||||
latitude: number,
|
||||
accuracy?: number,
|
||||
},
|
||||
permissions?: string[],
|
||||
extraHTTPHeaders?: NameValue[],
|
||||
offline?: boolean,
|
||||
httpCredentials?: {
|
||||
username: string,
|
||||
password: string,
|
||||
},
|
||||
deviceScaleFactor?: number,
|
||||
isMobile?: boolean,
|
||||
hasTouch?: boolean,
|
||||
colorScheme?: 'dark' | 'light' | 'no-preference',
|
||||
acceptDownloads?: boolean,
|
||||
_traceResourcesPath?: string,
|
||||
_tracePath?: string,
|
||||
recordVideo?: {
|
||||
dir: string,
|
||||
size?: {
|
||||
width: number,
|
||||
height: number,
|
||||
},
|
||||
},
|
||||
recordHar?: {
|
||||
omitContent?: boolean,
|
||||
path: string,
|
||||
},
|
||||
proxy?: {
|
||||
server: string,
|
||||
bypass?: string,
|
||||
username?: string,
|
||||
password?: string,
|
||||
},
|
||||
};
|
||||
export type AndroidDeviceLaunchBrowserResult = {
|
||||
context: BrowserContextChannel,
|
||||
};
|
||||
export type AndroidDeviceShellParams = {
|
||||
command: string,
|
||||
};
|
||||
export type AndroidDeviceShellOptions = {
|
||||
|
||||
};
|
||||
export type AndroidDeviceShellResult = {
|
||||
result: string,
|
||||
};
|
||||
export type AndroidDeviceCloseParams = {};
|
||||
export type AndroidDeviceCloseOptions = {};
|
||||
export type AndroidDeviceCloseResult = void;
|
||||
|
|
|
|||
|
|
@ -20,6 +20,22 @@ Metadata:
|
|||
stack: string?
|
||||
|
||||
|
||||
Point:
|
||||
type: object
|
||||
properties:
|
||||
x: number
|
||||
y: number
|
||||
|
||||
|
||||
Rect:
|
||||
type: object
|
||||
properties:
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
|
||||
|
||||
SerializedValue:
|
||||
type: object
|
||||
# Exactly one of the properties must be present.
|
||||
|
|
@ -183,10 +199,10 @@ Playwright:
|
|||
|
||||
initializer:
|
||||
chromium: BrowserType
|
||||
clank: BrowserType
|
||||
firefox: BrowserType
|
||||
webkit: BrowserType
|
||||
electron: Electron?
|
||||
android: Android
|
||||
electron: Electron
|
||||
deviceDescriptors:
|
||||
type: array
|
||||
items:
|
||||
|
|
@ -727,13 +743,7 @@ Page:
|
|||
quality: number?
|
||||
omitBackground: boolean?
|
||||
fullPage: boolean?
|
||||
clip:
|
||||
type: object?
|
||||
properties:
|
||||
width: number
|
||||
height: number
|
||||
x: number
|
||||
y: number
|
||||
clip: Rect?
|
||||
returns:
|
||||
binary: binary
|
||||
|
||||
|
|
@ -1062,11 +1072,7 @@ Frame:
|
|||
- Control
|
||||
- Meta
|
||||
- Shift
|
||||
position:
|
||||
type: object?
|
||||
properties:
|
||||
x: number
|
||||
y: number
|
||||
position: Point?
|
||||
delay: number?
|
||||
button:
|
||||
type: enum?
|
||||
|
|
@ -1095,11 +1101,7 @@ Frame:
|
|||
- Control
|
||||
- Meta
|
||||
- Shift
|
||||
position:
|
||||
type: object?
|
||||
properties:
|
||||
x: number
|
||||
y: number
|
||||
position: Point?
|
||||
delay: number?
|
||||
button:
|
||||
type: enum?
|
||||
|
|
@ -1193,11 +1195,7 @@ Frame:
|
|||
- Control
|
||||
- Meta
|
||||
- Shift
|
||||
position:
|
||||
type: object?
|
||||
properties:
|
||||
x: number
|
||||
y: number
|
||||
position: Point?
|
||||
timeout: number?
|
||||
|
||||
innerHTML:
|
||||
|
|
@ -1296,11 +1294,7 @@ Frame:
|
|||
- Control
|
||||
- Meta
|
||||
- Shift
|
||||
position:
|
||||
type: object?
|
||||
properties:
|
||||
x: number
|
||||
y: number
|
||||
position: Point?
|
||||
timeout: number?
|
||||
|
||||
textContent:
|
||||
|
|
@ -1501,13 +1495,7 @@ ElementHandle:
|
|||
|
||||
boundingBox:
|
||||
returns:
|
||||
value:
|
||||
type: object?
|
||||
properties:
|
||||
width: number
|
||||
height: number
|
||||
x: number
|
||||
y: number
|
||||
value: Rect?
|
||||
|
||||
check:
|
||||
parameters:
|
||||
|
|
@ -1528,11 +1516,7 @@ ElementHandle:
|
|||
- Control
|
||||
- Meta
|
||||
- Shift
|
||||
position:
|
||||
type: object?
|
||||
properties:
|
||||
x: number
|
||||
y: number
|
||||
position: Point?
|
||||
delay: number?
|
||||
button:
|
||||
type: enum?
|
||||
|
|
@ -1560,11 +1544,7 @@ ElementHandle:
|
|||
- Control
|
||||
- Meta
|
||||
- Shift
|
||||
position:
|
||||
type: object?
|
||||
properties:
|
||||
x: number
|
||||
y: number
|
||||
position: Point?
|
||||
delay: number?
|
||||
button:
|
||||
type: enum?
|
||||
|
|
@ -1605,11 +1585,7 @@ ElementHandle:
|
|||
- Control
|
||||
- Meta
|
||||
- Shift
|
||||
position:
|
||||
type: object?
|
||||
properties:
|
||||
x: number
|
||||
y: number
|
||||
position: Point?
|
||||
timeout: number?
|
||||
|
||||
innerHTML:
|
||||
|
|
@ -1712,11 +1688,7 @@ ElementHandle:
|
|||
- Control
|
||||
- Meta
|
||||
- Shift
|
||||
position:
|
||||
type: object?
|
||||
properties:
|
||||
x: number
|
||||
y: number
|
||||
position: Point?
|
||||
timeout: number?
|
||||
|
||||
textContent:
|
||||
|
|
@ -2095,3 +2067,263 @@ ElectronApplication:
|
|||
browserWindow: JSHandle
|
||||
|
||||
|
||||
|
||||
Android:
|
||||
type: interface
|
||||
|
||||
commands:
|
||||
|
||||
devices:
|
||||
returns:
|
||||
devices:
|
||||
type: array
|
||||
items: AndroidDevice
|
||||
|
||||
|
||||
AndroidSelector:
|
||||
type: object
|
||||
properties:
|
||||
checkable: boolean?
|
||||
checked: boolean?
|
||||
clazz: string?
|
||||
clickable: boolean?
|
||||
depth: number?
|
||||
desc: string?
|
||||
enabled: boolean?
|
||||
focusable: boolean?
|
||||
focused: boolean?
|
||||
hasChild:
|
||||
type: object?
|
||||
properties:
|
||||
selector: AndroidSelector
|
||||
hasDescendant:
|
||||
type: object?
|
||||
properties:
|
||||
selector: AndroidSelector
|
||||
maxDepth: number?
|
||||
longClickable: boolean?
|
||||
pkg: string?
|
||||
res: string?
|
||||
scrollable: boolean?
|
||||
selected: boolean?
|
||||
text: string?
|
||||
|
||||
|
||||
AndroidElementInfo:
|
||||
type: object
|
||||
properties:
|
||||
clazz: string
|
||||
desc: string
|
||||
res: string
|
||||
pkg: string
|
||||
text: string
|
||||
bounds: Rect
|
||||
checkable: boolean
|
||||
checked: boolean
|
||||
clickable: boolean
|
||||
enabled: boolean
|
||||
focusable: boolean
|
||||
focused: boolean
|
||||
longClickable: boolean
|
||||
scrollable: boolean
|
||||
selected: boolean
|
||||
|
||||
|
||||
AndroidDevice:
|
||||
type: interface
|
||||
|
||||
initializer:
|
||||
model: string
|
||||
serial: string
|
||||
|
||||
commands:
|
||||
wait:
|
||||
parameters:
|
||||
selector: AndroidSelector
|
||||
state:
|
||||
type: enum?
|
||||
literals:
|
||||
- gone
|
||||
timeout: number?
|
||||
|
||||
fill:
|
||||
parameters:
|
||||
selector: AndroidSelector
|
||||
text: string
|
||||
timeout: number?
|
||||
|
||||
tap:
|
||||
parameters:
|
||||
selector: AndroidSelector
|
||||
duration: number?
|
||||
timeout: number?
|
||||
|
||||
drag:
|
||||
parameters:
|
||||
selector: AndroidSelector
|
||||
dest: Point
|
||||
speed: number?
|
||||
timeout: number?
|
||||
|
||||
fling:
|
||||
parameters:
|
||||
selector: AndroidSelector
|
||||
direction:
|
||||
type: enum
|
||||
literals:
|
||||
- up
|
||||
- down
|
||||
- left
|
||||
- right
|
||||
speed: number?
|
||||
timeout: number?
|
||||
|
||||
longTap:
|
||||
parameters:
|
||||
selector: AndroidSelector
|
||||
timeout: number?
|
||||
|
||||
pinchClose:
|
||||
parameters:
|
||||
selector: AndroidSelector
|
||||
percent: number
|
||||
speed: number?
|
||||
timeout: number?
|
||||
|
||||
pinchOpen:
|
||||
parameters:
|
||||
selector: AndroidSelector
|
||||
percent: number
|
||||
speed: number?
|
||||
timeout: number?
|
||||
|
||||
scroll:
|
||||
parameters:
|
||||
selector: AndroidSelector
|
||||
direction:
|
||||
type: enum
|
||||
literals:
|
||||
- up
|
||||
- down
|
||||
- left
|
||||
- right
|
||||
percent: number
|
||||
speed: number?
|
||||
timeout: number?
|
||||
|
||||
swipe:
|
||||
parameters:
|
||||
selector: AndroidSelector
|
||||
direction:
|
||||
type: enum
|
||||
literals:
|
||||
- up
|
||||
- down
|
||||
- left
|
||||
- right
|
||||
percent: number
|
||||
speed: number?
|
||||
timeout: number?
|
||||
|
||||
info:
|
||||
parameters:
|
||||
selector: AndroidSelector
|
||||
returns:
|
||||
info: AndroidElementInfo
|
||||
|
||||
inputType:
|
||||
parameters:
|
||||
text: string
|
||||
|
||||
inputPress:
|
||||
parameters:
|
||||
key: string
|
||||
|
||||
inputTap:
|
||||
parameters:
|
||||
point: Point
|
||||
|
||||
inputSwipe:
|
||||
parameters:
|
||||
segments:
|
||||
type: array
|
||||
items: Point
|
||||
steps: number
|
||||
|
||||
inputDrag:
|
||||
parameters:
|
||||
from: Point
|
||||
to: Point
|
||||
steps: number
|
||||
|
||||
launchBrowser:
|
||||
parameters:
|
||||
packageName: string?
|
||||
ignoreHTTPSErrors: boolean?
|
||||
javaScriptEnabled: boolean?
|
||||
bypassCSP: boolean?
|
||||
userAgent: string?
|
||||
locale: string?
|
||||
timezoneId: string?
|
||||
geolocation:
|
||||
type: object?
|
||||
properties:
|
||||
longitude: number
|
||||
latitude: number
|
||||
accuracy: number?
|
||||
permissions:
|
||||
type: array?
|
||||
items: string
|
||||
extraHTTPHeaders:
|
||||
type: array?
|
||||
items: NameValue
|
||||
offline: boolean?
|
||||
httpCredentials:
|
||||
type: object?
|
||||
properties:
|
||||
username: string
|
||||
password: string
|
||||
deviceScaleFactor: number?
|
||||
isMobile: boolean?
|
||||
hasTouch: boolean?
|
||||
colorScheme:
|
||||
type: enum?
|
||||
literals:
|
||||
- dark
|
||||
- light
|
||||
- no-preference
|
||||
acceptDownloads: boolean?
|
||||
_traceResourcesPath: string?
|
||||
_tracePath: string?
|
||||
recordVideo:
|
||||
type: object?
|
||||
properties:
|
||||
dir: string
|
||||
size:
|
||||
type: object?
|
||||
properties:
|
||||
width: number
|
||||
height: number
|
||||
recordHar:
|
||||
type: object?
|
||||
properties:
|
||||
omitContent: boolean?
|
||||
path: string
|
||||
proxy:
|
||||
type: object?
|
||||
properties:
|
||||
server: string
|
||||
bypass: string?
|
||||
username: string?
|
||||
password: string?
|
||||
|
||||
returns:
|
||||
context: BrowserContext
|
||||
|
||||
shell:
|
||||
parameters:
|
||||
command: string
|
||||
returns:
|
||||
result: string
|
||||
|
||||
close:
|
||||
|
|
|
|||
|
|
@ -16,8 +16,21 @@
|
|||
|
||||
import { makeWaitForNextTask } from '../utils/utils';
|
||||
|
||||
export interface WritableStream {
|
||||
write(data: Buffer): void;
|
||||
}
|
||||
|
||||
export interface ReadableStream {
|
||||
on(event: 'data', callback: (b: Buffer) => void): void;
|
||||
on(event: 'close', callback: () => void): void;
|
||||
}
|
||||
|
||||
export interface ClosableStream {
|
||||
close(): void;
|
||||
}
|
||||
|
||||
export class Transport {
|
||||
private _pipeWrite: NodeJS.WritableStream;
|
||||
private _pipeWrite: WritableStream;
|
||||
private _data = Buffer.from([]);
|
||||
private _waitForNextTask = makeWaitForNextTask();
|
||||
private _closed = false;
|
||||
|
|
@ -26,8 +39,13 @@ export class Transport {
|
|||
onmessage?: (message: string) => void;
|
||||
onclose?: () => void;
|
||||
|
||||
constructor(pipeWrite: NodeJS.WritableStream, pipeRead: NodeJS.ReadableStream) {
|
||||
private _endian: 'be' | 'le';
|
||||
private _closeableStream: ClosableStream | undefined;
|
||||
|
||||
constructor(pipeWrite: WritableStream, pipeRead: ReadableStream, closeable?: ClosableStream, endian: 'be' | 'le' = 'le') {
|
||||
this._pipeWrite = pipeWrite;
|
||||
this._endian = endian;
|
||||
this._closeableStream = closeable;
|
||||
pipeRead.on('data', buffer => this._dispatch(buffer));
|
||||
pipeRead.on('close', () => this.onclose && this.onclose());
|
||||
this.onmessage = undefined;
|
||||
|
|
@ -39,13 +57,17 @@ export class Transport {
|
|||
throw new Error('Pipe has been closed');
|
||||
const data = Buffer.from(message, 'utf-8');
|
||||
const dataLength = Buffer.alloc(4);
|
||||
dataLength.writeUInt32LE(data.length, 0);
|
||||
if (this._endian === 'be')
|
||||
dataLength.writeUInt32BE(data.length, 0);
|
||||
else
|
||||
dataLength.writeUInt32LE(data.length, 0);
|
||||
this._pipeWrite.write(dataLength);
|
||||
this._pipeWrite.write(data);
|
||||
}
|
||||
|
||||
close() {
|
||||
throw new Error('unimplemented');
|
||||
// Let it throw.
|
||||
this._closeableStream!.close();
|
||||
}
|
||||
|
||||
_dispatch(buffer: Buffer) {
|
||||
|
|
@ -57,7 +79,7 @@ export class Transport {
|
|||
}
|
||||
|
||||
if (!this._bytesLeft) {
|
||||
this._bytesLeft = this._data.readUInt32LE(0);
|
||||
this._bytesLeft = this._endian === 'be' ? this._data.readUInt32BE(0) : this._data.readUInt32LE(0);
|
||||
this._data = this._data.slice(4);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,16 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
|||
scheme.Metadata = tObject({
|
||||
stack: tOptional(tString),
|
||||
});
|
||||
scheme.Point = tObject({
|
||||
x: tNumber,
|
||||
y: tNumber,
|
||||
});
|
||||
scheme.Rect = tObject({
|
||||
x: tNumber,
|
||||
y: tNumber,
|
||||
width: tNumber,
|
||||
height: tNumber,
|
||||
});
|
||||
scheme.SerializedValue = tObject({
|
||||
n: tOptional(tNumber),
|
||||
b: tOptional(tBoolean),
|
||||
|
|
@ -372,12 +382,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
|||
quality: tOptional(tNumber),
|
||||
omitBackground: tOptional(tBoolean),
|
||||
fullPage: tOptional(tBoolean),
|
||||
clip: tOptional(tObject({
|
||||
width: tNumber,
|
||||
height: tNumber,
|
||||
x: tNumber,
|
||||
y: tNumber,
|
||||
})),
|
||||
clip: tOptional(tType('Rect')),
|
||||
});
|
||||
scheme.PageSetExtraHTTPHeadersParams = tObject({
|
||||
headers: tArray(tType('NameValue')),
|
||||
|
|
@ -497,10 +502,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
|||
force: tOptional(tBoolean),
|
||||
noWaitAfter: tOptional(tBoolean),
|
||||
modifiers: tOptional(tArray(tEnum(['Alt', 'Control', 'Meta', 'Shift']))),
|
||||
position: tOptional(tObject({
|
||||
x: tNumber,
|
||||
y: tNumber,
|
||||
})),
|
||||
position: tOptional(tType('Point')),
|
||||
delay: tOptional(tNumber),
|
||||
button: tOptional(tEnum(['left', 'right', 'middle'])),
|
||||
clickCount: tOptional(tNumber),
|
||||
|
|
@ -512,10 +514,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
|||
force: tOptional(tBoolean),
|
||||
noWaitAfter: tOptional(tBoolean),
|
||||
modifiers: tOptional(tArray(tEnum(['Alt', 'Control', 'Meta', 'Shift']))),
|
||||
position: tOptional(tObject({
|
||||
x: tNumber,
|
||||
y: tNumber,
|
||||
})),
|
||||
position: tOptional(tType('Point')),
|
||||
delay: tOptional(tNumber),
|
||||
button: tOptional(tEnum(['left', 'right', 'middle'])),
|
||||
timeout: tOptional(tNumber),
|
||||
|
|
@ -564,10 +563,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
|||
selector: tString,
|
||||
force: tOptional(tBoolean),
|
||||
modifiers: tOptional(tArray(tEnum(['Alt', 'Control', 'Meta', 'Shift']))),
|
||||
position: tOptional(tObject({
|
||||
x: tNumber,
|
||||
y: tNumber,
|
||||
})),
|
||||
position: tOptional(tType('Point')),
|
||||
timeout: tOptional(tNumber),
|
||||
});
|
||||
scheme.FrameInnerHTMLParams = tObject({
|
||||
|
|
@ -622,10 +618,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
|||
force: tOptional(tBoolean),
|
||||
noWaitAfter: tOptional(tBoolean),
|
||||
modifiers: tOptional(tArray(tEnum(['Alt', 'Control', 'Meta', 'Shift']))),
|
||||
position: tOptional(tObject({
|
||||
x: tNumber,
|
||||
y: tNumber,
|
||||
})),
|
||||
position: tOptional(tType('Point')),
|
||||
timeout: tOptional(tNumber),
|
||||
});
|
||||
scheme.FrameTextContentParams = tObject({
|
||||
|
|
@ -716,10 +709,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
|||
force: tOptional(tBoolean),
|
||||
noWaitAfter: tOptional(tBoolean),
|
||||
modifiers: tOptional(tArray(tEnum(['Alt', 'Control', 'Meta', 'Shift']))),
|
||||
position: tOptional(tObject({
|
||||
x: tNumber,
|
||||
y: tNumber,
|
||||
})),
|
||||
position: tOptional(tType('Point')),
|
||||
delay: tOptional(tNumber),
|
||||
button: tOptional(tEnum(['left', 'right', 'middle'])),
|
||||
clickCount: tOptional(tNumber),
|
||||
|
|
@ -730,10 +720,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
|||
force: tOptional(tBoolean),
|
||||
noWaitAfter: tOptional(tBoolean),
|
||||
modifiers: tOptional(tArray(tEnum(['Alt', 'Control', 'Meta', 'Shift']))),
|
||||
position: tOptional(tObject({
|
||||
x: tNumber,
|
||||
y: tNumber,
|
||||
})),
|
||||
position: tOptional(tType('Point')),
|
||||
delay: tOptional(tNumber),
|
||||
button: tOptional(tEnum(['left', 'right', 'middle'])),
|
||||
timeout: tOptional(tNumber),
|
||||
|
|
@ -754,10 +741,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
|||
scheme.ElementHandleHoverParams = tObject({
|
||||
force: tOptional(tBoolean),
|
||||
modifiers: tOptional(tArray(tEnum(['Alt', 'Control', 'Meta', 'Shift']))),
|
||||
position: tOptional(tObject({
|
||||
x: tNumber,
|
||||
y: tNumber,
|
||||
})),
|
||||
position: tOptional(tType('Point')),
|
||||
timeout: tOptional(tNumber),
|
||||
});
|
||||
scheme.ElementHandleInnerHTMLParams = tOptional(tObject({}));
|
||||
|
|
@ -810,10 +794,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
|||
force: tOptional(tBoolean),
|
||||
noWaitAfter: tOptional(tBoolean),
|
||||
modifiers: tOptional(tArray(tEnum(['Alt', 'Control', 'Meta', 'Shift']))),
|
||||
position: tOptional(tObject({
|
||||
x: tNumber,
|
||||
y: tNumber,
|
||||
})),
|
||||
position: tOptional(tType('Point')),
|
||||
timeout: tOptional(tNumber),
|
||||
});
|
||||
scheme.ElementHandleTextContentParams = tOptional(tObject({}));
|
||||
|
|
@ -916,6 +897,175 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
|||
arg: tType('SerializedArgument'),
|
||||
});
|
||||
scheme.ElectronApplicationCloseParams = tOptional(tObject({}));
|
||||
scheme.AndroidDevicesParams = tOptional(tObject({}));
|
||||
scheme.AndroidSelector = tObject({
|
||||
checkable: tOptional(tBoolean),
|
||||
checked: tOptional(tBoolean),
|
||||
clazz: tOptional(tString),
|
||||
clickable: tOptional(tBoolean),
|
||||
depth: tOptional(tNumber),
|
||||
desc: tOptional(tString),
|
||||
enabled: tOptional(tBoolean),
|
||||
focusable: tOptional(tBoolean),
|
||||
focused: tOptional(tBoolean),
|
||||
hasChild: tOptional(tObject({
|
||||
selector: tType('AndroidSelector'),
|
||||
})),
|
||||
hasDescendant: tOptional(tObject({
|
||||
selector: tType('AndroidSelector'),
|
||||
maxDepth: tOptional(tNumber),
|
||||
})),
|
||||
longClickable: tOptional(tBoolean),
|
||||
pkg: tOptional(tString),
|
||||
res: tOptional(tString),
|
||||
scrollable: tOptional(tBoolean),
|
||||
selected: tOptional(tBoolean),
|
||||
text: tOptional(tString),
|
||||
});
|
||||
scheme.AndroidElementInfo = tObject({
|
||||
clazz: tString,
|
||||
desc: tString,
|
||||
res: tString,
|
||||
pkg: tString,
|
||||
text: tString,
|
||||
bounds: tType('Rect'),
|
||||
checkable: tBoolean,
|
||||
checked: tBoolean,
|
||||
clickable: tBoolean,
|
||||
enabled: tBoolean,
|
||||
focusable: tBoolean,
|
||||
focused: tBoolean,
|
||||
longClickable: tBoolean,
|
||||
scrollable: tBoolean,
|
||||
selected: tBoolean,
|
||||
});
|
||||
scheme.AndroidDeviceWaitParams = tObject({
|
||||
selector: tType('AndroidSelector'),
|
||||
state: tOptional(tEnum(['gone'])),
|
||||
timeout: tOptional(tNumber),
|
||||
});
|
||||
scheme.AndroidDeviceFillParams = tObject({
|
||||
selector: tType('AndroidSelector'),
|
||||
text: tString,
|
||||
timeout: tOptional(tNumber),
|
||||
});
|
||||
scheme.AndroidDeviceTapParams = tObject({
|
||||
selector: tType('AndroidSelector'),
|
||||
duration: tOptional(tNumber),
|
||||
timeout: tOptional(tNumber),
|
||||
});
|
||||
scheme.AndroidDeviceDragParams = tObject({
|
||||
selector: tType('AndroidSelector'),
|
||||
dest: tType('Point'),
|
||||
speed: tOptional(tNumber),
|
||||
timeout: tOptional(tNumber),
|
||||
});
|
||||
scheme.AndroidDeviceFlingParams = tObject({
|
||||
selector: tType('AndroidSelector'),
|
||||
direction: tEnum(['up', 'down', 'left', 'right']),
|
||||
speed: tOptional(tNumber),
|
||||
timeout: tOptional(tNumber),
|
||||
});
|
||||
scheme.AndroidDeviceLongTapParams = tObject({
|
||||
selector: tType('AndroidSelector'),
|
||||
timeout: tOptional(tNumber),
|
||||
});
|
||||
scheme.AndroidDevicePinchCloseParams = tObject({
|
||||
selector: tType('AndroidSelector'),
|
||||
percent: tNumber,
|
||||
speed: tOptional(tNumber),
|
||||
timeout: tOptional(tNumber),
|
||||
});
|
||||
scheme.AndroidDevicePinchOpenParams = tObject({
|
||||
selector: tType('AndroidSelector'),
|
||||
percent: tNumber,
|
||||
speed: tOptional(tNumber),
|
||||
timeout: tOptional(tNumber),
|
||||
});
|
||||
scheme.AndroidDeviceScrollParams = tObject({
|
||||
selector: tType('AndroidSelector'),
|
||||
direction: tEnum(['up', 'down', 'left', 'right']),
|
||||
percent: tNumber,
|
||||
speed: tOptional(tNumber),
|
||||
timeout: tOptional(tNumber),
|
||||
});
|
||||
scheme.AndroidDeviceSwipeParams = tObject({
|
||||
selector: tType('AndroidSelector'),
|
||||
direction: tEnum(['up', 'down', 'left', 'right']),
|
||||
percent: tNumber,
|
||||
speed: tOptional(tNumber),
|
||||
timeout: tOptional(tNumber),
|
||||
});
|
||||
scheme.AndroidDeviceInfoParams = tObject({
|
||||
selector: tType('AndroidSelector'),
|
||||
});
|
||||
scheme.AndroidDeviceInputTypeParams = tObject({
|
||||
text: tString,
|
||||
});
|
||||
scheme.AndroidDeviceInputPressParams = tObject({
|
||||
key: tString,
|
||||
});
|
||||
scheme.AndroidDeviceInputTapParams = tObject({
|
||||
point: tType('Point'),
|
||||
});
|
||||
scheme.AndroidDeviceInputSwipeParams = tObject({
|
||||
segments: tArray(tType('Point')),
|
||||
steps: tNumber,
|
||||
});
|
||||
scheme.AndroidDeviceInputDragParams = tObject({
|
||||
from: tType('Point'),
|
||||
to: tType('Point'),
|
||||
steps: tNumber,
|
||||
});
|
||||
scheme.AndroidDeviceLaunchBrowserParams = tObject({
|
||||
packageName: tOptional(tString),
|
||||
ignoreHTTPSErrors: tOptional(tBoolean),
|
||||
javaScriptEnabled: tOptional(tBoolean),
|
||||
bypassCSP: tOptional(tBoolean),
|
||||
userAgent: tOptional(tString),
|
||||
locale: tOptional(tString),
|
||||
timezoneId: tOptional(tString),
|
||||
geolocation: tOptional(tObject({
|
||||
longitude: tNumber,
|
||||
latitude: tNumber,
|
||||
accuracy: tOptional(tNumber),
|
||||
})),
|
||||
permissions: tOptional(tArray(tString)),
|
||||
extraHTTPHeaders: tOptional(tArray(tType('NameValue'))),
|
||||
offline: tOptional(tBoolean),
|
||||
httpCredentials: tOptional(tObject({
|
||||
username: tString,
|
||||
password: tString,
|
||||
})),
|
||||
deviceScaleFactor: tOptional(tNumber),
|
||||
isMobile: tOptional(tBoolean),
|
||||
hasTouch: tOptional(tBoolean),
|
||||
colorScheme: tOptional(tEnum(['dark', 'light', 'no-preference'])),
|
||||
acceptDownloads: tOptional(tBoolean),
|
||||
_traceResourcesPath: tOptional(tString),
|
||||
_tracePath: tOptional(tString),
|
||||
recordVideo: tOptional(tObject({
|
||||
dir: tString,
|
||||
size: tOptional(tObject({
|
||||
width: tNumber,
|
||||
height: tNumber,
|
||||
})),
|
||||
})),
|
||||
recordHar: tOptional(tObject({
|
||||
omitContent: tOptional(tBoolean),
|
||||
path: tString,
|
||||
})),
|
||||
proxy: tOptional(tObject({
|
||||
server: tString,
|
||||
bypass: tOptional(tString),
|
||||
username: tOptional(tString),
|
||||
password: tOptional(tString),
|
||||
})),
|
||||
});
|
||||
scheme.AndroidDeviceShellParams = tObject({
|
||||
command: tString,
|
||||
});
|
||||
scheme.AndroidDeviceCloseParams = tOptional(tObject({}));
|
||||
|
||||
return scheme;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ import * as WebSocket from 'ws';
|
|||
import { installDebugController } from '../debug/debugController';
|
||||
import { DispatcherConnection } from '../dispatchers/dispatcher';
|
||||
import { PlaywrightDispatcher } from '../dispatchers/playwrightDispatcher';
|
||||
import { Electron } from '../server/electron/electron';
|
||||
import { Playwright } from '../server/playwright';
|
||||
import { gracefullyCloseAll } from '../server/processLauncher';
|
||||
import { installTracer } from '../trace/tracer';
|
||||
|
|
@ -64,7 +63,6 @@ export class PlaywrightServer {
|
|||
});
|
||||
dispatcherConnection.onmessage = message => ws.send(JSON.stringify(message));
|
||||
const playwright = new Playwright(__dirname, require('../../browsers.json')['browsers']);
|
||||
(playwright as any).electron = new Electron();
|
||||
new PlaywrightDispatcher(dispatcherConnection.rootDispatcher(), playwright);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
280
src/server/android/android.ts
Normal file
280
src/server/android/android.ts
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
/**
|
||||
* Copyright Microsoft Corporation. All rights reserved.
|
||||
*
|
||||
* 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 debug from 'debug';
|
||||
import * as types from '../types';
|
||||
import { EventEmitter } from 'events';
|
||||
import * as fs from 'fs';
|
||||
import * as stream from 'stream';
|
||||
import * as util from 'util';
|
||||
import * as ws from 'ws';
|
||||
import { createGuid, makeWaitForNextTask } from '../../utils/utils';
|
||||
import { BrowserOptions, BrowserProcess } from '../browser';
|
||||
import { BrowserContext, validateBrowserContextOptions } from '../browserContext';
|
||||
import { ProgressController } from '../progress';
|
||||
import { CRBrowser } from '../chromium/crBrowser';
|
||||
import { helper } from '../helper';
|
||||
import { Transport } from '../../protocol/transport';
|
||||
import { RecentLogsCollector } from '../../utils/debugLogger';
|
||||
|
||||
const readFileAsync = util.promisify(fs.readFile);
|
||||
|
||||
export interface Backend {
|
||||
devices(): Promise<DeviceBackend[]>;
|
||||
}
|
||||
|
||||
export interface DeviceBackend {
|
||||
serial: string;
|
||||
close(): Promise<void>;
|
||||
init(): Promise<void>;
|
||||
runCommand(command: string): Promise<string>;
|
||||
open(command: string): Promise<SocketBackend>;
|
||||
}
|
||||
|
||||
export interface SocketBackend extends EventEmitter {
|
||||
write(data: Buffer): Promise<void>;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
export class Android {
|
||||
private _backend: Backend;
|
||||
|
||||
constructor(backend: Backend) {
|
||||
this._backend = backend;
|
||||
}
|
||||
|
||||
async devices(): Promise<AndroidDevice[]> {
|
||||
const devices = await this._backend.devices();
|
||||
return await Promise.all(devices.map(d => AndroidDevice.create(d)));
|
||||
}
|
||||
}
|
||||
|
||||
export class AndroidDevice {
|
||||
readonly _backend: DeviceBackend;
|
||||
readonly model: string;
|
||||
readonly serial: string;
|
||||
private _driverPromise: Promise<Transport> | undefined;
|
||||
private _lastId = 0;
|
||||
private _callbacks = new Map<number, { fulfill: (result: any) => void, reject: (error: Error) => void }>();
|
||||
|
||||
constructor(backend: DeviceBackend, model: string) {
|
||||
this._backend = backend;
|
||||
this.model = model;
|
||||
this.serial = backend.serial;
|
||||
}
|
||||
|
||||
static async create(backend: DeviceBackend): Promise<AndroidDevice> {
|
||||
await backend.init();
|
||||
const model = await backend.runCommand('shell:getprop ro.product.model');
|
||||
return new AndroidDevice(backend, model);
|
||||
}
|
||||
|
||||
async shell(command: string): Promise<string> {
|
||||
return await this._backend.runCommand(`shell:${command}`);
|
||||
}
|
||||
|
||||
private async _driver(): Promise<Transport> {
|
||||
if (this._driverPromise)
|
||||
return this._driverPromise;
|
||||
let callback: any;
|
||||
this._driverPromise = new Promise(f => callback = f);
|
||||
|
||||
debug('pw:android')('Stopping the old driver');
|
||||
await this.shell(`am force-stop com.microsoft.playwright.androiddriver`);
|
||||
|
||||
debug('pw:android')('Uninstalling the old driver');
|
||||
await this.shell(`cmd package uninstall com.microsoft.playwright.androiddriver`);
|
||||
await this.shell(`cmd package uninstall com.microsoft.playwright.androiddriver.test`);
|
||||
|
||||
debug('pw:android')('Installing the new driver');
|
||||
for (const file of ['android-driver.apk', 'android-driver-target.apk']) {
|
||||
const driverFile = await readFileAsync(require.resolve(`../../../bin/${file}`));
|
||||
const installSocket = await this._backend.open(`shell:cmd package install -r -t -S ${driverFile.length}`);
|
||||
debug('pw:android')('Writing driver bytes: ' + driverFile.length);
|
||||
await installSocket.write(driverFile);
|
||||
const success = await new Promise(f => installSocket.on('data', f));
|
||||
debug('pw:android')('Written driver bytes: ' + success);
|
||||
}
|
||||
|
||||
debug('pw:android')('Starting the new driver');
|
||||
this.shell(`am instrument -w com.microsoft.playwright.androiddriver.test/androidx.test.runner.AndroidJUnitRunner`);
|
||||
|
||||
debug('pw:android')('Polling the socket');
|
||||
let socket;
|
||||
while (!socket) {
|
||||
try {
|
||||
socket = await this._backend.open(`localabstract:playwright_android_driver_socket`);
|
||||
} catch (e) {
|
||||
await new Promise(f => setTimeout(f, 100));
|
||||
}
|
||||
}
|
||||
|
||||
debug('pw:android')('Connected to driver');
|
||||
const transport = new Transport(socket, socket, socket, 'be');
|
||||
transport.onmessage = message => {
|
||||
const response = JSON.parse(message);
|
||||
const { id, result, error } = response;
|
||||
const callback = this._callbacks.get(id);
|
||||
if (!callback)
|
||||
return;
|
||||
if (error)
|
||||
callback.reject(new Error(error));
|
||||
else
|
||||
callback.fulfill(result);
|
||||
this._callbacks.delete(id);
|
||||
};
|
||||
|
||||
callback(transport);
|
||||
return this._driverPromise;
|
||||
}
|
||||
|
||||
async send(method: string, params: any): Promise<any> {
|
||||
const driver = await this._driver();
|
||||
const id = ++this._lastId;
|
||||
const result = new Promise((fulfill, reject) => this._callbacks.set(id, { fulfill, reject }));
|
||||
driver.send(JSON.stringify({ id, method, params }));
|
||||
return result;
|
||||
}
|
||||
|
||||
async close() {
|
||||
const driver = await this._driver();
|
||||
driver.close();
|
||||
await this._backend.close();
|
||||
}
|
||||
|
||||
async launchBrowser(packageName: string = 'com.android.chrome', options: types.BrowserContextOptions = {}): Promise<BrowserContext> {
|
||||
debug('pw:android')('Force-stopping', packageName);
|
||||
await this._backend.runCommand(`shell:am force-stop ${packageName}`);
|
||||
|
||||
const socketName = createGuid();
|
||||
const commandLine = `_ --disable-fre --no-default-browser-check --no-first-run --remote-debugging-socket-name=${socketName}`;
|
||||
debug('pw:android')('Starting', packageName, commandLine);
|
||||
await this._backend.runCommand(`shell:echo "${commandLine}" > /data/local/tmp/chrome-command-line`);
|
||||
await this._backend.runCommand(`shell:am start -n ${packageName}/com.google.android.apps.chrome.Main about:blank`);
|
||||
|
||||
debug('pw:android')('Polling for socket', socketName);
|
||||
while (true) {
|
||||
const net = await this._backend.runCommand(`shell:cat /proc/net/unix | grep ${socketName}$`);
|
||||
if (net)
|
||||
break;
|
||||
await new Promise(f => setTimeout(f, 100));
|
||||
}
|
||||
debug('pw:android')('Got the socket, connecting');
|
||||
const androidBrowser = new AndroidBrowser(this, packageName, socketName);
|
||||
await androidBrowser._open();
|
||||
|
||||
const browserOptions: BrowserOptions = {
|
||||
name: 'clank',
|
||||
slowMo: 0,
|
||||
persistent: { ...options, noDefaultViewport: true },
|
||||
downloadsPath: undefined,
|
||||
browserProcess: new ClankBrowserProcess(androidBrowser),
|
||||
proxy: options.proxy,
|
||||
protocolLogger: helper.debugProtocolLogger(),
|
||||
browserLogsCollector: new RecentLogsCollector()
|
||||
};
|
||||
validateBrowserContextOptions(options, browserOptions);
|
||||
|
||||
const browser = await CRBrowser.connect(androidBrowser, browserOptions);
|
||||
const controller = new ProgressController();
|
||||
await controller.run(async progress => {
|
||||
await browser._defaultContext!._loadDefaultContext(progress);
|
||||
});
|
||||
return browser._defaultContext!;
|
||||
}
|
||||
}
|
||||
|
||||
class AndroidBrowser extends EventEmitter {
|
||||
readonly device: AndroidDevice;
|
||||
readonly socketName: string;
|
||||
private _socket: SocketBackend | undefined;
|
||||
private _receiver: stream.Writable;
|
||||
private _waitForNextTask = makeWaitForNextTask();
|
||||
onmessage?: (message: any) => void;
|
||||
onclose?: () => void;
|
||||
private _packageName: string;
|
||||
|
||||
constructor(device: AndroidDevice, packageName: string, socketName: string) {
|
||||
super();
|
||||
this._packageName = packageName;
|
||||
this.device = device;
|
||||
this.socketName = socketName;
|
||||
this._receiver = new (ws as any).Receiver() as stream.Writable;
|
||||
this._receiver.on('message', message => {
|
||||
this._waitForNextTask(() => {
|
||||
if (this.onmessage)
|
||||
this.onmessage(JSON.parse(message));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async _open() {
|
||||
this._socket = await this.device._backend.open(`localabstract:${this.socketName}`);
|
||||
this._socket.on('close', () => {
|
||||
this._waitForNextTask(() => {
|
||||
if (this.onclose)
|
||||
this.onclose();
|
||||
});
|
||||
});
|
||||
await this._socket.write(Buffer.from(`GET /devtools/browser HTTP/1.1\r
|
||||
Upgrade: WebSocket\r
|
||||
Connection: Upgrade\r
|
||||
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r
|
||||
Sec-WebSocket-Version: 13\r
|
||||
\r
|
||||
`));
|
||||
// HTTP Upgrade response.
|
||||
await new Promise(f => this._socket!.once('data', f));
|
||||
|
||||
// Start sending web frame to receiver.
|
||||
this._socket.on('data', data => this._receiver._write(data, 'binary', () => {}));
|
||||
}
|
||||
|
||||
async send(s: any) {
|
||||
await this._socket!.write(encodeWebFrame(JSON.stringify(s)));
|
||||
}
|
||||
|
||||
async close() {
|
||||
await this._socket!.close();
|
||||
await this.device._backend.runCommand(`shell:am force-stop ${this._packageName}`);
|
||||
}
|
||||
}
|
||||
|
||||
function encodeWebFrame(data: string): Buffer {
|
||||
return (ws as any).Sender.frame(Buffer.from(data), {
|
||||
opcode: 1,
|
||||
mask: true,
|
||||
fin: true,
|
||||
readOnly: true
|
||||
})[0];
|
||||
}
|
||||
|
||||
class ClankBrowserProcess implements BrowserProcess {
|
||||
private _browser: AndroidBrowser;
|
||||
|
||||
constructor(browser: AndroidBrowser) {
|
||||
this._browser = browser;
|
||||
}
|
||||
|
||||
onclose: ((exitCode: number | null, signal: string | null) => void) | undefined;
|
||||
|
||||
async kill(): Promise<void> {
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
await this._browser.close();
|
||||
}
|
||||
}
|
||||
15
src/server/android/driver/.gitignore
vendored
Normal file
15
src/server/android/driver/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
3
src/server/android/driver/.idea/.gitignore
vendored
Normal file
3
src/server/android/driver/.idea/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
1
src/server/android/driver/.idea/.name
Normal file
1
src/server/android/driver/.idea/.name
Normal file
|
|
@ -0,0 +1 @@
|
|||
Playwright Android Driver
|
||||
6
src/server/android/driver/.idea/compiler.xml
Normal file
6
src/server/android/driver/.idea/compiler.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="1.8" />
|
||||
</component>
|
||||
</project>
|
||||
4
src/server/android/driver/.idea/encodings.xml
Normal file
4
src/server/android/driver/.idea/encodings.xml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding" addBOMForNewFiles="with NO BOM" />
|
||||
</project>
|
||||
22
src/server/android/driver/.idea/gradle.xml
Normal file
22
src/server/android/driver/.idea/gradle.xml
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="testRunner" value="PLATFORM" />
|
||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleJvm" value="1.8" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
</set>
|
||||
</option>
|
||||
<option name="resolveModulePerSourceSet" value="false" />
|
||||
<option name="useQualifiedModuleNames" value="true" />
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
25
src/server/android/driver/.idea/jarRepositories.xml
Normal file
25
src/server/android/driver/.idea/jarRepositories.xml
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RemoteRepositoriesConfiguration">
|
||||
<remote-repository>
|
||||
<option name="id" value="central" />
|
||||
<option name="name" value="Maven Central repository" />
|
||||
<option name="url" value="https://repo1.maven.org/maven2" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="jboss.community" />
|
||||
<option name="name" value="JBoss Community repository" />
|
||||
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="BintrayJCenter" />
|
||||
<option name="name" value="BintrayJCenter" />
|
||||
<option name="url" value="https://jcenter.bintray.com/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="Google" />
|
||||
<option name="name" value="Google" />
|
||||
<option name="url" value="https://dl.google.com/dl/android/maven2/" />
|
||||
</remote-repository>
|
||||
</component>
|
||||
</project>
|
||||
9
src/server/android/driver/.idea/misc.xml
Normal file
9
src/server/android/driver/.idea/misc.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
</component>
|
||||
</project>
|
||||
7
src/server/android/driver/.idea/vcs.xml
Normal file
7
src/server/android/driver/.idea/vcs.xml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/../../../.." vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
1
src/server/android/driver/app/.gitignore
vendored
Normal file
1
src/server/android/driver/app/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
38
src/server/android/driver/app/build.gradle
Normal file
38
src/server/android/driver/app/build.gradle
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
plugins {
|
||||
id 'com.android.application'
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 29
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.microsoft.playwright.androiddriver"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||
implementation 'com.google.android.material:material:1.2.1'
|
||||
testImplementation 'junit:junit:4.+'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
||||
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
|
||||
}
|
||||
21
src/server/android/driver/app/proguard-rules.pro
vendored
Normal file
21
src/server/android/driver/app/proguard-rules.pro
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
|
@ -0,0 +1,367 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package com.microsoft.playwright.androiddriver;
|
||||
|
||||
import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
import android.net.LocalServerSocket;
|
||||
import android.net.LocalSocket;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.SdkSuppress;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import androidx.test.uiautomator.By;
|
||||
import androidx.test.uiautomator.BySelector;
|
||||
import androidx.test.uiautomator.Direction;
|
||||
import androidx.test.uiautomator.UiDevice;
|
||||
import androidx.test.uiautomator.UiObject2;
|
||||
import androidx.test.uiautomator.Until;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@SdkSuppress(minSdkVersion = 21)
|
||||
public class InstrumentedTest {
|
||||
|
||||
private static BySelector parseSelector(JSONObject param) throws JSONException{
|
||||
JSONObject selector = param.getJSONObject("selector");
|
||||
BySelector result = null;
|
||||
if (selector.has("checkable")) {
|
||||
boolean value = selector.getBoolean("checkable");
|
||||
result = result != null ? result.checkable(value) : By.checkable(value);
|
||||
}
|
||||
if (selector.has("checked")) {
|
||||
boolean value = selector.getBoolean("checked");
|
||||
result = result != null ? result.checked(value) : By.checked(value);
|
||||
}
|
||||
if (selector.has("clazz")) {
|
||||
Pattern value = Pattern.compile(selector.getString("clazz"));
|
||||
result = result != null ? result.clazz(value) : By.clazz(value);
|
||||
}
|
||||
if (selector.has("pkg")) {
|
||||
Pattern value = Pattern.compile(selector.getString("pkg"));
|
||||
result = result != null ? result.pkg(value) : By.pkg(value);
|
||||
}
|
||||
if (selector.has("desc")) {
|
||||
Pattern value = Pattern.compile(selector.getString("desc"));
|
||||
result = result != null ? result.desc(value) : By.desc(value);
|
||||
}
|
||||
if (selector.has("text")) {
|
||||
Pattern value = Pattern.compile(selector.getString("text"));
|
||||
result = result != null ? result.text(value) : By.text(value);
|
||||
}
|
||||
if (selector.has("clickable")) {
|
||||
boolean value = selector.getBoolean("clickable");
|
||||
result = result != null ? result.clickable(value) : By.clickable(value);
|
||||
}
|
||||
if (selector.has("depth")) {
|
||||
int value = selector.getInt("depth");
|
||||
result = result != null ? result.depth(value) : By.depth(value);
|
||||
}
|
||||
if (selector.has("enabled")) {
|
||||
boolean value = selector.getBoolean("enabled");
|
||||
result = result != null ? result.enabled(value) : By.enabled(value);
|
||||
}
|
||||
if (selector.has("focusable")) {
|
||||
boolean value = selector.getBoolean("focusable");
|
||||
result = result != null ? result.focusable(value) : By.focusable(value);
|
||||
}
|
||||
if (selector.has("focused")) {
|
||||
boolean value = selector.getBoolean("focused");
|
||||
result = result != null ? result.focused(value) : By.focused(value);
|
||||
}
|
||||
if (selector.has("hasChild")) {
|
||||
BySelector value = parseSelector(selector.getJSONObject("hasChild"));
|
||||
result = result != null ? result.hasChild(value) : By.hasChild(value);
|
||||
}
|
||||
if (selector.has("hasDescendant")) {
|
||||
JSONObject object = selector.getJSONObject("hasDescendant");
|
||||
BySelector value = parseSelector(object);
|
||||
int maxDepth = 10000;
|
||||
if (selector.has("maxDepth"))
|
||||
maxDepth = selector.getInt("maxDepth");
|
||||
result = result != null ? result.hasDescendant(value, maxDepth) : By.hasDescendant(value, maxDepth);
|
||||
}
|
||||
if (selector.has("longClickable")) {
|
||||
boolean value = selector.getBoolean("longClickable");
|
||||
result = result != null ? result.longClickable(value) : By.longClickable(value);
|
||||
}
|
||||
if (selector.has("res")) {
|
||||
Pattern value = Pattern.compile(selector.getString("res"));
|
||||
result = result != null ? result.res(value) : By.res(value);
|
||||
}
|
||||
if (selector.has("scrollable")) {
|
||||
boolean value = selector.getBoolean("scrollable");
|
||||
result = result != null ? result.scrollable(value) : By.scrollable(value);
|
||||
}
|
||||
if (selector.has("selected")) {
|
||||
boolean value = selector.getBoolean("selected");
|
||||
result = result != null ? result.selected(value) : By.selected(value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int parseTimeout(JSONObject params) throws JSONException {
|
||||
if (params.has("timeout"))
|
||||
return params.getInt("timeout");
|
||||
return 30000;
|
||||
}
|
||||
|
||||
private static Point parsePoint(JSONObject params, String propertyName) throws JSONException {
|
||||
JSONObject point = params.getJSONObject(propertyName);
|
||||
return new Point(params.getInt("x"), params.getInt("y"));
|
||||
}
|
||||
|
||||
private static Direction parseDirection(JSONObject params) throws JSONException {
|
||||
switch (params.getString("direction")) {
|
||||
case "up": return Direction.UP;
|
||||
case "down": return Direction.DOWN;
|
||||
case "left": return Direction.LEFT;
|
||||
case "right": return Direction.RIGHT;
|
||||
}
|
||||
throw new JSONException("Unsupported direction: " + params.getString("direction"));
|
||||
}
|
||||
|
||||
private static UiObject2 wait(UiDevice device, JSONObject params) throws JSONException {
|
||||
return device.wait(Until.findObject(parseSelector(params)), parseTimeout(params));
|
||||
}
|
||||
|
||||
private static void fill(UiDevice device, JSONObject params) throws JSONException {
|
||||
device.wait(Until.findObject(parseSelector(params)), parseTimeout(params)).setText(params.getString("text"));
|
||||
}
|
||||
|
||||
private static void click(UiDevice device, JSONObject params) throws JSONException {
|
||||
int duration = params.has("duration") ? params.getInt("duration") : 0;
|
||||
wait(device, params).click(duration);
|
||||
}
|
||||
|
||||
private static void drag(UiDevice device, JSONObject params) throws JSONException {
|
||||
int speed = params.has("speed") ? params.getInt("speed") : -1;
|
||||
if (speed >= 0)
|
||||
wait(device, params).drag(parsePoint(params, "dest"), speed);
|
||||
else
|
||||
wait(device, params).drag(parsePoint(params, "dest"));
|
||||
}
|
||||
|
||||
private static void fling(UiDevice device, JSONObject params) throws JSONException {
|
||||
int speed = params.has("speed") ? params.getInt("speed") : -1;
|
||||
if (speed >= 0)
|
||||
wait(device, params).fling(parseDirection(params), speed);
|
||||
else
|
||||
wait(device, params).fling(parseDirection(params));
|
||||
}
|
||||
|
||||
private static void longClick(UiDevice device, JSONObject params) throws JSONException {
|
||||
wait(device, params).longClick();
|
||||
}
|
||||
|
||||
private static void pinchClose(UiDevice device, JSONObject params) throws JSONException {
|
||||
int speed = params.has("speed") ? params.getInt("speed") : -1;
|
||||
if (speed >= 0)
|
||||
wait(device, params).pinchClose(params.getInt("percent"), speed);
|
||||
else
|
||||
wait(device, params).pinchClose(params.getInt("percent"));
|
||||
}
|
||||
|
||||
private static void pinchOpen(UiDevice device, JSONObject params) throws JSONException {
|
||||
int speed = params.has("speed") ? params.getInt("speed") : -1;
|
||||
if (speed >= 0)
|
||||
wait(device, params).pinchOpen(params.getInt("percent"), speed);
|
||||
else
|
||||
wait(device, params).pinchOpen(params.getInt("percent"));
|
||||
}
|
||||
|
||||
private static void scroll(UiDevice device, JSONObject params) throws JSONException {
|
||||
int speed = params.has("speed") ? params.getInt("speed") : -1;
|
||||
if (speed >= 0)
|
||||
wait(device, params).scroll(parseDirection(params), params.getInt("percent"), speed);
|
||||
else
|
||||
wait(device, params).scroll(parseDirection(params), params.getInt("percent"));
|
||||
}
|
||||
|
||||
private static void swipe(UiDevice device, JSONObject params) throws JSONException {
|
||||
int speed = params.has("speed") ? params.getInt("speed") : -1;
|
||||
if (speed >= 0)
|
||||
wait(device, params).swipe(parseDirection(params), params.getInt("percent"), speed);
|
||||
else
|
||||
wait(device, params).swipe(parseDirection(params), params.getInt("percent"));
|
||||
}
|
||||
|
||||
private static JSONObject info(UiDevice device, JSONObject params) throws JSONException {
|
||||
JSONObject info = new JSONObject();
|
||||
UiObject2 object = device.findObject(parseSelector(params));
|
||||
Rect bounds = object.getVisibleBounds();
|
||||
JSONObject boundsObject = new JSONObject();
|
||||
boundsObject.put("x", bounds.left);
|
||||
boundsObject.put("y", bounds.top);
|
||||
boundsObject.put("width", bounds.width());
|
||||
boundsObject.put("height", bounds.height());
|
||||
info.put("clazz", object.getClassName());
|
||||
info.put("pkg", object.getApplicationPackage());
|
||||
info.put("desc", object.getContentDescription());
|
||||
info.put("res", object.getResourceName());
|
||||
info.put("text", object.getText());
|
||||
info.put("bounds", boundsObject);
|
||||
info.put("checkable", object.isCheckable());
|
||||
info.put("checked", object.isChecked());
|
||||
info.put("clickable", object.isClickable());
|
||||
info.put("enabled", object.isEnabled());
|
||||
info.put("focusable", object.isFocusable());
|
||||
info.put("focused", object.isFocused());
|
||||
info.put("longClickable", object.isLongClickable());
|
||||
info.put("scrollable", object.isScrollable());
|
||||
info.put("selected", object.isSelected());
|
||||
return info;
|
||||
}
|
||||
|
||||
private static void inputPress(UiDevice device, JSONObject params) throws JSONException {
|
||||
device.pressKeyCode(params.getInt("keyCode"));
|
||||
}
|
||||
|
||||
private static void inputClick(UiDevice device, JSONObject params) throws JSONException {
|
||||
Point point = parsePoint(params, "point");
|
||||
device.click(point.x, point.y);
|
||||
}
|
||||
|
||||
private static void inputSwipe(UiDevice device, JSONObject params) throws JSONException {
|
||||
JSONArray items = params.getJSONArray("segments");
|
||||
Point[] segments = new Point[items.length()];
|
||||
for (int i = 0; i < items.length(); ++i) {
|
||||
JSONObject p = items.getJSONObject(i);
|
||||
segments[i] = new Point(p.getInt("x"), p.getInt("y"));
|
||||
}
|
||||
device.swipe(segments, params.getInt("steps"));
|
||||
}
|
||||
|
||||
private static void inputDrag(UiDevice device, JSONObject params) throws JSONException {
|
||||
Point from = parsePoint(params, "from");
|
||||
Point to = parsePoint(params, "to");
|
||||
device.drag(from.x, from.y, to.x, to.y, params.getInt("steps"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void main() {
|
||||
UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
|
||||
|
||||
try {
|
||||
LocalServerSocket serverSocket = new LocalServerSocket("playwright_android_driver_socket");
|
||||
LocalSocket socket = serverSocket.accept();
|
||||
InputStream is = socket.getInputStream();
|
||||
DataInputStream dis = new DataInputStream(is);
|
||||
DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
|
||||
|
||||
while (true) {
|
||||
int id = 0;
|
||||
String method = null;
|
||||
JSONObject params = null;
|
||||
try {
|
||||
int size = dis.readInt();
|
||||
byte[] buffer = new byte[size];
|
||||
dis.readFully(buffer);
|
||||
String s = new String(buffer, StandardCharsets.UTF_8);
|
||||
JSONObject message = new JSONObject(s);
|
||||
id = message.getInt("id");
|
||||
method = message.getString("method");
|
||||
params = message.getJSONObject("params");
|
||||
} catch (JSONException e) {
|
||||
}
|
||||
if (method == null)
|
||||
continue;
|
||||
|
||||
JSONObject response = new JSONObject();
|
||||
response.put("id", id);
|
||||
response.put("result", params);
|
||||
try {
|
||||
switch (method) {
|
||||
case "wait":
|
||||
wait(device, params);
|
||||
break;
|
||||
case "fill":
|
||||
fill(device, params);
|
||||
break;
|
||||
case "click":
|
||||
click(device, params);
|
||||
break;
|
||||
case "drag":
|
||||
drag(device, params);
|
||||
break;
|
||||
case "fling":
|
||||
fling(device, params);
|
||||
break;
|
||||
case "longClick":
|
||||
longClick(device, params);
|
||||
break;
|
||||
case "pinchClose":
|
||||
pinchClose(device, params);
|
||||
break;
|
||||
case "pinchOpen":
|
||||
pinchOpen(device, params);
|
||||
break;
|
||||
case "scroll":
|
||||
scroll(device, params);
|
||||
break;
|
||||
case "swipe":
|
||||
swipe(device, params);
|
||||
break;
|
||||
case "info":
|
||||
response.put("result", info(device, params));
|
||||
break;
|
||||
case "inputPress":
|
||||
inputPress(device, params);
|
||||
break;
|
||||
case "inputClick":
|
||||
inputClick(device, params);
|
||||
break;
|
||||
case "inputSwipe":
|
||||
inputSwipe(device, params);
|
||||
break;
|
||||
case "inputDrag":
|
||||
inputDrag(device, params);
|
||||
break;
|
||||
default:
|
||||
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
response.put("error", e.toString());
|
||||
}
|
||||
byte[] responseBytes = response.toString().getBytes(StandardCharsets.UTF_8);
|
||||
dos.writeInt(responseBytes.length);
|
||||
dos.write(responseBytes);
|
||||
dos.flush();
|
||||
}
|
||||
} catch (JSONException | IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/server/android/driver/app/src/main/AndroidManifest.xml
Normal file
13
src/server/android/driver/app/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.microsoft.playwright.androiddriver">
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.PlaywrightAndroidDriver" />
|
||||
|
||||
</manifest>
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:pathData="M57.814,12.557C58.249,20.66 58.405,26.384 58.28,29.731C57.996,37.327 56.71,43.174 55.834,46.634C54.204,53.072 51.186,58.388 49.458,61.375C45.641,67.978 40.199,71.434 33.101,71.689C29.413,71.211 26.647,70.515 24.818,69.555C20.577,67.329 16.148,63.147 13.7,59.245C11.686,56.036 9.094,50.998 7.091,44.254C5.708,39.596 3.853,30.848 1.52,18.011C14.2,18.244 23.774,18.044 30.242,17.417C36.711,16.79 45.903,15.171 57.814,12.557ZM33.642,52.26C31.371,52.379 28.856,53.136 26.931,54.127C25.884,54.666 25.008,55.278 24.408,55.885C23.596,56.707 23.267,57.556 23.305,58.293C23.343,59.01 23.66,59.468 24.12,59.775C24.456,60 24.931,60.15 25.56,60.135C26.396,60.115 27.665,59.768 29.114,59.343C30.695,58.879 32.528,58.328 34.182,58.242C35.699,58.162 37.235,58.353 38.551,58.512C39.977,58.684 41.181,58.797 41.948,58.652C42.547,58.539 42.975,58.289 43.267,57.97C43.596,57.609 43.807,57.135 43.773,56.473C43.733,55.718 43.356,54.975 42.585,54.334C42.01,53.856 41.194,53.426 40.208,53.088C38.392,52.466 35.967,52.138 33.642,52.26ZM17.816,31.819C16.348,33.06 14.848,34.71 13.723,36.742C13.325,37.461 13.315,38.133 13.58,38.758C13.783,39.236 14.192,39.727 14.909,40.153C15.098,40.266 15.281,40.357 15.457,40.43C15.619,40.377 15.784,40.323 15.951,40.268C20.151,38.884 22.766,37.972 23.8,37.537C24.441,37.267 24.847,37.139 25.124,37.051C25.882,36.812 26.126,36.652 26.45,36.246C26.978,35.584 27.251,34.872 27.336,34.161C27.429,33.387 27.295,32.609 26.994,31.9C26.668,31.132 26.147,30.447 25.539,29.94C24.865,29.378 24.089,29.041 23.354,28.979C22.637,28.918 20.271,29.744 17.816,31.819ZM38.043,26.999C37.344,27.265 36.718,27.79 36.261,28.49C35.844,29.13 35.571,29.913 35.511,30.728C35.408,32.125 35.903,33.618 37.406,34.687C37.855,35.008 37.783,35.085 39.973,35.348C41.086,35.482 43.852,35.637 48.27,35.809C48.446,35.816 48.619,35.822 48.79,35.829C48.939,35.71 49.089,35.572 49.24,35.412C49.812,34.804 50.07,34.22 50.133,33.704C50.216,33.03 50.021,32.387 49.44,31.806C47.799,30.163 45.902,28.99 44.149,28.202C41.218,26.884 38.715,26.743 38.043,26.999Z"
|
||||
android:strokeWidth="2.52930486"
|
||||
android:fillType="evenOdd">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:gradientRadius="54.024082"
|
||||
android:centerX="16.362686"
|
||||
android:centerY="46.999622"
|
||||
android:type="radial">
|
||||
<item android:offset="0" android:color="#FFDE6A6D"/>
|
||||
<item android:offset="1" android:color="#FFB54444"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
<aapt:attr name="android:strokeColor">
|
||||
<gradient
|
||||
android:gradientRadius="56.845688"
|
||||
android:centerX="16.362646"
|
||||
android:centerY="1.5783212"
|
||||
android:type="radial">
|
||||
<item android:offset="0" android:color="#FF913232"/>
|
||||
<item android:offset="1" android:color="#FFD44545"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:pathData="M50.308,36.282C62.666,39.148 72.074,40.945 78.533,41.678C84.993,42.411 94.323,42.74 106.521,42.66C105.262,50.683 104.225,56.32 103.407,59.57C101.55,66.946 99.076,72.402 97.5,75.607C94.567,81.569 90.508,86.144 88.197,88.709C83.088,94.378 77.045,96.628 70.047,95.399C66.539,94.164 63.978,92.907 62.388,91.587C58.701,88.525 55.237,83.509 53.653,79.18C52.35,75.619 50.86,70.148 50.303,63.13C49.918,58.283 49.922,49.333 50.308,36.282ZM61.204,76.319C60.878,76.8 60.693,77.577 60.955,78.607C61.236,79.714 62.063,81.206 63.579,82.63C65.384,84.326 68.185,85.959 72.147,86.658C76.065,87.349 79.401,86.616 81.883,85.435C83.902,84.473 85.362,83.205 86.156,82.139C86.837,81.224 87.119,80.401 86.26,78.529C85.963,78.312 85.549,78.146 84.974,78.146C84.033,78.146 82.874,78.173 81.66,78.202C79.139,78.262 76.368,78.335 74.904,78.155C72.971,77.917 65.751,76.194 63.535,75.578C63.104,75.459 62.73,75.309 61.204,76.319ZM84.054,55.732C82.55,56.463 81.57,57.452 81.085,58.428C80.771,59.06 80.649,59.702 80.699,60.311C80.751,60.945 80.988,61.552 81.428,62.092C82.461,63.358 84.902,64.397 89.351,63.692C91.166,63.405 92.422,62.879 93.753,62.208C95.085,61.538 96.628,60.43 98.372,58.87C98.899,58.398 99.383,57.94 99.824,57.496C99.236,57.24 98.621,56.989 97.978,56.745C91.863,54.423 87.239,54.185 84.054,55.732ZM55.216,49.342C55.318,49.945 55.454,50.565 55.624,51.201C56.482,54.408 57.877,56.826 59.757,58.491C60.96,59.557 63.658,61.195 65.467,61.54C68.672,62.151 71.141,61.71 72.202,60.673C72.586,60.298 72.841,59.85 72.959,59.332C73.06,58.889 73.061,58.386 72.915,57.821C72.715,57.044 72.206,56.091 71.255,54.985C70.506,54.112 69.562,53.334 68.417,52.657C66.651,51.614 62.875,50.577 57.049,49.626C56.415,49.523 55.804,49.428 55.216,49.342Z"
|
||||
android:strokeWidth="2.52930486"
|
||||
android:fillType="evenOdd">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:startY="39.30427"
|
||||
android:startX="78.2195"
|
||||
android:endY="92.342415"
|
||||
android:endX="78.2195"
|
||||
android:type="linear">
|
||||
<item android:offset="0" android:color="#FF59DC6F"/>
|
||||
<item android:offset="1" android:color="#FF48B656"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
<aapt:attr name="android:strokeColor">
|
||||
<gradient
|
||||
android:startY="89.28204"
|
||||
android:startX="70.226204"
|
||||
android:endY="66.455"
|
||||
android:endX="78.2195"
|
||||
android:type="linear">
|
||||
<item android:offset="0" android:color="#FF58C361"/>
|
||||
<item android:offset="1" android:color="#FF519F50"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#333DDC84" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#333DDC84" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#333DDC84" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#333DDC84" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#333DDC84" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#333DDC84" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#333DDC84" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#333DDC84" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#333DDC84" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#333DDC84" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#333DDC84" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#333DDC84" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#333DDC84" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#333DDC84" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#333DDC84" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#333DDC84" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#333DDC84" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#333DDC84" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#333DDC84" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#333DDC84" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#333DDC84" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#333DDC84" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#333DDC84" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#333DDC84" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#333DDC84" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#333DDC84" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#333DDC84" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#333DDC84" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#333DDC84" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#333DDC84" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#333DDC84" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#333DDC84" />
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.PlaywrightAndroidDriver" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/purple_200</item>
|
||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||
<item name="colorOnPrimary">@color/black</item>
|
||||
<!-- Secondary brand color. -->
|
||||
<item name="colorSecondary">@color/teal_200</item>
|
||||
<item name="colorSecondaryVariant">@color/teal_200</item>
|
||||
<item name="colorOnSecondary">@color/black</item>
|
||||
<!-- Status bar color. -->
|
||||
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
</resources>
|
||||
10
src/server/android/driver/app/src/main/res/values/colors.xml
Normal file
10
src/server/android/driver/app/src/main/res/values/colors.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<resources>
|
||||
<string name="app_name">Playwright Android Driver</string>
|
||||
</resources>
|
||||
16
src/server/android/driver/app/src/main/res/values/themes.xml
Normal file
16
src/server/android/driver/app/src/main/res/values/themes.xml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.PlaywrightAndroidDriver" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/purple_500</item>
|
||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||
<item name="colorOnPrimary">@color/white</item>
|
||||
<!-- Secondary brand color. -->
|
||||
<item name="colorSecondary">@color/teal_200</item>
|
||||
<item name="colorSecondaryVariant">@color/teal_700</item>
|
||||
<item name="colorOnSecondary">@color/black</item>
|
||||
<!-- Status bar color. -->
|
||||
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
</resources>
|
||||
24
src/server/android/driver/build.gradle
Normal file
24
src/server/android/driver/build.gradle
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath "com.android.tools.build:gradle:4.1.0"
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
19
src/server/android/driver/gradle.properties
Normal file
19
src/server/android/driver/gradle.properties
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# Project-wide Gradle settings.
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app"s APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
# Automatically convert third-party libraries to use AndroidX
|
||||
android.enableJetifier=true
|
||||
BIN
src/server/android/driver/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
src/server/android/driver/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
6
src/server/android/driver/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
6
src/server/android/driver/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#Tue Dec 08 09:38:33 PST 2020
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
|
||||
172
src/server/android/driver/gradlew
vendored
Executable file
172
src/server/android/driver/gradlew
vendored
Executable file
|
|
@ -0,0 +1,172 @@
|
|||
#!/usr/bin/env sh
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS=""
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=$((i+1))
|
||||
done
|
||||
case $i in
|
||||
(0) set -- ;;
|
||||
(1) set -- "$args0" ;;
|
||||
(2) set -- "$args0" "$args1" ;;
|
||||
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=$(save "$@")
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
|
||||
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
|
||||
cd "$(dirname "$0")"
|
||||
fi
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
84
src/server/android/driver/gradlew.bat
vendored
Normal file
84
src/server/android/driver/gradlew.bat
vendored
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS=
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windows variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
2
src/server/android/driver/settings.gradle
Normal file
2
src/server/android/driver/settings.gradle
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
include ':app'
|
||||
rootProject.name = "Playwright Android Driver"
|
||||
|
|
@ -1,160 +0,0 @@
|
|||
/**
|
||||
* Copyright Microsoft Corporation. All rights reserved.
|
||||
*
|
||||
* 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 debug from 'debug';
|
||||
import { EventEmitter } from 'events';
|
||||
import * as stream from 'stream';
|
||||
import * as ws from 'ws';
|
||||
import { createGuid, makeWaitForNextTask } from '../../utils/utils';
|
||||
|
||||
export interface Backend {
|
||||
devices(): Promise<DeviceBackend[]>;
|
||||
}
|
||||
|
||||
export interface DeviceBackend {
|
||||
close(): Promise<void>;
|
||||
init(): Promise<void>;
|
||||
runCommand(command: string): Promise<string>;
|
||||
open(command: string): Promise<SocketBackend>;
|
||||
}
|
||||
|
||||
export interface SocketBackend extends EventEmitter {
|
||||
write(data: Buffer): Promise<void>;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
export class AndroidClient {
|
||||
backend: Backend;
|
||||
|
||||
constructor(backend: Backend) {
|
||||
this.backend = backend;
|
||||
}
|
||||
|
||||
async devices(): Promise<AndroidDevice[]> {
|
||||
const devices = await this.backend.devices();
|
||||
return devices.map(b => new AndroidDevice(b));
|
||||
}
|
||||
}
|
||||
|
||||
export class AndroidDevice {
|
||||
readonly backend: DeviceBackend;
|
||||
private _model: string | undefined;
|
||||
|
||||
constructor(backend: DeviceBackend) {
|
||||
this.backend = backend;
|
||||
}
|
||||
|
||||
async init() {
|
||||
await this.backend.init();
|
||||
this._model = await this.backend.runCommand('shell:getprop ro.product.model');
|
||||
}
|
||||
|
||||
async close() {
|
||||
await this.backend.close();
|
||||
}
|
||||
|
||||
async launchBrowser(packageName: string): Promise<AndroidBrowser> {
|
||||
debug('pw:android')('Force-stopping', packageName);
|
||||
await this.backend.runCommand(`shell:am force-stop ${packageName}`);
|
||||
|
||||
const socketName = createGuid();
|
||||
const commandLine = `_ --disable-fre --no-default-browser-check --no-first-run --remote-debugging-socket-name=${socketName}`;
|
||||
debug('pw:android')('Starting', packageName, commandLine);
|
||||
await this.backend.runCommand(`shell:echo "${commandLine}" > /data/local/tmp/chrome-command-line`);
|
||||
await this.backend.runCommand(`shell:am start -n ${packageName}/com.google.android.apps.chrome.Main about:blank`);
|
||||
|
||||
debug('pw:android')('Polling for socket', socketName);
|
||||
while (true) {
|
||||
const net = await this.backend.runCommand(`shell:cat /proc/net/unix | grep ${socketName}$`);
|
||||
if (net)
|
||||
break;
|
||||
await new Promise(f => setTimeout(f, 100));
|
||||
}
|
||||
debug('pw:android')('Got the socket, connecting');
|
||||
const browser = new AndroidBrowser(this, packageName, socketName);
|
||||
await browser._open();
|
||||
return browser;
|
||||
}
|
||||
|
||||
model(): string | undefined {
|
||||
return this._model;
|
||||
}
|
||||
}
|
||||
|
||||
export class AndroidBrowser extends EventEmitter {
|
||||
readonly device: AndroidDevice;
|
||||
readonly socketName: string;
|
||||
private _socket: SocketBackend | undefined;
|
||||
private _receiver: stream.Writable;
|
||||
private _waitForNextTask = makeWaitForNextTask();
|
||||
onmessage?: (message: any) => void;
|
||||
onclose?: () => void;
|
||||
private _packageName: string;
|
||||
|
||||
constructor(device: AndroidDevice, packageName: string, socketName: string) {
|
||||
super();
|
||||
this._packageName = packageName;
|
||||
this.device = device;
|
||||
this.socketName = socketName;
|
||||
this._receiver = new (ws as any).Receiver() as stream.Writable;
|
||||
this._receiver.on('message', message => {
|
||||
this._waitForNextTask(() => {
|
||||
if (this.onmessage)
|
||||
this.onmessage(JSON.parse(message));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async _open() {
|
||||
this._socket = await this.device.backend.open(`localabstract:${this.socketName}`);
|
||||
this._socket.on('close', () => {
|
||||
this._waitForNextTask(() => {
|
||||
if (this.onclose)
|
||||
this.onclose();
|
||||
});
|
||||
});
|
||||
await this._socket.write(Buffer.from(`GET /devtools/browser HTTP/1.1\r
|
||||
Upgrade: WebSocket\r
|
||||
Connection: Upgrade\r
|
||||
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r
|
||||
Sec-WebSocket-Version: 13\r
|
||||
\r
|
||||
`));
|
||||
// HTTP Upgrade response.
|
||||
await new Promise(f => this._socket!.once('data', f));
|
||||
|
||||
// Start sending web frame to receiver.
|
||||
this._socket.on('data', data => this._receiver._write(data, 'binary', () => {}));
|
||||
}
|
||||
|
||||
async send(s: any) {
|
||||
await this._socket!.write(encodeWebFrame(JSON.stringify(s)));
|
||||
}
|
||||
|
||||
async close() {
|
||||
await this._socket!.close();
|
||||
await this.device.backend.runCommand(`shell:am force-stop ${this._packageName}`);
|
||||
}
|
||||
}
|
||||
|
||||
function encodeWebFrame(data: string): Buffer {
|
||||
return (ws as any).Sender.frame(Buffer.from(data), {
|
||||
opcode: 1,
|
||||
mask: true,
|
||||
fin: true,
|
||||
readOnly: true
|
||||
})[0];
|
||||
}
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
/**
|
||||
* 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 { BrowserType } from '../browserType';
|
||||
import { Browser, BrowserOptions, BrowserProcess } from '../browser';
|
||||
import * as types from '../types';
|
||||
import { normalizeProxySettings, validateBrowserContextOptions } from '../browserContext';
|
||||
import { Progress } from '../progress';
|
||||
import { ConnectionTransport } from '../transport';
|
||||
import { Env } from '../processLauncher';
|
||||
import { CRBrowser } from '../chromium/crBrowser';
|
||||
import { AndroidBrowser, AndroidClient, AndroidDevice } from './android';
|
||||
import { AdbBackend } from './backendAdb';
|
||||
import { RecentLogsCollector } from '../../utils/debugLogger';
|
||||
|
||||
export class Clank extends BrowserType {
|
||||
async _innerLaunch(progress: Progress, options: types.LaunchOptions, persistent: types.BrowserContextOptions | undefined, protocolLogger: types.ProtocolLogger, userDataDir?: string): Promise<Browser> {
|
||||
options.proxy = options.proxy ? normalizeProxySettings(options.proxy) : undefined;
|
||||
if ((options as any).__testHookBeforeCreateBrowser)
|
||||
await (options as any).__testHookBeforeCreateBrowser();
|
||||
|
||||
// const client = new AndroidClient(new UsbBackend());
|
||||
const client = new AndroidClient(new AdbBackend());
|
||||
const device = (await client.devices())[0];
|
||||
await device.init();
|
||||
const adbBrowser = await device.launchBrowser(options.executablePath || 'com.android.chrome'); // com.chrome.canary
|
||||
const transport = adbBrowser;
|
||||
|
||||
const browserOptions: BrowserOptions = {
|
||||
name: 'clank',
|
||||
slowMo: options.slowMo,
|
||||
persistent,
|
||||
headful: !options.headless,
|
||||
downloadsPath: undefined,
|
||||
browserProcess: new ClankBrowserProcess(device, adbBrowser),
|
||||
proxy: options.proxy,
|
||||
protocolLogger,
|
||||
browserLogsCollector: new RecentLogsCollector(),
|
||||
};
|
||||
if (persistent)
|
||||
validateBrowserContextOptions(persistent, browserOptions);
|
||||
|
||||
const browser = await this._connectToTransport(transport, browserOptions);
|
||||
// We assume no control when using custom arguments, and do not prepare the default context in that case.
|
||||
if (persistent && !options.ignoreAllDefaultArgs)
|
||||
await browser._defaultContext!._loadDefaultContext(progress);
|
||||
return browser;
|
||||
}
|
||||
|
||||
_defaultArgs(options: types.LaunchOptions, isPersistent: boolean, userDataDir: string): string[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
_connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise<Browser> {
|
||||
return CRBrowser.connect(transport, options);
|
||||
}
|
||||
|
||||
_amendEnvironment(env: Env, userDataDir: string, executable: string, browserArguments: string[]): Env {
|
||||
return env;
|
||||
}
|
||||
|
||||
_rewriteStartupError(error: Error): Error {
|
||||
return error;
|
||||
}
|
||||
|
||||
_attemptToGracefullyCloseBrowser(transport: ConnectionTransport): void {
|
||||
}
|
||||
}
|
||||
|
||||
class ClankBrowserProcess implements BrowserProcess {
|
||||
private _device: AndroidDevice;
|
||||
private _browser: AndroidBrowser;
|
||||
|
||||
constructor(device: AndroidDevice, browser: AndroidBrowser) {
|
||||
this._device = device;
|
||||
this._browser = browser;
|
||||
}
|
||||
|
||||
onclose: ((exitCode: number | null, signal: string | null) => void) | undefined;
|
||||
|
||||
async kill(): Promise<void> {
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
await this._browser.close();
|
||||
await this._device.close();
|
||||
}
|
||||
}
|
||||
|
|
@ -14,17 +14,20 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Chromium } from './chromium/chromium';
|
||||
import { Clank } from './clank/clank';
|
||||
import { WebKit } from './webkit/webkit';
|
||||
import { Firefox } from './firefox/firefox';
|
||||
import * as browserPaths from '../utils/browserPaths';
|
||||
import { Android } from './android/android';
|
||||
import { AdbBackend } from './android/backendAdb';
|
||||
import { Chromium } from './chromium/chromium';
|
||||
import { Electron } from './electron/electron';
|
||||
import { Firefox } from './firefox/firefox';
|
||||
import { serverSelectors } from './selectors';
|
||||
import { WebKit } from './webkit/webkit';
|
||||
|
||||
export class Playwright {
|
||||
readonly selectors = serverSelectors;
|
||||
readonly chromium: Chromium;
|
||||
readonly clank: Clank;
|
||||
readonly android: Android;
|
||||
readonly electron: Electron;
|
||||
readonly firefox: Firefox;
|
||||
readonly webkit: WebKit;
|
||||
|
||||
|
|
@ -38,10 +41,7 @@ export class Playwright {
|
|||
const webkit = browsers.find(browser => browser.name === 'webkit');
|
||||
this.webkit = new WebKit(packagePath, webkit!);
|
||||
|
||||
this.clank = new Clank(packagePath, {
|
||||
name: 'clank',
|
||||
revision: '0',
|
||||
download: false
|
||||
});
|
||||
this.electron = new Electron();
|
||||
this.android = new Android(new AdbBackend());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,15 +39,15 @@ it('should scope context handles', async ({browser, server}) => {
|
|||
const GOLDEN_PRECONDITION = {
|
||||
_guid: '',
|
||||
objects: [
|
||||
{ _guid: 'Android', objects: [] },
|
||||
{ _guid: 'BrowserType', objects: [] },
|
||||
{ _guid: 'BrowserType', objects: [] },
|
||||
{ _guid: 'BrowserType', objects: [
|
||||
{ _guid: 'Browser', objects: [] }
|
||||
] },
|
||||
{ _guid: 'BrowserType', objects: [] },
|
||||
{ _guid: 'BrowserType', objects: [] },
|
||||
{ _guid: 'BrowserType', objects: [] },
|
||||
{ _guid: 'Electron', objects: [] },
|
||||
{ _guid: 'Playwright', objects: [] },
|
||||
{ _guid: 'Selectors', objects: [] },
|
||||
{ _guid: 'Electron', objects: [] },
|
||||
]
|
||||
};
|
||||
await expectScopeState(browser, GOLDEN_PRECONDITION);
|
||||
|
|
@ -58,7 +58,7 @@ it('should scope context handles', async ({browser, server}) => {
|
|||
await expectScopeState(browser, {
|
||||
_guid: '',
|
||||
objects: [
|
||||
{ _guid: 'BrowserType', objects: [] },
|
||||
{ _guid: 'Android', objects: [] },
|
||||
{ _guid: 'BrowserType', objects: [] },
|
||||
{ _guid: 'BrowserType', objects: [] },
|
||||
{ _guid: 'BrowserType', objects: [
|
||||
|
|
@ -72,9 +72,9 @@ it('should scope context handles', async ({browser, server}) => {
|
|||
]},
|
||||
] },
|
||||
] },
|
||||
{ _guid: 'Electron', objects: [] },
|
||||
{ _guid: 'Playwright', objects: [] },
|
||||
{ _guid: 'Selectors', objects: [] },
|
||||
{ _guid: 'Electron', objects: [] },
|
||||
]
|
||||
});
|
||||
|
||||
|
|
@ -88,15 +88,15 @@ it('should scope CDPSession handles', (test, { browserName }) => {
|
|||
const GOLDEN_PRECONDITION = {
|
||||
_guid: '',
|
||||
objects: [
|
||||
{ _guid: 'Android', objects: [] },
|
||||
{ _guid: 'BrowserType', objects: [] },
|
||||
{ _guid: 'BrowserType', objects: [] },
|
||||
{ _guid: 'BrowserType', objects: [
|
||||
{ _guid: 'Browser', objects: [] }
|
||||
] },
|
||||
{ _guid: 'BrowserType', objects: [] },
|
||||
{ _guid: 'BrowserType', objects: [] },
|
||||
{ _guid: 'BrowserType', objects: [] },
|
||||
{ _guid: 'Electron', objects: [] },
|
||||
{ _guid: 'Playwright', objects: [] },
|
||||
{ _guid: 'Selectors', objects: [] },
|
||||
{ _guid: 'Electron', objects: [] },
|
||||
]
|
||||
};
|
||||
await expectScopeState(browserType, GOLDEN_PRECONDITION);
|
||||
|
|
@ -105,17 +105,17 @@ it('should scope CDPSession handles', (test, { browserName }) => {
|
|||
await expectScopeState(browserType, {
|
||||
_guid: '',
|
||||
objects: [
|
||||
{ _guid: 'Android', objects: [] },
|
||||
{ _guid: 'BrowserType', objects: [] },
|
||||
{ _guid: 'BrowserType', objects: [] },
|
||||
{ _guid: 'BrowserType', objects: [
|
||||
{ _guid: 'Browser', objects: [
|
||||
{ _guid: 'CDPSession', objects: [] },
|
||||
] },
|
||||
] },
|
||||
{ _guid: 'BrowserType', objects: [] },
|
||||
{ _guid: 'BrowserType', objects: [] },
|
||||
{ _guid: 'BrowserType', objects: [] },
|
||||
{ _guid: 'Electron', objects: [] },
|
||||
{ _guid: 'Playwright', objects: [] },
|
||||
{ _guid: 'Selectors', objects: [] },
|
||||
{ _guid: 'Electron', objects: [] },
|
||||
]
|
||||
});
|
||||
|
||||
|
|
@ -127,16 +127,16 @@ it('should scope browser handles', async ({browserType, browserOptions}) => {
|
|||
const GOLDEN_PRECONDITION = {
|
||||
_guid: '',
|
||||
objects: [
|
||||
{ _guid: 'BrowserType', objects: [] },
|
||||
{ _guid: 'Android', objects: [] },
|
||||
{ _guid: 'BrowserType', objects: [] },
|
||||
{ _guid: 'BrowserType', objects: [
|
||||
{ _guid: 'Browser', objects: [] },
|
||||
]
|
||||
},
|
||||
{ _guid: 'BrowserType', objects: [] },
|
||||
{ _guid: 'Electron', objects: [] },
|
||||
{ _guid: 'Playwright', objects: [] },
|
||||
{ _guid: 'Selectors', objects: [] },
|
||||
{ _guid: 'Electron', objects: [] },
|
||||
]
|
||||
};
|
||||
await expectScopeState(browserType, GOLDEN_PRECONDITION);
|
||||
|
|
@ -146,6 +146,9 @@ it('should scope browser handles', async ({browserType, browserOptions}) => {
|
|||
await expectScopeState(browserType, {
|
||||
_guid: '',
|
||||
objects: [
|
||||
{ _guid: 'Android', objects: [] },
|
||||
{ _guid: 'BrowserType', objects: [] },
|
||||
{ _guid: 'BrowserType', objects: [] },
|
||||
{ _guid: 'BrowserType', objects: [
|
||||
{ _guid: 'Browser', objects: [] },
|
||||
{
|
||||
|
|
@ -155,12 +158,9 @@ it('should scope browser handles', async ({browserType, browserOptions}) => {
|
|||
},
|
||||
]
|
||||
},
|
||||
{ _guid: 'BrowserType', objects: [] },
|
||||
{ _guid: 'BrowserType', objects: [] },
|
||||
{ _guid: 'BrowserType', objects: [] },
|
||||
{ _guid: 'Electron', objects: [] },
|
||||
{ _guid: 'Playwright', objects: [] },
|
||||
{ _guid: 'Selectors', objects: [] },
|
||||
{ _guid: 'Electron', objects: [] },
|
||||
]
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ describe('electron app', (suite, { browserName }) => {
|
|||
}, () => {
|
||||
it('should fire close event', async ({ playwright }) => {
|
||||
const electronPath = path.join(__dirname, '..', '..', 'node_modules', '.bin', electronName);
|
||||
const application = await playwright.electron.launch(electronPath, {
|
||||
const application = await playwright._electron.launch(electronPath, {
|
||||
args: [path.join(__dirname, 'testApp.js')],
|
||||
});
|
||||
const events = [];
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ const fixtures = base.extend<TestState>();
|
|||
|
||||
fixtures.application.init(async ({ playwright }, run) => {
|
||||
const electronPath = path.join(__dirname, '..', '..', 'node_modules', '.bin', electronName);
|
||||
const application = await playwright.electron.launch(electronPath, {
|
||||
const application = await playwright._electron.launch(electronPath, {
|
||||
args: [path.join(__dirname, 'testApp.js')],
|
||||
});
|
||||
await run(application);
|
||||
|
|
@ -44,5 +44,5 @@ fixtures.window.init(async ({ application }, run) => {
|
|||
export const folio = fixtures.build();
|
||||
|
||||
declare module '../../index' {
|
||||
const electron: ElectronLauncher;
|
||||
const _electron: ElectronLauncher;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,6 +56,8 @@ async function checkDeps() {
|
|||
}
|
||||
|
||||
function allowImport(from, to) {
|
||||
if (!to.startsWith('src' + path.sep))
|
||||
return true;
|
||||
from = from.substring(from.indexOf('src' + path.sep)).replace(/\\/g, '/');
|
||||
to = to.substring(to.indexOf('src' + path.sep)).replace(/\\/g, '/');
|
||||
const fromDirectory = from.substring(0, from.lastIndexOf('/') + 1);
|
||||
|
|
@ -111,10 +113,11 @@ DEPS['src/server/common/'] = [];
|
|||
DEPS['src/server/injected/'] = ['src/server/common/'];
|
||||
|
||||
// Electron and Clank use chromium internally.
|
||||
DEPS['src/server/android/'] = [...DEPS['src/server/'], 'src/server/chromium/', 'src/protocol/transport.ts'];
|
||||
DEPS['src/server/electron/'] = [...DEPS['src/server/'], 'src/server/chromium/'];
|
||||
DEPS['src/server/clank/'] = [...DEPS['src/server/'], 'src/server/chromium/'];
|
||||
|
||||
DEPS['src/server/playwright.ts'] = [...DEPS['src/server/'], 'src/server/chromium/', 'src/server/webkit/', 'src/server/firefox/', 'src/server/clank/'];
|
||||
DEPS['src/server/playwright.ts'] = [...DEPS['src/server/'], 'src/server/chromium/', 'src/server/webkit/', 'src/server/firefox/', 'src/server/android/', 'src/server/electron/'];
|
||||
DEPS['src/driver.ts'] = DEPS['src/inprocess.ts'] = DEPS['src/browserServerImpl.ts'] = ['src/**'];
|
||||
|
||||
// Tracing is a client/server plugin, nothing should depend on it.
|
||||
|
|
|
|||
Loading…
Reference in a new issue