fix: Service Workers+Interception: missing page-level Network events (#15549)

Fixes #15474.

Notes:

* page-level requests that are also handled by a SW's fetch handler, should not be interceptable at the page-level
* `Network.requestWillBeSent` does not provide enough metadata for Playwright to fire the `request` event at that time, so it does it as soon as it gets to the end of the request lifecycle
This commit is contained in:
Ross Wollman 2022-07-12 13:23:35 -07:00 committed by GitHub
parent 8402001c32
commit 8858162692
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 99 additions and 29 deletions

View File

@ -363,7 +363,19 @@ export class CRNetworkManager {
} }
_onResponseReceived(event: Protocol.Network.responseReceivedPayload) { _onResponseReceived(event: Protocol.Network.responseReceivedPayload) {
const request = this._requestIdToRequest.get(event.requestId); let request = this._requestIdToRequest.get(event.requestId);
// For frame-level Requests that are handled by a Service Worker's fetch handler, we'll never get a requestPaused event, so we need to
// manually create the request. In an ideal world, crNetworkManager would be able to know this on Network.requestWillBeSent, but there
// is not enough metadata there.
if (!request && event.response.fromServiceWorker) {
const requestWillBeSentEvent = this._requestIdToRequestWillBeSentEvent.get(event.requestId);
const frame = requestWillBeSentEvent?.frameId ? this._page?._frameManager.frame(requestWillBeSentEvent.frameId) : null;
if (requestWillBeSentEvent && frame) {
this._onRequest(frame, requestWillBeSentEvent, null /* requestPausedPayload */);
request = this._requestIdToRequest.get(event.requestId);
this._requestIdToRequestWillBeSentEvent.delete(event.requestId);
}
}
// FileUpload sends a response without a matching request. // FileUpload sends a response without a matching request.
if (!request) if (!request)
return; return;

View File

@ -3,6 +3,10 @@ self.addEventListener('fetch', event => {
event.respondWith(fetch(event.request)); event.respondWith(fetch(event.request));
return; return;
} }
if (event.request.url.includes('error')) {
event.respondWith(Promise.reject(new Error('uh oh')));
return;
}
const slash = event.request.url.lastIndexOf('/'); const slash = event.request.url.lastIndexOf('/');
const name = event.request.url.substring(slash + 1); const name = event.request.url.substring(slash + 1);
const blob = new Blob(["responseFromServiceWorker:" + name], {type : 'text/css'}); const blob = new Blob(["responseFromServiceWorker:" + name], {type : 'text/css'});

View File

@ -358,38 +358,92 @@ test('setOffline', async ({ context, page, server }) => {
expect(error).toMatch(/REJECTED.*Failed to fetch/); expect(error).toMatch(/REJECTED.*Failed to fetch/);
}); });
test('should emit page-level request event for respondWith', async ({ page, server }) => { test.describe('should emit page-level network events with service worker fetch handler', () => {
await page.goto(server.PREFIX + '/serviceworkers/fetchdummy/sw.html'); test.describe('when not using routing', () => {
await page.evaluate(() => window['activationPromise']); test('successful request', async ({ page, server }) => {
await page.goto(server.PREFIX + '/serviceworkers/fetchdummy/sw.html');
await page.evaluate(() => window['activationPromise']);
// Sanity check. const [pageReq, pageResp, /* pageFinished */, swResponse] = await Promise.all([
const [pageReq, swResponse] = await Promise.all([ page.waitForEvent('request'),
page.waitForEvent('request'), page.waitForEvent('response'),
page.evaluate(() => window['fetchDummy']('foo')), page.waitForEvent('requestfinished'),
]); page.evaluate(() => window['fetchDummy']('foo')),
expect(swResponse).toBe('responseFromServiceWorker:foo'); ]);
expect(pageReq.url()).toMatch(/fetchdummy\/foo$/); expect(swResponse).toBe('responseFromServiceWorker:foo');
expect(pageReq.serviceWorker()).toBe(null); expect(pageReq.url()).toMatch(/fetchdummy\/foo$/);
expect((await pageReq.response()).fromServiceWorker()).toBe(true); expect(pageReq.serviceWorker()).toBe(null);
}); expect(pageResp.fromServiceWorker()).toBe(true);
expect(pageResp).toBe(await pageReq.response());
expect((await pageReq.response()).fromServiceWorker()).toBe(true);
});
test('should emit page-level request event for respondWith when interception enabled', async ({ page, server, context }) => { test('failed request', async ({ page, server }) => {
test.fixme(); await page.goto(server.PREFIX + '/serviceworkers/fetchdummy/sw.html');
test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/15474' }); await page.evaluate(() => window['activationPromise']);
await context.route('**', route => route.continue()); const [pageReq] = await Promise.all([
await page.goto(server.PREFIX + '/serviceworkers/fetchdummy/sw.html'); page.waitForEvent('request'),
await page.evaluate(() => window['activationPromise']); page.waitForEvent('requestfailed'),
page.evaluate(() => window['fetchDummy']('error')).catch(e => e),
]);
expect(pageReq.url()).toMatch(/fetchdummy\/error$/);
expect(pageReq.failure().errorText).toMatch(/net::ERR_FAILED/);
expect(pageReq.serviceWorker()).toBe(null);
expect(await pageReq.response()).toBe(null);
});
});
// Sanity check. test.describe('when routing', () => {
const [pageReq, swResponse] = await Promise.all([ test('successful request', async ({ page, server, context }) => {
page.waitForEvent('request'), await context.route('**', route => route.continue());
page.evaluate(() => window['fetchDummy']('foo')), let markFailureIfPageRoutesARequestAlreadyHandledByServiceWorker = false;
]); await page.route('**', route => {
expect(swResponse).toBe('responseFromServiceWorker:foo'); if (route.request().url().endsWith('foo'))
expect(pageReq.url()).toMatch(/fetchdummy\/foo$/); markFailureIfPageRoutesARequestAlreadyHandledByServiceWorker = true;
expect(pageReq.serviceWorker()).toBe(null); route.continue();
expect((await pageReq.response()).fromServiceWorker()).toBe(true); });
await page.goto(server.PREFIX + '/serviceworkers/fetchdummy/sw.html');
await page.evaluate(() => window['activationPromise']);
const [pageReq, pageResp, /* pageFinished */, swResponse] = await Promise.all([
page.waitForEvent('request'),
page.waitForEvent('response'),
page.waitForEvent('requestfinished'),
page.evaluate(() => window['fetchDummy']('foo')),
]);
expect(swResponse).toBe('responseFromServiceWorker:foo');
expect(pageReq.url()).toMatch(/fetchdummy\/foo$/);
expect(pageReq.serviceWorker()).toBe(null);
expect(pageResp.fromServiceWorker()).toBe(true);
expect(pageResp).toBe(await pageReq.response());
expect((await pageReq.response()).fromServiceWorker()).toBe(true);
expect(markFailureIfPageRoutesARequestAlreadyHandledByServiceWorker).toBe(false);
});
test('failed request', async ({ page, server, context }) => {
await context.route('**', route => route.continue());
let markFailureIfPageRoutesARequestAlreadyHandledByServiceWorker = false;
await page.route('**', route => {
if (route.request().url().endsWith('foo'))
markFailureIfPageRoutesARequestAlreadyHandledByServiceWorker = true;
route.continue();
});
await page.goto(server.PREFIX + '/serviceworkers/fetchdummy/sw.html');
await page.evaluate(() => window['activationPromise']);
const [pageReq] = await Promise.all([
page.waitForEvent('request'),
page.waitForEvent('requestfailed'),
page.evaluate(() => window['fetchDummy']('error')).catch(e => e),
]);
expect(pageReq.url()).toMatch(/fetchdummy\/error$/);
expect(pageReq.failure().errorText).toMatch(/net::ERR_FAILED/);
expect(pageReq.serviceWorker()).toBe(null);
expect(await pageReq.response()).toBe(null);
expect(markFailureIfPageRoutesARequestAlreadyHandledByServiceWorker).toBe(false);
});
});
}); });
test('setExtraHTTPHeaders', async ({ context, page, server }) => { test('setExtraHTTPHeaders', async ({ context, page, server }) => {