fix(launch): handle timeout and exceptions during launch (#2185)

This commit is contained in:
Dmitry Gozman 2020-05-11 15:00:13 -07:00 committed by GitHub
parent 9895cd0a31
commit a2bee2ca73
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 120 additions and 44 deletions

View file

@ -470,7 +470,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
return injected.waitForDisplayedAtStablePosition(node, rafCount, timeout);
}, { rafCount, timeout: helper.timeUntilDeadline(deadline) });
const timeoutMessage = 'element to be displayed and not moving';
const injectedResult = await helper.waitWithDeadline(stablePromise, timeoutMessage, deadline);
const injectedResult = await helper.waitWithDeadline(stablePromise, timeoutMessage, deadline, 'pw:input');
handleInjectedResult(injectedResult, timeoutMessage);
this._page._log(inputLog, '...element is displayed and does not move');
}

View file

@ -158,13 +158,9 @@ class Helper {
});
}
static async waitWithTimeout<T>(promise: Promise<T>, taskName: string, timeout: number): Promise<T> {
return this.waitWithDeadline(promise, taskName, helper.monotonicTime() + timeout);
}
static async waitWithDeadline<T>(promise: Promise<T>, taskName: string, deadline: number): Promise<T> {
static async waitWithDeadline<T>(promise: Promise<T>, taskName: string, deadline: number, debugName: string): Promise<T> {
let reject: (error: Error) => void;
const timeoutError = new TimeoutError(`Waiting for ${taskName} failed: timeout exceeded. Re-run with the DEBUG=pw:input env variable to see the debug log.`);
const timeoutError = new TimeoutError(`Waiting for ${taskName} failed: timeout exceeded. Re-run with the DEBUG=${debugName} env variable to see the debug log.`);
const timeoutPromise = new Promise<T>((resolve, x) => reject = x);
const timeoutTimer = setTimeout(() => reject(timeoutError), helper.timeUntilDeadline(deadline));
try {

View file

@ -16,6 +16,7 @@
import { ChildProcess, execSync } from 'child_process';
import { EventEmitter } from 'events';
import { helper } from '../helper';
export class WebSocketWrapper {
readonly wsEndpoint: string;
@ -87,4 +88,22 @@ export class BrowserServer extends EventEmitter {
if (this._webSocketWrapper)
await this._webSocketWrapper.checkLeaks();
}
async _initializeOrClose<T>(deadline: number, init: () => Promise<T>): Promise<T> {
try {
const result = await helper.waitWithDeadline(init(), 'the browser to launch', deadline, 'pw:browser*');
return result;
} catch (e) {
await this._closeOrKill(deadline);
throw e;
}
}
async _closeOrKill(deadline: number): Promise<void> {
try {
await helper.waitWithDeadline(this.close(), '', deadline, ''); // The error message is ignored.
} catch (ignored) {
this.kill();
}
}
}

View file

@ -42,11 +42,17 @@ export class Chromium extends AbstractBrowserType<CRBrowser> {
async launch(options: LaunchOptions = {}): Promise<CRBrowser> {
assert(!(options as any).userDataDir, 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead');
const { timeout = 30000 } = options;
const deadline = TimeoutSettings.computeDeadline(timeout);
const { browserServer, transport, downloadsPath, logger } = await this._launchServer(options, 'local');
const browser = await CRBrowser.connect(transport!, false, logger, options);
browser._ownedServer = browserServer;
browser._downloadsPath = downloadsPath;
return browser;
return await browserServer._initializeOrClose(deadline, async () => {
if ((options as any).__testHookBeforeCreateBrowser)
await (options as any).__testHookBeforeCreateBrowser();
const browser = await CRBrowser.connect(transport!, false, logger, options);
browser._ownedServer = browserServer;
browser._downloadsPath = downloadsPath;
return browser;
});
}
async launchServer(options: LaunchServerOptions = {}): Promise<BrowserServer> {
@ -57,12 +63,16 @@ export class Chromium extends AbstractBrowserType<CRBrowser> {
const { timeout = 30000 } = options;
const deadline = TimeoutSettings.computeDeadline(timeout);
const { transport, browserServer, logger } = await this._launchServer(options, 'persistent', userDataDir);
const browser = await CRBrowser.connect(transport!, true, logger, options);
browser._ownedServer = browserServer;
const context = browser._defaultContext!;
if (!options.ignoreDefaultArgs || Array.isArray(options.ignoreDefaultArgs))
await helper.waitWithTimeout(context._loadDefaultContext(), 'first page', helper.timeUntilDeadline(deadline));
return context;
return await browserServer._initializeOrClose(deadline, async () => {
if ((options as any).__testHookBeforeCreateBrowser)
await (options as any).__testHookBeforeCreateBrowser();
const browser = await CRBrowser.connect(transport!, true, logger, options);
browser._ownedServer = browserServer;
const context = browser._defaultContext!;
if (!options.ignoreDefaultArgs || Array.isArray(options.ignoreDefaultArgs))
await context._loadDefaultContext();
return context;
});
}
private async _launchServer(options: LaunchServerOptions, launchType: LaunchType, userDataDir?: string): Promise<{ browserServer: BrowserServer, transport?: ConnectionTransport, downloadsPath: string, logger: InnerLogger }> {

View file

@ -44,13 +44,19 @@ export class Firefox extends AbstractBrowserType<FFBrowser> {
async launch(options: LaunchOptions = {}): Promise<FFBrowser> {
assert(!(options as any).userDataDir, 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead');
const { timeout = 30000 } = options;
const deadline = TimeoutSettings.computeDeadline(timeout);
const { browserServer, downloadsPath, logger } = await this._launchServer(options, 'local');
const browser = await WebSocketTransport.connect(browserServer.wsEndpoint()!, transport => {
return FFBrowser.connect(transport, logger, false, options.slowMo);
return await browserServer._initializeOrClose(deadline, async () => {
if ((options as any).__testHookBeforeCreateBrowser)
await (options as any).__testHookBeforeCreateBrowser();
const browser = await WebSocketTransport.connect(browserServer.wsEndpoint()!, transport => {
return FFBrowser.connect(transport, logger, false, options.slowMo);
});
browser._ownedServer = browserServer;
browser._downloadsPath = downloadsPath;
return browser;
});
browser._ownedServer = browserServer;
browser._downloadsPath = downloadsPath;
return browser;
}
async launchServer(options: LaunchServerOptions = {}): Promise<BrowserServer> {
@ -64,15 +70,19 @@ export class Firefox extends AbstractBrowserType<FFBrowser> {
} = options;
const deadline = TimeoutSettings.computeDeadline(timeout);
const { browserServer, downloadsPath, logger } = await this._launchServer(options, 'persistent', userDataDir);
const browser = await WebSocketTransport.connect(browserServer.wsEndpoint()!, transport => {
return FFBrowser.connect(transport, logger, true, slowMo);
return await browserServer._initializeOrClose(deadline, async () => {
if ((options as any).__testHookBeforeCreateBrowser)
await (options as any).__testHookBeforeCreateBrowser();
const browser = await WebSocketTransport.connect(browserServer.wsEndpoint()!, transport => {
return FFBrowser.connect(transport, logger, true, slowMo);
});
browser._ownedServer = browserServer;
browser._downloadsPath = downloadsPath;
const context = browser._defaultContext!;
if (!options.ignoreDefaultArgs || Array.isArray(options.ignoreDefaultArgs))
await context._loadDefaultContext();
return context;
});
browser._ownedServer = browserServer;
browser._downloadsPath = downloadsPath;
const context = browser._defaultContext!;
if (!options.ignoreDefaultArgs || Array.isArray(options.ignoreDefaultArgs))
await helper.waitWithTimeout(context._loadDefaultContext(), 'first page', helper.timeUntilDeadline(deadline));
return context;
}
private async _launchServer(options: LaunchServerOptions, launchType: LaunchType, userDataDir?: string): Promise<{ browserServer: BrowserServer, downloadsPath: string, logger: InnerLogger }> {

View file

@ -20,10 +20,11 @@ import { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../trans
import { logError, InnerLogger } from '../logger';
export class PipeTransport implements ConnectionTransport {
private _pipeWrite: NodeJS.WritableStream | null;
private _pipeWrite: NodeJS.WritableStream;
private _pendingMessage = '';
private _eventListeners: RegisteredListener[];
private _waitForNextTask = helper.makeWaitForNextTask();
private _closed = false;
onmessage?: (message: ProtocolResponse) => void;
onclose?: () => void;
@ -33,6 +34,7 @@ export class PipeTransport implements ConnectionTransport {
this._eventListeners = [
helper.addEventListener(pipeRead, 'data', buffer => this._dispatch(buffer)),
helper.addEventListener(pipeRead, 'close', () => {
this._closed = true;
helper.removeEventListeners(this._eventListeners);
if (this.onclose)
this.onclose.call(null);
@ -45,8 +47,10 @@ export class PipeTransport implements ConnectionTransport {
}
send(message: ProtocolRequest) {
this._pipeWrite!.write(JSON.stringify(message));
this._pipeWrite!.write('\0');
if (this._closed)
throw new Error('Pipe has been closed');
this._pipeWrite.write(JSON.stringify(message));
this._pipeWrite.write('\0');
}
close() {

View file

@ -42,11 +42,17 @@ export class WebKit extends AbstractBrowserType<WKBrowser> {
async launch(options: LaunchOptions = {}): Promise<WKBrowser> {
assert(!(options as any).userDataDir, 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead');
const { timeout = 30000 } = options;
const deadline = TimeoutSettings.computeDeadline(timeout);
const { browserServer, transport, downloadsPath, logger } = await this._launchServer(options, 'local');
const browser = await WKBrowser.connect(transport!, logger, options.slowMo, false);
browser._ownedServer = browserServer;
browser._downloadsPath = downloadsPath;
return browser;
return await browserServer._initializeOrClose(deadline, async () => {
if ((options as any).__testHookBeforeCreateBrowser)
await (options as any).__testHookBeforeCreateBrowser();
const browser = await WKBrowser.connect(transport!, logger, options.slowMo, false);
browser._ownedServer = browserServer;
browser._downloadsPath = downloadsPath;
return browser;
});
}
async launchServer(options: LaunchServerOptions = {}): Promise<BrowserServer> {
@ -60,12 +66,16 @@ export class WebKit extends AbstractBrowserType<WKBrowser> {
} = options;
const deadline = TimeoutSettings.computeDeadline(timeout);
const { transport, browserServer, logger } = await this._launchServer(options, 'persistent', userDataDir);
const browser = await WKBrowser.connect(transport!, logger, slowMo, true);
browser._ownedServer = browserServer;
const context = browser._defaultContext!;
if (!options.ignoreDefaultArgs || Array.isArray(options.ignoreDefaultArgs))
await helper.waitWithTimeout(context._loadDefaultContext(), 'first page', helper.timeUntilDeadline(deadline));
return context;
return await browserServer._initializeOrClose(deadline, async () => {
if ((options as any).__testHookBeforeCreateBrowser)
await (options as any).__testHookBeforeCreateBrowser();
const browser = await WKBrowser.connect(transport!, logger, slowMo, true);
browser._ownedServer = browserServer;
const context = browser._defaultContext!;
if (!options.ignoreDefaultArgs || Array.isArray(options.ignoreDefaultArgs))
await context._loadDefaultContext();
return context;
});
}
private async _launchServer(options: LaunchServerOptions, launchType: LaunchType, userDataDir?: string): Promise<{ browserServer: BrowserServer, transport?: ConnectionTransport, downloadsPath: string, logger: InnerLogger }> {
@ -123,7 +133,8 @@ export class WebKit extends AbstractBrowserType<WKBrowser> {
},
});
// For local launch scenario close will terminate the browser process.
// Note: it is important to define these variables before launchProcess, so that we don't get
// "Cannot access 'browserServer' before initialization" if something went wrong.
let transport: ConnectionTransport | undefined = undefined;
let browserServer: BrowserServer | undefined = undefined;
const stdio = launchedProcess.stdio as unknown as [NodeJS.ReadableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.ReadableStream];

View file

@ -51,6 +51,17 @@ describe('Playwright', function() {
await browserType.launch(options).catch(e => waitError = e);
expect(waitError.message).toContain('Failed to launch');
});
it('should handle timeout', async({browserType, defaultBrowserOptions}) => {
const options = { ...defaultBrowserOptions, timeout: 1000, __testHookBeforeCreateBrowser: () => new Promise(f => setTimeout(f, 2000)) };
const error = await browserType.launch(options).catch(e => e);
expect(error.message).toBe('Waiting for the browser to launch failed: timeout exceeded. Re-run with the DEBUG=pw:browser* env variable to see the debug log.');
});
it('should handle exception', async({browserType, defaultBrowserOptions}) => {
const e = new Error('Dummy');
const options = { ...defaultBrowserOptions, __testHookBeforeCreateBrowser: () => { throw e; } };
const error = await browserType.launch(options).catch(e => e);
expect(error).toBe(e);
});
});
describe('browserType.launchPersistentContext', function() {
@ -87,6 +98,21 @@ describe('Playwright', function() {
await browserContext.close();
await removeUserDataDir(userDataDir);
});
it('should handle timeout', async({browserType, defaultBrowserOptions}) => {
const userDataDir = await makeUserDataDir();
const options = { ...defaultBrowserOptions, timeout: 1000, __testHookBeforeCreateBrowser: () => new Promise(f => setTimeout(f, 2000)) };
const error = await browserType.launchPersistentContext(userDataDir, options).catch(e => e);
expect(error.message).toBe('Waiting for the browser to launch failed: timeout exceeded. Re-run with the DEBUG=pw:browser* env variable to see the debug log.');
await removeUserDataDir(userDataDir);
});
it('should handle exception', async({browserType, defaultBrowserOptions}) => {
const userDataDir = await makeUserDataDir();
const e = new Error('Dummy');
const options = { ...defaultBrowserOptions, __testHookBeforeCreateBrowser: () => { throw e; } };
const error = await browserType.launchPersistentContext(userDataDir, options).catch(e => e);
expect(error).toBe(e);
await removeUserDataDir(userDataDir);
});
});
describe('browserType.launchServer', function() {