mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
fix(webkit): always push state changes to the provisional page (#564)
This commit is contained in:
parent
27bdc664c1
commit
6308dbe01e
@ -16,7 +16,7 @@
|
||||
*/
|
||||
|
||||
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 network from '../network';
|
||||
import { WKSession } from './wkConnection';
|
||||
@ -33,6 +33,7 @@ import * as types from '../types';
|
||||
import * as accessibility from '../accessibility';
|
||||
import * as platform from '../platform';
|
||||
import { getAccessibilityTree } from './wkAccessibility';
|
||||
import { WKProvisionalPage } from './wkProvisionalPage';
|
||||
|
||||
const UTILITY_WORLD_NAME = '__playwright_utility_world__';
|
||||
const BINDING_CALL_MESSAGE = '__playwright_binding_call__';
|
||||
@ -41,6 +42,7 @@ export class WKPage implements PageDelegate {
|
||||
readonly rawMouse: RawMouseImpl;
|
||||
readonly rawKeyboard: RawKeyboardImpl;
|
||||
_session: WKSession;
|
||||
private _provisionalPage: WKProvisionalPage | null = null;
|
||||
readonly _page: Page;
|
||||
private readonly _pageProxySession: WKSession;
|
||||
private readonly _requestIdToRequest = new Map<string, WKInterceptableRequest>();
|
||||
@ -72,20 +74,17 @@ export class WKPage implements PageDelegate {
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
setSession(session: WKSession) {
|
||||
private _setSession(session: WKSession) {
|
||||
helper.removeEventListeners(this._sessionListeners);
|
||||
this.disconnectFromTarget();
|
||||
this._disconnectFromTarget();
|
||||
this._session = session;
|
||||
this.rawKeyboard.setSession(session);
|
||||
this._addSessionListeners();
|
||||
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([
|
||||
this._initializePageProxySession(),
|
||||
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
|
||||
// may be different from the current session and may be destroyed without becoming current.
|
||||
async _initializeSession(session: WKSession, resourceTreeHandler: (r: Protocol.Page.getResourceTreeReturnValue) => void) {
|
||||
const isProvisional = this._session !== session;
|
||||
const promises : Promise<any>[] = [
|
||||
// Page agent must be enabled before Runtime.
|
||||
session.send('Page.enable'),
|
||||
@ -121,12 +119,14 @@ export class WKPage implements PageDelegate {
|
||||
promises.push(session.send('Page.overrideUserAgent', { value: contextOptions.userAgent }));
|
||||
if (this._page._state.mediaType || this._page._state.colorScheme)
|
||||
promises.push(WKPage._setEmulateMedia(session, this._page._state.mediaType, this._page._state.colorScheme));
|
||||
if (isProvisional)
|
||||
promises.push(this._setBootstrapScripts(session));
|
||||
if (this._bootstrapScripts.length) {
|
||||
const source = this._bootstrapScripts.join(';');
|
||||
promises.push(session.send('Page.setBootstrapScript', { source }));
|
||||
}
|
||||
if (contextOptions.bypassCSP)
|
||||
promises.push(session.send('Page.setBypassCSP', { enabled: true }));
|
||||
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)
|
||||
promises.push(session.send('Page.setTouchEmulationEnabled', { enabled: true }));
|
||||
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) {
|
||||
helper.removeEventListeners(this._sessionListeners);
|
||||
this.disconnectFromTarget();
|
||||
this._disconnectFromTarget();
|
||||
if (crashed)
|
||||
this._page._didCrash();
|
||||
else
|
||||
this._page._didClose();
|
||||
}
|
||||
|
||||
didDisconnect() {
|
||||
dispose() {
|
||||
if (this._provisionalPage) {
|
||||
this._provisionalPage.dispose();
|
||||
this._provisionalPage = null;
|
||||
}
|
||||
this._page._didDisconnect();
|
||||
}
|
||||
|
||||
_addSessionListeners() {
|
||||
private _addSessionListeners() {
|
||||
this._sessionListeners = [
|
||||
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)),
|
||||
@ -180,7 +208,7 @@ export class WKPage implements PageDelegate {
|
||||
];
|
||||
}
|
||||
|
||||
disconnectFromTarget() {
|
||||
private _disconnectFromTarget() {
|
||||
for (const context of this._contextIdToContext.values()) {
|
||||
(context._delegate as WKExecutionContext)._dispose();
|
||||
context.frame._contextDestroyed(context);
|
||||
@ -188,15 +216,33 @@ export class WKPage implements PageDelegate {
|
||||
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);
|
||||
}
|
||||
|
||||
_onLifecycleEvent(frameId: string, event: frames.LifecycleEvent) {
|
||||
private _onLifecycleEvent(frameId: string, event: frames.LifecycleEvent) {
|
||||
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._onFrameNavigated(frameTree.frame, true);
|
||||
if (!frameTree.childFrames)
|
||||
@ -210,7 +256,7 @@ export class WKPage implements PageDelegate {
|
||||
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);
|
||||
for (const [contextId, context] of this._contextIdToContext) {
|
||||
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);
|
||||
}
|
||||
|
||||
_onFrameNavigatedWithinDocument(frameId: string, url: string) {
|
||||
private _onFrameNavigatedWithinDocument(frameId: string, url: string) {
|
||||
this._page._frameManager.frameCommittedSameDocumentNavigation(frameId, url);
|
||||
}
|
||||
|
||||
_onFrameDetached(frameId: string) {
|
||||
private _onFrameDetached(frameId: string) {
|
||||
this._page._frameManager.frameDetached(frameId);
|
||||
}
|
||||
|
||||
_onExecutionContextCreated(contextPayload : Protocol.Runtime.ExecutionContextDescription) {
|
||||
private _onExecutionContextCreated(contextPayload : Protocol.Runtime.ExecutionContextDescription) {
|
||||
if (this._contextIdToContext.has(contextPayload.id))
|
||||
return;
|
||||
const frame = this._page._frameManager.frame(contextPayload.frameId);
|
||||
@ -259,7 +305,7 @@ export class WKPage implements PageDelegate {
|
||||
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;
|
||||
if (level === 'debug' && parameters && parameters[0].value === BINDING_CALL_MESSAGE) {
|
||||
const parsedObjectId = JSON.parse(parameters[1].objectId!);
|
||||
@ -304,16 +350,12 @@ export class WKPage implements PageDelegate {
|
||||
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 handle = context._createHandle(event.element).asElement()!;
|
||||
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> {
|
||||
const promises = [];
|
||||
promises.push(session.send('Page.setEmulatedMedia', { media: mediaType || '' }));
|
||||
@ -329,11 +371,11 @@ export class WKPage implements PageDelegate {
|
||||
}
|
||||
|
||||
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> {
|
||||
await WKPage._setEmulateMedia(this._session, mediaType, colorScheme);
|
||||
await this._forAllSessions(session => WKPage._setEmulateMedia(session, mediaType, colorScheme));
|
||||
}
|
||||
|
||||
async setViewport(viewport: types.Viewport): Promise<void> {
|
||||
@ -344,21 +386,21 @@ export class WKPage implements PageDelegate {
|
||||
this._page._state.hasTouch = !!viewport.isMobile;
|
||||
await Promise.all([
|
||||
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> {
|
||||
const disabled = !enabled;
|
||||
await this._session.send('Network.setResourceCachingDisabled', { disabled });
|
||||
await this._updateState('Network.setResourceCachingDisabled', { disabled });
|
||||
}
|
||||
|
||||
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) {
|
||||
await this._session.send('Network.setEmulateOfflineState', { offline });
|
||||
await this._updateState('Network.setEmulateOfflineState', { offline });
|
||||
}
|
||||
|
||||
async authenticate(credentials: types.Credentials | null) {
|
||||
@ -388,18 +430,18 @@ export class WKPage implements PageDelegate {
|
||||
async exposeBinding(name: string, bindingFunction: string): Promise<void> {
|
||||
const script = `self.${name} = (param) => console.debug('${BINDING_CALL_MESSAGE}', {}, param); ${bindingFunction}`;
|
||||
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)));
|
||||
}
|
||||
|
||||
async evaluateOnNewDocument(script: string): Promise<void> {
|
||||
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(';');
|
||||
await session.send('Page.setBootstrapScript', { source });
|
||||
await this._updateState('Page.setBootstrapScript', { source });
|
||||
}
|
||||
|
||||
async closePage(runBeforeUnload: boolean): Promise<void> {
|
||||
|
@ -22,7 +22,6 @@ import { WKSession } from './wkConnection';
|
||||
import { WKPage } from './wkPage';
|
||||
import { RegisteredListener, helper, assert, debugError } from '../helper';
|
||||
import { Events } from '../events';
|
||||
import { WKProvisionalPage } from './wkProvisionalPage';
|
||||
|
||||
const isPovisionalSymbol = Symbol('isPovisional');
|
||||
|
||||
@ -31,7 +30,6 @@ export class WKPageProxy {
|
||||
readonly _browserContext: BrowserContext;
|
||||
private _pagePromise: Promise<Page> | null = null;
|
||||
private _wkPage: WKPage | null = null;
|
||||
private _provisionalPage: WKProvisionalPage | null = null;
|
||||
private readonly _firstTargetPromise: Promise<void>;
|
||||
private _firstTargetCallback?: () => void;
|
||||
private readonly _sessions = new Map<string, WKSession>();
|
||||
@ -68,12 +66,8 @@ export class WKPageProxy {
|
||||
for (const session of this._sessions.values())
|
||||
session.dispose();
|
||||
this._sessions.clear();
|
||||
if (this._provisionalPage) {
|
||||
this._provisionalPage.dispose();
|
||||
this._provisionalPage = null;
|
||||
}
|
||||
if (this._wkPage)
|
||||
this._wkPage.didDisconnect();
|
||||
this._wkPage.dispose();
|
||||
}
|
||||
|
||||
dispatchMessageToSession(message: any) {
|
||||
@ -123,8 +117,7 @@ export class WKPageProxy {
|
||||
}
|
||||
assert(session, 'One non-provisional target session must exist');
|
||||
this._wkPage = new WKPage(this._browserContext, this._pageProxySession);
|
||||
this._wkPage.setSession(session!);
|
||||
await this._wkPage.initialize();
|
||||
await this._wkPage.initialize(session!);
|
||||
return this._wkPage._page;
|
||||
}
|
||||
|
||||
@ -145,10 +138,8 @@ export class WKPageProxy {
|
||||
}
|
||||
if (targetInfo.isProvisional)
|
||||
(session as any)[isPovisionalSymbol] = true;
|
||||
if (targetInfo.isProvisional && this._wkPage) {
|
||||
assert(!this._provisionalPage);
|
||||
this._provisionalPage = new WKProvisionalPage(session, this._wkPage);
|
||||
}
|
||||
if (targetInfo.isProvisional && this._wkPage)
|
||||
this._wkPage.onProvisionalLoadStarted(session);
|
||||
if (targetInfo.isPaused)
|
||||
this._pageProxySession.send('Target.resume', { targetId: targetInfo.targetId }).catch(debugError);
|
||||
}
|
||||
@ -156,15 +147,11 @@ export class WKPageProxy {
|
||||
private _onTargetDestroyed(event: Protocol.Target.targetDestroyedPayload) {
|
||||
const { targetId, crashed } = event;
|
||||
const session = this._sessions.get(targetId);
|
||||
if (session)
|
||||
session.dispose();
|
||||
assert(session, 'Unknown target destroyed: ' + targetId);
|
||||
session!.dispose();
|
||||
this._sessions.delete(targetId);
|
||||
if (this._provisionalPage && this._provisionalPage._session === session) {
|
||||
this._provisionalPage.dispose();
|
||||
this._provisionalPage = null;
|
||||
}
|
||||
if (this._wkPage && this._wkPage._session === session && crashed)
|
||||
this._wkPage.didClose(crashed);
|
||||
if (this._wkPage)
|
||||
this._wkPage.onSessionDestroyed(session!, crashed);
|
||||
}
|
||||
|
||||
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.
|
||||
oldSession!.errorText = 'Target was swapped out.';
|
||||
(newSession as any)[isPovisionalSymbol] = undefined;
|
||||
if (this._provisionalPage) {
|
||||
this._provisionalPage.commit();
|
||||
this._provisionalPage.dispose();
|
||||
this._provisionalPage = null;
|
||||
}
|
||||
if (this._wkPage)
|
||||
this._wkPage.setSession(newSession!);
|
||||
this._wkPage.onProvisionalLoadCommitted(newSession!);
|
||||
}
|
||||
}
|
||||
|
@ -561,6 +561,19 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p
|
||||
await page.setOfflineMode(false);
|
||||
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', () => {
|
||||
|
@ -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.');
|
||||
});
|
||||
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() {
|
||||
|
Loading…
x
Reference in New Issue
Block a user