feat(android): add AndroidDevice.close event (#18306)

This commit is contained in:
Max Schmitt 2022-10-25 18:18:14 -07:00 committed by GitHub
parent 63c41f88cd
commit e25537f941
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 127 additions and 27 deletions

View file

@ -4,6 +4,12 @@
[AndroidDevice] represents a connected device, either real hardware or emulated. Devices can be obtained using [`method: Android.devices`].
## event: AndroidDevice.close
* since: v1.28
- argument: <[AndroidDevice]>
Emitted when the device connection gets closed.
## event: AndroidDevice.webView
* since: v1.9
- argument: <[AndroidWebView]>

View file

@ -57,6 +57,10 @@ export class AndroidServerLauncherImpl {
browserServer.wsEndpoint = () => wsEndpoint;
browserServer.close = () => device.close();
browserServer.kill = () => device.close();
device.on('close', () => {
server.close();
browserServer.emit('close');
});
return browserServer;
}
}

View file

@ -131,6 +131,7 @@ export class AndroidDevice extends ChannelOwner<channels.AndroidDeviceChannel> i
this._timeoutSettings = new TimeoutSettings((parent as Android)._timeoutSettings);
this._channel.on('webViewAdded', ({ webView }) => this._onWebViewAdded(webView));
this._channel.on('webViewRemoved', ({ socketName }) => this._onWebViewRemoved(socketName));
this._channel.on('close', () => this._didClose());
}
private _onWebViewAdded(webView: channels.AndroidWebView) {
@ -239,7 +240,6 @@ export class AndroidDevice extends ChannelOwner<channels.AndroidDeviceChannel> i
async close() {
try {
this._didClose();
if (this._shouldCloseConnectionOnClose)
this._connection.close(kBrowserClosedError);
else
@ -252,7 +252,7 @@ export class AndroidDevice extends ChannelOwner<channels.AndroidDeviceChannel> i
}
_didClose() {
this.emit(Events.AndroidDevice.Close);
this.emit(Events.AndroidDevice.Close, this);
}
async shell(command: string): Promise<Buffer> {

View file

@ -2209,6 +2209,7 @@ scheme.AndroidDeviceInitializer = tObject({
model: tString,
serial: tString,
});
scheme.AndroidDeviceCloseEvent = tOptional(tObject({}));
scheme.AndroidDeviceWebViewAddedEvent = tObject({
webView: tType('AndroidWebView'),
});

View file

@ -145,7 +145,7 @@ export class PlaywrightConnection {
this._debugLog(`engaged pre-launched (Android) mode`);
const playwright = this._preLaunched.playwright!;
const androidDevice = this._preLaunched.androidDevice!;
androidDevice.on(AndroidDevice.Events.Closed, () => {
androidDevice.on(AndroidDevice.Events.Close, () => {
// Underlying browser did close for some reason - force disconnect the client.
this.close({ code: 1001, reason: 'Android device disconnected' });
});

View file

@ -112,7 +112,7 @@ export class AndroidDevice extends SdkObject {
static Events = {
WebViewAdded: 'webViewAdded',
WebViewRemoved: 'webViewRemoved',
Closed: 'closed'
Close: 'close',
};
private _browserConnections = new Set<AndroidBrowser>();
@ -140,7 +140,11 @@ export class AndroidDevice extends SdkObject {
async _init() {
await this._refreshWebViews();
const poll = () => {
this._pollingWebViews = setTimeout(() => this._refreshWebViews().then(poll).catch(() => {}), 500);
this._pollingWebViews = setTimeout(() => this._refreshWebViews()
.then(poll)
.catch(() => {
this.close().catch(() => {});
}), 500);
};
poll();
}
@ -163,7 +167,9 @@ export class AndroidDevice extends SdkObject {
return await this._backend.runCommand(`shell:screencap -p`);
}
private async _driver(): Promise<PipeTransport> {
private async _driver(): Promise<PipeTransport | undefined> {
if (this._isClosed)
return;
if (!this._driverPromise)
this._driverPromise = this._installDriver();
return this._driverPromise;
@ -223,6 +229,8 @@ export class AndroidDevice extends SdkObject {
// Patch the timeout in!
params.timeout = this._timeoutSettings.timeout(params);
const driver = await this._driver();
if (!driver)
throw new Error('Device is closed');
const id = ++this._lastId;
const result = new Promise((fulfill, reject) => this._callbacks.set(id, { fulfill, reject }));
driver.send(JSON.stringify({ id, method, params }));
@ -230,6 +238,8 @@ export class AndroidDevice extends SdkObject {
}
async close() {
if (this._isClosed)
return;
this._isClosed = true;
if (this._pollingWebViews)
clearTimeout(this._pollingWebViews);
@ -237,11 +247,11 @@ export class AndroidDevice extends SdkObject {
await connection.close();
if (this._driverPromise) {
const driver = await this._driver();
driver.close();
driver?.close();
}
await this._backend.close();
this._android._deviceClosed(this);
this.emit(AndroidDevice.Events.Closed);
this.emit(AndroidDevice.Events.Close);
}
async launchBrowser(pkg: string = 'com.android.chrome', options: channels.BrowserNewContextParams): Promise<BrowserContext> {

View file

@ -71,23 +71,26 @@ class AdbDevice implements DeviceBackend {
async function runCommand(command: string, host: string = '127.0.0.1', port: number = 5037, serial?: string): Promise<Buffer> {
debug('pw:adb:runCommand')(command, serial);
const socket = new BufferedSocketWrapper(command, net.createConnection({ host, port }));
if (serial) {
await socket.write(encodeMessage(`host:transport:${serial}`));
try {
if (serial) {
await socket.write(encodeMessage(`host:transport:${serial}`));
const status = await socket.read(4);
assert(status.toString() === 'OKAY', status.toString());
}
await socket.write(encodeMessage(command));
const status = await socket.read(4);
assert(status.toString() === 'OKAY', status.toString());
let commandOutput: Buffer;
if (!command.startsWith('shell:')) {
const remainingLength = parseInt((await socket.read(4)).toString(), 16);
commandOutput = await socket.read(remainingLength);
} else {
commandOutput = await socket.readAll();
}
return commandOutput;
} finally {
socket.close();
}
await socket.write(encodeMessage(command));
const status = await socket.read(4);
assert(status.toString() === 'OKAY', status.toString());
let commandOutput: Buffer;
if (!command.startsWith('shell:')) {
const remainingLength = parseInt((await socket.read(4)).toString(), 16);
commandOutput = await socket.read(remainingLength);
} else {
commandOutput = await socket.readAll();
}
socket.close();
return commandOutput;
}
async function open(command: string, host: string = '127.0.0.1', port: number = 5037, serial?: string): Promise<BufferedSocketWrapper> {

View file

@ -58,6 +58,7 @@ export class AndroidDeviceDispatcher extends Dispatcher<AndroidDevice, channels.
this._dispatchEvent('webViewAdded', { webView });
this.addObjectListener(AndroidDevice.Events.WebViewAdded, webView => this._dispatchEvent('webViewAdded', { webView }));
this.addObjectListener(AndroidDevice.Events.WebViewRemoved, socketName => this._dispatchEvent('webViewRemoved', { socketName }));
this.addObjectListener(AndroidDevice.Events.Close, socketName => this._dispatchEvent('close'));
}
async wait(params: channels.AndroidDeviceWaitParams) {

View file

@ -12475,31 +12475,61 @@ export interface Android {
* [android.devices([options])](https://playwright.dev/docs/api/class-android#android-devices).
*/
export interface AndroidDevice {
/**
* Emitted when the device connection gets closed.
*/
on(event: 'close', listener: (androidDevice: AndroidDevice) => void): this;
/**
* Emitted when a new WebView instance is detected.
*/
on(event: 'webview', listener: (androidWebView: AndroidWebView) => void): this;
/**
* Adds an event listener that will be automatically removed after it is triggered once. See `addListener` for more information about this event.
*/
once(event: 'close', listener: (androidDevice: AndroidDevice) => void): this;
/**
* Adds an event listener that will be automatically removed after it is triggered once. See `addListener` for more information about this event.
*/
once(event: 'webview', listener: (androidWebView: AndroidWebView) => void): this;
/**
* Emitted when the device connection gets closed.
*/
addListener(event: 'close', listener: (androidDevice: AndroidDevice) => void): this;
/**
* Emitted when a new WebView instance is detected.
*/
addListener(event: 'webview', listener: (androidWebView: AndroidWebView) => void): this;
/**
* Removes an event listener added by `on` or `addListener`.
*/
removeListener(event: 'close', listener: (androidDevice: AndroidDevice) => void): this;
/**
* Removes an event listener added by `on` or `addListener`.
*/
removeListener(event: 'webview', listener: (androidWebView: AndroidWebView) => void): this;
/**
* Removes an event listener added by `on` or `addListener`.
*/
off(event: 'close', listener: (androidDevice: AndroidDevice) => void): this;
/**
* Removes an event listener added by `on` or `addListener`.
*/
off(event: 'webview', listener: (androidWebView: AndroidWebView) => void): this;
/**
* Emitted when the device connection gets closed.
*/
prependListener(event: 'close', listener: (androidDevice: AndroidDevice) => void): this;
/**
* Emitted when a new WebView instance is detected.
*/
@ -13105,6 +13135,11 @@ export interface AndroidDevice {
timeout?: number;
}): Promise<void>;
/**
* Emitted when the device connection gets closed.
*/
waitForEvent(event: 'close', optionsOrPredicate?: { predicate?: (androidDevice: AndroidDevice) => boolean | Promise<boolean>, timeout?: number } | ((androidDevice: AndroidDevice) => boolean | Promise<boolean>)): Promise<AndroidDevice>;
/**
* Emitted when a new WebView instance is detected.
*/

View file

@ -4045,6 +4045,7 @@ export type AndroidDeviceInitializer = {
serial: string,
};
export interface AndroidDeviceEventTarget {
on(event: 'close', callback: (params: AndroidDeviceCloseEvent) => void): this;
on(event: 'webViewAdded', callback: (params: AndroidDeviceWebViewAddedEvent) => void): this;
on(event: 'webViewRemoved', callback: (params: AndroidDeviceWebViewRemovedEvent) => void): this;
}
@ -4076,6 +4077,7 @@ export interface AndroidDeviceChannel extends AndroidDeviceEventTarget, EventTar
connectToWebView(params: AndroidDeviceConnectToWebViewParams, metadata?: Metadata): Promise<AndroidDeviceConnectToWebViewResult>;
close(params?: AndroidDeviceCloseParams, metadata?: Metadata): Promise<AndroidDeviceCloseResult>;
}
export type AndroidDeviceCloseEvent = {};
export type AndroidDeviceWebViewAddedEvent = {
webView: AndroidWebView,
};
@ -4406,6 +4408,7 @@ export type AndroidDeviceCloseOptions = {};
export type AndroidDeviceCloseResult = void;
export interface AndroidDeviceEvents {
'close': AndroidDeviceCloseEvent;
'webViewAdded': AndroidDeviceWebViewAddedEvent;
'webViewRemoved': AndroidDeviceWebViewRemovedEvent;
}

View file

@ -3267,6 +3267,8 @@ AndroidDevice:
close:
events:
close:
webViewAdded:
parameters:
webView: AndroidWebView

View file

@ -92,3 +92,14 @@ test('androidDevice.options.omitDriverInstall', async function({ playwright }) {
expect(fillStatus).toBe('success');
});
test('androidDevice.close', async function({ playwright }) {
const devices = await playwright._android.devices();
expect(devices.length).toBe(1);
const device = devices[0];
const events = [];
device.on('close', () => events.push('close'));
await device.close();
await device.close();
expect(events).toEqual(['close']);
});

View file

@ -26,7 +26,29 @@ test('android.launchServer should connect to a device', async ({ playwright }) =
await browserServer.close();
});
test('android.launchServer should be abe to reconnect to a device', async ({ playwright }) => {
test('android.launchServer should handle close event correctly', async ({ playwright }) => {
const receivedEvents = [];
const browserServer = await playwright._android.launchServer();
const device = await playwright._android.connect(browserServer.wsEndpoint());
device.on('close', () => receivedEvents.push('device'));
browserServer.on('close', () => receivedEvents.push('browserServer'));
{
const waitForDeviceClose = new Promise(f => device.on('close', f));
await device.close();
await waitForDeviceClose;
}
expect(receivedEvents).toEqual(['device']);
await device.close();
expect(receivedEvents).toEqual(['device']);
await browserServer.close();
expect(receivedEvents).toEqual(['device', 'browserServer']);
await browserServer.close();
expect(receivedEvents).toEqual(['device', 'browserServer']);
await device.close();
expect(receivedEvents).toEqual(['device', 'browserServer']);
});
test('android.launchServer should be able to reconnect to a device', async ({ playwright }) => {
const browserServer = await playwright._android.launchServer();
try {
{
@ -94,10 +116,12 @@ test('android.launchServer should terminate WS connection when device gets disco
forwardingServer.on('connection', connection => {
receivedConnection = connection;
const actualConnection = new ws.WebSocket(browserServer.wsEndpoint());
actualConnection.on('message', message => connection.send(message));
connection.on('message', message => actualConnection.send(message));
connection.on('close', () => actualConnection.close());
actualConnection.on('close', () => connection.close());
actualConnection.on('open', () => {
actualConnection.on('message', message => connection.send(message));
connection.on('message', message => actualConnection.send(message));
connection.on('close', () => actualConnection.close());
actualConnection.on('close', () => connection.close());
});
});
try {
const device = await playwright._android.connect(`ws://localhost:${(forwardingServer.address() as ws.AddressInfo).port}/connect`);