chore(chromium): move Page to common, implement PageDelegate (#184)

This commit is contained in:
Dmitry Gozman 2019-12-09 13:08:21 -08:00 committed by Yury Semikhatsky
parent 122837113b
commit c323a3e50b
13 changed files with 342 additions and 224 deletions

View File

@ -21,11 +21,12 @@ import { Events } from './events';
import { assert, helper } from '../helper';
import { BrowserContext } from './BrowserContext';
import { Connection, ConnectionEvents, CDPSession } from './Connection';
import { Page } from './Page';
import { Page } from '../page';
import { Target } from './Target';
import { Protocol } from './protocol';
import { Chromium } from './features/chromium';
import * as types from '../types';
import { FrameManager } from './FrameManager';
export class Browser extends EventEmitter {
private _ignoreHTTPSErrors: boolean;
@ -133,11 +134,11 @@ export class Browser extends EventEmitter {
this.chromium.emit(Events.Chromium.TargetChanged, target);
}
async newPage(): Promise<Page> {
async newPage(): Promise<Page<Browser, BrowserContext>> {
return this._defaultContext.newPage();
}
async _createPageInContext(contextId: string | null): Promise<Page> {
async _createPageInContext(contextId: string | null): Promise<Page<Browser, BrowserContext>> {
const { targetId } = await this._client.send('Target.createTarget', { url: 'about:blank', browserContextId: contextId || undefined });
const target = this._targets.get(targetId);
assert(await target._initializedPromise, 'Failed to create target for page');
@ -145,7 +146,7 @@ export class Browser extends EventEmitter {
return page;
}
async _closePage(page: Page) {
async _closePage(page: Page<Browser, BrowserContext>) {
await this._client.send('Target.closeTarget', { targetId: Target.fromPage(page)._targetId });
}
@ -153,14 +154,14 @@ export class Browser extends EventEmitter {
return Array.from(this._targets.values()).filter(target => target._isInitialized);
}
async _pages(context: BrowserContext): Promise<Page[]> {
async _pages(context: BrowserContext): Promise<Page<Browser, BrowserContext>[]> {
const targets = this._allTargets().filter(target => target.browserContext() === context && target.type() === 'page');
const pages = await Promise.all(targets.map(target => target.page()));
return pages.filter(page => !!page);
}
async _activatePage(page: Page) {
await page._client.send('Target.activateTarget', {targetId: Target.fromPage(page)._targetId});
async _activatePage(page: Page<Browser, BrowserContext>) {
await (page._delegate as FrameManager)._client.send('Target.activateTarget', {targetId: Target.fromPage(page)._targetId});
}
async _waitForTarget(predicate: (arg0: Target) => boolean, options: { timeout?: number; } | undefined = {}): Promise<Target> {
@ -189,7 +190,7 @@ export class Browser extends EventEmitter {
}
}
async pages(): Promise<Page[]> {
async pages(): Promise<Page<Browser, BrowserContext>[]> {
const contextPages = await Promise.all(this.browserContexts().map(context => context.pages()));
// Flatten array.
return contextPages.reduce((acc, x) => acc.concat(x), []);

View File

@ -20,7 +20,7 @@ import { filterCookies, NetworkCookie, rewriteCookies, SetNetworkCookieParam } f
import { Browser } from './Browser';
import { CDPSession } from './Connection';
import { Permissions } from './features/permissions';
import { Page } from './Page';
import { Page } from '../page';
export class BrowserContext {
readonly permissions: Permissions;
@ -34,7 +34,7 @@ export class BrowserContext {
this.permissions = new Permissions(client, contextId);
}
pages(): Promise<Page[]> {
pages(): Promise<Page<Browser, BrowserContext>[]> {
return this._browser._pages(this);
}
@ -42,7 +42,7 @@ export class BrowserContext {
return !!this._id;
}
newPage(): Promise<Page> {
newPage(): Promise<Page<Browser, BrowserContext>> {
return this._browser._createPageInContext(this._id);
}

View File

@ -21,18 +21,30 @@ import * as frames from '../frames';
import { assert, debugError } from '../helper';
import * as js from '../javascript';
import * as network from '../network';
import { TimeoutSettings } from '../TimeoutSettings';
import { CDPSession } from './Connection';
import { EVALUATION_SCRIPT_URL, ExecutionContextDelegate } from './ExecutionContext';
import { DOMWorldDelegate } from './JSHandle';
import { LifecycleWatcher } from './LifecycleWatcher';
import { NetworkManager } from './NetworkManager';
import { Page } from './Page';
import { NetworkManager, NetworkManagerEvents } from './NetworkManager';
import { Page } from '../page';
import { Protocol } from './protocol';
import { Events } from './events';
import { Events as CommonEvents } from '../events';
import { toConsoleMessageLocation, exceptionToError, releaseObject } from './protocolHelper';
import * as dialog from '../dialog';
import * as console from '../console';
import { PageDelegate } from '../page';
import { RawMouseImpl, RawKeyboardImpl } from './Input';
import { CRScreenshotDelegate } from './Screenshotter';
import { Accessibility } from './features/accessibility';
import { Coverage } from './features/coverage';
import { PDF } from './features/pdf';
import { Workers } from './features/workers';
import { Overrides } from './features/overrides';
import { Interception } from './features/interception';
import { Browser } from './Browser';
import { BrowserContext } from './BrowserContext';
import * as types from '../types';
import * as input from '../input';
const UTILITY_WORLD_NAME = '__playwright_utility_world__';
@ -51,26 +63,41 @@ type FrameData = {
lifecycleEvents: Set<string>,
};
export class FrameManager extends EventEmitter implements frames.FrameDelegate {
export class FrameManager extends EventEmitter implements frames.FrameDelegate, PageDelegate {
_client: CDPSession;
private _page: Page;
private _page: Page<Browser, BrowserContext>;
private _networkManager: NetworkManager;
_timeoutSettings: TimeoutSettings;
private _frames = new Map<string, frames.Frame>();
private _contextIdToContext = new Map<number, js.ExecutionContext>();
private _isolatedWorlds = new Set<string>();
private _mainFrame: frames.Frame;
rawMouse: RawMouseImpl;
rawKeyboard: RawKeyboardImpl;
screenshotterDelegate: CRScreenshotDelegate;
constructor(client: CDPSession, page: Page, ignoreHTTPSErrors: boolean, timeoutSettings: TimeoutSettings) {
constructor(client: CDPSession, browserContext: BrowserContext, ignoreHTTPSErrors: boolean) {
super();
this._client = client;
this._page = page;
this.rawKeyboard = new RawKeyboardImpl(client);
this.rawMouse = new RawMouseImpl(client);
this.screenshotterDelegate = new CRScreenshotDelegate(client);
this._networkManager = new NetworkManager(client, ignoreHTTPSErrors, this);
this._timeoutSettings = timeoutSettings;
this._page = new Page(this, browserContext, ignoreHTTPSErrors);
(this._page as any).accessibility = new Accessibility(client);
(this._page as any).coverage = new Coverage(client);
(this._page as any).pdf = new PDF(client);
(this._page as any).workers = new Workers(client, this._page._addConsoleMessage.bind(this._page), error => this._page.emit(CommonEvents.Page.PageError, error));
(this._page as any).overrides = new Overrides(client);
(this._page as any).interception = new Interception(this._networkManager);
this._networkManager.on(NetworkManagerEvents.Request, event => this._page.emit(CommonEvents.Page.Request, event));
this._networkManager.on(NetworkManagerEvents.Response, event => this._page.emit(CommonEvents.Page.Response, event));
this._networkManager.on(NetworkManagerEvents.RequestFailed, event => this._page.emit(CommonEvents.Page.RequestFailed, event));
this._networkManager.on(NetworkManagerEvents.RequestFinished, event => this._page.emit(CommonEvents.Page.RequestFinished, event));
this._client.on('Inspector.targetCrashed', event => this._onTargetCrashed());
this._client.on('Log.entryAdded', event => this._onLogEntryAdded(event));
this._client.on('Page.domContentEventFired', event => page.emit(Events.Page.DOMContentLoaded));
this._client.on('Page.domContentEventFired', event => this._page.emit(CommonEvents.Page.DOMContentLoaded));
this._client.on('Page.fileChooserOpened', event => this._onFileChooserOpened(event));
this._client.on('Page.frameAttached', event => this._onFrameAttached(event.frameId, event.parentFrameId));
this._client.on('Page.frameDetached', event => this._onFrameDetached(event.frameId));
@ -78,7 +105,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate {
this._client.on('Page.frameStoppedLoading', event => this._onFrameStoppedLoading(event.frameId));
this._client.on('Page.javascriptDialogOpening', event => this._onDialog(event));
this._client.on('Page.lifecycleEvent', event => this._onLifecycleEvent(event));
this._client.on('Page.loadEventFired', event => page.emit(Events.Page.Load));
this._client.on('Page.loadEventFired', event => this._page.emit(CommonEvents.Page.Load));
this._client.on('Page.navigatedWithinDocument', event => this._onFrameNavigatedWithinDocument(event.frameId, event.url));
this._client.on('Runtime.bindingCalled', event => this._onBindingCalled(event));
this._client.on('Runtime.consoleAPICalled', event => this._onConsoleAPI(event));
@ -119,7 +146,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate {
const {
referer = this._networkManager.extraHTTPHeaders()['referer'],
waitUntil = ['load'],
timeout = this._timeoutSettings.navigationTimeout(),
timeout = this._page._timeoutSettings.navigationTimeout(),
} = options;
const watcher = new LifecycleWatcher(this, frame, waitUntil, timeout);
@ -157,7 +184,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate {
assertNoLegacyNavigationOptions(options);
const {
waitUntil = ['load'],
timeout = this._timeoutSettings.navigationTimeout(),
timeout = this._page._timeoutSettings.navigationTimeout(),
} = options;
const watcher = new LifecycleWatcher(this, frame, waitUntil, timeout);
const error = await Promise.race([
@ -174,7 +201,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate {
async setFrameContent(frame: frames.Frame, html: string, options: frames.NavigateOptions = {}) {
const {
waitUntil = ['load'],
timeout = this._timeoutSettings.navigationTimeout(),
timeout = this._page._timeoutSettings.navigationTimeout(),
} = options;
const context = await frame._utilityContext();
// We rely upon the fact that document.open() will reset frame lifecycle with "init"
@ -228,7 +255,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate {
this._handleFrameTree(child);
}
page(): Page {
page(): Page<Browser, BrowserContext> {
return this._page;
}
@ -249,7 +276,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate {
return;
assert(parentFrameId);
const parentFrame = this._frames.get(parentFrameId);
const frame = new frames.Frame(this, this._timeoutSettings, parentFrame);
const frame = new frames.Frame(this, this._page._timeoutSettings, parentFrame);
const data: FrameData = {
id: frameId,
loaderId: '',
@ -258,6 +285,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate {
frame[frameDataSymbol] = data;
this._frames.set(frameId, frame);
this.emit(FrameManagerEvents.FrameAttached, frame);
this._page.emit(CommonEvents.Page.FrameAttached, frame);
}
_onFrameNavigated(framePayload: Protocol.Page.Frame) {
@ -280,7 +308,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate {
data.id = framePayload.id;
} else {
// Initial main frame navigation.
frame = new frames.Frame(this, this._timeoutSettings, null);
frame = new frames.Frame(this, this._page._timeoutSettings, null);
const data: FrameData = {
id: framePayload.id,
loaderId: '',
@ -296,6 +324,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate {
frame._navigated(framePayload.url, framePayload.name);
this.emit(FrameManagerEvents.FrameNavigated, frame);
this._page.emit(CommonEvents.Page.FrameNavigated, frame);
}
async _ensureIsolatedWorld(name: string) {
@ -320,6 +349,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate {
frame._navigated(url, frame.name());
this.emit(FrameManagerEvents.FrameNavigatedWithinDocument, frame);
this.emit(FrameManagerEvents.FrameNavigated, frame);
this._page.emit(CommonEvents.Page.FrameNavigated, frame);
}
_onFrameDetached(frameId: string) {
@ -371,6 +401,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate {
frame._detach();
this._frames.delete(this._frameData(frame).id);
this.emit(FrameManagerEvents.FrameDetached, frame);
this._page.emit(CommonEvents.Page.FrameDetached, frame);
}
async _onConsoleAPI(event: Protocol.Runtime.consoleAPICalledPayload) {
@ -395,7 +426,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate {
this._page._addConsoleMessage(event.type, values, toConsoleMessageLocation(event.stackTrace));
}
async _exposeBinding(name: string, bindingFunction: string) {
async exposeBinding(name: string, bindingFunction: string) {
await this._client.send('Runtime.addBinding', {name: name});
await this._client.send('Page.addScriptToEvaluateOnNewDocument', {source: bindingFunction});
await Promise.all(this.frames().map(frame => frame.evaluate(bindingFunction).catch(debugError)));
@ -407,7 +438,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate {
}
_onDialog(event : Protocol.Page.javascriptDialogOpeningPayload) {
this._page.emit(Events.Page.Dialog, new dialog.Dialog(
this._page.emit(CommonEvents.Page.Dialog, new dialog.Dialog(
event.type as dialog.DialogType,
event.message,
async (accept: boolean, promptText?: string) => {
@ -417,7 +448,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate {
}
_handleException(exceptionDetails: Protocol.Runtime.ExceptionDetails) {
this._page.emit(Events.Page.PageError, exceptionToError(exceptionDetails));
this._page.emit(CommonEvents.Page.PageError, exceptionToError(exceptionDetails));
}
_onTargetCrashed() {
@ -429,7 +460,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate {
if (args)
args.map(arg => releaseObject(this._client, arg));
if (source !== 'worker')
this._page.emit(Events.Page.Console, new console.ConsoleMessage(level, text, [], {url, lineNumber}));
this._page.emit(CommonEvents.Page.Console, new console.ConsoleMessage(level, text, [], {url, lineNumber}));
}
async _onFileChooserOpened(event: Protocol.Page.fileChooserOpenedPayload) {
@ -438,6 +469,88 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate {
const handle = await (utilityWorld.delegate as DOMWorldDelegate).adoptBackendNodeId(event.backendNodeId, utilityWorld);
this._page._onFileChooserOpened(handle);
}
setExtraHTTPHeaders(extraHTTPHeaders: network.Headers): Promise<void> {
return this._networkManager.setExtraHTTPHeaders(extraHTTPHeaders);
}
setUserAgent(userAgent: string): Promise<void> {
return this._networkManager.setUserAgent(userAgent);
}
async setJavaScriptEnabled(enabled: boolean): Promise<void> {
await this._client.send('Emulation.setScriptExecutionDisabled', { value: !enabled });
}
async setBypassCSP(enabled: boolean): Promise<void> {
await this._client.send('Page.setBypassCSP', { enabled });
}
async setViewport(viewport: types.Viewport): Promise<void> {
const {
width,
height,
isMobile = false,
deviceScaleFactor = 1,
hasTouch = false,
isLandscape = false,
} = viewport;
const screenOrientation: Protocol.Emulation.ScreenOrientation = isLandscape ? { angle: 90, type: 'landscapePrimary' } : { angle: 0, type: 'portraitPrimary' };
await Promise.all([
this._client.send('Emulation.setDeviceMetricsOverride', { mobile: isMobile, width, height, deviceScaleFactor, screenOrientation }),
this._client.send('Emulation.setTouchEmulationEnabled', {
enabled: hasTouch
})
]);
}
async setEmulateMedia(mediaType: input.MediaType | null, mediaColorScheme: input.MediaColorScheme | null): Promise<void> {
const features = mediaColorScheme ? [{ name: 'prefers-color-scheme', value: mediaColorScheme }] : [];
await this._client.send('Emulation.setEmulatedMedia', { media: mediaType || '', features });
}
setCacheEnabled(enabled: boolean): Promise<void> {
return this._networkManager.setCacheEnabled(enabled);
}
async reload(options?: frames.NavigateOptions): Promise<network.Response | null> {
const [response] = await Promise.all([
this._page.waitForNavigation(options),
this._client.send('Page.reload')
]);
return response;
}
private async _go(delta: number, options?: frames.NavigateOptions): Promise<network.Response | null> {
const history = await this._client.send('Page.getNavigationHistory');
const entry = history.entries[history.currentIndex + delta];
if (!entry)
return null;
const [response] = await Promise.all([
this._page.waitForNavigation(options),
this._client.send('Page.navigateToHistoryEntry', {entryId: entry.id}),
]);
return response;
}
goBack(options?: frames.NavigateOptions): Promise<network.Response | null> {
return this._go(-1, options);
}
goForward(options?: frames.NavigateOptions): Promise<network.Response | null> {
return this._go(+1, options);
}
async evaluateOnNewDocument(source: string): Promise<void> {
await this._client.send('Page.addScriptToEvaluateOnNewDocument', { source });
}
async closePage(runBeforeUnload: boolean): Promise<void> {
if (runBeforeUnload)
await this._client.send('Page.close');
else
await this._page.browser()._closePage(this._page);
}
}
function assertNoLegacyNavigationOptions(options) {

View File

@ -50,7 +50,7 @@ export class DOMWorldDelegate implements dom.DOMWorldDelegate {
}
isJavascriptEnabled(): boolean {
return this._frameManager.page()._javascriptEnabled;
return this._frameManager.page()._state.javascriptEnabled;
}
isElement(remoteObject: any): boolean {

View File

@ -19,11 +19,12 @@ import * as types from '../types';
import { Browser } from './Browser';
import { BrowserContext } from './BrowserContext';
import { CDPSession, CDPSessionEvents } from './Connection';
import { Events } from './events';
import { Events as CommonEvents } from '../events';
import { Worker } from './features/workers';
import { Page } from './Page';
import { Page } from '../page';
import { Protocol } from './protocol';
import { debugError } from '../helper';
import { FrameManager } from './FrameManager';
const targetSymbol = Symbol('target');
@ -34,14 +35,14 @@ export class Target {
private _sessionFactory: () => Promise<CDPSession>;
private _ignoreHTTPSErrors: boolean;
private _defaultViewport: types.Viewport;
private _pagePromise: Promise<Page> | null = null;
private _page: Page | null = null;
private _pagePromise: Promise<Page<Browser, BrowserContext>> | null = null;
private _page: Page<Browser, BrowserContext> | null = null;
private _workerPromise: Promise<Worker> | null = null;
_initializedPromise: Promise<boolean>;
_initializedCallback: (value?: unknown) => void;
_isInitialized: boolean;
static fromPage(page: Page): Target {
static fromPage(page: Page<Browser, BrowserContext>): Target {
return (page as any)[targetSymbol];
}
@ -64,10 +65,10 @@ export class Target {
if (!opener || !opener._pagePromise || this.type() !== 'page')
return true;
const openerPage = await opener._pagePromise;
if (!openerPage.listenerCount(Events.Page.Popup))
if (!openerPage.listenerCount(CommonEvents.Page.Popup))
return true;
const popupPage = await this.page();
openerPage.emit(Events.Page.Popup, popupPage);
openerPage.emit(CommonEvents.Page.Popup, popupPage);
return true;
});
this._isInitialized = this._targetInfo.type !== 'page' || this._targetInfo.url !== '';
@ -80,10 +81,11 @@ export class Target {
this._page._didClose();
}
async page(): Promise<Page | null> {
async page(): Promise<Page<Browser, BrowserContext> | null> {
if ((this._targetInfo.type === 'page' || this._targetInfo.type === 'background_page') && !this._pagePromise) {
this._pagePromise = this._sessionFactory().then(async client => {
const page = new Page(client, this._browserContext, this._ignoreHTTPSErrors);
const frameManager = new FrameManager(client, this._browserContext, this._ignoreHTTPSErrors);
const page = frameManager.page();
this._page = page;
page[targetSymbol] = this;
client.once(CDPSessionEvents.Disconnected, () => page._didDisconnect());
@ -93,7 +95,7 @@ export class Target {
client.send('Target.detachFromTarget', { sessionId: event.sessionId }).catch(debugError);
}
});
await page._frameManager.initialize();
await frameManager.initialize();
await client.send('Target.setAutoAttach', {autoAttach: true, waitForDebuggerOnStart: false, flatten: true});
if (this._defaultViewport)
await page.setViewport(this._defaultViewport);

View File

@ -21,7 +21,7 @@ export { Overrides } from './features/overrides';
export { PDF } from './features/pdf';
export { Permissions } from './features/permissions';
export { Worker, Workers } from './features/workers';
export { Page } from './Page';
export { Page } from '../page';
export { Playwright } from './Playwright';
export { Target } from './Target';

View File

@ -16,26 +16,6 @@
*/
export const Events = {
Page: {
Close: 'close',
Console: 'console',
Dialog: 'dialog',
FileChooser: 'filechooser',
DOMContentLoaded: 'domcontentloaded',
// Can't use just 'error' due to node.js special treatment of error events.
// @see https://nodejs.org/api/events.html#events_error_events
PageError: 'pageerror',
Request: 'request',
Response: 'response',
RequestFailed: 'requestfailed',
RequestFinished: 'requestfinished',
FrameAttached: 'frameattached',
FrameDetached: 'framedetached',
FrameNavigated: 'framenavigated',
Load: 'load',
Popup: 'popup',
},
Browser: {
Disconnected: 'disconnected'
},

View File

@ -19,10 +19,11 @@ import { assert } from '../../helper';
import { Browser } from '../Browser';
import { BrowserContext } from '../BrowserContext';
import { CDPSession, Connection } from '../Connection';
import { Page } from '../Page';
import { Page } from '../../page';
import { readProtocolStream } from '../protocolHelper';
import { Target } from '../Target';
import { Worker } from './workers';
import { FrameManager } from '../FrameManager';
export class Chromium extends EventEmitter {
private _connection: Connection;
@ -47,9 +48,9 @@ export class Chromium extends EventEmitter {
return target._worker();
}
async startTracing(page: Page | undefined, options: { path?: string; screenshots?: boolean; categories?: string[]; } = {}) {
async startTracing(page: Page<Browser, BrowserContext> | undefined, options: { path?: string; screenshots?: boolean; categories?: string[]; } = {}) {
assert(!this._recording, 'Cannot start recording trace while already recording trace.');
this._tracingClient = page ? page._client : this._client;
this._tracingClient = page ? (page._delegate as FrameManager)._client : this._client;
const defaultCategories = [
'-*', 'devtools.timeline', 'v8.execute', 'disabled-by-default-devtools.timeline',
@ -91,7 +92,7 @@ export class Chromium extends EventEmitter {
return context ? targets.filter(t => t.browserContext() === context) : targets;
}
pageTarget(page: Page): Target {
pageTarget(page: Page<Browser, BrowserContext>): Target {
return Target.fromPage(page);
}

38
src/events.ts Normal file
View File

@ -0,0 +1,38 @@
/**
* Copyright 2019 Google Inc. All rights reserved.
* Modifications copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const Events = {
Page: {
Close: 'close',
Console: 'console',
Dialog: 'dialog',
FileChooser: 'filechooser',
DOMContentLoaded: 'domcontentloaded',
// Can't use just 'error' due to node.js special treatment of error events.
// @see https://nodejs.org/api/events.html#events_error_events
PageError: 'pageerror',
Request: 'request',
Response: 'response',
RequestFailed: 'requestfailed',
RequestFinished: 'requestfinished',
FrameAttached: 'frameattached',
FrameDetached: 'framedetached',
FrameNavigated: 'framenavigated',
Load: 'load',
Popup: 'popup',
},
};

View File

@ -113,8 +113,8 @@ export class Page extends EventEmitter {
}
async emulateMedia(options: {
type?: ''|'screen'|'print',
colorScheme?: 'dark' | 'light' | 'no-preference' }) {
type?: input.MediaType,
colorScheme?: input.MediaColorScheme }) {
assert(!options.type || input.mediaTypes.has(options.type), 'Unsupported media type: ' + options.type);
assert(!options.colorScheme || input.mediaColorSchemes.has(options.colorScheme), 'Unsupported color scheme: ' + options.colorScheme);
await this._session.send('Page.setEmulatedMedia', options);

View File

@ -379,5 +379,7 @@ export type FilePayload = {
data: string
};
export const mediaTypes = new Set(['screen', 'print']);
export const mediaColorSchemes = new Set(['dark', 'light', 'no-preference']);
export type MediaType = 'screen' | 'print';
export const mediaTypes: Set<MediaType> = new Set(['screen', 'print']);
export type MediaColorScheme = 'dark' | 'light' | 'no-preference';
export const mediaColorSchemes: Set<MediaColorScheme> = new Set(['dark', 'light', 'no-preference']);

View File

@ -16,86 +16,97 @@
*/
import { EventEmitter } from 'events';
import * as console from '../console';
import * as dom from '../dom';
import * as frames from '../frames';
import { assert, debugError, helper } from '../helper';
import * as input from '../input';
import { ClickOptions, mediaColorSchemes, mediaTypes, MultiClickOptions, PointerActionOptions, SelectOption } from '../input';
import * as js from '../javascript';
import * as network from '../network';
import { Screenshotter } from '../screenshotter';
import { TimeoutSettings } from '../TimeoutSettings';
import * as types from '../types';
import { Browser } from './Browser';
import { BrowserContext } from './BrowserContext';
import { CDPSession } from './Connection';
import * as console from './console';
import * as dom from './dom';
import * as frames from './frames';
import { assert, debugError, helper } from './helper';
import * as input from './input';
import * as js from './javascript';
import * as network from './network';
import { Screenshotter, ScreenshotterDelegate } from './screenshotter';
import { TimeoutSettings } from './TimeoutSettings';
import * as types from './types';
import { Events } from './events';
import { Accessibility } from './features/accessibility';
import { Coverage } from './features/coverage';
import { Interception } from './features/interception';
import { Overrides } from './features/overrides';
import { PDF } from './features/pdf';
import { Workers } from './features/workers';
import { FrameManager, FrameManagerEvents } from './FrameManager';
import { RawKeyboardImpl, RawMouseImpl } from './Input';
import { NetworkManagerEvents } from './NetworkManager';
import { CRScreenshotDelegate } from './Screenshotter';
import { Protocol } from './protocol';
export class Page extends EventEmitter {
export interface PageDelegate {
readonly rawMouse: input.RawMouse;
readonly rawKeyboard: input.RawKeyboard;
readonly screenshotterDelegate: ScreenshotterDelegate;
mainFrame(): frames.Frame;
frames(): frames.Frame[];
reload(options?: frames.NavigateOptions): Promise<network.Response | null>;
goBack(options?: frames.NavigateOptions): Promise<network.Response | null>;
goForward(options?: frames.NavigateOptions): Promise<network.Response | null>;
exposeBinding(name: string, bindingFunction: string): Promise<void>;
evaluateOnNewDocument(source: string): Promise<void>;
closePage(runBeforeUnload: boolean): Promise<void>;
setExtraHTTPHeaders(extraHTTPHeaders: network.Headers): Promise<void>;
setUserAgent(userAgent: string): Promise<void>;
setJavaScriptEnabled(enabled: boolean): Promise<void>;
setBypassCSP(enabled: boolean): Promise<void>;
setViewport(viewport: types.Viewport): Promise<void>;
setEmulateMedia(mediaType: input.MediaType | null, mediaColorScheme: input.MediaColorScheme | null): Promise<void>;
setCacheEnabled(enabled: boolean): Promise<void>;
}
interface BrowserContextInterface<Browser> {
browser(): Browser;
}
type PageState = {
viewport: types.Viewport | null;
userAgent: string | null;
mediaType: input.MediaType | null;
mediaColorScheme: input.MediaColorScheme | null;
javascriptEnabled: boolean | null;
extraHTTPHeaders: network.Headers | null;
bypassCSP: boolean | null;
cacheEnabled: boolean | null;
};
export type FileChooser = {
element: dom.ElementHandle,
multiple: boolean
};
export class Page<Browser, BrowserContext extends BrowserContextInterface<Browser>> extends EventEmitter {
private _closed = false;
private _closedCallback: () => void;
private _closedPromise: Promise<void>;
private _disconnected = false;
private _disconnectedCallback: (e: Error) => void;
private _disconnectedPromise: Promise<Error>;
_client: CDPSession;
private _browserContext: BrowserContext;
readonly keyboard: input.Keyboard;
readonly mouse: input.Mouse;
private _timeoutSettings: TimeoutSettings;
_frameManager: FrameManager;
readonly accessibility: Accessibility;
readonly coverage: Coverage;
readonly overrides: Overrides;
readonly interception: Interception;
readonly pdf: PDF;
readonly workers: Workers;
readonly _timeoutSettings: TimeoutSettings;
readonly _delegate: PageDelegate;
readonly _state: PageState;
private _pageBindings = new Map<string, Function>();
_javascriptEnabled = true;
private _viewport: types.Viewport | null = null;
_screenshotter: Screenshotter;
readonly _screenshotter: Screenshotter;
private _fileChooserInterceptors = new Set<(chooser: FileChooser) => void>();
private _emulatedMediaType: string | undefined;
constructor(client: CDPSession, browserContext: BrowserContext, ignoreHTTPSErrors: boolean) {
constructor(delegate: PageDelegate, browserContext: BrowserContext, ignoreHTTPSErrors: boolean) {
super();
this._client = client;
this._delegate = delegate;
this._closedPromise = new Promise(f => this._closedCallback = f);
this._disconnectedPromise = new Promise(f => this._disconnectedCallback = f);
this._browserContext = browserContext;
this.keyboard = new input.Keyboard(new RawKeyboardImpl(client));
this.mouse = new input.Mouse(new RawMouseImpl(client), this.keyboard);
this._state = {
viewport: null,
userAgent: null,
mediaType: null,
mediaColorScheme: null,
javascriptEnabled: null,
extraHTTPHeaders: null,
bypassCSP: null,
cacheEnabled: null,
};
this.keyboard = new input.Keyboard(delegate.rawKeyboard);
this.mouse = new input.Mouse(delegate.rawMouse, this.keyboard);
this._timeoutSettings = new TimeoutSettings();
this.accessibility = new Accessibility(client);
this._frameManager = new FrameManager(client, this, ignoreHTTPSErrors, this._timeoutSettings);
this.coverage = new Coverage(client);
this.pdf = new PDF(client);
this.workers = new Workers(client, this._addConsoleMessage.bind(this), error => this.emit(Events.Page.PageError, error));
this.overrides = new Overrides(client);
this.interception = new Interception(this._frameManager.networkManager());
this._screenshotter = new Screenshotter(this, new CRScreenshotDelegate(this._client), browserContext.browser());
this._frameManager.on(FrameManagerEvents.FrameAttached, event => this.emit(Events.Page.FrameAttached, event));
this._frameManager.on(FrameManagerEvents.FrameDetached, event => this.emit(Events.Page.FrameDetached, event));
this._frameManager.on(FrameManagerEvents.FrameNavigated, event => this.emit(Events.Page.FrameNavigated, event));
const networkManager = this._frameManager.networkManager();
networkManager.on(NetworkManagerEvents.Request, event => this.emit(Events.Page.Request, event));
networkManager.on(NetworkManagerEvents.Response, event => this.emit(Events.Page.Response, event));
networkManager.on(NetworkManagerEvents.RequestFailed, event => this.emit(Events.Page.RequestFailed, event));
networkManager.on(NetworkManagerEvents.RequestFinished, event => this.emit(Events.Page.RequestFinished, event));
this._screenshotter = new Screenshotter(this, delegate.screenshotterDelegate, browserContext.browser());
}
_didClose() {
@ -147,11 +158,11 @@ export class Page extends EventEmitter {
}
mainFrame(): frames.Frame {
return this._frameManager.mainFrame();
return this._delegate.mainFrame();
}
frames(): frames.Frame[] {
return this._frameManager.frames();
return this._delegate.frames();
}
setDefaultNavigationTimeout(timeout: number) {
@ -199,7 +210,7 @@ export class Page extends EventEmitter {
if (this._pageBindings.has(name))
throw new Error(`Failed to add page binding with name ${name}: window['${name}'] already exists!`);
this._pageBindings.set(name, playwrightFunction);
await this._frameManager._exposeBinding(name, helper.evaluationString(addPageBinding, name));
await this._delegate.exposeBinding(name, helper.evaluationString(addPageBinding, name));
function addPageBinding(bindingName: string) {
const binding = window[bindingName];
@ -219,12 +230,14 @@ export class Page extends EventEmitter {
}
}
async setExtraHTTPHeaders(headers: { [s: string]: string; }) {
return this._frameManager.networkManager().setExtraHTTPHeaders(headers);
setExtraHTTPHeaders(headers: network.Headers) {
this._state.extraHTTPHeaders = {...headers};
return this._delegate.setExtraHTTPHeaders(headers);
}
async setUserAgent(userAgent: string) {
return this._frameManager.networkManager().setUserAgent(userAgent);
setUserAgent(userAgent: string) {
this._state.userAgent = userAgent;
return this._delegate.setUserAgent(userAgent);
}
async _onBindingCalled(payload: string, context: js.ExecutionContext) {
@ -271,28 +284,24 @@ export class Page extends EventEmitter {
return this.mainFrame().url();
}
async content(): Promise<string> {
return await this.mainFrame().content();
content(): Promise<string> {
return this.mainFrame().content();
}
async setContent(html: string, options: { timeout?: number; waitUntil?: string | string[]; } | undefined) {
await this.mainFrame().setContent(html, options);
setContent(html: string, options?: frames.NavigateOptions): Promise<void> {
return this.mainFrame().setContent(html, options);
}
async goto(url: string, options: { referer?: string; timeout?: number; waitUntil?: string | string[]; } | undefined): Promise<network.Response | null> {
return await this.mainFrame().goto(url, options);
goto(url: string, options?: frames.GotoOptions): Promise<network.Response | null> {
return this.mainFrame().goto(url, options);
}
async reload(options: { timeout?: number; waitUntil?: string | string[]; } = {}): Promise<network.Response | null> {
const [response] = await Promise.all([
this.waitForNavigation(options),
this._client.send('Page.reload')
]);
return response;
reload(options?: frames.NavigateOptions): Promise<network.Response | null> {
return this._delegate.reload(options);
}
async waitForNavigation(options: { timeout?: number; waitUntil?: string | string[]; } = {}): Promise<network.Response | null> {
return await this.mainFrame().waitForNavigation(options);
waitForNavigation(options?: frames.NavigateOptions): Promise<network.Response | null> {
return this.mainFrame().waitForNavigation(options);
}
async waitForRequest(urlOrPredicate: (string | Function), options: { timeout?: number; } = {}): Promise<Request> {
@ -321,24 +330,12 @@ export class Page extends EventEmitter {
}, timeout, this._disconnectedPromise);
}
async goBack(options: { timeout?: number; waitUntil?: string | string[]; } | undefined): Promise<network.Response | null> {
return this._go(-1, options);
goBack(options?: frames.NavigateOptions): Promise<network.Response | null> {
return this._delegate.goBack(options);
}
async goForward(options: { timeout?: number; waitUntil?: string | string[]; } | undefined): Promise<network.Response | null> {
return this._go(+1, options);
}
async _go(delta, options: { timeout?: number; waitUntil?: string | string[]; } | undefined): Promise<network.Response | null> {
const history = await this._client.send('Page.getNavigationHistory');
const entry = history.entries[history.currentIndex + delta];
if (!entry)
return null;
const [response] = await Promise.all([
this.waitForNavigation(options),
this._client.send('Page.navigateToHistoryEntry', {entryId: entry.id}),
]);
return response;
goForward(options?: frames.NavigateOptions): Promise<network.Response | null> {
return this._delegate.goForward(options);
}
async emulate(options: { viewport: types.Viewport; userAgent: string; }) {
@ -349,52 +346,41 @@ export class Page extends EventEmitter {
}
async setJavaScriptEnabled(enabled: boolean) {
if (this._javascriptEnabled === enabled)
if (this._state.javascriptEnabled === enabled)
return;
this._javascriptEnabled = enabled;
await this._client.send('Emulation.setScriptExecutionDisabled', { value: !enabled });
this._state.javascriptEnabled = enabled;
await this._delegate.setJavaScriptEnabled(enabled);
}
async setBypassCSP(enabled: boolean) {
await this._client.send('Page.setBypassCSP', { enabled });
if (this._state.bypassCSP === enabled)
return;
await this._delegate.setBypassCSP(enabled);
}
async emulateMedia(options: {
type?: string,
colorScheme?: 'dark' | 'light' | 'no-preference' }) {
assert(!options.type || mediaTypes.has(options.type), 'Unsupported media type: ' + options.type);
assert(!options.colorScheme || mediaColorSchemes.has(options.colorScheme), 'Unsupported color scheme: ' + options.colorScheme);
const media = typeof options.type === 'undefined' ? this._emulatedMediaType : options.type;
const features = typeof options.colorScheme === 'undefined' ? [] : [{ name: 'prefers-color-scheme', value: options.colorScheme }];
await this._client.send('Emulation.setEmulatedMedia', { media: media || '', features });
this._emulatedMediaType = options.type;
async emulateMedia(options: { type?: input.MediaType, colorScheme?: input.MediaColorScheme }) {
assert(!options.type || input.mediaTypes.has(options.type), 'Unsupported media type: ' + options.type);
assert(!options.colorScheme || input.mediaColorSchemes.has(options.colorScheme), 'Unsupported color scheme: ' + options.colorScheme);
if (options.type !== undefined)
this._state.mediaType = options.type;
if (options.colorScheme !== undefined)
this._state.mediaColorScheme = options.colorScheme;
await this._delegate.setEmulateMedia(this._state.mediaType, this._state.mediaColorScheme);
}
async setViewport(viewport: types.Viewport) {
const {
width,
height,
isMobile = false,
deviceScaleFactor = 1,
hasTouch = false,
isLandscape = false,
} = viewport;
const screenOrientation: Protocol.Emulation.ScreenOrientation = isLandscape ? { angle: 90, type: 'landscapePrimary' } : { angle: 0, type: 'portraitPrimary' };
await Promise.all([
this._client.send('Emulation.setDeviceMetricsOverride', { mobile: isMobile, width, height, deviceScaleFactor, screenOrientation }),
this._client.send('Emulation.setTouchEmulationEnabled', {
enabled: hasTouch
})
]);
const oldIsMobile = this._viewport ? !!this._viewport.isMobile : false;
const oldHasTouch = this._viewport ? !!this._viewport.hasTouch : false;
this._viewport = viewport;
if (oldIsMobile !== isMobile || oldHasTouch !== hasTouch)
const oldIsMobile = this._state.viewport ? !!this._state.viewport.isMobile : false;
const oldHasTouch = this._state.viewport ? !!this._state.viewport.hasTouch : false;
const newIsMobile = !!viewport.isMobile;
const newHasTouch = !!viewport.hasTouch;
this._state.viewport = { ...viewport };
await this._delegate.setViewport(viewport);
if (oldIsMobile !== newIsMobile || oldHasTouch !== newHasTouch)
await this.reload();
}
viewport(): types.Viewport | null {
return this._viewport;
return this._state.viewport;
}
evaluate: types.Evaluate = (pageFunction, ...args) => {
@ -403,45 +389,45 @@ export class Page extends EventEmitter {
async evaluateOnNewDocument(pageFunction: Function | string, ...args: any[]) {
const source = helper.evaluationString(pageFunction, ...args);
await this._client.send('Page.addScriptToEvaluateOnNewDocument', { source });
await this._delegate.evaluateOnNewDocument(source);
}
async setCacheEnabled(enabled: boolean = true) {
await this._frameManager.networkManager().setCacheEnabled(enabled);
if (this._state.cacheEnabled === enabled)
return;
this._state.cacheEnabled = enabled;
await this._delegate.setCacheEnabled(enabled);
}
screenshot(options?: types.ScreenshotOptions): Promise<Buffer> {
return this._screenshotter.screenshotPage(options);
}
async title(): Promise<string> {
title(): Promise<string> {
return this.mainFrame().title();
}
async close(options: { runBeforeUnload: (boolean | undefined); } = {runBeforeUnload: undefined}) {
assert(!this._disconnected, 'Protocol error: Connection closed. Most likely the page has been closed.');
const runBeforeUnload = !!options.runBeforeUnload;
if (runBeforeUnload) {
await this._client.send('Page.close');
} else {
await this.browser()._closePage(this);
await this._delegate.closePage(runBeforeUnload);
if (!runBeforeUnload)
await this._closedPromise;
}
}
isClosed(): boolean {
return this._closed;
}
click(selector: string | types.Selector, options?: ClickOptions) {
click(selector: string | types.Selector, options?: input.ClickOptions) {
return this.mainFrame().click(selector, options);
}
dblclick(selector: string | types.Selector, options?: MultiClickOptions) {
dblclick(selector: string | types.Selector, options?: input.MultiClickOptions) {
return this.mainFrame().dblclick(selector, options);
}
tripleclick(selector: string | types.Selector, options?: MultiClickOptions) {
tripleclick(selector: string | types.Selector, options?: input.MultiClickOptions) {
return this.mainFrame().tripleclick(selector, options);
}
@ -453,11 +439,11 @@ export class Page extends EventEmitter {
return this.mainFrame().focus(selector);
}
hover(selector: string | types.Selector, options?: PointerActionOptions) {
hover(selector: string | types.Selector, options?: input.PointerActionOptions) {
return this.mainFrame().hover(selector, options);
}
select(selector: string | types.Selector, ...values: (string | dom.ElementHandle | SelectOption)[]): Promise<string[]> {
select(selector: string | types.Selector, ...values: (string | dom.ElementHandle | input.SelectOption)[]): Promise<string[]> {
return this.mainFrame().select(selector, ...values);
}
@ -481,8 +467,3 @@ export class Page extends EventEmitter {
return this.mainFrame().waitForFunction(pageFunction, options, ...args);
}
}
type FileChooser = {
element: dom.ElementHandle,
multiple: boolean
};

View File

@ -263,8 +263,8 @@ export class Page extends EventEmitter {
}
async emulateMedia(options: {
type?: string | null,
colorScheme?: 'dark' | 'light' | 'no-preference' | null }) {
type?: input.MediaType | null,
colorScheme?: input.MediaColorScheme | null }) {
assert(!options.type || mediaTypes.has(options.type), 'Unsupported media type: ' + options.type);
assert(!options.colorScheme || mediaColorSchemes.has(options.colorScheme), 'Unsupported color scheme: ' + options.colorScheme);
assert(!options.colorScheme, 'Media feature emulation is not supported');