Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/playwright-core/browsers.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
},
{
"name": "webkit",
"revision": "2048",
"revision": "2051",
"installByDefault": true,
"revisionOverrides": {
"mac10.14": "1446",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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 });
Expand Down
28 changes: 28 additions & 0 deletions packages/playwright-core/src/server/webkit/wkPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
45 changes: 42 additions & 3 deletions packages/playwright-core/src/server/webkit/wkProvisionalPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,29 @@ 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>;

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) => {
Expand All @@ -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);
}
Expand All @@ -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) {
Comment thread
yury-s marked this conversation as resolved.
// 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;
Expand Down
3 changes: 1 addition & 2 deletions tests/library/capabilities.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
56 changes: 54 additions & 2 deletions tests/page/page-goto.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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);
Expand Down