mirror of
https://github.com/microsoft/playwright.git
synced 2024-11-28 17:44:33 +03:00
fix(webkit): reenable CrossOriginOpenerPolicy (#31765)
Depends on https://github.com/microsoft/playwright-browsers/pull/1160 Fixes: https://github.com/microsoft/playwright/issues/14043
This commit is contained in:
parent
c4862c022c
commit
1918ae5c4a
@ -27,7 +27,7 @@
|
||||
},
|
||||
{
|
||||
"name": "webkit",
|
||||
"revision": "2048",
|
||||
"revision": "2051",
|
||||
"installByDefault": true,
|
||||
"revisionOverrides": {
|
||||
"mac10.14": "1446",
|
||||
|
@ -40,9 +40,9 @@ const errorReasons: { [reason: string]: Protocol.Network.ResourceErrorType } = {
|
||||
};
|
||||
|
||||
export class WKInterceptableRequest {
|
||||
private readonly _session: WKSession;
|
||||
private _session: WKSession;
|
||||
private _requestId: string;
|
||||
readonly request: network.Request;
|
||||
private readonly _requestId: string;
|
||||
_timestamp: number;
|
||||
_wallTime: number;
|
||||
|
||||
@ -59,6 +59,11 @@ export class WKInterceptableRequest {
|
||||
resourceType, event.request.method, postDataBuffer, headersObjectToArray(event.request.headers));
|
||||
}
|
||||
|
||||
adoptRequestFromNewProcess(newSession: WKSession, requestId: string) {
|
||||
this._session = newSession;
|
||||
this._requestId = requestId;
|
||||
}
|
||||
|
||||
createResponse(responsePayload: Protocol.Network.Response): network.Response {
|
||||
const getResponseBody = async () => {
|
||||
const response = await this._session.send('Network.getResponseBody', { requestId: this._requestId });
|
||||
|
@ -250,6 +250,7 @@ export class WKPage implements PageDelegate {
|
||||
private _onTargetDestroyed(event: Protocol.Target.targetDestroyedPayload) {
|
||||
const { targetId, crashed } = event;
|
||||
if (this._provisionalPage && this._provisionalPage._session.sessionId === targetId) {
|
||||
this._maybeCancelCoopNavigationRequest(this._provisionalPage);
|
||||
this._provisionalPage._session.dispose();
|
||||
this._provisionalPage.dispose();
|
||||
this._provisionalPage = null;
|
||||
@ -1015,6 +1016,33 @@ export class WKPage implements PageDelegate {
|
||||
return context.createHandle(result.object) as dom.ElementHandle;
|
||||
}
|
||||
|
||||
private _maybeCancelCoopNavigationRequest(provisionalPage: WKProvisionalPage) {
|
||||
const navigationRequest = provisionalPage.coopNavigationRequest();
|
||||
for (const [requestId, request] of this._requestIdToRequest) {
|
||||
if (request.request === navigationRequest) {
|
||||
// Make sure the request completes if the provisional navigation is canceled.
|
||||
this._onLoadingFailed(provisionalPage._session, {
|
||||
requestId: requestId,
|
||||
errorText: 'Provisiolal navigation canceled.',
|
||||
timestamp: request._timestamp,
|
||||
canceled: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_adoptRequestFromNewProcess(navigationRequest: network.Request, newSession: WKSession, newRequestId: string) {
|
||||
for (const [requestId, request] of this._requestIdToRequest) {
|
||||
if (request.request === navigationRequest) {
|
||||
this._requestIdToRequest.delete(requestId);
|
||||
request.adoptRequestFromNewProcess(newSession, newRequestId);
|
||||
this._requestIdToRequest.set(newRequestId, request);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onRequestWillBeSent(session: WKSession, event: Protocol.Network.requestWillBeSentPayload) {
|
||||
if (event.request.url.startsWith('data:'))
|
||||
return;
|
||||
|
@ -20,10 +20,12 @@ import type { RegisteredListener } from '../../utils/eventsHelper';
|
||||
import { eventsHelper } from '../../utils/eventsHelper';
|
||||
import type { Protocol } from './protocol';
|
||||
import { assert } from '../../utils';
|
||||
import type * as network from '../network';
|
||||
|
||||
export class WKProvisionalPage {
|
||||
readonly _session: WKSession;
|
||||
private readonly _wkPage: WKPage;
|
||||
private _coopNavigationRequest: network.Request | undefined;
|
||||
private _sessionListeners: RegisteredListener[] = [];
|
||||
private _mainFrameId: string | null = null;
|
||||
readonly initializationPromise: Promise<void>;
|
||||
@ -31,6 +33,16 @@ export class WKProvisionalPage {
|
||||
constructor(session: WKSession, page: WKPage) {
|
||||
this._session = session;
|
||||
this._wkPage = page;
|
||||
// Cross-Origin-Opener-Policy (COOP) request starts in one process and once response headers
|
||||
// have been received, continues in another.
|
||||
//
|
||||
// Network.requestWillBeSent and requestIntercepted (if intercepting) from the original web process
|
||||
// will always come before a provisional page is created based on the response COOP headers.
|
||||
// Thereafter we'll receive targetCreated (provisional) and later on in some order loadingFailed from the
|
||||
// original process and requestWillBeSent from the provisional one. We should ignore loadingFailed
|
||||
// as the original request continues in the provisional process. But if the provisional load is later
|
||||
// canceled we should dispatch loadingFailed to the client.
|
||||
this._coopNavigationRequest = page._page.mainFrame().pendingDocument()?.request;
|
||||
|
||||
const overrideFrameId = (handler: (p: any) => void) => {
|
||||
return (payload: any) => {
|
||||
@ -43,16 +55,20 @@ export class WKProvisionalPage {
|
||||
const wkPage = this._wkPage;
|
||||
|
||||
this._sessionListeners = [
|
||||
eventsHelper.addEventListener(session, 'Network.requestWillBeSent', overrideFrameId(e => wkPage._onRequestWillBeSent(session, e))),
|
||||
eventsHelper.addEventListener(session, 'Network.requestWillBeSent', overrideFrameId(e => this._onRequestWillBeSent(e))),
|
||||
eventsHelper.addEventListener(session, 'Network.requestIntercepted', overrideFrameId(e => wkPage._onRequestIntercepted(session, e))),
|
||||
eventsHelper.addEventListener(session, 'Network.responseReceived', overrideFrameId(e => wkPage._onResponseReceived(session, e))),
|
||||
eventsHelper.addEventListener(session, 'Network.loadingFinished', overrideFrameId(e => wkPage._onLoadingFinished(e))),
|
||||
eventsHelper.addEventListener(session, 'Network.loadingFailed', overrideFrameId(e => wkPage._onLoadingFailed(session, e))),
|
||||
eventsHelper.addEventListener(session, 'Network.loadingFinished', overrideFrameId(e => this._onLoadingFinished(e))),
|
||||
eventsHelper.addEventListener(session, 'Network.loadingFailed', overrideFrameId(e => this._onLoadingFailed(e))),
|
||||
];
|
||||
|
||||
this.initializationPromise = this._wkPage._initializeSession(session, true, ({ frameTree }) => this._handleFrameTree(frameTree));
|
||||
}
|
||||
|
||||
coopNavigationRequest(): network.Request | undefined {
|
||||
return this._coopNavigationRequest;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
eventsHelper.removeEventListeners(this._sessionListeners);
|
||||
}
|
||||
@ -62,6 +78,29 @@ export class WKProvisionalPage {
|
||||
this._wkPage._onFrameAttached(this._mainFrameId, null);
|
||||
}
|
||||
|
||||
private _onRequestWillBeSent(event: Protocol.Network.requestWillBeSentPayload) {
|
||||
if (this._coopNavigationRequest && this._coopNavigationRequest.url() === event.request.url) {
|
||||
// If it's a continuation of the main frame navigation request after COOP headers were received,
|
||||
// take over original request, and replace its request id with the new one.
|
||||
this._wkPage._adoptRequestFromNewProcess(this._coopNavigationRequest, this._session, event.requestId);
|
||||
// Simply ignore this event as it has already been dispatched from the original process
|
||||
// and there will ne no requestIntercepted event from the provisional process as it resumes
|
||||
// existing network load (that has already received reponse headers).
|
||||
return;
|
||||
}
|
||||
this._wkPage._onRequestWillBeSent(this._session, event);
|
||||
}
|
||||
|
||||
private _onLoadingFinished(event: Protocol.Network.loadingFinishedPayload): void {
|
||||
this._coopNavigationRequest = undefined;
|
||||
this._wkPage._onLoadingFinished(event);
|
||||
}
|
||||
|
||||
private _onLoadingFailed(event: Protocol.Network.loadingFailedPayload) {
|
||||
this._coopNavigationRequest = undefined;
|
||||
this._wkPage._onLoadingFailed(this._session, event);
|
||||
}
|
||||
|
||||
private _handleFrameTree(frameTree: Protocol.Page.FrameResourceTree) {
|
||||
assert(!frameTree.frame.parentId);
|
||||
this._mainFrameId = frameTree.frame.id;
|
||||
|
@ -19,8 +19,7 @@ import url from 'url';
|
||||
import { contextTest as it, expect } from '../config/browserTest';
|
||||
import { hostPlatform } from '../../packages/playwright-core/src/utils/hostPlatform';
|
||||
|
||||
it('SharedArrayBuffer should work @smoke', async function({ contextFactory, httpsServer, browserName }) {
|
||||
it.fail(browserName === 'webkit', 'no shared array buffer on webkit');
|
||||
it('SharedArrayBuffer should work @smoke', async function({ contextFactory, httpsServer }) {
|
||||
const context = await contextFactory({ ignoreHTTPSErrors: true });
|
||||
const page = await context.newPage();
|
||||
httpsServer.setRoute('/sharedarraybuffer', (req, res) => {
|
||||
|
@ -78,7 +78,7 @@ it('should work with cross-process that fails before committing', async ({ page,
|
||||
expect(error instanceof Error).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should work with Cross-Origin-Opener-Policy', async ({ page, server, browserName }) => {
|
||||
it('should work with Cross-Origin-Opener-Policy', async ({ page, server }) => {
|
||||
server.setRoute('/empty.html', (req, res) => {
|
||||
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
|
||||
res.end();
|
||||
@ -109,7 +109,42 @@ it('should work with Cross-Origin-Opener-Policy', async ({ page, server, browser
|
||||
expect(response.request().failure()).toBeNull();
|
||||
});
|
||||
|
||||
it('should work with Cross-Origin-Opener-Policy after redirect', async ({ page, server, browserName }) => {
|
||||
it('should work with Cross-Origin-Opener-Policy and interception', async ({ page, server }) => {
|
||||
server.setRoute('/empty.html', (req, res) => {
|
||||
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
|
||||
res.end();
|
||||
});
|
||||
const requests = new Set();
|
||||
const events = [];
|
||||
page.on('request', r => {
|
||||
events.push('request');
|
||||
requests.add(r);
|
||||
});
|
||||
page.on('requestfailed', r => {
|
||||
events.push('requestfailed');
|
||||
requests.add(r);
|
||||
});
|
||||
page.on('requestfinished', r => {
|
||||
events.push('requestfinished');
|
||||
requests.add(r);
|
||||
});
|
||||
page.on('response', r => {
|
||||
events.push('response');
|
||||
requests.add(r.request());
|
||||
});
|
||||
await page.route('**/*', async route => {
|
||||
await new Promise(f => setTimeout(f, 100));
|
||||
await route.continue();
|
||||
});
|
||||
const response = await page.goto(server.EMPTY_PAGE);
|
||||
expect(page.url()).toBe(server.EMPTY_PAGE);
|
||||
await response.finished();
|
||||
expect(events).toEqual(['request', 'response', 'requestfinished']);
|
||||
expect(requests.size).toBe(1);
|
||||
expect(response.request().failure()).toBeNull();
|
||||
});
|
||||
|
||||
it('should work with Cross-Origin-Opener-Policy after redirect', async ({ page, server }) => {
|
||||
server.setRedirect('/redirect', '/empty.html');
|
||||
server.setRoute('/empty.html', (req, res) => {
|
||||
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
|
||||
@ -144,6 +179,23 @@ it('should work with Cross-Origin-Opener-Policy after redirect', async ({ page,
|
||||
expect(firstRequest.url()).toBe(server.PREFIX + '/redirect');
|
||||
});
|
||||
|
||||
it('should properly cancel Cross-Origin-Opener-Policy navigation', async ({ page, server }) => {
|
||||
server.setRoute('/empty.html', (req, res) => {
|
||||
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
|
||||
res.end();
|
||||
});
|
||||
const requestPromise = page.waitForRequest(server.EMPTY_PAGE);
|
||||
page.goto(server.EMPTY_PAGE).catch(() => {});
|
||||
await new Promise(f => setTimeout(f, 50));
|
||||
// Non COOP response.
|
||||
await page.goto(server.CROSS_PROCESS_PREFIX + '/error.html');
|
||||
const req = await requestPromise;
|
||||
const response = await Promise.race([req.response(), new Promise(f => setTimeout(() => f('timeout'), 5_000))]);
|
||||
// First navigation request should either receive response or be canceled by the second
|
||||
// navigation, but never hang unresolved.
|
||||
expect(response).not.toBe('timeout');
|
||||
});
|
||||
|
||||
it('should capture iframe navigation request', async ({ page, server }) => {
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
expect(page.url()).toBe(server.EMPTY_PAGE);
|
||||
|
Loading…
Reference in New Issue
Block a user