From 905f28c339b73c30291c0c8d7a3c24a7ac7ae069 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Sat, 26 Dec 2020 20:25:18 -0800 Subject: [PATCH] feat(types): simplify android and electron types (#4829) These now follow the scheme for regular types. --- .eslintignore | 1 - android-types.d.ts | 28 ------------- packages/build_package.js | 4 +- packages/common/.npmignore | 5 --- .../installation-tests/installation-tests.sh | 16 +++++++ packages/playwright-android/index.d.ts | 4 +- packages/playwright-electron/index.d.ts | 4 +- src/client/android.ts | 42 +++++++++---------- src/client/electron.ts | 27 +++++++----- test/android/android.fixtures.ts | 2 +- test/electron/electron.fixture.ts | 2 +- test/fixtures.ts | 4 +- .../android.d.ts | 20 +++++---- electron-types.d.ts => types/electron.d.ts | 11 +++-- utils/doclint/check_public_api/JSBuilder.js | 2 +- 15 files changed, 85 insertions(+), 87 deletions(-) delete mode 100644 android-types.d.ts rename android-types-internal.d.ts => types/android.d.ts (91%) rename electron-types.d.ts => types/electron.d.ts (92%) diff --git a/.eslintignore b/.eslintignore index 2badb26775..bdd07eeb3d 100644 --- a/.eslintignore +++ b/.eslintignore @@ -8,7 +8,6 @@ src/server/firefox/protocol.ts src/server/webkit/protocol.ts /types/* /index.d.ts -/electron-types.d.ts utils/generate_types/overrides.d.ts utils/generate_types/test/test.ts node_modules/ diff --git a/android-types.d.ts b/android-types.d.ts deleted file mode 100644 index c646b5cd6c..0000000000 --- a/android-types.d.ts +++ /dev/null @@ -1,28 +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 { Page, BrowserContext, BrowserContextOptions } from './types/types'; -import * as apiInternal from './android-types-internal'; -import { EventEmitter } from 'events'; - -export { AndroidElementInfo, AndroidSelector } from './android-types-internal'; -export type AndroidDevice = apiInternal.AndroidDevice; -export type AndroidWebView = apiInternal.AndroidWebView; - -export interface Android extends EventEmitter { - setDefaultTimeout(timeout: number): void; - devices(): Promise; -} diff --git a/packages/build_package.js b/packages/build_package.js index e511399376..7d91881065 100755 --- a/packages/build_package.js +++ b/packages/build_package.js @@ -62,13 +62,13 @@ const PACKAGES = { version: '0.4.0', // Manually manage playwright-electron version. description: 'A high-level API to automate Electron', browsers: [], - files: [...PLAYWRIGHT_CORE_FILES, ...FFMPEG_FILES, 'electron-types.d.ts'], + files: [...PLAYWRIGHT_CORE_FILES, ...FFMPEG_FILES], }, 'playwright-android': { version: '0.0.8', // 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', 'android-types-internal.d.ts', 'bin/android-driver.apk', 'bin/android-driver-target.apk'], + files: [...PLAYWRIGHT_CORE_FILES, ...FFMPEG_FILES, 'bin/android-driver.apk', 'bin/android-driver-target.apk'], }, }; diff --git a/packages/common/.npmignore b/packages/common/.npmignore index 3344ccf792..7f9e8c66cd 100644 --- a/packages/common/.npmignore +++ b/packages/common/.npmignore @@ -18,11 +18,6 @@ lib/server/injected/ # Include generated types and entrypoint. !types/* !index.d.ts -# Include separate android types. -!android-types.d.ts -!android-types-internal.d.ts -# Include separate electron types. -!electron-types.d.ts # Include main entrypoint. !index.js # Include main entrypoint for ES Modules. diff --git a/packages/installation-tests/installation-tests.sh b/packages/installation-tests/installation-tests.sh index adad67007f..06a3d8381c 100755 --- a/packages/installation-tests/installation-tests.sh +++ b/packages/installation-tests/installation-tests.sh @@ -22,6 +22,7 @@ PLAYWRIGHT_CHROMIUM_TGZ="$(node ${PACKAGE_BUILDER} playwright-chromium ./playwri PLAYWRIGHT_WEBKIT_TGZ="$(node ${PACKAGE_BUILDER} playwright-webkit ./playwright-webkit.tgz)" PLAYWRIGHT_FIREFOX_TGZ="$(node ${PACKAGE_BUILDER} playwright-firefox ./playwright-firefox.tgz)" PLAYWRIGHT_ELECTRON_TGZ="$(node ${PACKAGE_BUILDER} playwright-electron ./playwright-electron.tgz)" +PLAYWRIGHT_ANDROID_TGZ="$(node ${PACKAGE_BUILDER} playwright-android ./playwright-android.tgz)" SCRIPTS_PATH="$(pwd -P)/.." TEST_ROOT="$(pwd -P)" @@ -52,6 +53,7 @@ function run_tests { test_playwright_global_installation_cross_package test_playwright_electron_should_work test_electron_types + test_android_types test_playwright_cli_should_work test_playwright_cli_install_should_work } @@ -316,6 +318,20 @@ function test_electron_types { echo "${FUNCNAME[0]} success" } +function test_android_types { + initialize_test "${FUNCNAME[0]}" + + npm install ${PLAYWRIGHT_ANDROID_TGZ} + npm install -D typescript@3.8 + npm install -D @types/node@10.17 + echo "import { AndroidDevice, android, AndroidWebView, Page } from 'playwright-android';" > "test.ts" + + echo "Running tsc" + npx tsc "test.ts" + + echo "${FUNCNAME[0]} success" +} + function test_playwright_cli_should_work { initialize_test "${FUNCNAME[0]}" diff --git a/packages/playwright-android/index.d.ts b/packages/playwright-android/index.d.ts index 7fb6a6c144..2e11a85a6c 100644 --- a/packages/playwright-android/index.d.ts +++ b/packages/playwright-android/index.d.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Android } from './android-types'; +import { Android } from './types/android'; export * from './types/types'; -export * from './android-types'; +export * from './types/android'; export const android: Android; diff --git a/packages/playwright-electron/index.d.ts b/packages/playwright-electron/index.d.ts index 96b8d85a39..b83cfea185 100644 --- a/packages/playwright-electron/index.d.ts +++ b/packages/playwright-electron/index.d.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ElectronLauncher } from './electron-types'; +import { ElectronLauncher } from './types/electron'; export * from './types/types'; -export * from './electron-types'; +export * from './types/electron'; export const electron: ElectronLauncher; diff --git a/src/client/android.ts b/src/client/android.ts index 21220bf630..a20ece1929 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 apiInternal from '../../android-types-internal'; +import * as androidApi from '../../types/android'; import * as types from './types'; import { Page } from './page'; import { TimeoutSettings } from '../utils/timeoutSettings'; @@ -31,7 +31,7 @@ import { EventEmitter } from 'events'; type Direction = 'down' | 'up' | 'left' | 'right'; type SpeedOptions = { speed?: number }; -export class Android extends ChannelOwner { +export class Android extends ChannelOwner implements androidApi.Android { readonly _timeoutSettings: TimeoutSettings; static from(android: channels.AndroidChannel): Android { @@ -56,7 +56,7 @@ export class Android extends ChannelOwner implements apiInternal.AndroidDevice { +export class AndroidDevice extends ChannelOwner implements androidApi.AndroidDevice { readonly _timeoutSettings: TimeoutSettings; private _webViews = new Map(); @@ -114,78 +114,78 @@ export class AndroidDevice extends ChannelOwner { await this._channel.wait({ selector: toSelectorChannel(selector), ...options }); }); } - async fill(selector: apiInternal.AndroidSelector, text: string, options?: types.TimeoutOptions) { + async fill(selector: androidApi.AndroidSelector, text: string, options?: types.TimeoutOptions) { await this._wrapApiCall('androidDevice.fill', async () => { await this._channel.fill({ selector: toSelectorChannel(selector), text, ...options }); }); } - async press(selector: apiInternal.AndroidSelector, key: apiInternal.AndroidKey, options?: types.TimeoutOptions) { + async press(selector: androidApi.AndroidSelector, key: androidApi.AndroidKey, options?: types.TimeoutOptions) { await this.tap(selector, options); await this.input.press(key); } - async tap(selector: apiInternal.AndroidSelector, options?: { duration?: number } & types.TimeoutOptions) { + async tap(selector: androidApi.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) { + async drag(selector: androidApi.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) { + async fling(selector: androidApi.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) { + async longTap(selector: androidApi.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) { + async pinchClose(selector: androidApi.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) { + async pinchOpen(selector: androidApi.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) { + async scroll(selector: androidApi.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) { + async swipe(selector: androidApi.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 { + async info(selector: androidApi.AndroidSelector): Promise { return await this._wrapApiCall('androidDevice.info', async () => { return (await this._channel.info({ selector: toSelectorChannel(selector) })).info; }); } - async tree(): Promise { + async tree(): Promise { return await this._wrapApiCall('androidDevice.tree', async () => { return (await this._channel.tree()).tree; }); @@ -254,7 +254,7 @@ export class AndroidDevice extends ChannelOwner { +export class AndroidSocket extends ChannelOwner implements androidApi.AndroidSocket { static from(androidDevice: channels.AndroidSocketChannel): AndroidSocket { return (androidDevice as any)._object; } @@ -284,7 +284,7 @@ async function loadFile(file: string | Buffer): Promise { return file.toString('base64'); } -class Input implements apiInternal.AndroidInput { +class Input implements androidApi.AndroidInput { private _device: AndroidDevice; constructor(device: AndroidDevice) { @@ -297,7 +297,7 @@ class Input implements apiInternal.AndroidInput { }); } - async press(key: apiInternal.AndroidKey) { + async press(key: androidApi.AndroidKey) { return this._device._wrapApiCall('androidDevice.inputPress', async () => { await this._device._channel.inputPress({ key }); }); @@ -322,7 +322,7 @@ class Input implements apiInternal.AndroidInput { } } -function toSelectorChannel(selector: apiInternal.AndroidSelector): channels.AndroidSelector { +function toSelectorChannel(selector: androidApi.AndroidSelector): channels.AndroidSelector { const { checkable, checked, @@ -372,7 +372,7 @@ function toSelectorChannel(selector: apiInternal.AndroidSelector): channels.Andr }; } -export class AndroidWebView extends EventEmitter { +export class AndroidWebView extends EventEmitter implements androidApi.AndroidWebView { private _device: AndroidDevice; private _data: channels.AndroidWebView; private _pagePromise: Promise | undefined; diff --git a/src/client/electron.ts b/src/client/electron.ts index 31ec467e5f..f0bcd4744d 100644 --- a/src/client/electron.ts +++ b/src/client/electron.ts @@ -24,14 +24,18 @@ import { Waiter } from './waiter'; import { Events } from './events'; import { WaitForEventOptions, Env, Logger } from './types'; import { envObjectToArray } from './clientHelper'; +import * as electronApi from '../../types/electron'; import * as structs from '../../types/structs'; +import type { ChromiumBrowserContext } from './chromiumBrowserContext'; type ElectronOptions = Omit & { env?: Env, logger?: Logger, }; -export class Electron extends ChannelOwner { +type ElectronAppType = typeof import('electron'); + +export class Electron extends ChannelOwner implements electronApi.ElectronLauncher { static from(electron: channels.ElectronChannel): Electron { return (electron as any)._object; } @@ -54,7 +58,7 @@ export class Electron extends ChannelOwner { +export class ElectronApplication extends ChannelOwner implements electronApi.ElectronApplication { private _context?: BrowserContext; private _windows = new Set(); private _timeoutSettings = new TimeoutSettings(); @@ -76,23 +80,24 @@ export class ElectronApplication extends ChannelOwner this.emit(Events.ElectronApplication.Close)); } - windows(): Page[] { - return [...this._windows]; + windows(): electronApi.ElectronPage[] { + // TODO: add ElectronPage class inherting from Page. + return [...this._windows] as any as electronApi.ElectronPage[]; } - async firstWindow(): Promise { + async firstWindow(): Promise { if (this._windows.size) return this._windows.values().next().value; return this.waitForEvent('window'); } - async newBrowserWindow(options: any): Promise { + async newBrowserWindow(options: any): Promise { const result = await this._channel.newBrowserWindow({ arg: serializeArgument(options) }); - return Page.from(result.page); + return Page.from(result.page) as any as electronApi.ElectronPage; } - context(): BrowserContext { - return this._context!; + context(): ChromiumBrowserContext { + return this._context! as ChromiumBrowserContext; } async close() { @@ -111,12 +116,12 @@ export class ElectronApplication extends ChannelOwner(pageFunction: structs.PageFunctionOn, arg: Arg): Promise { + async evaluate(pageFunction: structs.PageFunctionOn, arg: Arg): Promise { const result = await this._channel.evaluateExpression({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) }); return parseResult(result.value); } - async evaluateHandle(pageFunction: structs.PageFunctionOn, arg: Arg): Promise> { + async evaluateHandle(pageFunction: structs.PageFunctionOn, arg: Arg): Promise> { const result = await this._channel.evaluateExpressionHandle({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) }); return JSHandle.from(result.handle) as any as structs.SmartHandle; } diff --git a/test/android/android.fixtures.ts b/test/android/android.fixtures.ts index 2a7e3bdc6c..8c8fa1746a 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 '../../android-types'; +import type { Android, AndroidDevice } from '../../types/android'; import { folio as baseFolio } from '../fixtures'; const fixtures = baseFolio.extend<{ diff --git a/test/electron/electron.fixture.ts b/test/electron/electron.fixture.ts index cdece774da..15d72bf26d 100644 --- a/test/electron/electron.fixture.ts +++ b/test/electron/electron.fixture.ts @@ -15,7 +15,7 @@ */ import { folio as base } from '../fixtures'; -import type { ElectronApplication, ElectronPage } from '../../electron-types'; +import type { ElectronApplication, ElectronPage } from '../../types/electron'; import path from 'path'; const electronName = process.platform === 'win32' ? 'electron.cmd' : 'electron'; diff --git a/test/fixtures.ts b/test/fixtures.ts index ed43163ce7..1c784a0854 100644 --- a/test/fixtures.ts +++ b/test/fixtures.ts @@ -27,8 +27,8 @@ 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 '../android-types'; -import type { ElectronLauncher } from '../electron-types'; +import type { Android } from '../types/android'; +import type { ElectronLauncher } from '../types/electron'; export { expect, config } from 'folio'; const removeFolderAsync = util.promisify(require('rimraf')); diff --git a/android-types-internal.d.ts b/types/android.d.ts similarity index 91% rename from android-types-internal.d.ts rename to types/android.d.ts index 6a109454e0..01813a5bc8 100644 --- a/android-types-internal.d.ts +++ b/types/android.d.ts @@ -15,18 +15,24 @@ */ import { EventEmitter } from 'events'; +import { BrowserContextOptions, BrowserContext, Page } from './types'; -export interface AndroidDevice extends EventEmitter { +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; + 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>; + 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; @@ -53,8 +59,8 @@ export interface AndroidDevice exte 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 + write(data: Buffer): Promise; + close(): Promise; } export interface AndroidInput { @@ -65,7 +71,7 @@ export interface AndroidInput { drag(from: { x: number, y: number }, to: { x: number, y: number }, steps: number): Promise; } -export interface AndroidWebView extends EventEmitter { +export interface AndroidWebView extends EventEmitter { on(event: 'close', handler: () => void): this; pid(): number; pkg(): string; diff --git a/electron-types.d.ts b/types/electron.d.ts similarity index 92% rename from electron-types.d.ts rename to types/electron.d.ts index f08095239d..43f0b648d2 100644 --- a/electron-types.d.ts +++ b/types/electron.d.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Logger, Page, JSHandle, ChromiumBrowserContext } from './types/types'; +import { Logger, Page, JSHandle, ChromiumBrowserContext } from './types'; import { BrowserWindow, BrowserWindowConstructorOptions } from 'electron'; export type ElectronLaunchOptions = { @@ -27,9 +27,11 @@ export type ElectronLaunchOptions = { timeout?: number, logger?: Logger, }; + export interface ElectronLauncher { launch(executablePath: string, options?: ElectronLaunchOptions): Promise; } + export interface ElectronApplication { on(event: 'window', listener: (page : ElectronPage) => void): this; addListener(event: 'window', listener: (page : ElectronPage) => void): this; @@ -44,9 +46,12 @@ export interface ElectronApplication { firstWindow(): Promise; newBrowserWindow(options?: BrowserWindowConstructorOptions): Promise; close(): Promise; - evaluate: JSHandle['evaluate']; - evaluateHandle: JSHandle['evaluateHandle']; + evaluate: HandleToElectron['evaluate']; + evaluateHandle: HandleToElectron['evaluateHandle']; } + export interface ElectronPage extends Page { browserWindow: JSHandle; } + +type HandleToElectron = JSHandle; diff --git a/utils/doclint/check_public_api/JSBuilder.js b/utils/doclint/check_public_api/JSBuilder.js index 649b23cdba..5cd6e4d0dd 100644 --- a/utils/doclint/check_public_api/JSBuilder.js +++ b/utils/doclint/check_public_api/JSBuilder.js @@ -99,7 +99,7 @@ function checkSources(sources) { parent = parent.parent; className = path.basename(parent.fileName, '.js'); } - if (className && !excludeClasses.has(className) && !fileName.endsWith('/protocol.ts') && !fileName.endsWith('/protocol.d.ts') && !fileName.endsWith('/types.d.ts')) { + if (className && !excludeClasses.has(className) && !fileName.endsWith('/protocol.ts') && !fileName.endsWith('/protocol.d.ts') && !fileName.endsWith('/types.d.ts') && !fileName.endsWith('node_modules/electron/electron.d.ts')) { excludeClasses.add(className); classes.push(serializeClass(className, symbol, node)); inheritance.set(className, parentClasses(node));