diff --git a/package.json b/package.json index 0a19dadaae..f3ff2b26dd 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ "roll-browser": "node utils/roll_browser.js", "coverage": "node test/checkCoverage.js", "check-deps": "node utils/check_deps.js", - "build-android-driver": "./utils/build_android_driver.sh" + "build-android-driver": "./utils/build_android_driver.sh", + "test-android-driver": "PW_ANDROID_TESTS=1 npx folio test/android -p browserName=chromium --workers=1" }, "author": { "name": "Microsoft Corporation" diff --git a/src/dispatchers/androidDispatcher.ts b/src/dispatchers/androidDispatcher.ts index a6f40ec308..5cedf60e31 100644 --- a/src/dispatchers/androidDispatcher.ts +++ b/src/dispatchers/androidDispatcher.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Dispatcher, DispatcherScope } from './dispatcher'; +import { Dispatcher, DispatcherScope, existingDispatcher } from './dispatcher'; import { Android, AndroidDevice } from '../server/android/android'; import * as channels from '../protocol/channels'; import { BrowserContextDispatcher } from './browserContextDispatcher'; @@ -27,7 +27,7 @@ export class AndroidDispatcher extends Dispatcher { const devices = await this._object.devices(); return { - devices: devices.map(d => new AndroidDeviceDispatcher(this._scope, d)) + devices: devices.map(d => AndroidDeviceDispatcher.from(this._scope, d)) }; } @@ -37,6 +37,12 @@ export class AndroidDispatcher extends Dispatcher implements channels.AndroidDeviceChannel { + + static from(scope: DispatcherScope, device: AndroidDevice): AndroidDeviceDispatcher { + const result = existingDispatcher(device); + return result || new AndroidDeviceDispatcher(scope, device); + } + constructor(scope: DispatcherScope, device: AndroidDevice) { super(scope, device, 'AndroidDevice', { model: device.model, diff --git a/src/server/android/android.ts b/src/server/android/android.ts index 257184f5f8..19c19e1ecc 100644 --- a/src/server/android/android.ts +++ b/src/server/android/android.ts @@ -82,6 +82,10 @@ export class Android { } return [...this._devices.values()]; } + + _deviceClosed(device: AndroidDevice) { + this._devices.delete(device.serial); + } } export class AndroidDevice extends EventEmitter { @@ -98,12 +102,16 @@ export class AndroidDevice extends EventEmitter { static Events = { WebViewAdded: 'webViewAdded', WebViewRemoved: 'webViewRemoved', + Closed: 'closed' }; private _browserConnections = new Set(); + private _android: Android; + private _isClosed = false; constructor(android: Android, backend: DeviceBackend, model: string) { super(); + this._android = android; this._backend = backend; this.model = model; this.serial = backend.serial; @@ -202,6 +210,7 @@ export class AndroidDevice extends EventEmitter { } async close() { + this._isClosed = true; if (this._pollingWebViews) clearTimeout(this._pollingWebViews); for (const connection of this._browserConnections) @@ -211,6 +220,8 @@ export class AndroidDevice extends EventEmitter { driver.close(); } await this._backend.close(); + this._android._deviceClosed(this); + this.emit(AndroidDevice.Events.Closed); } async launchBrowser(pkg: string = 'com.android.chrome', options: types.BrowserContextOptions = {}): Promise { @@ -234,11 +245,11 @@ export class AndroidDevice extends EventEmitter { return await this._connectToBrowser(socketName, options); } - connectToWebView(pid: number): Promise { + async connectToWebView(pid: number): Promise { const webView = this._webViews.get(pid); if (!webView) throw new Error('WebView has been closed'); - return this._connectToBrowser(`webview_devtools_remote_${pid}`); + return await this._connectToBrowser(`webview_devtools_remote_${pid}`); } private async _connectToBrowser(socketName: string, options: types.BrowserContextOptions = {}): Promise { @@ -272,6 +283,8 @@ export class AndroidDevice extends EventEmitter { private async _refreshWebViews() { const sockets = (await this._backend.runCommand(`shell:cat /proc/net/unix | grep webview_devtools_remote`)).split('\n'); + if (this._isClosed) + return; const newPids = new Set(); for (const line of sockets) { @@ -286,6 +299,8 @@ export class AndroidDevice extends EventEmitter { continue; const procs = (await this._backend.runCommand(`shell:ps -A | grep ${pid}`)).split('\n'); + if (this._isClosed) + return; let pkg = ''; for (const proc of procs) { const match = proc.match(/[^\s]+\s+(\d+).*$/); diff --git a/test/android/webview.spec.ts b/test/android/webview.spec.ts index d96a609e12..2d909d94fc 100644 --- a/test/android/webview.spec.ts +++ b/test/android/webview.spec.ts @@ -23,6 +23,7 @@ if (process.env.PW_ANDROID_TESTS) { await device.shell('am start org.chromium.webview_shell/.WebViewBrowserActivity'); const webview = await device.webView({ pkg: 'org.chromium.webview_shell' }); expect(webview.pkg()).toBe('org.chromium.webview_shell'); + expect(device.webViews().length).toBe(1); }); it('should connect to page', async function({ device }) { @@ -33,7 +34,7 @@ if (process.env.PW_ANDROID_TESTS) { expect(page.url()).toBe('about:blank'); }); - it('should navigate page externally', async function({ device, server }) { + it('should navigate page internally', async function({ device, server }) { expect(device.webViews().length).toBe(0); await device.shell('am start org.chromium.webview_shell/.WebViewBrowserActivity'); const webview = await device.webView({ pkg: 'org.chromium.webview_shell' });