diff --git a/docs/src/api/class-android.md b/docs/src/api/class-android.md new file mode 100644 index 0000000000..eafb50258f --- /dev/null +++ b/docs/src/api/class-android.md @@ -0,0 +1,81 @@ +# class: Android +* langs: js + +Playwright has **experimental** support for Android automation. You can access android namespace via: + +```js +const { _android } = require('playwright'); +``` + +An example of the Android automation script would be: + +```js +const { _android } = require('playwright'); + +(async () => { + // Connect to the device. + const [device] = await playwright._android.devices(); + console.log(`Model: ${device.model()}`); + console.log(`Serial: ${device.serial()}`); + // Take screenshot of the whole device. + await device.screenshot({ path: 'device.png' }); + + { + // --------------------- WebView ----------------------- + + // Launch an application with WebView. + await device.shell('am force-stop org.chromium.webview_shell'); + await device.shell('am start org.chromium.webview_shell/.WebViewBrowserActivity'); + // Get the WebView. + const webview = await device.webView({ pkg: 'org.chromium.webview_shell' }); + + // Fill the input box. + await device.fill({ res: 'org.chromium.webview_shell:id/url_field' }, 'github.com/microsoft/playwright'); + await device.press({ res: 'org.chromium.webview_shell:id/url_field' }, 'Enter'); + + // Work with WebView's page as usual. + const page = await webview.page(); + await page.page.waitForNavigation({ url: /.*microsoft\/playwright.*/ }); + console.log(await page.title()); + } + + { + // --------------------- Browser ----------------------- + + // Launch Chrome browser. + await device.shell('am force-stop com.android.chrome'); + const context = await device.launchBrowser(); + + // Use BrowserContext as usual. + const page = await context.newPage(); + await page.goto('https://webkit.org/'); + console.log(await page.evaluate(() => window.location.href)); + await page.screenshot({ path: 'page.png' }); + + await context.close(); + } + + // Close the device. + await device.close(); +})(); +``` + +Note that since you don't need Playwright to install web browsers when testing Android, you can omit browser download via setting the following environment variable when installing Playwright: + +```sh js +$ PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm i -D playwright +``` + +## async method: Android.devices +- returns: <[Array]<[AndroidDevice]>> + +Returns the list of detected Android devices. + +## method: Android.setDefaultTimeout + +This setting will change the default maximum time for all the methods accepting [`param: timeout`] option. + +### param: Android.setDefaultTimeout.timeout +- `timeout` <[float]> + +Maximum time in milliseconds diff --git a/docs/src/api/class-androiddevice.md b/docs/src/api/class-androiddevice.md new file mode 100644 index 0000000000..50c2db9355 --- /dev/null +++ b/docs/src/api/class-androiddevice.md @@ -0,0 +1,368 @@ +# class: AndroidDevice +* langs: js + +[AndroidDevice] represents a connected device, either real hardware or emulated. Devices can be obtained using [`method: Android.devices`]. + +## event: AndroidDevice.webView +- type: <[AndroidWebView]> + +Emitted when a new WebView instance is detected. + +## async method: AndroidDevice.close + +Disconnects from the device. + +## async method: AndroidDevice.drag + +Drags the widget defined by [`param: selector`] towards [`param: dest`] point. + +### param: AndroidDevice.drag.selector +- `selector` <[AndroidSelector]> + +Selector to drag. + +### param: AndroidDevice.drag.dest +- `dest` <[Object]> + - `x` <[float]> + - `y` <[float]> + +Point to drag to. + +### option: AndroidDevice.drag.speed +- `speed` <[float]> + +Optional speed of the drag in pixels per second. + +### option: AndroidDevice.drag.timeout = %%-android-timeout-%% + +## async method: AndroidDevice.fill + +Fills the specific [`param: selector`] input box with [`param: text`]. + +### param: AndroidDevice.fill.selector +- `selector` <[AndroidSelector]> + +Selector to fill. + +### param: AndroidDevice.fill.text +- `text` <[string]> + +Text to be filled in the input box. + +### option: AndroidDevice.fill.timeout = %%-android-timeout-%% + +## async method: AndroidDevice.fling + +Flings the widget defined by [`param: selector`] in the specified [`param: direction`]. + +### param: AndroidDevice.fling.selector +- `selector` <[AndroidSelector]> + +Selector to fling. + +### param: AndroidDevice.fling.direction +- `direction` <[AndroidFlingDirection]<"down"|"up"|"left"|"right">> + +Fling direction. + +### option: AndroidDevice.fling.speed +- `speed` <[float]> + +Optional speed of the fling in pixels per second. + +### option: AndroidDevice.fling.timeout = %%-android-timeout-%% + +## async method: AndroidDevice.info +- returns: <[AndroidElementInfo]> + +Returns information about a widget defined by [`param: selector`]. + +### param: AndroidDevice.info.selector +- `selector` <[AndroidSelector]> + +Selector to return information about. + +## property: AndroidDevice.input +- type: <[AndroidInput]> + +## async method: AndroidDevice.installApk + +Installs an apk on the device. + +### param: AndroidDevice.installApk.file +- `file` <[string]|[Buffer]> + +Either a path to the apk file, or apk file content. + +### option: AndroidDevice.installApk.args +- `args` <[Array]<[string]>> + +Optional arguments to pass to the `shell:cmd package install` call. Defaults to `-r -t -S`. + +## async method: AndroidDevice.launchBrowser +- returns: <[ChromiumBrowserContext]> + +Launches Chrome browser on the device, and returns its persistent context. + +### option: AndroidDevice.launchBrowser.pkg +- `command` <[string]> + +Optional package name to launch instead of default Chrome for Android. + +### option: AndroidDevice.launchBrowser.-inline- = %%-shared-context-params-list-%% + +## async method: AndroidDevice.longTap + +Performs a long tap on the widget defined by [`param: selector`]. + +### param: AndroidDevice.longTap.selector +- `selector` <[AndroidSelector]> + +Selector to tap on. + +### option: AndroidDevice.longTap.timeout = %%-android-timeout-%% + +## method: AndroidDevice.model +- returns: <[string]> + +Device model. + +## async method: AndroidDevice.open +- returns: <[AndroidSocket]> + +Launches a process in the shell on the device and returns a socket to communicate with the launched process. + +### param: AndroidDevice.open.command +- `command` <[string]> Shell command to execute. + + +## async method: AndroidDevice.pinchClose + +Pinches the widget defined by [`param: selector`] in the closing direction. + +### param: AndroidDevice.pinchClose.selector +- `selector` <[AndroidSelector]> + +Selector to pinch close. + +### param: AndroidDevice.pinchClose.percent +- `percent` <[float]> + +The size of the pinch as a percentage of the widget's size. + +### option: AndroidDevice.pinchClose.speed +- `speed` <[float]> + +Optional speed of the pinch in pixels per second. + +### option: AndroidDevice.pinchClose.timeout = %%-android-timeout-%% + +## async method: AndroidDevice.pinchOpen + +Pinches the widget defined by [`param: selector`] in the open direction. + +### param: AndroidDevice.pinchOpen.selector +- `selector` <[AndroidSelector]> + +Selector to pinch open. + +### param: AndroidDevice.pinchOpen.percent +- `percent` <[float]> + +The size of the pinch as a percentage of the widget's size. + +### option: AndroidDevice.pinchOpen.speed +- `speed` <[float]> + +Optional speed of the pinch in pixels per second. + +### option: AndroidDevice.pinchOpen.timeout = %%-android-timeout-%% + +## async method: AndroidDevice.press + +Presses the specific [`param: key`] in the widget defined by [`param: selector`]. + +### param: AndroidDevice.press.selector +- `selector` <[AndroidSelector]> + +Selector to press the key in. + +### param: AndroidDevice.press.key +- `key` <[AndroidKey]> + +The key to press. + +### option: AndroidDevice.press.timeout = %%-android-timeout-%% + +## async method: AndroidDevice.push + +Copies a file to the device. + +### param: AndroidDevice.push.file +- `file` <[string]|[Buffer]> + +Either a path to the file, or file content. + +### param: AndroidDevice.push.path +- `path` <[string]> + +Path to the file on the device. + +### option: AndroidDevice.push.mode +- `mode` <[int]> + +Optional file mode, defaults to `644` (`rw-r--r--`). + +## async method: AndroidDevice.screenshot +- returns: <[Buffer]> + +Returns the buffer with the captured screenshot of the device. + +### option: AndroidDevice.screenshot.path +- `path` <[path]> + +The file path to save the image to. If [`option: path`] is a +relative path, then it is resolved relative to the current working directory. If no path is provided, the image won't be +saved to the disk. + +## async method: AndroidDevice.scroll + +Scrolls the widget defined by [`param: selector`] in the specified [`param: direction`]. + +### param: AndroidDevice.scroll.selector +- `selector` <[AndroidSelector]> + +Selector to scroll. + +### param: AndroidDevice.scroll.direction +- `direction` <[AndroidScrollDirection]<"down"|"up"|"left"|"right">> + +Scroll direction. + +### param: AndroidDevice.scroll.percent +- `percent` <[float]> + +Distance to scroll as a percentage of the widget's size. + +### option: AndroidDevice.scroll.speed +- `speed` <[float]> + +Optional speed of the scroll in pixels per second. + +### option: AndroidDevice.scroll.timeout = %%-android-timeout-%% + +## method: AndroidDevice.serial +- returns: <[string]> + +Device serial number. + +## method: AndroidDevice.setDefaultTimeout + +This setting will change the default maximum time for all the methods accepting [`param: timeout`] option. + +### param: AndroidDevice.setDefaultTimeout.timeout +- `timeout` <[float]> + +Maximum time in milliseconds + +## async method: AndroidDevice.shell +- returns: <[Buffer]> + +Executes a shell command on the device and returns its output. + +### param: AndroidDevice.shell.command +- `command` <[string]> + +Shell command to execute. + + +## async method: AndroidDevice.swipe + +Swipes the widget defined by [`param: selector`] in the specified [`param: direction`]. + +### param: AndroidDevice.swipe.selector +- `selector` <[AndroidSelector]> + +Selector to swipe. + +### param: AndroidDevice.swipe.direction +- `direction` <[AndroidSwipeDirection]<"down"|"up"|"left"|"right">> + +Swipe direction. + +### param: AndroidDevice.swipe.percent +- `percent` <[float]> + +Distance to swipe as a percentage of the widget's size. + +### option: AndroidDevice.swipe.speed +- `speed` <[float]> + +Optional speed of the swipe in pixels per second. + +### option: AndroidDevice.swipe.timeout = %%-android-timeout-%% + +## async method: AndroidDevice.tap + +Taps on the widget defined by [`param: selector`]. + +### param: AndroidDevice.tap.selector +- `selector` <[AndroidSelector]> + +Selector to tap on. + +### option: AndroidDevice.tap.duration +- `duration` <[float]> + +Optional duration of the tap in milliseconds. + +### option: AndroidDevice.tap.timeout = %%-android-timeout-%% + +## async method: AndroidDevice.wait + +Waits for the specific [`param: selector`] to either appear or disappear, depending on the [`option: state`]. + +### param: AndroidDevice.wait.selector +- `selector` <[AndroidSelector]> + +Selector to wait for. + +### option: AndroidDevice.wait.state +- `state` <"gone"> + +Optional state. Can be either: +* default - wait for element to be present. +* `'gone'` - wait for element to not be present. + +### option: AndroidDevice.wait.timeout = %%-android-timeout-%% + +## async method: AndroidDevice.waitForEvent +- returns: <[any]> + +Waits for event to fire and passes its value into the predicate function. Returns when the predicate returns truthy value. + +### param: AndroidDevice.waitForEvent.event = %%-wait-for-event-event-%% + +### param: AndroidDevice.waitForEvent.optionsOrPredicate +- `optionsOrPredicate` <[function]|[Object]> + - `predicate` <[function]> receives the event data and resolves to truthy value when the waiting should resolve. + - `timeout` <[float]> maximum time to wait for in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to + disable timeout. The default value can be changed by using the [`method: AndroidDevice.setDefaultTimeout`]. + +Either a predicate that receives an event or an options object. Optional. + +## async method: AndroidDevice.webView +- returns: <[AndroidWebView]> + +This method waits until [AndroidWebView] matching the [`option: selector`] is opened and returns it. If there is already an open [AndroidWebView] matching the [`option: selector`], returns immediately. + +### param: AndroidDevice.webView.selector +- `selector` <[Object]> + - `pkg` <[string]> Package identifier. + +### option: AndroidDevice.webView.timeout = %%-android-timeout-%% + +## method: AndroidDevice.webViews +- returns: <[Array]<[AndroidWebView]>> + +Currently open WebViews. diff --git a/docs/src/api/class-androidinput.md b/docs/src/api/class-androidinput.md new file mode 100644 index 0000000000..1e22561000 --- /dev/null +++ b/docs/src/api/class-androidinput.md @@ -0,0 +1,78 @@ +# class: AndroidInput +* langs: js + +## async method: AndroidInput.drag + +Performs a drag between [`param: from`] and [`param: to`] points. + +### param: AndroidInput.drag.from +- `from` <[Object]> + - `x` <[float]> + - `y` <[float]> + +The start point of the drag. + +### param: AndroidInput.drag.to +- `to` <[Object]> + - `x` <[float]> + - `y` <[float]> + +The end point of the drag. + +### param: AndroidInput.drag.steps +- `steps` <[int]> + +The number of steps in the drag. Each step takes 5 milliseconds to complete. + +## async method: AndroidInput.press + +Presses the [`param: key`]. + +### param: AndroidInput.press.key +- `key` <[AndroidKey]> + +Key to press. + + +## async method: AndroidInput.swipe + +Swipes following the path defined by [`param: segments`]. + +### param: AndroidInput.swipe.from +- `from` <[Object]> + - `x` <[float]> + - `y` <[float]> + +The point to start swiping from. + +### param: AndroidInput.swipe.segments +- `segments` <[Array]<[Object]>> + - `x` <[float]> + - `y` <[float]> + +Points following the [`param: from`] point in the swipe gesture. + +### param: AndroidInput.swipe.steps +- `steps` <[int]> + +The number of steps for each segment. Each step takes 5 milliseconds to complete, so 100 steps means half a second per each segment. + +## async method: AndroidInput.tap + +Taps at the specified [`param: point`]. + +### param: AndroidInput.tap.point +- `point` <[Object]> + - `x` <[float]> + - `y` <[float]> + +The point to tap at. + +## async method: AndroidInput.type + +Types [`param: text`] into currently focused widget. + +### param: AndroidInput.type.text +- `text` <[string]> + +Text to type. diff --git a/docs/src/api/class-androidsocket.md b/docs/src/api/class-androidsocket.md new file mode 100644 index 0000000000..b63e760eda --- /dev/null +++ b/docs/src/api/class-androidsocket.md @@ -0,0 +1,26 @@ +# class: AndroidSocket +* langs: js + +[AndroidSocket] is a way to communicate with a process launched on the [AndroidDevice]. Use [`method: AndroidDevice.open`] to open a socket. + +## event: AndroidSocket.close + +Emitted when the socket is closed. + +## event: AndroidSocket.data +- type: <[Buffer]> + +Emitted when data is available to read from the socket. + +## async method: AndroidSocket.close + +Closes the socket. + +## async method: AndroidSocket.write + +Writes some [`param: data`] to the socket. + +### param: AndroidSocket.write.data +- `data` <[Buffer]> + +Data to write. diff --git a/docs/src/api/class-androidwebview.md b/docs/src/api/class-androidwebview.md new file mode 100644 index 0000000000..c1bcb16721 --- /dev/null +++ b/docs/src/api/class-androidwebview.md @@ -0,0 +1,23 @@ +# class: AndroidWebView +* langs: js + +[AndroidWebView] represents a WebView open on the [AndroidDevice]. WebView is usually obtained using [`method: AndroidDevice.webView`]. + +## event: AndroidWebView.close + +Emitted when the WebView is closed. + +## async method: AndroidWebView.page +- returns: <[Page]> + +Connects to the WebView and returns a regular Playwright [Page] to interact with. + +## method: AndroidWebView.pid +- returns: <[int]> + +WebView process PID. + +## method: AndroidWebView.pkg +- returns: <[string]> + +WebView package identifier. diff --git a/docs/src/api/params.md b/docs/src/api/params.md index f5e868c05d..0a3c70850c 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -498,6 +498,13 @@ Receives the event data and resolves to truthy value when the waiting should res Maximum time to wait for in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default value can be changed by using the [`method: BrowserContext.setDefaultTimeout`]. +## android-timeout +* langs: js +- `timeout` <[float]> + +Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by +using the [`method: AndroidDevice.setDefaultTimeout`] method. + ## shared-context-params-list - %%-context-option-acceptdownloads-%% - %%-context-option-ignorehttpserrors-%% diff --git a/docs/src/mobile.md b/docs/src/mobile.md index 960f2fc310..35c09d1859 100644 --- a/docs/src/mobile.md +++ b/docs/src/mobile.md @@ -8,38 +8,71 @@ title: "Mobile (experimental)" Mobile support is experimental and uses prefixed provisional API. ::: -You can try Playwright against Chrome for Android today. This support is experimental. Support for devices is tracked in the issue [#1122](https://github.com/microsoft/playwright/issues/1122). +You can try Playwright against Android, Chrome for Android and Android WebView today. This support is experimental. Support for devices is tracked in the issue [#1122](https://github.com/microsoft/playwright/issues/1122). + +See [Android] for documentation. ## Requirements -- [ADB daemon](https://developer.android.com/studio/command-line/adb) running and authenticated with your device. +- 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`. - > Playwright will be looking for ADB daemon on the default port `5037`. It will use the first device available. Typically running `adb devices` is all you need to do. - ## How to run ```js -const { _clank } = require('playwright'); +const { _android } = require('playwright'); (async () => { - const context = await _clank.launchPersistentContext('', { - viewport: null - }); - const [page] = context.pages(); - await page.goto('https://webkit.org/'); - console.log(await page.evaluate(() => window.location.href)); - await page.screenshot({ path: 'example.png' }); - await context.close(); + // Connect to the device. + const [device] = await playwright._android.devices(); + console.log(`Model: ${device.model()}`); + console.log(`Serial: ${device.serial()}`); + // Take screenshot of the whole device. + await device.screenshot({ path: 'device.png' }); + + { + // --------------------- WebView ----------------------- + + // Launch an application with WebView. + await device.shell('am force-stop org.chromium.webview_shell'); + await device.shell('am start org.chromium.webview_shell/.WebViewBrowserActivity'); + // Get the WebView. + const webview = await device.webView({ pkg: 'org.chromium.webview_shell' }); + + // Fill the input box. + await device.fill({ res: 'org.chromium.webview_shell:id/url_field' }, 'github.com/microsoft/playwright'); + await device.press({ res: 'org.chromium.webview_shell:id/url_field' }, 'Enter'); + + // Work with WebView's page as usual. + const page = await webview.page(); + await page.page.waitForNavigation({ url: /.*microsoft\/playwright.*/ }); + console.log(await page.title()); + } + + { + // --------------------- Browser ----------------------- + + // Launch Chrome browser. + await device.shell('am force-stop com.android.chrome'); + const context = await device.launchBrowser(); + + // Use BrowserContext as usual. + const page = await context.newPage(); + await page.goto('https://webkit.org/'); + console.log(await page.evaluate(() => window.location.href)); + await page.screenshot({ path: 'page.png' }); + + await context.close(); + } + + // Close the device. + await device.close(); })(); ``` -> [Clank](https://chromium.googlesource.com/chromium/src/+/master/docs/memory/android_dev_tips.md) is a code name for Chrome for Android. - ## Known limitations - Raw USB operation is not yet supported, so you need ADB. -- Only `launchPersistentContext` works, launching ephemeral contexts is not supported. -- Passing `viewport: null` is necessary to make sure resolution is not emulated. - Device needs to be awake to produce screenshots. Enabling "Stay awake" developer mode will help. - We didn't run all the tests against the device, so not everything works. diff --git a/index.d.ts b/index.d.ts index a5ca210dbf..4c0c836d4d 100644 --- a/index.d.ts +++ b/index.d.ts @@ -21,3 +21,4 @@ export const webkit: types.BrowserType; export const chromium: types.BrowserType; export const firefox: types.BrowserType; export const _electron: types.Electron; +export const _android: types.Android; diff --git a/packages/playwright-android/index.d.ts b/packages/playwright-android/index.d.ts index 2e11a85a6c..e12e3c245f 100644 --- a/packages/playwright-android/index.d.ts +++ b/packages/playwright-android/index.d.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { Android } from './types/android'; +import { Android } from './types/types'; export * from './types/types'; -export * from './types/android'; export const android: Android; diff --git a/src/client/android.ts b/src/client/android.ts index 6b15a19584..63b5d19743 100644 --- a/src/client/android.ts +++ b/src/client/android.ts @@ -21,7 +21,7 @@ import * as channels from '../protocol/channels'; import { Events } from './events'; import { BrowserContext, prepareBrowserContextOptions } from './browserContext'; import { ChannelOwner } from './channelOwner'; -import * as androidApi from '../../types/android'; +import * as api from '../../types/types'; import * as types from './types'; import { Page } from './page'; import { TimeoutSettings } from '../utils/timeoutSettings'; @@ -29,10 +29,10 @@ import { Waiter } from './waiter'; import { EventEmitter } from 'events'; import { ChromiumBrowserContext } from './chromiumBrowserContext'; -type Direction = 'down' | 'up' | 'left' | 'right'; +type Direction = 'down' | 'up' | 'left' | 'right'; type SpeedOptions = { speed?: number }; -export class Android extends ChannelOwner implements androidApi.Android { +export class Android extends ChannelOwner implements api.Android { readonly _timeoutSettings: TimeoutSettings; static from(android: channels.AndroidChannel): Android { @@ -57,7 +57,7 @@ export class Android extends ChannelOwner implements androidApi.AndroidDevice { +export class AndroidDevice extends ChannelOwner implements api.AndroidDevice { readonly _timeoutSettings: TimeoutSettings; private _webViews = new Map(); @@ -65,11 +65,11 @@ export class AndroidDevice extends ChannelOwner this._onWebViewAdded(webView)); this._channel.on('webViewRemoved', ({ pid }) => this._onWebViewRemoved(pid)); @@ -115,83 +115,77 @@ export class AndroidDevice extends ChannelOwner { await this._channel.wait({ selector: toSelectorChannel(selector), ...options }); }); } - async fill(selector: androidApi.AndroidSelector, text: string, options?: types.TimeoutOptions) { + async fill(selector: api.AndroidSelector, text: string, options?: types.TimeoutOptions) { await this._wrapApiCall('androidDevice.fill', async () => { await this._channel.fill({ selector: toSelectorChannel(selector), text, ...options }); }); } - async press(selector: androidApi.AndroidSelector, key: androidApi.AndroidKey, options?: types.TimeoutOptions) { + async press(selector: api.AndroidSelector, key: api.AndroidKey, options?: types.TimeoutOptions) { await this.tap(selector, options); await this.input.press(key); } - async tap(selector: androidApi.AndroidSelector, options?: { duration?: number } & types.TimeoutOptions) { + async tap(selector: api.AndroidSelector, options?: { duration?: number } & types.TimeoutOptions) { await this._wrapApiCall('androidDevice.tap', async () => { await this._channel.tap({ selector: toSelectorChannel(selector), ...options }); }); } - async drag(selector: androidApi.AndroidSelector, dest: types.Point, options?: SpeedOptions & types.TimeoutOptions) { + async drag(selector: api.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: androidApi.AndroidSelector, direction: Direction, options?: SpeedOptions & types.TimeoutOptions) { + async fling(selector: api.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: androidApi.AndroidSelector, options?: types.TimeoutOptions) { + async longTap(selector: api.AndroidSelector, options?: types.TimeoutOptions) { await this._wrapApiCall('androidDevice.longTap', async () => { await this._channel.longTap({ selector: toSelectorChannel(selector), ...options }); }); } - async pinchClose(selector: androidApi.AndroidSelector, percent: number, options?: SpeedOptions & types.TimeoutOptions) { + async pinchClose(selector: api.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: androidApi.AndroidSelector, percent: number, options?: SpeedOptions & types.TimeoutOptions) { + async pinchOpen(selector: api.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: androidApi.AndroidSelector, direction: Direction, percent: number, options?: SpeedOptions & types.TimeoutOptions) { + async scroll(selector: api.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: androidApi.AndroidSelector, direction: Direction, percent: number, options?: SpeedOptions & types.TimeoutOptions) { + async swipe(selector: api.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: androidApi.AndroidSelector): Promise { + async info(selector: api.AndroidSelector): Promise { return await this._wrapApiCall('androidDevice.info', async () => { return (await this._channel.info({ selector: toSelectorChannel(selector) })).info; }); } - async tree(): Promise { - return await this._wrapApiCall('androidDevice.tree', async () => { - return (await this._channel.tree()).tree; - }); - } - async screenshot(options: { path?: string } = {}): Promise { return await this._wrapApiCall('androidDevice.screenshot', async () => { const { binary } = await this._channel.screenshot(); @@ -255,7 +249,7 @@ export class AndroidDevice extends ChannelOwner implements androidApi.AndroidSocket { +export class AndroidSocket extends ChannelOwner implements api.AndroidSocket { static from(androidDevice: channels.AndroidSocketChannel): AndroidSocket { return (androidDevice as any)._object; } @@ -285,7 +279,7 @@ async function loadFile(file: string | Buffer): Promise { return file.toString('base64'); } -class Input implements androidApi.AndroidInput { +export class AndroidInput implements api.AndroidInput { private _device: AndroidDevice; constructor(device: AndroidDevice) { @@ -298,7 +292,7 @@ class Input implements androidApi.AndroidInput { }); } - async press(key: androidApi.AndroidKey) { + async press(key: api.AndroidKey) { return this._device._wrapApiCall('androidDevice.inputPress', async () => { await this._device._channel.inputPress({ key }); }); @@ -323,7 +317,7 @@ class Input implements androidApi.AndroidInput { } } -function toSelectorChannel(selector: androidApi.AndroidSelector): channels.AndroidSelector { +function toSelectorChannel(selector: api.AndroidSelector): channels.AndroidSelector { const { checkable, checked, @@ -373,7 +367,7 @@ function toSelectorChannel(selector: androidApi.AndroidSelector): channels.Andro }; } -export class AndroidWebView extends EventEmitter implements androidApi.AndroidWebView { +export class AndroidWebView extends EventEmitter implements api.AndroidWebView { private _device: AndroidDevice; private _data: channels.AndroidWebView; private _pagePromise: Promise | undefined; diff --git a/src/client/api.ts b/src/client/api.ts index 9350833cbe..968ef5a7ab 100644 --- a/src/client/api.ts +++ b/src/client/api.ts @@ -15,6 +15,7 @@ */ export { Accessibility } from './accessibility'; +export { Android, AndroidDevice, AndroidWebView, AndroidInput, AndroidSocket } from './android'; export { Browser } from './browser'; export { BrowserContext } from './browserContext'; export { BrowserServer } from './browserType'; diff --git a/src/dispatchers/androidDispatcher.ts b/src/dispatchers/androidDispatcher.ts index 0d00f4a3c2..9ecb05747c 100644 --- a/src/dispatchers/androidDispatcher.ts +++ b/src/dispatchers/androidDispatcher.ts @@ -100,10 +100,6 @@ export class AndroidDeviceDispatcher extends Dispatcher { - return { tree: await this._object.send('tree', params) }; - } - async inputType(params: channels.AndroidDeviceInputTypeParams) { const text = params.text; const keyCodes: number[] = []; diff --git a/src/protocol/channels.ts b/src/protocol/channels.ts index 1c667b9767..e8ec138629 100644 --- a/src/protocol/channels.ts +++ b/src/protocol/channels.ts @@ -2604,7 +2604,6 @@ export interface AndroidDeviceChannel extends Channel { scroll(params: AndroidDeviceScrollParams, metadata?: Metadata): Promise; swipe(params: AndroidDeviceSwipeParams, metadata?: Metadata): Promise; info(params: AndroidDeviceInfoParams, metadata?: Metadata): Promise; - tree(params?: AndroidDeviceTreeParams, metadata?: Metadata): Promise; screenshot(params?: AndroidDeviceScreenshotParams, metadata?: Metadata): Promise; inputType(params: AndroidDeviceInputTypeParams, metadata?: Metadata): Promise; inputPress(params: AndroidDeviceInputPressParams, metadata?: Metadata): Promise; @@ -2740,11 +2739,6 @@ export type AndroidDeviceInfoOptions = { export type AndroidDeviceInfoResult = { info: AndroidElementInfo, }; -export type AndroidDeviceTreeParams = {}; -export type AndroidDeviceTreeOptions = {}; -export type AndroidDeviceTreeResult = { - tree: AndroidElementInfo, -}; export type AndroidDeviceScreenshotParams = {}; export type AndroidDeviceScreenshotOptions = {}; export type AndroidDeviceScreenshotResult = { diff --git a/src/protocol/protocol.yml b/src/protocol/protocol.yml index c2cf47f291..e46126160b 100644 --- a/src/protocol/protocol.yml +++ b/src/protocol/protocol.yml @@ -2291,10 +2291,6 @@ AndroidDevice: returns: info: AndroidElementInfo - tree: - returns: - tree: AndroidElementInfo - screenshot: returns: binary: binary diff --git a/src/protocol/validator.ts b/src/protocol/validator.ts index 37e95bf7e7..ce965611da 100644 --- a/src/protocol/validator.ts +++ b/src/protocol/validator.ts @@ -1010,7 +1010,6 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { scheme.AndroidDeviceInfoParams = tObject({ selector: tType('AndroidSelector'), }); - scheme.AndroidDeviceTreeParams = tOptional(tObject({})); scheme.AndroidDeviceScreenshotParams = tOptional(tObject({})); scheme.AndroidDeviceInputTypeParams = tObject({ text: tString, diff --git a/test/android/android.fixtures.ts b/test/android/android.fixtures.ts index 8c8fa1746a..8e3ae02901 100644 --- a/test/android/android.fixtures.ts +++ b/test/android/android.fixtures.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Android, AndroidDevice } from '../../types/android'; +import type { Android, AndroidDevice } from '../..'; import { folio as baseFolio } from '../fixtures'; const fixtures = baseFolio.extend<{ diff --git a/test/checkCoverage.js b/test/checkCoverage.js index 8b92af4505..dddee8869d 100644 --- a/test/checkCoverage.js +++ b/test/checkCoverage.js @@ -19,7 +19,7 @@ const {installCoverageHooks} = require('./coverage'); const browserName = process.env.BROWSER || 'chromium'; -const api = new Set(installCoverageHooks(browserName).coverage.keys()); +let api = new Set(installCoverageHooks(browserName).coverage.keys()); // coverage exceptions @@ -38,8 +38,8 @@ if (browserName !== 'chromium') { if (browserName === 'webkit') api.delete('browserContext.clearPermissions'); -// Screencast APIs that are not publicly available. -api.delete('browserContext.emit("screencaststarted")'); +// Android coverage is abysmal. +api = new Set(Array.from(api).filter(name => !name.toLowerCase().startsWith('android'))); const coverageDir = path.join(__dirname, 'coverage-report'); diff --git a/test/coverage.js b/test/coverage.js index cd5267f008..4e7f7bb1d1 100644 --- a/test/coverage.js +++ b/test/coverage.js @@ -29,7 +29,7 @@ function traceAPICoverage(apiCoverage, api, events) { const method = Reflect.get(classType.prototype, methodName); if (methodName === 'constructor' || typeof methodName !== 'string' || methodName.startsWith('_') || typeof method !== 'function') continue; - + apiCoverage.set(`${className}.${methodName}`, false); const override = function(...args) { apiCoverage.set(`${className}.${methodName}`, true); diff --git a/test/fixtures.ts b/test/fixtures.ts index 37c4bb1065..478738f887 100644 --- a/test/fixtures.ts +++ b/test/fixtures.ts @@ -27,7 +27,6 @@ import { installCoverageHooks } from './coverage'; import { folio as httpFolio } from './http.fixtures'; import { folio as playwrightFolio } from './playwright.fixtures'; import { PlaywrightClient } from '../lib/remote/playwrightClient'; -import type { Android } from '../types/android'; export { expect, config } from 'folio'; const removeFolderAsync = util.promisify(require('rimraf')); @@ -190,8 +189,3 @@ export const beforeEach = folio.beforeEach; export const afterEach = folio.afterEach; export const beforeAll = folio.beforeAll; export const afterAll = folio.afterAll; - - -declare module '../index' { - const _android: Android; -} diff --git a/types/android.d.ts b/types/android.d.ts deleted file mode 100644 index eb87e975d9..0000000000 --- a/types/android.d.ts +++ /dev/null @@ -1,161 +0,0 @@ -/** - * 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 { EventEmitter } from 'events'; -import { BrowserContextOptions, Page, ChromiumBrowserContext } from './types'; - -export interface Android extends EventEmitter { - setDefaultTimeout(timeout: number): void; - devices(): Promise; -} - -export interface AndroidDevice extends EventEmitter { - input: AndroidInput; - - setDefaultTimeout(timeout: number): void; - on(event: 'webview', handler: (webView: AndroidWebView) => void): this; - waitForEvent(event: string, optionsOrPredicate?: (data: any) => boolean | { timeout?: number, predicate?: (data: any) => boolean }): Promise; - - serial(): string; - model(): string; - webViews(): AndroidWebView[]; - webView(selector: { pkg: string }, options?: { timeout?: number }): Promise; - shell(command: string): Promise; - open(command: string): Promise; - installApk(file: string | Buffer, options?: { args?: string[] }): Promise; - push(file: string | Buffer, path: string, options?: { mode?: number }): Promise; - launchBrowser(options?: BrowserContextOptions & { pkg?: string }): Promise; - close(): Promise; - - wait(selector: AndroidSelector, options?: { state?: 'gone' } & { timeout?: number }): Promise; - fill(selector: AndroidSelector, text: string, options?: { timeout?: number }): Promise; - press(selector: AndroidSelector, key: AndroidKey, options?: { duration?: number } & { timeout?: number }): Promise; - tap(selector: AndroidSelector, options?: { duration?: number } & { timeout?: number }): Promise; - drag(selector: AndroidSelector, dest: { x: number, y: number }, options?: { speed?: number } & { timeout?: number }): Promise; - fling(selector: AndroidSelector, direction: 'down' | 'up' | 'left' | 'right', options?: { speed?: number } & { timeout?: number }): Promise; - longTap(selector: AndroidSelector, options?: { timeout?: number }): Promise; - pinchClose(selector: AndroidSelector, percent: number, options?: { speed?: number } & { timeout?: number }): Promise; - pinchOpen(selector: AndroidSelector, percent: number, options?: { speed?: number } & { timeout?: number }): Promise; - scroll(selector: AndroidSelector, direction: 'down' | 'up' | 'left' | 'right', percent: number, options?: { speed?: number } & { timeout?: number }): Promise; - swipe(selector: AndroidSelector, direction: 'down' | 'up' | 'left' | 'right', percent: number, options?: { speed?: number } & { timeout?: number }): Promise; - - info(selector: AndroidSelector): Promise; - screenshot(options?: { path?: string }): Promise; -} - -export interface AndroidSocket extends EventEmitter { - on(event: 'data', handler: (data: Buffer) => void): this; - on(event: 'close', handler: () => void): this; - write(data: Buffer): Promise; - close(): Promise; -} - -export interface AndroidInput { - type(text: string): Promise; - press(key: AndroidKey): Promise; - tap(point: { x: number, y: number }): Promise; - swipe(from: { x: number, y: number }, segments: { x: number, y: number }[], steps: number): Promise; - drag(from: { x: number, y: number }, to: { x: number, y: number }, steps: number): Promise; -} - -export interface AndroidWebView extends EventEmitter { - on(event: 'close', handler: () => void): this; - pid(): number; - pkg(): string; - page(): Promise; -} - -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 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'; diff --git a/types/types.d.ts b/types/types.d.ts index 6294d91c88..9a97beb80e 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -7217,10 +7217,909 @@ export interface ElectronApplication { */ windows(): Array;} +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 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'; + // This is required to not export everything by default. See https://github.com/Microsoft/TypeScript/issues/19545#issuecomment-340490459 export {}; +/** + * Playwright has **experimental** support for Android automation. You can access android namespace via: + * + * ```js + * const { _android } = require('playwright'); + * ``` + * + * An example of the Android automation script would be: + * + * ```js + * const { _android } = require('playwright'); + * + * (async () => { + * // Connect to the device. + * const [device] = await playwright._android.devices(); + * console.log(`Model: ${device.model()}`); + * console.log(`Serial: ${device.serial()}`); + * // Take screenshot of the whole device. + * await device.screenshot({ path: 'device.png' }); + * + * { + * // --------------------- WebView ----------------------- + * + * // Launch an application with WebView. + * await device.shell('am force-stop org.chromium.webview_shell'); + * await device.shell('am start org.chromium.webview_shell/.WebViewBrowserActivity'); + * // Get the WebView. + * const webview = await device.webView({ pkg: 'org.chromium.webview_shell' }); + * + * // Fill the input box. + * await device.fill({ res: 'org.chromium.webview_shell:id/url_field' }, 'github.com/microsoft/playwright'); + * await device.press({ res: 'org.chromium.webview_shell:id/url_field' }, 'Enter'); + * + * // Work with WebView's page as usual. + * const page = await webview.page(); + * await page.page.waitForNavigation({ url: /.*microsoft\/playwright.*\/ }); + * console.log(await page.title()); + * } + * + * { + * // --------------------- Browser ----------------------- + * + * // Launch Chrome browser. + * await device.shell('am force-stop com.android.chrome'); + * const context = await device.launchBrowser(); + * + * // Use BrowserContext as usual. + * const page = await context.newPage(); + * await page.goto('https://webkit.org/'); + * console.log(await page.evaluate(() => window.location.href)); + * await page.screenshot({ path: 'page.png' }); + * + * await context.close(); + * } + * + * // Close the device. + * await device.close(); + * })(); + * ``` + * + * Note that since you don't need Playwright to install web browsers when testing Android, you can omit browser download + * via setting the following environment variable when installing Playwright: + * + * ```sh js + * $ PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm i -D playwright + * ``` + * + */ +export interface Android { + /** + * Returns the list of detected Android devices. + */ + devices(): Promise>; + + /** + * This setting will change the default maximum time for all the methods accepting `timeout` option. + * @param timeout Maximum time in milliseconds + */ + setDefaultTimeout(timeout: number): void; +} + +/** + * [AndroidDevice] represents a connected device, either real hardware or emulated. Devices can be obtained using + * [android.devices()](https://playwright.dev/docs/api/class-android#androiddevices). + */ +export interface AndroidDevice { + /** + * Emitted when a new WebView instance is detected. + */ + on(event: 'webview', listener: (androidWebView: AndroidWebView) => void): this; + + /** + * Emitted when a new WebView instance is detected. + */ + once(event: 'webview', listener: (androidWebView: AndroidWebView) => void): this; + + /** + * Emitted when a new WebView instance is detected. + */ + addListener(event: 'webview', listener: (androidWebView: AndroidWebView) => void): this; + + /** + * Emitted when a new WebView instance is detected. + */ + removeListener(event: 'webview', listener: (androidWebView: AndroidWebView) => void): this; + + /** + * Emitted when a new WebView instance is detected. + */ + off(event: 'webview', listener: (androidWebView: AndroidWebView) => void): this; + + /** + * Disconnects from the device. + */ + close(): Promise; + + /** + * Drags the widget defined by `selector` towards `dest` point. + * @param selector Selector to drag. + * @param dest Point to drag to. + * @param options + */ + drag(selector: AndroidSelector, dest: { + x: number; + + y: number; + }, options?: { + /** + * Optional speed of the drag in pixels per second. + */ + speed?: number; + + /** + * Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by + * using the + * [androidDevice.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-androiddevice#androiddevicesetdefaulttimeouttimeout) + * method. + */ + timeout?: number; + }): Promise; + + /** + * Fills the specific `selector` input box with `text`. + * @param selector Selector to fill. + * @param text Text to be filled in the input box. + * @param options + */ + fill(selector: AndroidSelector, text: string, options?: { + /** + * Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by + * using the + * [androidDevice.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-androiddevice#androiddevicesetdefaulttimeouttimeout) + * method. + */ + timeout?: number; + }): Promise; + + /** + * Flings the widget defined by `selector` in the specified `direction`. + * @param selector Selector to fling. + * @param direction Fling direction. + * @param options + */ + fling(selector: AndroidSelector, direction: "down"|"up"|"left"|"right", options?: { + /** + * Optional speed of the fling in pixels per second. + */ + speed?: number; + + /** + * Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by + * using the + * [androidDevice.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-androiddevice#androiddevicesetdefaulttimeouttimeout) + * method. + */ + timeout?: number; + }): Promise; + + /** + * Returns information about a widget defined by `selector`. + * @param selector Selector to return information about. + */ + info(selector: AndroidSelector): Promise; + + input: AndroidInput; + + /** + * Installs an apk on the device. + * @param file Either a path to the apk file, or apk file content. + * @param options + */ + installApk(file: string|Buffer, options?: { + /** + * Optional arguments to pass to the `shell:cmd package install` call. Defaults to `-r -t -S`. + */ + args?: Array; + }): Promise; + + /** + * Launches Chrome browser on the device, and returns its persistent context. + * @param options + */ + launchBrowser(options?: { + /** + * Whether to automatically download all the attachments. Defaults to `false` where all the downloads are canceled. + */ + acceptDownloads?: boolean; + + /** + * Toggles bypassing page's Content-Security-Policy. + */ + bypassCSP?: boolean; + + /** + * Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. See + * [page.emulateMedia([options])](https://playwright.dev/docs/api/class-page#pageemulatemediaoptions) for more details. + * Defaults to '`light`'. + */ + colorScheme?: "light"|"dark"|"no-preference"; + + /** + * Optional package name to launch instead of default Chrome for Android. + */ + command?: string; + + /** + * Specify device scale factor (can be thought of as dpr). Defaults to `1`. + */ + deviceScaleFactor?: number; + + /** + * An object containing additional HTTP headers to be sent with every request. All header values must be strings. + */ + extraHTTPHeaders?: { [key: string]: string; }; + + geolocation?: { + /** + * Latitude between -90 and 90. + */ + latitude: number; + + /** + * Longitude between -180 and 180. + */ + longitude: number; + + /** + * Non-negative accuracy value. Defaults to `0`. + */ + accuracy?: number; + }; + + /** + * Specifies if viewport supports touch events. Defaults to false. + */ + hasTouch?: boolean; + + /** + * Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). + */ + httpCredentials?: { + username: string; + + password: string; + }; + + /** + * Whether to ignore HTTPS errors during navigation. Defaults to `false`. + */ + ignoreHTTPSErrors?: boolean; + + /** + * Whether the `meta viewport` tag is taken into account and touch events are enabled. Defaults to `false`. Not supported + * in Firefox. + */ + isMobile?: boolean; + + /** + * Whether or not to enable JavaScript in the context. Defaults to `true`. + */ + javaScriptEnabled?: boolean; + + /** + * Specify user locale, for example `en-GB`, `de-DE`, etc. Locale will affect `navigator.language` value, `Accept-Language` + * request header value as well as number and date formatting rules. + */ + locale?: string; + + /** + * Logger sink for Playwright logging. + */ + logger?: Logger; + + /** + * Whether to emulate network being offline. Defaults to `false`. + */ + offline?: boolean; + + /** + * A list of permissions to grant to all pages in this context. See + * [browserContext.grantPermissions(permissions[, options])](https://playwright.dev/docs/api/class-browsercontext#browsercontextgrantpermissionspermissions-options) + * for more details. + */ + permissions?: Array; + + /** + * Enables [HAR](http://www.softwareishard.com/blog/har-12-spec) recording for all pages into `recordHar.path` file. If not + * specified, the HAR is not recorded. Make sure to await + * [browserContext.close()](https://playwright.dev/docs/api/class-browsercontext#browsercontextclose) for the HAR to be + * saved. + */ + recordHar?: { + /** + * Optional setting to control whether to omit request content from the HAR. Defaults to `false`. + */ + omitContent?: boolean; + + /** + * Path on the filesystem to write the HAR file to. + */ + path: string; + }; + + /** + * Enables video recording for all pages into `recordVideo.dir` directory. If not specified videos are not recorded. Make + * sure to await [browserContext.close()](https://playwright.dev/docs/api/class-browsercontext#browsercontextclose) for + * videos to be saved. + */ + recordVideo?: { + /** + * Path to the directory to put videos into. + */ + dir: string; + + /** + * Optional dimensions of the recorded videos. If not specified the size will be equal to `viewport` scaled down to fit + * into 800x800. If `viewport` is not configured explicitly the video size defaults to 800x450. Actual picture of each page + * will be scaled down if necessary to fit the specified size. + */ + size?: { + /** + * Video frame width. + */ + width: number; + + /** + * Video frame height. + */ + height: number; + }; + }; + + /** + * Changes the timezone of the context. See + * [ICU's metaZones.txt](https://cs.chromium.org/chromium/src/third_party/icu/source/data/misc/metaZones.txt?rcl=faee8bc70570192d82d2978a71e2a615788597d1) + * for a list of supported timezone IDs. + */ + timezoneId?: string; + + /** + * Specific user agent to use in this context. + */ + userAgent?: string; + + /** + * **DEPRECATED** Use `recordVideo` instead. + * @deprecated + */ + videoSize?: { + /** + * Video frame width. + */ + width: number; + + /** + * Video frame height. + */ + height: number; + }; + + /** + * **DEPRECATED** Use `recordVideo` instead. + * @deprecated + */ + videosPath?: string; + + /** + * Sets a consistent viewport for each page. Defaults to an 1280x720 viewport. `null` disables the default viewport. + */ + viewport?: null|{ + /** + * page width in pixels. + */ + width: number; + + /** + * page height in pixels. + */ + height: number; + }; + }): Promise; + + /** + * Performs a long tap on the widget defined by `selector`. + * @param selector Selector to tap on. + * @param options + */ + longTap(selector: AndroidSelector, options?: { + /** + * Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by + * using the + * [androidDevice.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-androiddevice#androiddevicesetdefaulttimeouttimeout) + * method. + */ + timeout?: number; + }): Promise; + + /** + * Device model. + */ + model(): string; + + /** + * Launches a process in the shell on the device and returns a socket to communicate with the launched process. + * @param command + */ + open(command: string): Promise; + + /** + * Pinches the widget defined by `selector` in the closing direction. + * @param selector Selector to pinch close. + * @param percent The size of the pinch as a percentage of the widget's size. + * @param options + */ + pinchClose(selector: AndroidSelector, percent: number, options?: { + /** + * Optional speed of the pinch in pixels per second. + */ + speed?: number; + + /** + * Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by + * using the + * [androidDevice.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-androiddevice#androiddevicesetdefaulttimeouttimeout) + * method. + */ + timeout?: number; + }): Promise; + + /** + * Pinches the widget defined by `selector` in the open direction. + * @param selector Selector to pinch open. + * @param percent The size of the pinch as a percentage of the widget's size. + * @param options + */ + pinchOpen(selector: AndroidSelector, percent: number, options?: { + /** + * Optional speed of the pinch in pixels per second. + */ + speed?: number; + + /** + * Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by + * using the + * [androidDevice.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-androiddevice#androiddevicesetdefaulttimeouttimeout) + * method. + */ + timeout?: number; + }): Promise; + + /** + * Presses the specific `key` in the widget defined by `selector`. + * @param selector Selector to press the key in. + * @param key The key to press. + * @param options + */ + press(selector: AndroidSelector, key: AndroidKey, options?: { + /** + * Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by + * using the + * [androidDevice.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-androiddevice#androiddevicesetdefaulttimeouttimeout) + * method. + */ + timeout?: number; + }): Promise; + + /** + * Copies a file to the device. + * @param file Either a path to the file, or file content. + * @param path Path to the file on the device. + * @param options + */ + push(file: string|Buffer, path: string, options?: { + /** + * Optional file mode, defaults to `644` (`rw-r--r--`). + */ + mode?: number; + }): Promise; + + /** + * Returns the buffer with the captured screenshot of the device. + * @param options + */ + screenshot(options?: { + /** + * The file path to save the image to. If `path` is a relative path, then it is resolved relative to the current working + * directory. If no path is provided, the image won't be saved to the disk. + */ + path?: string; + }): Promise; + + /** + * Scrolls the widget defined by `selector` in the specified `direction`. + * @param selector Selector to scroll. + * @param direction Scroll direction. + * @param percent Distance to scroll as a percentage of the widget's size. + * @param options + */ + scroll(selector: AndroidSelector, direction: "down"|"up"|"left"|"right", percent: number, options?: { + /** + * Optional speed of the scroll in pixels per second. + */ + speed?: number; + + /** + * Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by + * using the + * [androidDevice.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-androiddevice#androiddevicesetdefaulttimeouttimeout) + * method. + */ + timeout?: number; + }): Promise; + + /** + * Device serial number. + */ + serial(): string; + + /** + * This setting will change the default maximum time for all the methods accepting `timeout` option. + * @param timeout Maximum time in milliseconds + */ + setDefaultTimeout(timeout: number): void; + + /** + * Executes a shell command on the device and returns its output. + * @param command Shell command to execute. + */ + shell(command: string): Promise; + + /** + * Swipes the widget defined by `selector` in the specified `direction`. + * @param selector Selector to swipe. + * @param direction Swipe direction. + * @param percent Distance to swipe as a percentage of the widget's size. + * @param options + */ + swipe(selector: AndroidSelector, direction: "down"|"up"|"left"|"right", percent: number, options?: { + /** + * Optional speed of the swipe in pixels per second. + */ + speed?: number; + + /** + * Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by + * using the + * [androidDevice.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-androiddevice#androiddevicesetdefaulttimeouttimeout) + * method. + */ + timeout?: number; + }): Promise; + + /** + * Taps on the widget defined by `selector`. + * @param selector Selector to tap on. + * @param options + */ + tap(selector: AndroidSelector, options?: { + /** + * Optional duration of the tap in milliseconds. + */ + duration?: number; + + /** + * Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by + * using the + * [androidDevice.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-androiddevice#androiddevicesetdefaulttimeouttimeout) + * method. + */ + timeout?: number; + }): Promise; + + /** + * Waits for the specific `selector` to either appear or disappear, depending on the `state`. + * @param selector Selector to wait for. + * @param options + */ + wait(selector: AndroidSelector, options?: { + /** + * Optional state. Can be either: + * - default - wait for element to be present. + * - `'gone'` - wait for element to not be present. + */ + state?: "gone"; + + /** + * Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by + * using the + * [androidDevice.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-androiddevice#androiddevicesetdefaulttimeouttimeout) + * method. + */ + timeout?: number; + }): Promise; + + /** + * Emitted when a new WebView instance is detected. + */ + waitForEvent(event: 'webview', optionsOrPredicate?: { predicate?: (androidWebView: AndroidWebView) => boolean, timeout?: number } | ((androidWebView: AndroidWebView) => boolean)): Promise; + + + /** + * This method waits until [AndroidWebView] matching the `selector` is opened and returns it. If there is already an open + * [AndroidWebView] matching the `selector`, returns immediately. + * @param selector + * @param options + */ + webView(selector: { + /** + * Package identifier. + */ + pkg: string; + }, options?: { + /** + * Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by + * using the + * [androidDevice.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-androiddevice#androiddevicesetdefaulttimeouttimeout) + * method. + */ + timeout?: number; + }): Promise; + + /** + * Currently open WebViews. + */ + webViews(): Array; +} + +export interface AndroidInput { + /** + * Performs a drag between `from` and `to` points. + * @param from The start point of the drag. + * @param to The end point of the drag. + * @param steps The number of steps in the drag. Each step takes 5 milliseconds to complete. + */ + drag(from: { + x: number; + + y: number; + }, to: { + x: number; + + y: number; + }, steps: number): Promise; + + /** + * Presses the `key`. + * @param key Key to press. + */ + press(key: AndroidKey): Promise; + + /** + * Swipes following the path defined by `segments`. + * @param from The point to start swiping from. + * @param segments Points following the `from` point in the swipe gesture. + * @param steps The number of steps for each segment. Each step takes 5 milliseconds to complete, so 100 steps means half a second per each segment. + */ + swipe(from: { + x: number; + + y: number; + }, segments: Array<{ + x: number; + + y: number; + }>, steps: number): Promise; + + /** + * Taps at the specified `point`. + * @param point The point to tap at. + */ + tap(point: { + x: number; + + y: number; + }): Promise; + + /** + * Types `text` into currently focused widget. + * @param text Text to type. + */ + type(text: string): Promise; +} + +/** + * [AndroidSocket] is a way to communicate with a process launched on the [AndroidDevice]. Use + * [androidDevice.open(command)](https://playwright.dev/docs/api/class-androiddevice#androiddeviceopencommand) to open a + * socket. + */ +export interface AndroidSocket { + /** + * Emitted when the socket is closed. + */ + on(event: 'close', listener: () => void): this; + + /** + * Emitted when data is available to read from the socket. + */ + on(event: 'data', listener: (buffer: Buffer) => void): this; + + /** + * Emitted when the socket is closed. + */ + once(event: 'close', listener: () => void): this; + + /** + * Emitted when data is available to read from the socket. + */ + once(event: 'data', listener: (buffer: Buffer) => void): this; + + /** + * Emitted when the socket is closed. + */ + addListener(event: 'close', listener: () => void): this; + + /** + * Emitted when data is available to read from the socket. + */ + addListener(event: 'data', listener: (buffer: Buffer) => void): this; + + /** + * Emitted when the socket is closed. + */ + removeListener(event: 'close', listener: () => void): this; + + /** + * Emitted when data is available to read from the socket. + */ + removeListener(event: 'data', listener: (buffer: Buffer) => void): this; + + /** + * Emitted when the socket is closed. + */ + off(event: 'close', listener: () => void): this; + + /** + * Emitted when data is available to read from the socket. + */ + off(event: 'data', listener: (buffer: Buffer) => void): this; + + /** + * Closes the socket. + */ + close(): Promise; + + /** + * Writes some `data` to the socket. + * @param data Data to write. + */ + write(data: Buffer): Promise; +} + +/** + * [AndroidWebView] represents a WebView open on the [AndroidDevice]. WebView is usually obtained using + * [androidDevice.webView(selector[, options])](https://playwright.dev/docs/api/class-androiddevice#androiddevicewebviewselector-options). + */ +export interface AndroidWebView { + /** + * Emitted when the WebView is closed. + */ + on(event: 'close', listener: () => void): this; + + /** + * Emitted when the WebView is closed. + */ + once(event: 'close', listener: () => void): this; + + /** + * Emitted when the WebView is closed. + */ + addListener(event: 'close', listener: () => void): this; + + /** + * Emitted when the WebView is closed. + */ + removeListener(event: 'close', listener: () => void): this; + + /** + * Emitted when the WebView is closed. + */ + off(event: 'close', listener: () => void): this; + + /** + * Connects to the WebView and returns a regular Playwright [Page] to interact with. + */ + page(): Promise; + + /** + * WebView process PID. + */ + pid(): number; + + /** + * WebView package identifier. + */ + pkg(): string; +} + /** * - extends: [EventEmitter] * diff --git a/utils/generate_types/overrides.d.ts b/utils/generate_types/overrides.d.ts index 9f502a21ba..6fed0b2dc2 100644 --- a/utils/generate_types/overrides.d.ts +++ b/utils/generate_types/overrides.d.ts @@ -230,5 +230,87 @@ export interface ElectronApplication { evaluateHandle(pageFunction: PageFunctionOn, arg?: any): Promise>; } +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 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'; + // This is required to not export everything by default. See https://github.com/Microsoft/TypeScript/issues/19545#issuecomment-340490459 export {};