diff --git a/src/chromium/FrameManager.ts b/src/chromium/FrameManager.ts index 352d3e9239..4356794c5a 100644 --- a/src/chromium/FrameManager.ts +++ b/src/chromium/FrameManager.ts @@ -82,7 +82,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate, this.rawMouse = new RawMouseImpl(client); this.screenshotterDelegate = new CRScreenshotDelegate(client); this._networkManager = new NetworkManager(client, ignoreHTTPSErrors, this); - this._page = new Page(this, browserContext, ignoreHTTPSErrors); + this._page = new Page(this, browserContext); (this._page as any).accessibility = new Accessibility(client); (this._page as any).coverage = new Coverage(client); (this._page as any).pdf = new PDF(client); @@ -130,6 +130,10 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate, ]); } + didClose() { + // TODO: remove listeners. + } + networkManager(): NetworkManager { return this._networkManager; } diff --git a/src/chromium/JSHandle.ts b/src/chromium/JSHandle.ts index 9e1c6918d8..30220b6224 100644 --- a/src/chromium/JSHandle.ts +++ b/src/chromium/JSHandle.ts @@ -50,7 +50,7 @@ export class DOMWorldDelegate implements dom.DOMWorldDelegate { } isJavascriptEnabled(): boolean { - return this._frameManager.page()._state.javascriptEnabled; + return !!this._frameManager.page()._state.javascriptEnabled; } isElement(remoteObject: any): boolean { diff --git a/src/firefox/Browser.ts b/src/firefox/Browser.ts index 865a353f7a..e2c8880d10 100644 --- a/src/firefox/Browser.ts +++ b/src/firefox/Browser.ts @@ -21,8 +21,9 @@ import { filterCookies, NetworkCookie, SetNetworkCookieParam, rewriteCookies } f import { Connection, ConnectionEvents, JugglerSessionEvents } from './Connection'; import { Events } from './events'; import { Permissions } from './features/permissions'; -import { Page } from './Page'; +import { Page } from '../page'; import * as types from '../types'; +import { FrameManager } from './FrameManager'; export class Browser extends EventEmitter { private _connection: Connection; @@ -130,11 +131,11 @@ export class Browser extends EventEmitter { } } - newPage(): Promise { + newPage(): Promise> { return this._createPageInContext(this._defaultContext._browserContextId); } - async _createPageInContext(browserContextId: string | null): Promise { + async _createPageInContext(browserContextId: string | null): Promise> { const {targetId} = await this._connection.send('Target.newPage', { browserContextId: browserContextId || undefined }); @@ -151,7 +152,7 @@ export class Browser extends EventEmitter { return Array.from(this._targets.values()); } - async _pages(context: BrowserContext): Promise { + async _pages(context: BrowserContext): Promise[]> { 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); @@ -188,8 +189,8 @@ export class Browser extends EventEmitter { } export class Target { - _pagePromise?: Promise; - private _page: Page | null = null; + _pagePromise?: Promise>; + private _page: Page | null = null; private _browser: Browser; _context: BrowserContext; private _connection: Connection; @@ -217,7 +218,6 @@ export class Target { return this._openerId ? this._browser._targets.get(this._openerId) : null; } - type(): 'page' | 'browser' { return this._type; } @@ -226,19 +226,19 @@ export class Target { return this._url; } - browserContext(): BrowserContext { return this._context; } - page(): Promise { + page(): Promise> { if (this._type === 'page' && !this._pagePromise) { this._pagePromise = new Promise(async f => { const session = await this._connection.createSession(this._targetId); - const page = new Page(session, this._context); + const frameManager = new FrameManager(session, this._context); + const page = frameManager._page; this._page = page; session.once(JugglerSessionEvents.Disconnected, () => page._didDisconnect()); - await page._frameManager._initialize(); + await frameManager._initialize(); if (this._browser._defaultViewport) await page.setViewport(this._browser._defaultViewport); f(page); @@ -265,7 +265,7 @@ export class BrowserContext { this.permissions = new Permissions(connection, browserContextId); } - pages(): Promise { + pages(): Promise[]> { return this._browser._pages(this); } diff --git a/src/firefox/FrameManager.ts b/src/firefox/FrameManager.ts index 8d5d446a6e..a03253c287 100644 --- a/src/firefox/FrameManager.ts +++ b/src/firefox/FrameManager.ts @@ -21,16 +21,24 @@ import * as frames from '../frames'; import { assert, helper, RegisteredListener, debugError } from '../helper'; import * as js from '../javascript'; import * as dom from '../dom'; -import { TimeoutSettings } from '../TimeoutSettings'; import { JugglerSession } from './Connection'; import { ExecutionContextDelegate } from './ExecutionContext'; import { NavigationWatchdog, NextNavigationWatchdog } from './NavigationWatchdog'; -import { Page } from './Page'; -import { NetworkManager } from './NetworkManager'; +import { Page, PageDelegate } from '../page'; +import { NetworkManager, NetworkManagerEvents } from './NetworkManager'; import { DOMWorldDelegate } from './JSHandle'; import { Events } from './events'; +import { Events as CommonEvents } from '../events'; import * as dialog from '../dialog'; import { Protocol } from './protocol'; +import * as input from '../input'; +import { RawMouseImpl, RawKeyboardImpl } from './Input'; +import { FFScreenshotDelegate } from './Screenshotter'; +import { Browser, BrowserContext } from './Browser'; +import { Interception } from './features/interception'; +import { Accessibility } from './features/accessibility'; +import * as network from '../network'; +import * as types from '../types'; export const FrameManagerEvents = { FrameNavigated: Symbol('FrameManagerEvents.FrameNavigated'), @@ -47,22 +55,25 @@ type FrameData = { firedEvents: Set, }; -export class FrameManager extends EventEmitter implements frames.FrameDelegate { - _session: JugglerSession; - _page: Page; - _networkManager: NetworkManager; - _timeoutSettings: TimeoutSettings; - _mainFrame: frames.Frame; - _frames: Map; - _contextIdToContext: Map; - _eventListeners: RegisteredListener[]; +export class FrameManager extends EventEmitter implements frames.FrameDelegate, PageDelegate { + readonly rawMouse: RawMouseImpl; + readonly rawKeyboard: RawKeyboardImpl; + readonly screenshotterDelegate: FFScreenshotDelegate; + readonly _session: JugglerSession; + readonly _page: Page; + private readonly _networkManager: NetworkManager; + private _mainFrame: frames.Frame; + private readonly _frames: Map; + private readonly _contextIdToContext: Map; + private _eventListeners: RegisteredListener[]; - constructor(session: JugglerSession, page: Page, networkManager, timeoutSettings) { + constructor(session: JugglerSession, browserContext: BrowserContext) { super(); this._session = session; - this._page = page; - this._networkManager = networkManager; - this._timeoutSettings = timeoutSettings; + this.rawKeyboard = new RawKeyboardImpl(session); + this.rawMouse = new RawMouseImpl(session); + this.screenshotterDelegate = new FFScreenshotDelegate(session, this); + this._networkManager = new NetworkManager(session, this); this._mainFrame = null; this._frames = new Map(); this._contextIdToContext = new Map(); @@ -79,7 +90,14 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate { helper.addEventListener(this._session, 'Page.dialogOpened', this._onDialogOpened.bind(this)), helper.addEventListener(this._session, 'Page.bindingCalled', this._onBindingCalled.bind(this)), helper.addEventListener(this._session, 'Page.fileChooserOpened', this._onFileChooserOpened.bind(this)), + helper.addEventListener(this._networkManager, NetworkManagerEvents.Request, request => this._page.emit(CommonEvents.Page.Request, request)), + helper.addEventListener(this._networkManager, NetworkManagerEvents.Response, response => this._page.emit(CommonEvents.Page.Response, response)), + helper.addEventListener(this._networkManager, NetworkManagerEvents.RequestFinished, request => this._page.emit(CommonEvents.Page.RequestFinished, request)), + helper.addEventListener(this._networkManager, NetworkManagerEvents.RequestFailed, request => this._page.emit(CommonEvents.Page.RequestFailed, request)), ]; + this._page = new Page(this, browserContext); + (this._page as any).interception = new Interception(this._networkManager); + (this._page as any).accessibility = new Accessibility(session); } async _initialize() { @@ -147,17 +165,19 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate { data.lastCommittedNavigationId = params.navigationId; data.firedEvents.clear(); this.emit(FrameManagerEvents.FrameNavigated, frame); + this._page.emit(CommonEvents.Page.FrameNavigated, frame); } _onSameDocumentNavigation(params) { const frame = this._frames.get(params.frameId); frame._navigated(params.url, frame.name()); this.emit(FrameManagerEvents.FrameNavigated, frame); + this._page.emit(CommonEvents.Page.FrameNavigated, frame); } _onFrameAttached(params) { const parentFrame = this._frames.get(params.parentFrameId) || null; - const frame = new frames.Frame(this, this._timeoutSettings, parentFrame); + const frame = new frames.Frame(this, this._page._timeoutSettings, parentFrame); const data: FrameData = { frameId: params.frameId, lastCommittedNavigationId: '', @@ -170,6 +190,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate { } this._frames.set(params.frameId, frame); this.emit(FrameManagerEvents.FrameAttached, frame); + this._page.emit(CommonEvents.Page.FrameAttached, frame); } _onFrameDetached(params) { @@ -177,16 +198,20 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate { this._frames.delete(params.frameId); frame._detach(); this.emit(FrameManagerEvents.FrameDetached, frame); + this._page.emit(CommonEvents.Page.FrameDetached, frame); } _onEventFired({frameId, name}) { const frame = this._frames.get(frameId); this._frameData(frame).firedEvents.add(name.toLowerCase()); if (frame === this._mainFrame) { - if (name === 'load') + if (name === 'load') { this.emit(FrameManagerEvents.Load); - else if (name === 'DOMContentLoaded') + this._page.emit(CommonEvents.Page.Load); + } else if (name === 'DOMContentLoaded') { this.emit(FrameManagerEvents.DOMContentLoaded); + this._page.emit(CommonEvents.Page.DOMContentLoaded); + } } } @@ -222,19 +247,20 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate { this._page._onFileChooserOpened(handle); } - async _exposeBinding(name: string, bindingFunction: string) { + async exposeBinding(name: string, bindingFunction: string): Promise { await this._session.send('Page.addBinding', {name: name}); await this._session.send('Page.addScriptToEvaluateOnNewDocument', {script: bindingFunction}); await Promise.all(this.frames().map(frame => frame.evaluate(bindingFunction).catch(debugError))); } - dispose() { + didClose() { helper.removeEventListeners(this._eventListeners); + this._networkManager.dispose(); } - async waitForFrameNavigation(frame: frames.Frame, options: { timeout?: number; waitUntil?: string | Array; } = {}) { + async waitForFrameNavigation(frame: frames.Frame, options: frames.NavigateOptions = {}) { const { - timeout = this._timeoutSettings.navigationTimeout(), + timeout = this._page._timeoutSettings.navigationTimeout(), waitUntil = ['load'], } = options; const normalizedWaitUntil = normalizeWaitUntil(waitUntil); @@ -277,9 +303,9 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate { return watchDog.navigationResponse(); } - async navigateFrame(frame: frames.Frame, url: string, options: { timeout?: number; waitUntil?: string | Array; referer?: string; } = {}) { + async navigateFrame(frame: frames.Frame, url: string, options: frames.GotoOptions = {}) { const { - timeout = this._timeoutSettings.navigationTimeout(), + timeout = this._page._timeoutSettings.navigationTimeout(), waitUntil = ['load'], referer, } = options; @@ -317,6 +343,95 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate { document.close(); }, html); } + + setExtraHTTPHeaders(extraHTTPHeaders: network.Headers): Promise { + return this._networkManager.setExtraHTTPHeaders(extraHTTPHeaders); + } + + async setUserAgent(userAgent: string): Promise { + await this._session.send('Page.setUserAgent', { userAgent }); + } + + async setJavaScriptEnabled(enabled: boolean): Promise { + await this._session.send('Page.setJavascriptEnabled', { enabled }); + } + + async setBypassCSP(enabled: boolean): Promise { + await this._session.send('Page.setBypassCSP', { enabled }); + } + + async setViewport(viewport: types.Viewport): Promise { + const { + width, + height, + isMobile = false, + deviceScaleFactor = 1, + hasTouch = false, + isLandscape = false, + } = viewport; + await this._session.send('Page.setViewport', { + viewport: { width, height, isMobile, deviceScaleFactor, hasTouch, isLandscape }, + }); + } + + async setEmulateMedia(mediaType: input.MediaType | null, mediaColorScheme: input.MediaColorScheme | null): Promise { + await this._session.send('Page.setEmulatedMedia', { + type: mediaType === null ? undefined : mediaType, + colorScheme: mediaColorScheme === null ? undefined : mediaColorScheme + }); + } + + async setCacheEnabled(enabled: boolean): Promise { + await this._session.send('Page.setCacheDisabled', {cacheDisabled: !enabled}); + } + + private async _go(action: () => Promise<{ navigationId: string | null, navigationURL: string | null }>, options: frames.NavigateOptions = {}) { + const { + timeout = this._page._timeoutSettings.navigationTimeout(), + waitUntil = ['load'], + } = options; + const frame = this.mainFrame(); + const normalizedWaitUntil = normalizeWaitUntil(waitUntil); + const { navigationId, navigationURL } = await action(); + if (!navigationId) + return null; + + const timeoutError = new TimeoutError('Navigation timeout of ' + timeout + ' ms exceeded'); + let timeoutCallback; + const timeoutPromise = new Promise(resolve => timeoutCallback = resolve.bind(null, timeoutError)); + const timeoutId = timeout ? setTimeout(timeoutCallback, timeout) : null; + + const watchDog = new NavigationWatchdog(this, frame, this._networkManager, navigationId, navigationURL, normalizedWaitUntil); + const error = await Promise.race([ + timeoutPromise, + watchDog.promise(), + ]); + watchDog.dispose(); + clearTimeout(timeoutId); + if (error) + throw error; + return watchDog.navigationResponse(); + } + + reload(options?: frames.NavigateOptions): Promise { + return this._go(() => this._session.send('Page.reload', { frameId: this._frameData(this.mainFrame()).frameId }), options); + } + + goBack(options?: frames.NavigateOptions): Promise { + return this._go(() => this._session.send('Page.goBack', { frameId: this._frameData(this.mainFrame()).frameId }), options); + } + + goForward(options?: frames.NavigateOptions): Promise { + return this._go(() => this._session.send('Page.goForward', { frameId: this._frameData(this.mainFrame()).frameId }), options); + } + + async evaluateOnNewDocument(source: string): Promise { + await this._session.send('Page.addScriptToEvaluateOnNewDocument', { script: source }); + } + + async closePage(runBeforeUnload: boolean): Promise { + await this._session.send('Page.close', { runBeforeUnload }); + } } export function normalizeWaitUntil(waitUntil) { diff --git a/src/firefox/JSHandle.ts b/src/firefox/JSHandle.ts index 51f300e29b..c76f05e402 100644 --- a/src/firefox/JSHandle.ts +++ b/src/firefox/JSHandle.ts @@ -53,7 +53,7 @@ export class DOMWorldDelegate implements dom.DOMWorldDelegate { } isJavascriptEnabled(): boolean { - return this._frameManager._page._javascriptEnabled; + return !!this._frameManager._page._state.javascriptEnabled; } isElement(remoteObject: any): boolean { diff --git a/src/firefox/NetworkManager.ts b/src/firefox/NetworkManager.ts index 2512918e39..c87264dbc8 100644 --- a/src/firefox/NetworkManager.ts +++ b/src/firefox/NetworkManager.ts @@ -35,12 +35,12 @@ export class NetworkManager extends EventEmitter { private _frameManager: FrameManager; private _eventListeners: RegisteredListener[]; - constructor(session: JugglerSession) { + constructor(session: JugglerSession, frameManager: FrameManager) { super(); this._session = session; this._requests = new Map(); - this._frameManager = null; + this._frameManager = frameManager; this._eventListeners = [ helper.addEventListener(session, 'Network.requestWillBeSent', this._onRequestWillBeSent.bind(this)), @@ -54,10 +54,6 @@ export class NetworkManager extends EventEmitter { helper.removeEventListeners(this._eventListeners); } - setFrameManager(frameManager: FrameManager) { - this._frameManager = frameManager; - } - async setExtraHTTPHeaders(headers: network.Headers) { const array = []; for (const [name, value] of Object.entries(headers)) { diff --git a/src/firefox/Page.ts b/src/firefox/Page.ts deleted file mode 100644 index b44c900b01..0000000000 --- a/src/firefox/Page.ts +++ /dev/null @@ -1,544 +0,0 @@ -/** - * 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. - */ - -import { EventEmitter } from 'events'; -import * as console from '../console'; -import * as dom from '../dom'; -import { TimeoutError } from '../Errors'; -import * as frames from '../frames'; -import { assert, debugError, helper, RegisteredListener } from '../helper'; -import * as input 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 { BrowserContext } from './Browser'; -import { JugglerSession } from './Connection'; -import { Events } from './events'; -import { Accessibility } from './features/accessibility'; -import { Interception } from './features/interception'; -import { FrameManager, FrameManagerEvents, normalizeWaitUntil } from './FrameManager'; -import { RawKeyboardImpl, RawMouseImpl } from './Input'; -import { NavigationWatchdog } from './NavigationWatchdog'; -import { NetworkManager, NetworkManagerEvents } from './NetworkManager'; -import { FFScreenshotDelegate } from './Screenshotter'; - -export class Page extends EventEmitter { - private _timeoutSettings: TimeoutSettings; - private _session: JugglerSession; - private _browserContext: BrowserContext; - private _keyboard: input.Keyboard; - private _mouse: input.Mouse; - readonly accessibility: Accessibility; - readonly interception: Interception; - private _closed: boolean; - private _closedCallback: () => void; - private _closedPromise: Promise; - private _disconnected = false; - private _disconnectedCallback: (e: Error) => void; - private _disconnectedPromise: Promise; - private _pageBindings: Map; - private _networkManager: NetworkManager; - _frameManager: FrameManager; - _javascriptEnabled = true; - private _eventListeners: RegisteredListener[]; - private _viewport: types.Viewport; - private _fileChooserInterceptors = new Set<(chooser: FileChooser) => void>(); - _screenshotter: Screenshotter; - - constructor(session: JugglerSession, browserContext: BrowserContext) { - super(); - this._timeoutSettings = new TimeoutSettings(); - this._session = session; - this._browserContext = browserContext; - this._keyboard = new input.Keyboard(new RawKeyboardImpl(session)); - this._mouse = new input.Mouse(new RawMouseImpl(session), this._keyboard); - this.accessibility = new Accessibility(session); - this._closed = false; - this._closedPromise = new Promise(f => this._closedCallback = f); - this._disconnectedPromise = new Promise(f => this._disconnectedCallback = f); - this._pageBindings = new Map(); - this._networkManager = new NetworkManager(session); - this._frameManager = new FrameManager(session, this, this._networkManager, this._timeoutSettings); - this._networkManager.setFrameManager(this._frameManager); - this.interception = new Interception(this._networkManager); - this._eventListeners = [ - helper.addEventListener(this._frameManager, FrameManagerEvents.Load, () => this.emit(Events.Page.Load)), - helper.addEventListener(this._frameManager, FrameManagerEvents.DOMContentLoaded, () => this.emit(Events.Page.DOMContentLoaded)), - helper.addEventListener(this._frameManager, FrameManagerEvents.FrameAttached, frame => this.emit(Events.Page.FrameAttached, frame)), - helper.addEventListener(this._frameManager, FrameManagerEvents.FrameDetached, frame => this.emit(Events.Page.FrameDetached, frame)), - helper.addEventListener(this._frameManager, FrameManagerEvents.FrameNavigated, frame => this.emit(Events.Page.FrameNavigated, frame)), - helper.addEventListener(this._networkManager, NetworkManagerEvents.Request, request => this.emit(Events.Page.Request, request)), - helper.addEventListener(this._networkManager, NetworkManagerEvents.Response, response => this.emit(Events.Page.Response, response)), - helper.addEventListener(this._networkManager, NetworkManagerEvents.RequestFinished, request => this.emit(Events.Page.RequestFinished, request)), - helper.addEventListener(this._networkManager, NetworkManagerEvents.RequestFailed, request => this.emit(Events.Page.RequestFailed, request)), - ]; - this._viewport = null; - this._screenshotter = new Screenshotter(this, new FFScreenshotDelegate(session, this._frameManager), browserContext.browser()); - } - - _didClose() { - assert(!this._closed, 'Page closed twice'); - this._closed = true; - this._frameManager.dispose(); - this._networkManager.dispose(); - helper.removeEventListeners(this._eventListeners); - this.emit(Events.Page.Close); - this._closedCallback(); - } - - _didDisconnect() { - assert(!this._disconnected, 'Page disconnected twice'); - this._disconnected = true; - this._disconnectedCallback(new Error('Target closed')); - } - - async setExtraHTTPHeaders(headers) { - await this._networkManager.setExtraHTTPHeaders(headers); - } - - 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); - await this._session.send('Page.setEmulatedMedia', options); - } - - async exposeFunction(name: string, playwrightFunction: Function) { - 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)); - - function addPageBinding(bindingName: string) { - const binding: (string) => void = window[bindingName]; - window[bindingName] = (...args) => { - const me = window[bindingName]; - let callbacks = me['callbacks']; - if (!callbacks) { - callbacks = new Map(); - me['callbacks'] = callbacks; - } - const seq = (me['lastSeq'] || 0) + 1; - me['lastSeq'] = seq; - const promise = new Promise((resolve, reject) => callbacks.set(seq, {resolve, reject})); - binding(JSON.stringify({name: bindingName, seq, args})); - return promise; - }; - } - } - - async _onBindingCalled(payload: string, context: js.ExecutionContext) { - const {name, seq, args} = JSON.parse(payload); - let expression = null; - try { - const result = await this._pageBindings.get(name)(...args); - expression = helper.evaluationString(deliverResult, name, seq, result); - } catch (error) { - if (error instanceof Error) - expression = helper.evaluationString(deliverError, name, seq, error.message, error.stack); - else - expression = helper.evaluationString(deliverErrorValue, name, seq, error); - } - context.evaluate(expression).catch(debugError); - - function deliverResult(name: string, seq: number, result: any) { - window[name]['callbacks'].get(seq).resolve(result); - window[name]['callbacks'].delete(seq); - } - - function deliverError(name: string, seq: number, message: string, stack: string) { - const error = new Error(message); - error.stack = stack; - window[name]['callbacks'].get(seq).reject(error); - window[name]['callbacks'].delete(seq); - } - - function deliverErrorValue(name: string, seq: number, value: any) { - window[name]['callbacks'].get(seq).reject(value); - window[name]['callbacks'].delete(seq); - } - } - - async waitForRequest(urlOrPredicate: (string | Function), options: { timeout?: number; } | undefined = {}): Promise { - const { - timeout = this._timeoutSettings.timeout(), - } = options; - return helper.waitForEvent(this._networkManager, NetworkManagerEvents.Request, request => { - if (helper.isString(urlOrPredicate)) - return (urlOrPredicate === request.url()); - if (typeof urlOrPredicate === 'function') - return !!(urlOrPredicate(request)); - return false; - }, timeout, this._disconnectedPromise); - } - - async waitForResponse(urlOrPredicate: (string | Function), options: { timeout?: number; } | undefined = {}): Promise { - const { - timeout = this._timeoutSettings.timeout(), - } = options; - return helper.waitForEvent(this._networkManager, NetworkManagerEvents.Response, response => { - if (helper.isString(urlOrPredicate)) - return (urlOrPredicate === response.url()); - if (typeof urlOrPredicate === 'function') - return !!(urlOrPredicate(response)); - return false; - }, timeout, this._disconnectedPromise); - } - - setDefaultNavigationTimeout(timeout: number) { - this._timeoutSettings.setDefaultNavigationTimeout(timeout); - } - - setDefaultTimeout(timeout: number) { - this._timeoutSettings.setDefaultTimeout(timeout); - } - - async setUserAgent(userAgent: string) { - await this._session.send('Page.setUserAgent', {userAgent}); - } - - async setJavaScriptEnabled(enabled) { - this._javascriptEnabled = enabled; - await this._session.send('Page.setJavascriptEnabled', {enabled}); - } - - async setBypassCSP(enabled: boolean) { - await this._session.send('Page.setBypassCSP', { enabled }); - } - - async setCacheEnabled(enabled) { - await this._session.send('Page.setCacheDisabled', {cacheDisabled: !enabled}); - } - - async emulate(options: { viewport: types.Viewport; userAgent: string; }) { - await Promise.all([ - this.setViewport(options.viewport), - this.setUserAgent(options.userAgent), - ]); - } - - browserContext(): BrowserContext { - return this._browserContext; - } - - viewport() { - return this._viewport; - } - - async setViewport(viewport: types.Viewport) { - const { - width, - height, - isMobile = false, - deviceScaleFactor = 1, - hasTouch = false, - isLandscape = false, - } = viewport; - await this._session.send('Page.setViewport', { - viewport: { width, height, isMobile, deviceScaleFactor, hasTouch, isLandscape }, - }); - 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(); - } - - async evaluateOnNewDocument(pageFunction: Function | string, ...args: Array) { - const script = helper.evaluationString(pageFunction, ...args); - await this._session.send('Page.addScriptToEvaluateOnNewDocument', { script }); - } - - browser() { - return this._browserContext.browser(); - } - - url() { - return this._frameManager.mainFrame().url(); - } - - frames() { - return this._frameManager.frames(); - } - - mainFrame(): frames.Frame { - return this._frameManager.mainFrame(); - } - - get keyboard(): input.Keyboard { - return this._keyboard; - } - - get mouse(): input.Mouse { - return this._mouse; - } - - async waitForNavigation(options: { timeout?: number; waitUntil?: string | Array; } = {}) { - return this._frameManager.mainFrame().waitForNavigation(options); - } - - async goto(url: string, options: { timeout?: number; waitUntil?: string | Array; } = {}) { - return this._frameManager.mainFrame().goto(url, options); - } - - async goBack(options: { timeout?: number; waitUntil?: string | Array; } = {}) { - const { - timeout = this._timeoutSettings.navigationTimeout(), - waitUntil = ['load'], - } = options; - const frame = this._frameManager.mainFrame(); - const normalizedWaitUntil = normalizeWaitUntil(waitUntil); - const {navigationId, navigationURL} = await this._session.send('Page.goBack', { - frameId: this._frameManager._frameData(frame).frameId, - }); - if (!navigationId) - return null; - - const timeoutError = new TimeoutError('Navigation timeout of ' + timeout + ' ms exceeded'); - let timeoutCallback; - const timeoutPromise = new Promise(resolve => timeoutCallback = resolve.bind(null, timeoutError)); - const timeoutId = timeout ? setTimeout(timeoutCallback, timeout) : null; - - const watchDog = new NavigationWatchdog(this._frameManager, frame, this._networkManager, navigationId, navigationURL, normalizedWaitUntil); - const error = await Promise.race([ - timeoutPromise, - watchDog.promise(), - ]); - watchDog.dispose(); - clearTimeout(timeoutId); - if (error) - throw error; - return watchDog.navigationResponse(); - } - - async goForward(options: { timeout?: number; waitUntil?: string | Array; } = {}) { - const { - timeout = this._timeoutSettings.navigationTimeout(), - waitUntil = ['load'], - } = options; - const frame = this._frameManager.mainFrame(); - const normalizedWaitUntil = normalizeWaitUntil(waitUntil); - const {navigationId, navigationURL} = await this._session.send('Page.goForward', { - frameId: this._frameManager._frameData(frame).frameId, - }); - if (!navigationId) - return null; - - const timeoutError = new TimeoutError('Navigation timeout of ' + timeout + ' ms exceeded'); - let timeoutCallback; - const timeoutPromise = new Promise(resolve => timeoutCallback = resolve.bind(null, timeoutError)); - const timeoutId = timeout ? setTimeout(timeoutCallback, timeout) : null; - - const watchDog = new NavigationWatchdog(this._frameManager, frame, this._networkManager, navigationId, navigationURL, normalizedWaitUntil); - const error = await Promise.race([ - timeoutPromise, - watchDog.promise(), - ]); - watchDog.dispose(); - clearTimeout(timeoutId); - if (error) - throw error; - return watchDog.navigationResponse(); - } - - async reload(options: { timeout?: number; waitUntil?: string | Array; } = {}) { - const { - timeout = this._timeoutSettings.navigationTimeout(), - waitUntil = ['load'], - } = options; - const frame = this._frameManager.mainFrame(); - const normalizedWaitUntil = normalizeWaitUntil(waitUntil); - const {navigationId, navigationURL} = await this._session.send('Page.reload', { - frameId: this._frameManager._frameData(frame).frameId, - }); - if (!navigationId) - return null; - - const timeoutError = new TimeoutError('Navigation timeout of ' + timeout + ' ms exceeded'); - let timeoutCallback; - const timeoutPromise = new Promise(resolve => timeoutCallback = resolve.bind(null, timeoutError)); - const timeoutId = timeout ? setTimeout(timeoutCallback, timeout) : null; - - const watchDog = new NavigationWatchdog(this._frameManager, frame, this._networkManager, navigationId, navigationURL, normalizedWaitUntil); - const error = await Promise.race([ - timeoutPromise, - watchDog.promise(), - ]); - watchDog.dispose(); - clearTimeout(timeoutId); - if (error) - throw error; - return watchDog.navigationResponse(); - } - - screenshot(options: types.ScreenshotOptions = {}): Promise { - return this._screenshotter.screenshotPage(options); - } - - evaluate: types.Evaluate = (pageFunction, ...args) => { - return this.mainFrame().evaluate(pageFunction, ...args as any); - } - - addScriptTag(options: { content?: string; path?: string; type?: string; url?: string; }): Promise { - return this.mainFrame().addScriptTag(options); - } - - addStyleTag(options: { content?: string; path?: string; url?: string; }): Promise { - return this.mainFrame().addStyleTag(options); - } - - click(selector: string | types.Selector, options?: input.ClickOptions) { - return this.mainFrame().click(selector, options); - } - - dblclick(selector: string | types.Selector, options?: input.MultiClickOptions) { - return this.mainFrame().dblclick(selector, options); - } - - tripleclick(selector: string | types.Selector, options?: input.MultiClickOptions) { - return this.mainFrame().tripleclick(selector, options); - } - - fill(selector: string | types.Selector, value: string) { - return this.mainFrame().fill(selector, value); - } - - select(selector: string | types.Selector, ...values: Array): Promise> { - return this._frameManager.mainFrame().select(selector, ...values); - } - - type(selector: string | types.Selector, text: string, options: { delay: (number | undefined); } | undefined) { - return this._frameManager.mainFrame().type(selector, text, options); - } - - focus(selector: string | types.Selector) { - return this._frameManager.mainFrame().focus(selector); - } - - hover(selector: string | types.Selector) { - return this._frameManager.mainFrame().hover(selector); - } - - waitFor(selectorOrFunctionOrTimeout: (string | number | Function), options: { polling?: string | number; timeout?: number; visible?: boolean; hidden?: boolean; } | undefined = {}, ...args: Array): Promise { - return this._frameManager.mainFrame().waitFor(selectorOrFunctionOrTimeout, options, ...args); - } - - waitForFunction(pageFunction: Function | string, options: types.WaitForFunctionOptions, ...args): Promise { - return this._frameManager.mainFrame().waitForFunction(pageFunction, options, ...args); - } - - waitForSelector(selector: string | types.Selector, options?: types.TimeoutOptions): Promise { - return this._frameManager.mainFrame().waitForSelector(selector, options); - } - - waitForXPath(xpath: string, options?: types.TimeoutOptions): Promise { - return this._frameManager.mainFrame().waitForXPath(xpath, options); - } - - title(): Promise { - return this._frameManager.mainFrame().title(); - } - - $(selector: string | types.Selector): Promise { - return this._frameManager.mainFrame().$(selector); - } - - $$(selector: string | types.Selector): Promise> { - return this._frameManager.mainFrame().$$(selector); - } - - $eval: types.$Eval = (selector, pageFunction, ...args) => { - return this._frameManager.mainFrame().$eval(selector, pageFunction, ...args as any); - } - - $$eval: types.$$Eval = (selector, pageFunction, ...args) => { - return this._frameManager.mainFrame().$$eval(selector, pageFunction, ...args as any); - } - - $x(expression: string): Promise> { - return this._frameManager.mainFrame().$x(expression); - } - - evaluateHandle: types.EvaluateHandle = async (pageFunction, ...args) => { - return this._frameManager.mainFrame().evaluateHandle(pageFunction, ...args as any); - } - - async close(options: any = {}) { - assert(!this._disconnected, 'Protocol error: Connection closed. Most likely the page has been closed.'); - const { - runBeforeUnload = false, - } = options; - await this._session.send('Page.close', { runBeforeUnload }); - if (!runBeforeUnload) - await this._closedPromise; - } - - async content() { - return await this._frameManager.mainFrame().content(); - } - - async setContent(html: string) { - return await this._frameManager.mainFrame().setContent(html); - } - - _addConsoleMessage(type: string, args: js.JSHandle[], location: console.ConsoleMessageLocation) { - if (!this.listenerCount(Events.Page.Console)) { - args.forEach(arg => arg.dispose()); - return; - } - this.emit(Events.Page.Console, new console.ConsoleMessage(type, undefined, args, location)); - } - - isClosed(): boolean { - return this._closed; - } - - async waitForFileChooser(options: { timeout?: number; } = {}): Promise { - const { - timeout = this._timeoutSettings.timeout(), - } = options; - let callback; - const promise = new Promise(x => callback = x); - this._fileChooserInterceptors.add(callback); - return helper.waitWithTimeout(promise, 'waiting for file chooser', timeout).catch(e => { - this._fileChooserInterceptors.delete(callback); - throw e; - }); - } - - async _onFileChooserOpened(handle: dom.ElementHandle) { - if (!this._fileChooserInterceptors.size) { - await handle.dispose(); - return; - } - const interceptors = Array.from(this._fileChooserInterceptors); - this._fileChooserInterceptors.clear(); - const multiple = await handle.evaluate((element: HTMLInputElement) => !!element.multiple); - const fileChooser = { element: handle, multiple }; - for (const interceptor of interceptors) - interceptor.call(null, fileChooser); - this.emit(Events.Page.FileChooser, fileChooser); - } -} - -type FileChooser = { - element: dom.ElementHandle, - multiple: boolean -}; diff --git a/src/firefox/api.ts b/src/firefox/api.ts index b6e684c276..63375e11d8 100644 --- a/src/firefox/api.ts +++ b/src/firefox/api.ts @@ -13,6 +13,6 @@ export { Interception } from './features/interception'; export { Permissions } from './features/permissions'; export { Frame } from '../frames'; export { Request, Response } from '../network'; -export { Page } from './Page'; +export { Page } from '../page'; export { Playwright } from './Playwright'; export { ConsoleMessage } from '../console'; diff --git a/src/frames.ts b/src/frames.ts index 36ed432d70..51bff3314c 100644 --- a/src/frames.ts +++ b/src/frames.ts @@ -74,11 +74,11 @@ export class Frame { this._parentFrame._childFrames.add(this); } - goto(url: string, options?: GotoOptions): Promise { + async goto(url: string, options?: GotoOptions): Promise { return this._delegate.navigateFrame(this, url, options); } - waitForNavigation(options?: NavigateOptions): Promise { + async waitForNavigation(options?: NavigateOptions): Promise { return this._delegate.waitForFrameNavigation(this, options); } @@ -159,7 +159,7 @@ export class Frame { }); } - setContent(html: string, options?: NavigateOptions) { + async setContent(html: string, options?: NavigateOptions): Promise { return this._delegate.setFrameContent(this, html, options); } diff --git a/src/page.ts b/src/page.ts index 7cf697f3f0..290b65990b 100644 --- a/src/page.ts +++ b/src/page.ts @@ -40,6 +40,8 @@ export interface PageDelegate { exposeBinding(name: string, bindingFunction: string): Promise; evaluateOnNewDocument(source: string): Promise; closePage(runBeforeUnload: boolean): Promise; + // TODO: reverse didClose call sequence. + didClose(): void; setExtraHTTPHeaders(extraHTTPHeaders: network.Headers): Promise; setUserAgent(userAgent: string): Promise; @@ -87,7 +89,7 @@ export class Page void>(); - constructor(delegate: PageDelegate, browserContext: BrowserContext, ignoreHTTPSErrors: boolean) { + constructor(delegate: PageDelegate, browserContext: BrowserContext) { super(); this._delegate = delegate; this._closedPromise = new Promise(f => this._closedCallback = f); @@ -112,6 +114,7 @@ export class Page { + async reload(options?: frames.NavigateOptions): Promise { return this._delegate.reload(options); } @@ -330,11 +333,11 @@ export class Page { + async goBack(options?: frames.NavigateOptions): Promise { return this._delegate.goBack(options); } - goForward(options?: frames.NavigateOptions): Promise { + async goForward(options?: frames.NavigateOptions): Promise { return this._delegate.goForward(options); } @@ -399,7 +402,7 @@ export class Page { + async screenshot(options?: types.ScreenshotOptions): Promise { return this._screenshotter.screenshotPage(options); } diff --git a/test/evaluation.spec.js b/test/evaluation.spec.js index ad682f1c88..de31394566 100644 --- a/test/evaluation.spec.js +++ b/test/evaluation.spec.js @@ -158,9 +158,18 @@ module.exports.addTests = function({testRunner, expect, FFOX, CHROME, WEBKIT}) { const result = await page.evaluate((a, b) => Object.is(a, undefined) && Object.is(b, 'foo'), undefined, 'foo'); expect(result).toBe(true); }); - it('should properly serialize null fields', async({page}) => { + it('should properly serialize undefined arguments', async({page}) => { + expect(await page.evaluate(x => ({a: x}), undefined)).toEqual({}); + }); + it('should properly serialize undefined fields', async({page}) => { expect(await page.evaluate(() => ({a: undefined}))).toEqual({}); }); + it.skip(FFOX)('should properly serialize null arguments', async({page}) => { + expect(await page.evaluate(x => x, null)).toEqual(null); + }); + it('should properly serialize null fields', async({page}) => { + expect(await page.evaluate(() => ({a: null}))).toEqual({a: null}); + }); it('should return undefined for non-serializable objects', async({page, server}) => { expect(await page.evaluate(() => window)).toBe(undefined); });