fix(webkit): always push state changes to the provisional page (#564)

This commit is contained in:
Yury Semikhatsky 2020-01-22 14:17:44 -08:00 committed by Dmitry Gozman
parent 27bdc664c1
commit 6308dbe01e
4 changed files with 118 additions and 66 deletions

View File

@ -16,7 +16,7 @@
*/ */
import * as frames from '../frames'; import * as frames from '../frames';
import { debugError, helper, RegisteredListener } from '../helper'; import { debugError, helper, RegisteredListener, assert } from '../helper';
import * as dom from '../dom'; import * as dom from '../dom';
import * as network from '../network'; import * as network from '../network';
import { WKSession } from './wkConnection'; import { WKSession } from './wkConnection';
@ -33,6 +33,7 @@ import * as types from '../types';
import * as accessibility from '../accessibility'; import * as accessibility from '../accessibility';
import * as platform from '../platform'; import * as platform from '../platform';
import { getAccessibilityTree } from './wkAccessibility'; import { getAccessibilityTree } from './wkAccessibility';
import { WKProvisionalPage } from './wkProvisionalPage';
const UTILITY_WORLD_NAME = '__playwright_utility_world__'; const UTILITY_WORLD_NAME = '__playwright_utility_world__';
const BINDING_CALL_MESSAGE = '__playwright_binding_call__'; const BINDING_CALL_MESSAGE = '__playwright_binding_call__';
@ -41,6 +42,7 @@ export class WKPage implements PageDelegate {
readonly rawMouse: RawMouseImpl; readonly rawMouse: RawMouseImpl;
readonly rawKeyboard: RawKeyboardImpl; readonly rawKeyboard: RawKeyboardImpl;
_session: WKSession; _session: WKSession;
private _provisionalPage: WKProvisionalPage | null = null;
readonly _page: Page; readonly _page: Page;
private readonly _pageProxySession: WKSession; private readonly _pageProxySession: WKSession;
private readonly _requestIdToRequest = new Map<string, WKInterceptableRequest>(); private readonly _requestIdToRequest = new Map<string, WKInterceptableRequest>();
@ -72,20 +74,17 @@ export class WKPage implements PageDelegate {
await Promise.all(promises); await Promise.all(promises);
} }
setSession(session: WKSession) { private _setSession(session: WKSession) {
helper.removeEventListeners(this._sessionListeners); helper.removeEventListeners(this._sessionListeners);
this.disconnectFromTarget(); this._disconnectFromTarget();
this._session = session; this._session = session;
this.rawKeyboard.setSession(session); this.rawKeyboard.setSession(session);
this._addSessionListeners(); this._addSessionListeners();
this._workers.setSession(session); this._workers.setSession(session);
// New bootstrap scripts may have been added during provisional load, push them
// again to be on the safe side.
if (this._bootstrapScripts.length)
this._setBootstrapScripts(session).catch(e => debugError(e));
} }
async initialize() { async initialize(session: WKSession) {
this._setSession(session);
await Promise.all([ await Promise.all([
this._initializePageProxySession(), this._initializePageProxySession(),
this._initializeSession(this._session, ({frameTree}) => this._handleFrameTree(frameTree)), this._initializeSession(this._session, ({frameTree}) => this._handleFrameTree(frameTree)),
@ -95,7 +94,6 @@ export class WKPage implements PageDelegate {
// This method is called for provisional targets as well. The session passed as the parameter // This method is called for provisional targets as well. The session passed as the parameter
// may be different from the current session and may be destroyed without becoming current. // may be different from the current session and may be destroyed without becoming current.
async _initializeSession(session: WKSession, resourceTreeHandler: (r: Protocol.Page.getResourceTreeReturnValue) => void) { async _initializeSession(session: WKSession, resourceTreeHandler: (r: Protocol.Page.getResourceTreeReturnValue) => void) {
const isProvisional = this._session !== session;
const promises : Promise<any>[] = [ const promises : Promise<any>[] = [
// Page agent must be enabled before Runtime. // Page agent must be enabled before Runtime.
session.send('Page.enable'), session.send('Page.enable'),
@ -121,12 +119,14 @@ export class WKPage implements PageDelegate {
promises.push(session.send('Page.overrideUserAgent', { value: contextOptions.userAgent })); promises.push(session.send('Page.overrideUserAgent', { value: contextOptions.userAgent }));
if (this._page._state.mediaType || this._page._state.colorScheme) if (this._page._state.mediaType || this._page._state.colorScheme)
promises.push(WKPage._setEmulateMedia(session, this._page._state.mediaType, this._page._state.colorScheme)); promises.push(WKPage._setEmulateMedia(session, this._page._state.mediaType, this._page._state.colorScheme));
if (isProvisional) if (this._bootstrapScripts.length) {
promises.push(this._setBootstrapScripts(session)); const source = this._bootstrapScripts.join(';');
promises.push(session.send('Page.setBootstrapScript', { source }));
}
if (contextOptions.bypassCSP) if (contextOptions.bypassCSP)
promises.push(session.send('Page.setBypassCSP', { enabled: true })); promises.push(session.send('Page.setBypassCSP', { enabled: true }));
if (this._page._state.extraHTTPHeaders !== null) if (this._page._state.extraHTTPHeaders !== null)
promises.push(WKPage._setExtraHTTPHeaders(session, this._page._state.extraHTTPHeaders)); promises.push(session.send('Network.setExtraHTTPHeaders', { headers: this._page._state.extraHTTPHeaders }));
if (this._page._state.hasTouch) if (this._page._state.hasTouch)
promises.push(session.send('Page.setTouchEmulationEnabled', { enabled: true })); promises.push(session.send('Page.setTouchEmulationEnabled', { enabled: true }));
await Promise.all(promises).catch(e => { await Promise.all(promises).catch(e => {
@ -139,20 +139,48 @@ export class WKPage implements PageDelegate {
}); });
} }
onProvisionalLoadStarted(provisionalSession: WKSession) {
assert(!this._provisionalPage);
this._provisionalPage = new WKProvisionalPage(provisionalSession, this);
}
onProvisionalLoadCommitted(session: WKSession) {
assert(this._provisionalPage);
assert(this._provisionalPage!._session === session);
this._provisionalPage!.commit();
this._provisionalPage!.dispose();
this._provisionalPage = null;
this._setSession(session);
}
onSessionDestroyed(session: WKSession, crashed: boolean) {
if (this._provisionalPage && this._provisionalPage._session === session) {
this._provisionalPage.dispose();
this._provisionalPage = null;
return;
}
if (this._session === session && crashed)
this.didClose(crashed);
}
didClose(crashed: boolean) { didClose(crashed: boolean) {
helper.removeEventListeners(this._sessionListeners); helper.removeEventListeners(this._sessionListeners);
this.disconnectFromTarget(); this._disconnectFromTarget();
if (crashed) if (crashed)
this._page._didCrash(); this._page._didCrash();
else else
this._page._didClose(); this._page._didClose();
} }
didDisconnect() { dispose() {
if (this._provisionalPage) {
this._provisionalPage.dispose();
this._provisionalPage = null;
}
this._page._didDisconnect(); this._page._didDisconnect();
} }
_addSessionListeners() { private _addSessionListeners() {
this._sessionListeners = [ this._sessionListeners = [
helper.addEventListener(this._session, 'Page.frameNavigated', event => this._onFrameNavigated(event.frame, false)), helper.addEventListener(this._session, 'Page.frameNavigated', event => this._onFrameNavigated(event.frame, false)),
helper.addEventListener(this._session, 'Page.navigatedWithinDocument', event => this._onFrameNavigatedWithinDocument(event.frameId, event.url)), helper.addEventListener(this._session, 'Page.navigatedWithinDocument', event => this._onFrameNavigatedWithinDocument(event.frameId, event.url)),
@ -180,7 +208,7 @@ export class WKPage implements PageDelegate {
]; ];
} }
disconnectFromTarget() { private _disconnectFromTarget() {
for (const context of this._contextIdToContext.values()) { for (const context of this._contextIdToContext.values()) {
(context._delegate as WKExecutionContext)._dispose(); (context._delegate as WKExecutionContext)._dispose();
context.frame._contextDestroyed(context); context.frame._contextDestroyed(context);
@ -188,15 +216,33 @@ export class WKPage implements PageDelegate {
this._contextIdToContext.clear(); this._contextIdToContext.clear();
} }
_onFrameStoppedLoading(frameId: string) { private async _updateState<T extends keyof Protocol.CommandParameters>(
method: T,
params?: Protocol.CommandParameters[T]
): Promise<void> {
await this._forAllSessions(session => session.send(method, params).then());
}
private async _forAllSessions(callback: ((session: WKSession) => Promise<void>)): Promise<void> {
const sessions = [
this._session
];
// If the state changes during provisional load, push it to the provisional page
// as well to always be in sync with the backend.
if (this._provisionalPage)
sessions.push(this._provisionalPage._session);
await Promise.all(sessions.map(session => callback(session).catch(debugError)));
}
private _onFrameStoppedLoading(frameId: string) {
this._page._frameManager.frameStoppedLoading(frameId); this._page._frameManager.frameStoppedLoading(frameId);
} }
_onLifecycleEvent(frameId: string, event: frames.LifecycleEvent) { private _onLifecycleEvent(frameId: string, event: frames.LifecycleEvent) {
this._page._frameManager.frameLifecycleEvent(frameId, event); this._page._frameManager.frameLifecycleEvent(frameId, event);
} }
_handleFrameTree(frameTree: Protocol.Page.FrameResourceTree) { private _handleFrameTree(frameTree: Protocol.Page.FrameResourceTree) {
this._onFrameAttached(frameTree.frame.id, frameTree.frame.parentId || null); this._onFrameAttached(frameTree.frame.id, frameTree.frame.parentId || null);
this._onFrameNavigated(frameTree.frame, true); this._onFrameNavigated(frameTree.frame, true);
if (!frameTree.childFrames) if (!frameTree.childFrames)
@ -210,7 +256,7 @@ export class WKPage implements PageDelegate {
this._page._frameManager.frameAttached(frameId, parentFrameId); this._page._frameManager.frameAttached(frameId, parentFrameId);
} }
_onFrameNavigated(framePayload: Protocol.Page.Frame, initial: boolean) { private _onFrameNavigated(framePayload: Protocol.Page.Frame, initial: boolean) {
const frame = this._page._frameManager.frame(framePayload.id); const frame = this._page._frameManager.frame(framePayload.id);
for (const [contextId, context] of this._contextIdToContext) { for (const [contextId, context] of this._contextIdToContext) {
if (context.frame === frame) { if (context.frame === frame) {
@ -224,15 +270,15 @@ export class WKPage implements PageDelegate {
this._page._frameManager.frameCommittedNewDocumentNavigation(framePayload.id, framePayload.url, framePayload.name || '', framePayload.loaderId, initial); this._page._frameManager.frameCommittedNewDocumentNavigation(framePayload.id, framePayload.url, framePayload.name || '', framePayload.loaderId, initial);
} }
_onFrameNavigatedWithinDocument(frameId: string, url: string) { private _onFrameNavigatedWithinDocument(frameId: string, url: string) {
this._page._frameManager.frameCommittedSameDocumentNavigation(frameId, url); this._page._frameManager.frameCommittedSameDocumentNavigation(frameId, url);
} }
_onFrameDetached(frameId: string) { private _onFrameDetached(frameId: string) {
this._page._frameManager.frameDetached(frameId); this._page._frameManager.frameDetached(frameId);
} }
_onExecutionContextCreated(contextPayload : Protocol.Runtime.ExecutionContextDescription) { private _onExecutionContextCreated(contextPayload : Protocol.Runtime.ExecutionContextDescription) {
if (this._contextIdToContext.has(contextPayload.id)) if (this._contextIdToContext.has(contextPayload.id))
return; return;
const frame = this._page._frameManager.frame(contextPayload.frameId); const frame = this._page._frameManager.frame(contextPayload.frameId);
@ -259,7 +305,7 @@ export class WKPage implements PageDelegate {
return true; return true;
} }
async _onConsoleMessage(event: Protocol.Console.messageAddedPayload) { private async _onConsoleMessage(event: Protocol.Console.messageAddedPayload) {
const { type, level, text, parameters, url, line: lineNumber, column: columnNumber, source } = event.message; const { type, level, text, parameters, url, line: lineNumber, column: columnNumber, source } = event.message;
if (level === 'debug' && parameters && parameters[0].value === BINDING_CALL_MESSAGE) { if (level === 'debug' && parameters && parameters[0].value === BINDING_CALL_MESSAGE) {
const parsedObjectId = JSON.parse(parameters[1].objectId!); const parsedObjectId = JSON.parse(parameters[1].objectId!);
@ -304,16 +350,12 @@ export class WKPage implements PageDelegate {
event.defaultPrompt)); event.defaultPrompt));
} }
async _onFileChooserOpened(event: {frameId: Protocol.Network.FrameId, element: Protocol.Runtime.RemoteObject}) { private async _onFileChooserOpened(event: {frameId: Protocol.Network.FrameId, element: Protocol.Runtime.RemoteObject}) {
const context = await this._page._frameManager.frame(event.frameId)!._mainContext(); const context = await this._page._frameManager.frame(event.frameId)!._mainContext();
const handle = context._createHandle(event.element).asElement()!; const handle = context._createHandle(event.element).asElement()!;
this._page._onFileChooserOpened(handle); this._page._onFileChooserOpened(handle);
} }
private static async _setExtraHTTPHeaders(session: WKSession, headers: network.Headers): Promise<void> {
await session.send('Network.setExtraHTTPHeaders', { headers });
}
private static async _setEmulateMedia(session: WKSession, mediaType: types.MediaType | null, colorScheme: types.ColorScheme | null): Promise<void> { private static async _setEmulateMedia(session: WKSession, mediaType: types.MediaType | null, colorScheme: types.ColorScheme | null): Promise<void> {
const promises = []; const promises = [];
promises.push(session.send('Page.setEmulatedMedia', { media: mediaType || '' })); promises.push(session.send('Page.setEmulatedMedia', { media: mediaType || '' }));
@ -329,11 +371,11 @@ export class WKPage implements PageDelegate {
} }
async setExtraHTTPHeaders(headers: network.Headers): Promise<void> { async setExtraHTTPHeaders(headers: network.Headers): Promise<void> {
await WKPage._setExtraHTTPHeaders(this._session, headers); await this._updateState('Network.setExtraHTTPHeaders', { headers });
} }
async setEmulateMedia(mediaType: types.MediaType | null, colorScheme: types.ColorScheme | null): Promise<void> { async setEmulateMedia(mediaType: types.MediaType | null, colorScheme: types.ColorScheme | null): Promise<void> {
await WKPage._setEmulateMedia(this._session, mediaType, colorScheme); await this._forAllSessions(session => WKPage._setEmulateMedia(session, mediaType, colorScheme));
} }
async setViewport(viewport: types.Viewport): Promise<void> { async setViewport(viewport: types.Viewport): Promise<void> {
@ -344,21 +386,21 @@ export class WKPage implements PageDelegate {
this._page._state.hasTouch = !!viewport.isMobile; this._page._state.hasTouch = !!viewport.isMobile;
await Promise.all([ await Promise.all([
this._pageProxySession.send('Emulation.setDeviceMetricsOverride', {width, height, fixedLayout, deviceScaleFactor }), this._pageProxySession.send('Emulation.setDeviceMetricsOverride', {width, height, fixedLayout, deviceScaleFactor }),
this._session.send('Page.setTouchEmulationEnabled', { enabled: !!viewport.isMobile }), this._updateState('Page.setTouchEmulationEnabled', { enabled: !!viewport.isMobile }),
]); ]);
} }
async setCacheEnabled(enabled: boolean): Promise<void> { async setCacheEnabled(enabled: boolean): Promise<void> {
const disabled = !enabled; const disabled = !enabled;
await this._session.send('Network.setResourceCachingDisabled', { disabled }); await this._updateState('Network.setResourceCachingDisabled', { disabled });
} }
async setRequestInterception(enabled: boolean): Promise<void> { async setRequestInterception(enabled: boolean): Promise<void> {
await this._session.send('Network.setInterceptionEnabled', { enabled, interceptRequests: enabled }); await this._updateState('Network.setInterceptionEnabled', { enabled, interceptRequests: enabled });
} }
async setOfflineMode(offline: boolean) { async setOfflineMode(offline: boolean) {
await this._session.send('Network.setEmulateOfflineState', { offline }); await this._updateState('Network.setEmulateOfflineState', { offline });
} }
async authenticate(credentials: types.Credentials | null) { async authenticate(credentials: types.Credentials | null) {
@ -388,18 +430,18 @@ export class WKPage implements PageDelegate {
async exposeBinding(name: string, bindingFunction: string): Promise<void> { async exposeBinding(name: string, bindingFunction: string): Promise<void> {
const script = `self.${name} = (param) => console.debug('${BINDING_CALL_MESSAGE}', {}, param); ${bindingFunction}`; const script = `self.${name} = (param) => console.debug('${BINDING_CALL_MESSAGE}', {}, param); ${bindingFunction}`;
this._bootstrapScripts.unshift(script); this._bootstrapScripts.unshift(script);
await this._setBootstrapScripts(this._session); await this._setBootstrapScripts();
await Promise.all(this._page.frames().map(frame => frame.evaluate(script).catch(debugError))); await Promise.all(this._page.frames().map(frame => frame.evaluate(script).catch(debugError)));
} }
async evaluateOnNewDocument(script: string): Promise<void> { async evaluateOnNewDocument(script: string): Promise<void> {
this._bootstrapScripts.push(script); this._bootstrapScripts.push(script);
await this._setBootstrapScripts(this._session); await this._setBootstrapScripts();
} }
private async _setBootstrapScripts(session: WKSession) { private async _setBootstrapScripts() {
const source = this._bootstrapScripts.join(';'); const source = this._bootstrapScripts.join(';');
await session.send('Page.setBootstrapScript', { source }); await this._updateState('Page.setBootstrapScript', { source });
} }
async closePage(runBeforeUnload: boolean): Promise<void> { async closePage(runBeforeUnload: boolean): Promise<void> {

View File

@ -22,7 +22,6 @@ import { WKSession } from './wkConnection';
import { WKPage } from './wkPage'; import { WKPage } from './wkPage';
import { RegisteredListener, helper, assert, debugError } from '../helper'; import { RegisteredListener, helper, assert, debugError } from '../helper';
import { Events } from '../events'; import { Events } from '../events';
import { WKProvisionalPage } from './wkProvisionalPage';
const isPovisionalSymbol = Symbol('isPovisional'); const isPovisionalSymbol = Symbol('isPovisional');
@ -31,7 +30,6 @@ export class WKPageProxy {
readonly _browserContext: BrowserContext; readonly _browserContext: BrowserContext;
private _pagePromise: Promise<Page> | null = null; private _pagePromise: Promise<Page> | null = null;
private _wkPage: WKPage | null = null; private _wkPage: WKPage | null = null;
private _provisionalPage: WKProvisionalPage | null = null;
private readonly _firstTargetPromise: Promise<void>; private readonly _firstTargetPromise: Promise<void>;
private _firstTargetCallback?: () => void; private _firstTargetCallback?: () => void;
private readonly _sessions = new Map<string, WKSession>(); private readonly _sessions = new Map<string, WKSession>();
@ -68,12 +66,8 @@ export class WKPageProxy {
for (const session of this._sessions.values()) for (const session of this._sessions.values())
session.dispose(); session.dispose();
this._sessions.clear(); this._sessions.clear();
if (this._provisionalPage) {
this._provisionalPage.dispose();
this._provisionalPage = null;
}
if (this._wkPage) if (this._wkPage)
this._wkPage.didDisconnect(); this._wkPage.dispose();
} }
dispatchMessageToSession(message: any) { dispatchMessageToSession(message: any) {
@ -123,8 +117,7 @@ export class WKPageProxy {
} }
assert(session, 'One non-provisional target session must exist'); assert(session, 'One non-provisional target session must exist');
this._wkPage = new WKPage(this._browserContext, this._pageProxySession); this._wkPage = new WKPage(this._browserContext, this._pageProxySession);
this._wkPage.setSession(session!); await this._wkPage.initialize(session!);
await this._wkPage.initialize();
return this._wkPage._page; return this._wkPage._page;
} }
@ -145,10 +138,8 @@ export class WKPageProxy {
} }
if (targetInfo.isProvisional) if (targetInfo.isProvisional)
(session as any)[isPovisionalSymbol] = true; (session as any)[isPovisionalSymbol] = true;
if (targetInfo.isProvisional && this._wkPage) { if (targetInfo.isProvisional && this._wkPage)
assert(!this._provisionalPage); this._wkPage.onProvisionalLoadStarted(session);
this._provisionalPage = new WKProvisionalPage(session, this._wkPage);
}
if (targetInfo.isPaused) if (targetInfo.isPaused)
this._pageProxySession.send('Target.resume', { targetId: targetInfo.targetId }).catch(debugError); this._pageProxySession.send('Target.resume', { targetId: targetInfo.targetId }).catch(debugError);
} }
@ -156,15 +147,11 @@ export class WKPageProxy {
private _onTargetDestroyed(event: Protocol.Target.targetDestroyedPayload) { private _onTargetDestroyed(event: Protocol.Target.targetDestroyedPayload) {
const { targetId, crashed } = event; const { targetId, crashed } = event;
const session = this._sessions.get(targetId); const session = this._sessions.get(targetId);
if (session) assert(session, 'Unknown target destroyed: ' + targetId);
session.dispose(); session!.dispose();
this._sessions.delete(targetId); this._sessions.delete(targetId);
if (this._provisionalPage && this._provisionalPage._session === session) { if (this._wkPage)
this._provisionalPage.dispose(); this._wkPage.onSessionDestroyed(session!, crashed);
this._provisionalPage = null;
}
if (this._wkPage && this._wkPage._session === session && crashed)
this._wkPage.didClose(crashed);
} }
private _onDispatchMessageFromTarget(event: Protocol.Target.dispatchMessageFromTargetPayload) { private _onDispatchMessageFromTarget(event: Protocol.Target.dispatchMessageFromTargetPayload) {
@ -183,12 +170,7 @@ export class WKPageProxy {
// TODO: make some calls like screenshot catch swapped out error and retry. // TODO: make some calls like screenshot catch swapped out error and retry.
oldSession!.errorText = 'Target was swapped out.'; oldSession!.errorText = 'Target was swapped out.';
(newSession as any)[isPovisionalSymbol] = undefined; (newSession as any)[isPovisionalSymbol] = undefined;
if (this._provisionalPage) {
this._provisionalPage.commit();
this._provisionalPage.dispose();
this._provisionalPage = null;
}
if (this._wkPage) if (this._wkPage)
this._wkPage.setSession(newSession!); this._wkPage.onProvisionalLoadCommitted(newSession!);
} }
} }

View File

@ -561,6 +561,19 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p
await page.setOfflineMode(false); await page.setOfflineMode(false);
expect(await page.evaluate(() => window.navigator.onLine)).toBe(true); expect(await page.evaluate(() => window.navigator.onLine)).toBe(true);
}); });
it('should continue if the interception gets disabled during provisional load', async({page, server}) => {
await page.goto(server.EMPTY_PAGE);
await page.setRequestInterception(true);
expect(await page.evaluate(() => navigator.onLine)).toBe(true);
let intercepted;
page.on('request', async request => {
intercepted = true;
await page.setRequestInterception(false);
});
const response = await page.goto(server.CROSS_PROCESS_PREFIX + '/empty.html');
expect(intercepted).toBe(true);
expect(response.status()).toBe(200);
});
}); });
describe('Interception vs isNavigationRequest', () => { describe('Interception vs isNavigationRequest', () => {

View File

@ -342,6 +342,21 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
} }
expect(error.message).toBe('Expected value of header "foo" to be String, but "number" is found.'); expect(error.message).toBe('Expected value of header "foo" to be String, but "number" is found.');
}); });
WEBKIT && it('should be pushed to cross-process provisional page', async({page, server}) => {
await page.goto(server.EMPTY_PAGE);
const pagePath = '/one-style.html';
server.setRoute(pagePath, async (req, res) => {
await page.setExtraHTTPHeaders({ foo: 'bar' });
server.serveFile(req, res, pagePath);
});
const [htmlReq, cssReq] = await Promise.all([
server.waitForRequest(pagePath),
server.waitForRequest('/one-style.css'),
page.goto(server.CROSS_PROCESS_PREFIX + pagePath)
]);
expect(htmlReq.headers['foo']).toBe(undefined);
expect(cssReq.headers['foo']).toBe('bar');
});
}); });
false && describe.skip(FFOX)('WebSocket', function() { false && describe.skip(FFOX)('WebSocket', function() {