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 { assert, helper } from '../helper';
import { BrowserContext } from './BrowserContext'; import { BrowserContext } from './BrowserContext';
import { Connection, ConnectionEvents, CDPSession } from './Connection'; import { Connection, ConnectionEvents, CDPSession } from './Connection';
import { Page } from './Page'; import { Page } from '../page';
import { Target } from './Target'; import { Target } from './Target';
import { Protocol } from './protocol'; import { Protocol } from './protocol';
import { Chromium } from './features/chromium'; import { Chromium } from './features/chromium';
import * as types from '../types'; import * as types from '../types';
import { FrameManager } from './FrameManager';
export class Browser extends EventEmitter { export class Browser extends EventEmitter {
private _ignoreHTTPSErrors: boolean; private _ignoreHTTPSErrors: boolean;
@ -133,11 +134,11 @@ export class Browser extends EventEmitter {
this.chromium.emit(Events.Chromium.TargetChanged, target); this.chromium.emit(Events.Chromium.TargetChanged, target);
} }
async newPage(): Promise<Page> { async newPage(): Promise<Page<Browser, BrowserContext>> {
return this._defaultContext.newPage(); 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 { targetId } = await this._client.send('Target.createTarget', { url: 'about:blank', browserContextId: contextId || undefined });
const target = this._targets.get(targetId); const target = this._targets.get(targetId);
assert(await target._initializedPromise, 'Failed to create target for page'); assert(await target._initializedPromise, 'Failed to create target for page');
@ -145,7 +146,7 @@ export class Browser extends EventEmitter {
return page; return page;
} }
async _closePage(page: Page) { async _closePage(page: Page<Browser, BrowserContext>) {
await this._client.send('Target.closeTarget', { targetId: Target.fromPage(page)._targetId }); 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); 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 targets = this._allTargets().filter(target => target.browserContext() === context && target.type() === 'page');
const pages = await Promise.all(targets.map(target => target.page())); const pages = await Promise.all(targets.map(target => target.page()));
return pages.filter(page => !!page); return pages.filter(page => !!page);
} }
async _activatePage(page: Page) { async _activatePage(page: Page<Browser, BrowserContext>) {
await page._client.send('Target.activateTarget', {targetId: Target.fromPage(page)._targetId}); 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> { 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())); const contextPages = await Promise.all(this.browserContexts().map(context => context.pages()));
// Flatten array. // Flatten array.
return contextPages.reduce((acc, x) => acc.concat(x), []); 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 { Browser } from './Browser';
import { CDPSession } from './Connection'; import { CDPSession } from './Connection';
import { Permissions } from './features/permissions'; import { Permissions } from './features/permissions';
import { Page } from './Page'; import { Page } from '../page';
export class BrowserContext { export class BrowserContext {
readonly permissions: Permissions; readonly permissions: Permissions;
@ -34,7 +34,7 @@ export class BrowserContext {
this.permissions = new Permissions(client, contextId); this.permissions = new Permissions(client, contextId);
} }
pages(): Promise<Page[]> { pages(): Promise<Page<Browser, BrowserContext>[]> {
return this._browser._pages(this); return this._browser._pages(this);
} }
@ -42,7 +42,7 @@ export class BrowserContext {
return !!this._id; return !!this._id;
} }
newPage(): Promise<Page> { newPage(): Promise<Page<Browser, BrowserContext>> {
return this._browser._createPageInContext(this._id); return this._browser._createPageInContext(this._id);
} }

View File

@ -21,18 +21,30 @@ import * as frames from '../frames';
import { assert, debugError } from '../helper'; import { assert, debugError } from '../helper';
import * as js from '../javascript'; import * as js from '../javascript';
import * as network from '../network'; import * as network from '../network';
import { TimeoutSettings } from '../TimeoutSettings';
import { CDPSession } from './Connection'; import { CDPSession } from './Connection';
import { EVALUATION_SCRIPT_URL, ExecutionContextDelegate } from './ExecutionContext'; import { EVALUATION_SCRIPT_URL, ExecutionContextDelegate } from './ExecutionContext';
import { DOMWorldDelegate } from './JSHandle'; import { DOMWorldDelegate } from './JSHandle';
import { LifecycleWatcher } from './LifecycleWatcher'; import { LifecycleWatcher } from './LifecycleWatcher';
import { NetworkManager } from './NetworkManager'; import { NetworkManager, NetworkManagerEvents } from './NetworkManager';
import { Page } from './Page'; import { Page } from '../page';
import { Protocol } from './protocol'; import { Protocol } from './protocol';
import { Events } from './events'; import { Events as CommonEvents } from '../events';
import { toConsoleMessageLocation, exceptionToError, releaseObject } from './protocolHelper'; import { toConsoleMessageLocation, exceptionToError, releaseObject } from './protocolHelper';
import * as dialog from '../dialog'; import * as dialog from '../dialog';
import * as console from '../console'; 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__'; const UTILITY_WORLD_NAME = '__playwright_utility_world__';
@ -51,26 +63,41 @@ type FrameData = {
lifecycleEvents: Set<string>, lifecycleEvents: Set<string>,
}; };
export class FrameManager extends EventEmitter implements frames.FrameDelegate { export class FrameManager extends EventEmitter implements frames.FrameDelegate, PageDelegate {
_client: CDPSession; _client: CDPSession;
private _page: Page; private _page: Page<Browser, BrowserContext>;
private _networkManager: NetworkManager; private _networkManager: NetworkManager;
_timeoutSettings: TimeoutSettings;
private _frames = new Map<string, frames.Frame>(); private _frames = new Map<string, frames.Frame>();
private _contextIdToContext = new Map<number, js.ExecutionContext>(); private _contextIdToContext = new Map<number, js.ExecutionContext>();
private _isolatedWorlds = new Set<string>(); private _isolatedWorlds = new Set<string>();
private _mainFrame: frames.Frame; 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(); super();
this._client = client; 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._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('Inspector.targetCrashed', event => this._onTargetCrashed());
this._client.on('Log.entryAdded', event => this._onLogEntryAdded(event)); 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.fileChooserOpened', event => this._onFileChooserOpened(event));
this._client.on('Page.frameAttached', event => this._onFrameAttached(event.frameId, event.parentFrameId)); this._client.on('Page.frameAttached', event => this._onFrameAttached(event.frameId, event.parentFrameId));
this._client.on('Page.frameDetached', event => this._onFrameDetached(event.frameId)); 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.frameStoppedLoading', event => this._onFrameStoppedLoading(event.frameId));
this._client.on('Page.javascriptDialogOpening', event => this._onDialog(event)); this._client.on('Page.javascriptDialogOpening', event => this._onDialog(event));
this._client.on('Page.lifecycleEvent', event => this._onLifecycleEvent(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('Page.navigatedWithinDocument', event => this._onFrameNavigatedWithinDocument(event.frameId, event.url));
this._client.on('Runtime.bindingCalled', event => this._onBindingCalled(event)); this._client.on('Runtime.bindingCalled', event => this._onBindingCalled(event));
this._client.on('Runtime.consoleAPICalled', event => this._onConsoleAPI(event)); this._client.on('Runtime.consoleAPICalled', event => this._onConsoleAPI(event));
@ -119,7 +146,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate {
const { const {
referer = this._networkManager.extraHTTPHeaders()['referer'], referer = this._networkManager.extraHTTPHeaders()['referer'],
waitUntil = ['load'], waitUntil = ['load'],
timeout = this._timeoutSettings.navigationTimeout(), timeout = this._page._timeoutSettings.navigationTimeout(),
} = options; } = options;
const watcher = new LifecycleWatcher(this, frame, waitUntil, timeout); const watcher = new LifecycleWatcher(this, frame, waitUntil, timeout);
@ -157,7 +184,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate {
assertNoLegacyNavigationOptions(options); assertNoLegacyNavigationOptions(options);
const { const {
waitUntil = ['load'], waitUntil = ['load'],
timeout = this._timeoutSettings.navigationTimeout(), timeout = this._page._timeoutSettings.navigationTimeout(),
} = options; } = options;
const watcher = new LifecycleWatcher(this, frame, waitUntil, timeout); const watcher = new LifecycleWatcher(this, frame, waitUntil, timeout);
const error = await Promise.race([ 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 = {}) { async setFrameContent(frame: frames.Frame, html: string, options: frames.NavigateOptions = {}) {
const { const {
waitUntil = ['load'], waitUntil = ['load'],
timeout = this._timeoutSettings.navigationTimeout(), timeout = this._page._timeoutSettings.navigationTimeout(),
} = options; } = options;
const context = await frame._utilityContext(); const context = await frame._utilityContext();
// We rely upon the fact that document.open() will reset frame lifecycle with "init" // 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); this._handleFrameTree(child);
} }
page(): Page { page(): Page<Browser, BrowserContext> {
return this._page; return this._page;
} }
@ -249,7 +276,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate {
return; return;
assert(parentFrameId); assert(parentFrameId);
const parentFrame = this._frames.get(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 = { const data: FrameData = {
id: frameId, id: frameId,
loaderId: '', loaderId: '',
@ -258,6 +285,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate {
frame[frameDataSymbol] = data; frame[frameDataSymbol] = data;
this._frames.set(frameId, frame); this._frames.set(frameId, frame);
this.emit(FrameManagerEvents.FrameAttached, frame); this.emit(FrameManagerEvents.FrameAttached, frame);
this._page.emit(CommonEvents.Page.FrameAttached, frame);
} }
_onFrameNavigated(framePayload: Protocol.Page.Frame) { _onFrameNavigated(framePayload: Protocol.Page.Frame) {
@ -280,7 +308,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate {
data.id = framePayload.id; data.id = framePayload.id;
} else { } else {
// Initial main frame navigation. // Initial main frame navigation.
frame = new frames.Frame(this, this._timeoutSettings, null); frame = new frames.Frame(this, this._page._timeoutSettings, null);
const data: FrameData = { const data: FrameData = {
id: framePayload.id, id: framePayload.id,
loaderId: '', loaderId: '',
@ -296,6 +324,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate {
frame._navigated(framePayload.url, framePayload.name); frame._navigated(framePayload.url, framePayload.name);
this.emit(FrameManagerEvents.FrameNavigated, frame); this.emit(FrameManagerEvents.FrameNavigated, frame);
this._page.emit(CommonEvents.Page.FrameNavigated, frame);
} }
async _ensureIsolatedWorld(name: string) { async _ensureIsolatedWorld(name: string) {
@ -320,6 +349,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate {
frame._navigated(url, frame.name()); frame._navigated(url, frame.name());
this.emit(FrameManagerEvents.FrameNavigatedWithinDocument, frame); this.emit(FrameManagerEvents.FrameNavigatedWithinDocument, frame);
this.emit(FrameManagerEvents.FrameNavigated, frame); this.emit(FrameManagerEvents.FrameNavigated, frame);
this._page.emit(CommonEvents.Page.FrameNavigated, frame);
} }
_onFrameDetached(frameId: string) { _onFrameDetached(frameId: string) {
@ -371,6 +401,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate {
frame._detach(); frame._detach();
this._frames.delete(this._frameData(frame).id); this._frames.delete(this._frameData(frame).id);
this.emit(FrameManagerEvents.FrameDetached, frame); this.emit(FrameManagerEvents.FrameDetached, frame);
this._page.emit(CommonEvents.Page.FrameDetached, frame);
} }
async _onConsoleAPI(event: Protocol.Runtime.consoleAPICalledPayload) { 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)); 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('Runtime.addBinding', {name: name});
await this._client.send('Page.addScriptToEvaluateOnNewDocument', {source: bindingFunction}); await this._client.send('Page.addScriptToEvaluateOnNewDocument', {source: bindingFunction});
await Promise.all(this.frames().map(frame => frame.evaluate(bindingFunction).catch(debugError))); 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) { _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.type as dialog.DialogType,
event.message, event.message,
async (accept: boolean, promptText?: string) => { async (accept: boolean, promptText?: string) => {
@ -417,7 +448,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate {
} }
_handleException(exceptionDetails: Protocol.Runtime.ExceptionDetails) { _handleException(exceptionDetails: Protocol.Runtime.ExceptionDetails) {
this._page.emit(Events.Page.PageError, exceptionToError(exceptionDetails)); this._page.emit(CommonEvents.Page.PageError, exceptionToError(exceptionDetails));
} }
_onTargetCrashed() { _onTargetCrashed() {
@ -429,7 +460,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate {
if (args) if (args)
args.map(arg => releaseObject(this._client, arg)); args.map(arg => releaseObject(this._client, arg));
if (source !== 'worker') 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) { 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); const handle = await (utilityWorld.delegate as DOMWorldDelegate).adoptBackendNodeId(event.backendNodeId, utilityWorld);
this._page._onFileChooserOpened(handle); 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) { function assertNoLegacyNavigationOptions(options) {

View File

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

View File

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

View File

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

View File

@ -16,26 +16,6 @@
*/ */
export const Events = { 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: { Browser: {
Disconnected: 'disconnected' Disconnected: 'disconnected'
}, },

View File

@ -19,10 +19,11 @@ import { assert } from '../../helper';
import { Browser } from '../Browser'; import { Browser } from '../Browser';
import { BrowserContext } from '../BrowserContext'; import { BrowserContext } from '../BrowserContext';
import { CDPSession, Connection } from '../Connection'; import { CDPSession, Connection } from '../Connection';
import { Page } from '../Page'; import { Page } from '../../page';
import { readProtocolStream } from '../protocolHelper'; import { readProtocolStream } from '../protocolHelper';
import { Target } from '../Target'; import { Target } from '../Target';
import { Worker } from './workers'; import { Worker } from './workers';
import { FrameManager } from '../FrameManager';
export class Chromium extends EventEmitter { export class Chromium extends EventEmitter {
private _connection: Connection; private _connection: Connection;
@ -47,9 +48,9 @@ export class Chromium extends EventEmitter {
return target._worker(); 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.'); 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 = [ const defaultCategories = [
'-*', 'devtools.timeline', 'v8.execute', 'disabled-by-default-devtools.timeline', '-*', '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; return context ? targets.filter(t => t.browserContext() === context) : targets;
} }
pageTarget(page: Page): Target { pageTarget(page: Page<Browser, BrowserContext>): Target {
return Target.fromPage(page); 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: { async emulateMedia(options: {
type?: ''|'screen'|'print', type?: input.MediaType,
colorScheme?: 'dark' | 'light' | 'no-preference' }) { colorScheme?: input.MediaColorScheme }) {
assert(!options.type || input.mediaTypes.has(options.type), 'Unsupported media type: ' + options.type); 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); assert(!options.colorScheme || input.mediaColorSchemes.has(options.colorScheme), 'Unsupported color scheme: ' + options.colorScheme);
await this._session.send('Page.setEmulatedMedia', options); await this._session.send('Page.setEmulatedMedia', options);

View File

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