feat(android): add AndroidDevice.close event (#18306)
This commit is contained in:
parent
63c41f88cd
commit
e25537f941
|
|
@ -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]>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -2209,6 +2209,7 @@ scheme.AndroidDeviceInitializer = tObject({
|
|||
model: tString,
|
||||
serial: tString,
|
||||
});
|
||||
scheme.AndroidDeviceCloseEvent = tOptional(tObject({}));
|
||||
scheme.AndroidDeviceWebViewAddedEvent = tObject({
|
||||
webView: tType('AndroidWebView'),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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' });
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
35
packages/playwright-core/types/types.d.ts
vendored
35
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3267,6 +3267,8 @@ AndroidDevice:
|
|||
close:
|
||||
|
||||
events:
|
||||
close:
|
||||
|
||||
webViewAdded:
|
||||
parameters:
|
||||
webView: AndroidWebView
|
||||
|
|
|
|||
|
|
@ -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']);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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`);
|
||||
|
|
|
|||
Loading…
Reference in a new issue