feat: handle unhandled promise rejections in tests (#7722)

In some situations, Puppeteer is left in an invalid state because protocol errors that could have been handled by the user where just hidden from them. This patch removes some of these cases and also makes sure that unhandled promise rejections lead to a test failure in mocha.
This commit is contained in:
Jan Scheffler 2021-10-27 13:43:57 +02:00 committed by GitHub
parent 4f540d49fe
commit 07febca04b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 70 additions and 33 deletions

View File

@ -240,6 +240,7 @@ export class Browser extends EventEmitter {
private _defaultContext: BrowserContext; private _defaultContext: BrowserContext;
private _contexts: Map<string, BrowserContext>; private _contexts: Map<string, BrowserContext>;
private _screenshotTaskQueue: TaskQueue; private _screenshotTaskQueue: TaskQueue;
private _ignoredTargets = new Set<string>();
/** /**
* @internal * @internal
* Used in Target.ts directly so cannot be marked private. * Used in Target.ts directly so cannot be marked private.
@ -374,6 +375,7 @@ export class Browser extends EventEmitter {
const shouldAttachToTarget = this._targetFilterCallback(targetInfo); const shouldAttachToTarget = this._targetFilterCallback(targetInfo);
if (!shouldAttachToTarget) { if (!shouldAttachToTarget) {
this._ignoredTargets.add(targetInfo.targetId);
return; return;
} }
@ -398,6 +400,7 @@ export class Browser extends EventEmitter {
} }
private async _targetDestroyed(event: { targetId: string }): Promise<void> { private async _targetDestroyed(event: { targetId: string }): Promise<void> {
if (this._ignoredTargets.has(event.targetId)) return;
const target = this._targets.get(event.targetId); const target = this._targets.get(event.targetId);
target._initializedCallback(false); target._initializedCallback(false);
this._targets.delete(event.targetId); this._targets.delete(event.targetId);
@ -413,6 +416,7 @@ export class Browser extends EventEmitter {
private _targetInfoChanged( private _targetInfoChanged(
event: Protocol.Target.TargetInfoChangedEvent event: Protocol.Target.TargetInfoChangedEvent
): void { ): void {
if (this._ignoredTargets.has(event.targetInfo.targetId)) return;
const target = this._targets.get(event.targetInfo.targetId); const target = this._targets.get(event.targetInfo.targetId);
assert(target, 'target should exist before targetInfoChanged'); assert(target, 'target should exist before targetInfoChanged');
const previousURL = target.url(); const previousURL = target.url();

View File

@ -115,6 +115,10 @@ export class DOMWorld {
async _setContext(context?: ExecutionContext): Promise<void> { async _setContext(context?: ExecutionContext): Promise<void> {
if (context) { if (context) {
assert(
this._contextResolveCallback,
'Execution Context has already been set.'
);
this._ctxBindings.clear(); this._ctxBindings.clear();
this._contextResolveCallback.call(null, context); this._contextResolveCallback.call(null, context);
this._contextResolveCallback = null; this._contextResolveCallback = null;

View File

@ -229,8 +229,7 @@ export class HTTPRequest {
*/ */
async finalizeInterceptions(): Promise<void> { async finalizeInterceptions(): Promise<void> {
await this._interceptActions.reduce( await this._interceptActions.reduce(
(promiseChain, interceptAction) => (promiseChain, interceptAction) => promiseChain.then(interceptAction),
promiseChain.then(interceptAction).catch(handleError),
Promise.resolve() Promise.resolve()
); );
const [resolution] = this.interceptResolution(); const [resolution] = this.interceptResolution();
@ -440,7 +439,10 @@ export class HTTPRequest {
postData: postDataBinaryBase64, postData: postDataBinaryBase64,
headers: headers ? headersArray(headers) : undefined, headers: headers ? headersArray(headers) : undefined,
}) })
.catch(handleError); .catch((error) => {
this._interceptionHandled = false;
return handleError(error);
});
} }
/** /**
@ -532,7 +534,10 @@ export class HTTPRequest {
responseHeaders: headersArray(responseHeaders), responseHeaders: headersArray(responseHeaders),
body: responseBody ? responseBody.toString('base64') : undefined, body: responseBody ? responseBody.toString('base64') : undefined,
}) })
.catch(handleError); .catch((error) => {
this._interceptionHandled = false;
return handleError(error);
});
} }
/** /**

View File

@ -88,7 +88,7 @@ describe('Frame specs', function () {
// This test checks if Frame.evaluate allows a readonly array to be an argument. // This test checks if Frame.evaluate allows a readonly array to be an argument.
// See https://github.com/puppeteer/puppeteer/issues/6953. // See https://github.com/puppeteer/puppeteer/issues/6953.
const readonlyArray: readonly string[] = ['a', 'b', 'c']; const readonlyArray: readonly string[] = ['a', 'b', 'c'];
mainFrame.evaluate((arr) => arr, readonlyArray); await mainFrame.evaluate((arr) => arr, readonlyArray);
}); });
}); });

View File

@ -589,7 +589,8 @@ describe('Launcher specs', function () {
await page.close(); await page.close();
await browser.close(); await browser.close();
}); });
it('should support targetFilter option', async () => { // @see https://github.com/puppeteer/puppeteer/issues/4197
itFailsFirefox('should support targetFilter option', async () => {
const { server, puppeteer, defaultBrowserOptions } = getTestState(); const { server, puppeteer, defaultBrowserOptions } = getTestState();
const originalBrowser = await puppeteer.launch(defaultBrowserOptions); const originalBrowser = await puppeteer.launch(defaultBrowserOptions);
@ -604,14 +605,15 @@ describe('Launcher specs', function () {
const browser = await puppeteer.connect({ const browser = await puppeteer.connect({
browserWSEndpoint, browserWSEndpoint,
targetFilter: (targetInfo: Protocol.Target.TargetInfo) => targetFilter: (targetInfo: Protocol.Target.TargetInfo) =>
!targetInfo.url.includes('should-be-ignored'), !targetInfo.url?.includes('should-be-ignored'),
}); });
const pages = await browser.pages(); const pages = await browser.pages();
await page2.close(); await page2.close();
await page1.close(); await page1.close();
await browser.close(); await browser.disconnect();
await originalBrowser.close();
expect(pages.map((p: Page) => p.url()).sort()).toEqual([ expect(pages.map((p: Page) => p.url()).sort()).toEqual([
'about:blank', 'about:blank',

View File

@ -221,6 +221,10 @@ console.log(
}` }`
); );
process.on('unhandledRejection', (reason) => {
throw reason;
});
export const setupTestBrowserHooks = (): void => { export const setupTestBrowserHooks = (): void => {
before(async () => { before(async () => {
const browser = await puppeteer.launch(defaultBrowserOptions); const browser = await puppeteer.launch(defaultBrowserOptions);

View File

@ -67,31 +67,6 @@ describe('network', function () {
expect(requests.length).toBe(2); expect(requests.length).toBe(2);
}); });
}); });
describeFailsFirefox('Request.continue', () => {
it('should split a request header at new line characters and add the header multiple times instead', async () => {
const { page, server } = getTestState();
let resolve;
const errorPromise = new Promise((r) => {
resolve = r;
});
await page.setRequestInterception(true);
page.on('request', async (request) => {
await request
.continue({
headers: {
'X-Test-Header': 'a\nb',
},
})
.catch(resolve);
});
page.goto(server.PREFIX + '/empty.html');
const error = await errorPromise;
expect(error).toBeTruthy();
});
});
describe('Request.frame', function () { describe('Request.frame', function () {
it('should work for main frame navigation request', async () => { it('should work for main frame navigation request', async () => {
const { page, server } = getTestState(); const { page, server } = getTestState();

View File

@ -626,6 +626,26 @@ describe('request interception', function () {
expect(serverRequest.method).toBe('POST'); expect(serverRequest.method).toBe('POST');
expect(await serverRequest.postBody).toBe('doggo'); expect(await serverRequest.postBody).toBe('doggo');
}); });
it('should fail if the header value is invalid', async () => {
const { page, server } = getTestState();
let error;
await page.setRequestInterception(true);
page.on('request', async (request) => {
await request
.continue({
headers: {
'X-Invalid-Header': 'a\nb',
},
})
.catch((error_) => {
error = error_;
});
await request.continue();
});
await page.goto(server.PREFIX + '/empty.html');
expect(error.message).toMatch(/Invalid header/);
});
}); });
describeFailsFirefox('Request.respond', function () { describeFailsFirefox('Request.respond', function () {
@ -732,6 +752,29 @@ describe('request interception', function () {
'Yo, page!' 'Yo, page!'
); );
}); });
it('should fail if the header value is invalid', async () => {
const { page, server } = getTestState();
let error;
await page.setRequestInterception(true);
page.on('request', async (request) => {
await request
.respond({
headers: {
'X-Invalid-Header': 'a\nb',
},
})
.catch((error_) => {
error = error_;
});
await request.respond({
status: 200,
body: 'Hello World',
});
});
await page.goto(server.PREFIX + '/empty.html');
expect(error.message).toMatch(/Invalid header/);
});
}); });
}); });