diff --git a/src/USKeyboardLayout.ts b/src/USKeyboardLayout.ts index 0c15fd4720..40d7f7a9d6 100644 --- a/src/USKeyboardLayout.ts +++ b/src/USKeyboardLayout.ts @@ -359,8 +359,8 @@ export const macEditingCommands: {[key: string]: string|string[]} = { 'Alt+Tab': 'insertTabIgnoringFieldEditor:', 'Alt+Enter': 'insertNewlineIgnoringFieldEditor:', 'Alt+Escape': 'complete:', - "Alt+ArrowUp": ['moveBackward:', 'moveToBeginningOfParagraph:'], - "Alt+ArrowDown": ['moveForward:', 'moveToEndOfParagraph:'], + 'Alt+ArrowUp': ['moveBackward:', 'moveToBeginningOfParagraph:'], + 'Alt+ArrowDown': ['moveForward:', 'moveToEndOfParagraph:'], 'Alt+ArrowLeft': 'moveWordLeft:', 'Alt+ArrowRight': 'moveWordRight:', 'Alt+Delete': 'deleteWordForward:', diff --git a/src/browserContext.ts b/src/browserContext.ts new file mode 100644 index 0000000000..169d60fdf8 --- /dev/null +++ b/src/browserContext.ts @@ -0,0 +1,74 @@ +/** + * Copyright 2017 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 { assert } from './helper'; +import { Page } from './page'; +import * as network from './network'; + +export interface BrowserDelegate { + contextPages(): Promise[]>; + createPageInContext(): Promise>; + closeContext(): Promise; + getContextCookies(): Promise; + clearContextCookies(): Promise; + setContextCookies(cookies: network.SetNetworkCookieParam[]): Promise; +} + +export class BrowserContext { + private readonly _delegate: BrowserDelegate; + private readonly _browser: Browser; + private readonly _isIncognito: boolean; + + constructor(delegate: BrowserDelegate, browser: Browser, isIncognito: boolean) { + this._delegate = delegate; + this._browser = browser; + this._isIncognito = isIncognito; + } + + async pages(): Promise[]> { + return this._delegate.contextPages(); + } + + isIncognito(): boolean { + return this._isIncognito; + } + + async newPage(): Promise> { + return this._delegate.createPageInContext(); + } + + browser(): Browser { + return this._browser; + } + + async cookies(...urls: string[]): Promise { + return network.filterCookies(await this._delegate.getContextCookies(), urls); + } + + async clearCookies() { + await this._delegate.clearContextCookies(); + } + + async setCookies(cookies: network.SetNetworkCookieParam[]) { + await this._delegate.setContextCookies(network.rewriteCookies(cookies)); + } + + async close() { + assert(this._isIncognito, 'Non-incognito profiles cannot be closed!'); + await this._delegate.closeContext(); + } +} diff --git a/src/chromium/Browser.ts b/src/chromium/Browser.ts index f4a8a1d013..0dbe477f13 100644 --- a/src/chromium/Browser.ts +++ b/src/chromium/Browser.ts @@ -19,7 +19,7 @@ import * as childProcess from 'child_process'; import { EventEmitter } from 'events'; import { Events } from './events'; import { assert, helper } from '../helper'; -import { BrowserContext } from './BrowserContext'; +import { BrowserContext } from '../browserContext'; import { Connection, ConnectionEvents, CDPSession } from './Connection'; import { Page } from '../page'; import { Target } from './Target'; @@ -27,6 +27,8 @@ import { Protocol } from './protocol'; import { Chromium } from './features/chromium'; import * as types from '../types'; import { FrameManager } from './FrameManager'; +import * as network from '../network'; +import { Permissions } from './features/permissions'; export class Browser extends EventEmitter { private _ignoreHTTPSErrors: boolean; @@ -35,8 +37,8 @@ export class Browser extends EventEmitter { _connection: Connection; _client: CDPSession; private _closeCallback: () => Promise; - private _defaultContext: BrowserContext; - private _contexts = new Map(); + private _defaultContext: BrowserContext; + private _contexts = new Map>(); _targets = new Map(); readonly chromium: Chromium; @@ -68,9 +70,9 @@ export class Browser extends EventEmitter { this._closeCallback = closeCallback || (() => Promise.resolve()); this.chromium = new Chromium(this); - this._defaultContext = new BrowserContext(this._client, this, null); + this._defaultContext = this._createBrowserContext(null); for (const contextId of contextIds) - this._contexts.set(contextId, new BrowserContext(this._client, this, contextId)); + this._contexts.set(contextId, this._createBrowserContext(contextId)); this._connection.on(ConnectionEvents.Disconnected, () => this.emit(Events.Browser.Disconnected)); this._client.on('Target.targetCreated', this._targetCreated.bind(this)); @@ -78,30 +80,68 @@ export class Browser extends EventEmitter { this._client.on('Target.targetInfoChanged', this._targetInfoChanged.bind(this)); } + _createBrowserContext(contextId: string | null): BrowserContext { + const isIncognito = !!contextId; + const context = new BrowserContext({ + contextPages: async (): 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); + }, + + createPageInContext: async (): Promise> => { + const { targetId } = await this._client.send('Target.createTarget', { url: 'about:blank', browserContextId: contextId || undefined }); + const target = this._targets.get(targetId); + assert(await target._initializedPromise, 'Failed to create target for page'); + const page = await target.page(); + return page; + }, + + closeContext: async (): Promise => { + await this._client.send('Target.disposeBrowserContext', {browserContextId: contextId || undefined}); + this._contexts.delete(contextId); + }, + + getContextCookies: async (): Promise => { + const { cookies } = await this._client.send('Storage.getCookies', { browserContextId: contextId || undefined }); + return cookies.map(c => { + const copy: any = { sameSite: 'None', ...c }; + delete copy.size; + return copy as network.NetworkCookie; + }); + }, + + clearContextCookies: async (): Promise => { + await this._client.send('Storage.clearCookies', { browserContextId: contextId || undefined }); + }, + + setContextCookies: async (cookies: network.SetNetworkCookieParam[]): Promise => { + await this._client.send('Storage.setCookies', { cookies, browserContextId: contextId || undefined }); + }, + }, this, isIncognito); + (context as any).permissions = new Permissions(this._client, contextId); + return context; + } + process(): childProcess.ChildProcess | null { return this._process; } - async createIncognitoBrowserContext(): Promise { + async createIncognitoBrowserContext(): Promise> { const {browserContextId} = await this._client.send('Target.createBrowserContext'); - const context = new BrowserContext(this._client, this, browserContextId); + const context = this._createBrowserContext(browserContextId); this._contexts.set(browserContextId, context); return context; } - browserContexts(): BrowserContext[] { + browserContexts(): BrowserContext[] { return [this._defaultContext, ...Array.from(this._contexts.values())]; } - defaultBrowserContext(): BrowserContext { + defaultBrowserContext(): BrowserContext { return this._defaultContext; } - async _disposeContext(contextId: string | null) { - await this._client.send('Target.disposeBrowserContext', {browserContextId: contextId || undefined}); - this._contexts.delete(contextId); - } - async _targetCreated(event: Protocol.Target.targetCreatedPayload) { const targetInfo = event.targetInfo; const {browserContextId} = targetInfo; @@ -134,19 +174,11 @@ export class Browser extends EventEmitter { this.chromium.emit(Events.Chromium.TargetChanged, target); } - async newPage(): Promise> { + async newPage(): Promise> { return this._defaultContext.newPage(); } - async _createPageInContext(contextId: string | null): Promise> { - const { targetId } = await this._client.send('Target.createTarget', { url: 'about:blank', browserContextId: contextId || undefined }); - const target = this._targets.get(targetId); - assert(await target._initializedPromise, 'Failed to create target for page'); - const page = await target.page(); - return page; - } - - async _closePage(page: Page) { + async _closePage(page: Page) { await this._client.send('Target.closeTarget', { targetId: Target.fromPage(page)._targetId }); } @@ -154,13 +186,7 @@ export class Browser extends EventEmitter { return Array.from(this._targets.values()).filter(target => target._isInitialized); } - 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); - } - - async _activatePage(page: Page) { + async _activatePage(page: Page) { await (page._delegate as FrameManager)._client.send('Target.activateTarget', {targetId: Target.fromPage(page)._targetId}); } @@ -190,7 +216,7 @@ export class Browser extends EventEmitter { } } - async pages(): Promise[]> { + async pages(): Promise[]> { const contextPages = await Promise.all(this.browserContexts().map(context => context.pages())); // Flatten array. return contextPages.reduce((acc, x) => acc.concat(x), []); diff --git a/src/chromium/BrowserContext.ts b/src/chromium/BrowserContext.ts deleted file mode 100644 index b20ecd8fc3..0000000000 --- a/src/chromium/BrowserContext.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Copyright 2017 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 { assert } from '../helper'; -import { filterCookies, NetworkCookie, rewriteCookies, SetNetworkCookieParam } from '../network'; -import { Browser } from './Browser'; -import { CDPSession } from './Connection'; -import { Permissions } from './features/permissions'; -import { Page } from '../page'; - -export class BrowserContext { - readonly permissions: Permissions; - - private _browser: Browser; - private _id: string; - - constructor(client: CDPSession, browser: Browser, contextId: string | null) { - this._browser = browser; - this._id = contextId; - this.permissions = new Permissions(client, contextId); - } - - pages(): Promise[]> { - return this._browser._pages(this); - } - - isIncognito(): boolean { - return !!this._id; - } - - newPage(): Promise> { - return this._browser._createPageInContext(this._id); - } - - browser(): Browser { - return this._browser; - } - - async cookies(...urls: string[]): Promise { - const { cookies } = await this._browser._client.send('Storage.getCookies', { browserContextId: this._id || undefined }); - return filterCookies(cookies.map(c => { - const copy: any = { sameSite: 'None', ...c }; - delete copy.size; - return copy as NetworkCookie; - }), urls); - } - - async clearCookies() { - await this._browser._client.send('Storage.clearCookies', { browserContextId: this._id || undefined }); - } - - async setCookies(cookies: SetNetworkCookieParam[]) { - cookies = rewriteCookies(cookies); - await this._browser._client.send('Storage.setCookies', { cookies, browserContextId: this._id || undefined }); - } - - async close() { - assert(this._id, 'Non-incognito profiles cannot be closed!'); - await this._browser._disposeContext(this._id); - } -} diff --git a/src/chromium/FrameManager.ts b/src/chromium/FrameManager.ts index 520408ce63..544029830c 100644 --- a/src/chromium/FrameManager.ts +++ b/src/chromium/FrameManager.ts @@ -41,7 +41,7 @@ import { Workers } from './features/workers'; import { Overrides } from './features/overrides'; import { Interception } from './features/interception'; import { Browser } from './Browser'; -import { BrowserContext } from './BrowserContext'; +import { BrowserContext } from '../browserContext'; import * as types from '../types'; import * as input from '../input'; import { ConsoleMessage } from '../console'; @@ -64,7 +64,7 @@ type FrameData = { export class FrameManager extends EventEmitter implements frames.FrameDelegate, PageDelegate { _client: CDPSession; - private _page: Page; + private _page: Page; private _networkManager: NetworkManager; private _frames = new Map(); private _contextIdToContext = new Map(); @@ -74,7 +74,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate, rawKeyboard: RawKeyboardImpl; screenshotterDelegate: CRScreenshotDelegate; - constructor(client: CDPSession, browserContext: BrowserContext, ignoreHTTPSErrors: boolean) { + constructor(client: CDPSession, browserContext: BrowserContext, ignoreHTTPSErrors: boolean) { super(); this._client = client; this.rawKeyboard = new RawKeyboardImpl(client); @@ -254,7 +254,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate, this._handleFrameTree(child); } - page(): Page { + page(): Page { return this._page; } diff --git a/src/chromium/Target.ts b/src/chromium/Target.ts index 07af08c8de..da9f1f0a21 100644 --- a/src/chromium/Target.ts +++ b/src/chromium/Target.ts @@ -17,7 +17,7 @@ import * as types from '../types'; import { Browser } from './Browser'; -import { BrowserContext } from './BrowserContext'; +import { BrowserContext } from '../browserContext'; import { CDPSession, CDPSessionEvents } from './Connection'; import { Events } from '../events'; import { Worker } from './features/workers'; @@ -30,25 +30,25 @@ const targetSymbol = Symbol('target'); export class Target { private _targetInfo: Protocol.Target.TargetInfo; - private _browserContext: BrowserContext; + private _browserContext: BrowserContext; _targetId: string; private _sessionFactory: () => Promise; private _ignoreHTTPSErrors: boolean; private _defaultViewport: types.Viewport; - private _pagePromise: Promise> | null = null; - private _page: Page | null = null; + private _pagePromise: Promise> | null = null; + private _page: Page | null = null; private _workerPromise: Promise | null = null; _initializedPromise: Promise; _initializedCallback: (value?: unknown) => void; _isInitialized: boolean; - static fromPage(page: Page): Target { + static fromPage(page: Page): Target { return (page as any)[targetSymbol]; } constructor( targetInfo: Protocol.Target.TargetInfo, - browserContext: BrowserContext, + browserContext: BrowserContext, sessionFactory: () => Promise, ignoreHTTPSErrors: boolean, defaultViewport: types.Viewport | null) { @@ -81,7 +81,7 @@ export class Target { this._page._didClose(); } - async page(): Promise | null> { + async page(): Promise | null> { if ((this._targetInfo.type === 'page' || this._targetInfo.type === 'background_page') && !this._pagePromise) { this._pagePromise = this._sessionFactory().then(async client => { const frameManager = new FrameManager(client, this._browserContext, this._ignoreHTTPSErrors); @@ -131,7 +131,7 @@ export class Target { return this._browserContext.browser(); } - browserContext(): BrowserContext { + browserContext(): BrowserContext { return this._browserContext; } diff --git a/src/chromium/api.ts b/src/chromium/api.ts index 360089e0b5..e50a936c02 100644 --- a/src/chromium/api.ts +++ b/src/chromium/api.ts @@ -10,7 +10,7 @@ export { Keyboard, Mouse } from '../input'; export { ExecutionContext, JSHandle } from '../javascript'; export { Request, Response } from '../network'; export { Browser } from './Browser'; -export { BrowserContext } from './BrowserContext'; +export { BrowserContext } from '../browserContext'; export { BrowserFetcher } from '../browserFetcher'; export { CDPSession } from './Connection'; export { Accessibility } from './features/accessibility'; diff --git a/src/chromium/features/chromium.ts b/src/chromium/features/chromium.ts index bf16aca0a8..db8cda487a 100644 --- a/src/chromium/features/chromium.ts +++ b/src/chromium/features/chromium.ts @@ -17,7 +17,7 @@ import { EventEmitter } from 'events'; import { assert } from '../../helper'; import { Browser } from '../Browser'; -import { BrowserContext } from '../BrowserContext'; +import { BrowserContext } from '../../browserContext'; import { CDPSession, Connection } from '../Connection'; import { Page } from '../../page'; import { readProtocolStream } from '../protocolHelper'; @@ -48,7 +48,7 @@ export class Chromium extends EventEmitter { return target._worker(); } - async startTracing(page: Page | undefined, options: { path?: string; screenshots?: boolean; categories?: string[]; } = {}) { + async startTracing(page: Page | undefined, options: { path?: string; screenshots?: boolean; categories?: string[]; } = {}) { assert(!this._recording, 'Cannot start recording trace while already recording trace.'); this._tracingClient = page ? (page._delegate as FrameManager)._client : this._client; @@ -87,12 +87,12 @@ export class Chromium extends EventEmitter { return contentPromise; } - targets(context?: BrowserContext): Target[] { + targets(context?: BrowserContext): Target[] { const targets = this._browser._allTargets(); return context ? targets.filter(t => t.browserContext() === context) : targets; } - pageTarget(page: Page): Target { + pageTarget(page: Page): Target { return Target.fromPage(page); } diff --git a/src/firefox/Browser.ts b/src/firefox/Browser.ts index 839370348c..07c8ca79cc 100644 --- a/src/firefox/Browser.ts +++ b/src/firefox/Browser.ts @@ -16,8 +16,7 @@ */ import { EventEmitter } from 'events'; -import { assert, helper, RegisteredListener } from '../helper'; -import { filterCookies, NetworkCookie, SetNetworkCookieParam, rewriteCookies } from '../network'; +import { helper, RegisteredListener } from '../helper'; import { Connection, ConnectionEvents, JugglerSessionEvents } from './Connection'; import { Events } from './events'; import { Events as CommonEvents } from '../events'; @@ -25,6 +24,8 @@ import { Permissions } from './features/permissions'; import { Page } from '../page'; import * as types from '../types'; import { FrameManager } from './FrameManager'; +import * as network from '../network'; +import { BrowserContext } from '../browserContext'; export class Browser extends EventEmitter { private _connection: Connection; @@ -32,8 +33,8 @@ export class Browser extends EventEmitter { private _process: import('child_process').ChildProcess; private _closeCallback: () => void; _targets: Map; - private _defaultContext: BrowserContext; - private _contexts: Map; + private _defaultContext: BrowserContext; + private _contexts: Map>; private _eventListeners: RegisteredListener[]; static async create(connection: Connection, defaultViewport: types.Viewport | null, process: import('child_process').ChildProcess | null, closeCallback: () => void) { @@ -52,10 +53,10 @@ export class Browser extends EventEmitter { this._targets = new Map(); - this._defaultContext = new BrowserContext(this._connection, this, null); + this._defaultContext = this._createBrowserContext(null); this._contexts = new Map(); for (const browserContextId of browserContextIds) - this._contexts.set(browserContextId, new BrowserContext(this._connection, this, browserContextId)); + this._contexts.set(browserContextId, this._createBrowserContext(browserContextId)); this._connection.on(ConnectionEvents.Disconnected, () => this.emit(Events.Browser.Disconnected)); @@ -74,14 +75,14 @@ export class Browser extends EventEmitter { return !this._connection._closed; } - async createIncognitoBrowserContext(): Promise { + async createIncognitoBrowserContext(): Promise> { const {browserContextId} = await this._connection.send('Target.createBrowserContext'); - const context = new BrowserContext(this._connection, this, browserContextId); + const context = this._createBrowserContext(browserContextId); this._contexts.set(browserContextId, context); return context; } - browserContexts(): Array { + browserContexts(): Array> { return [this._defaultContext, ...Array.from(this._contexts.values())]; } @@ -89,11 +90,6 @@ export class Browser extends EventEmitter { return this._defaultContext; } - async _disposeContext(browserContextId) { - await this._connection.send('Target.removeBrowserContext', {browserContextId}); - this._contexts.delete(browserContextId); - } - async userAgent(): Promise { const info = await this._connection.send('Browser.getInfo'); return info.userAgent; @@ -132,16 +128,8 @@ export class Browser extends EventEmitter { } } - newPage(): Promise> { - return this._createPageInContext(this._defaultContext._browserContextId); - } - - async _createPageInContext(browserContextId: string | null): Promise> { - const {targetId} = await this._connection.send('Target.newPage', { - browserContextId: browserContextId || undefined - }); - const target = this._targets.get(targetId); - return await target.page(); + newPage(): Promise> { + return this._defaultContext.newPage(); } async pages() { @@ -153,12 +141,6 @@ export class Browser extends EventEmitter { return Array.from(this._targets.values()); } - 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); - } - async _onTargetCreated({targetId, url, browserContextId, openerId, type}) { const context = browserContextId ? this._contexts.get(browserContextId) : this._defaultContext; const target = new Target(this._connection, this, context, targetId, type, url, openerId); @@ -187,20 +169,63 @@ export class Browser extends EventEmitter { helper.removeEventListeners(this._eventListeners); this._closeCallback(); } + + _createBrowserContext(browserContextId: string | null): BrowserContext { + const isIncognito = !!browserContextId; + const context = new BrowserContext({ + contextPages: async (): 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); + }, + + createPageInContext: async (): Promise> => { + const {targetId} = await this._connection.send('Target.newPage', { + browserContextId: browserContextId || undefined + }); + const target = this._targets.get(targetId); + return await target.page(); + }, + + closeContext: async (): Promise => { + await this._connection.send('Target.removeBrowserContext', { browserContextId }); + this._contexts.delete(browserContextId); + }, + + getContextCookies: async (): Promise => { + const { cookies } = await this._connection.send('Browser.getCookies', { browserContextId: browserContextId || undefined }); + return cookies.map(c => { + const copy: any = { ... c }; + delete copy.size; + return copy as network.NetworkCookie; + }); + }, + + clearContextCookies: async (): Promise => { + await this._connection.send('Browser.clearCookies', { browserContextId: browserContextId || undefined }); + }, + + setContextCookies: async (cookies: network.SetNetworkCookieParam[]): Promise => { + await this._connection.send('Browser.setCookies', { browserContextId: browserContextId || undefined, cookies }); + }, + }, this, isIncognito); + (context as any).permissions = new Permissions(this._connection, browserContextId); + return context; + } } export class Target { - _pagePromise?: Promise>; - private _page: Page | null = null; + _pagePromise?: Promise>; + private _page: Page | null = null; private _browser: Browser; - _context: BrowserContext; + _context: BrowserContext; private _connection: Connection; private _targetId: string; private _type: 'page' | 'browser'; _url: string; private _openerId: string; - constructor(connection: any, browser: Browser, context: BrowserContext, targetId: string, type: 'page' | 'browser', url: string, openerId: string | undefined) { + constructor(connection: any, browser: Browser, context: BrowserContext, targetId: string, type: 'page' | 'browser', url: string, openerId: string | undefined) { this._browser = browser; this._context = context; this._connection = connection; @@ -227,11 +252,11 @@ export class Target { return this._url; } - browserContext(): BrowserContext { + 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); @@ -252,64 +277,3 @@ export class Target { return this._browser; } } - -export class BrowserContext { - _connection: Connection; - _browser: Browser; - _browserContextId: string; - readonly permissions: Permissions; - - constructor(connection: Connection, browser: Browser, browserContextId: string | null) { - this._connection = connection; - this._browser = browser; - this._browserContextId = browserContextId; - this.permissions = new Permissions(connection, browserContextId); - } - - pages(): Promise[]> { - return this._browser._pages(this); - } - - isIncognito(): boolean { - return !!this._browserContextId; - } - - newPage() { - return this._browser._createPageInContext(this._browserContextId); - } - - - browser(): Browser { - return this._browser; - } - - async cookies(...urls: string[]): Promise { - const { cookies } = await this._connection.send('Browser.getCookies', { - browserContextId: this._browserContextId || undefined - }); - return filterCookies(cookies, urls).map(c => { - const copy: any = { ... c }; - delete copy.size; - return copy as NetworkCookie; - }); - } - - async clearCookies() { - await this._connection.send('Browser.clearCookies', { - browserContextId: this._browserContextId || undefined, - }); - } - - async setCookies(cookies: SetNetworkCookieParam[]) { - cookies = rewriteCookies(cookies); - await this._connection.send('Browser.setCookies', { - browserContextId: this._browserContextId || undefined, - cookies - }); - } - - async close() { - assert(this._browserContextId, 'Non-incognito contexts cannot be closed!'); - await this._browser._disposeContext(this._browserContextId); - } -} diff --git a/src/firefox/FrameManager.ts b/src/firefox/FrameManager.ts index 6c12273dda..7848daa2e9 100644 --- a/src/firefox/FrameManager.ts +++ b/src/firefox/FrameManager.ts @@ -33,7 +33,8 @@ import { Protocol } from './protocol'; import * as input from '../input'; import { RawMouseImpl, RawKeyboardImpl } from './Input'; import { FFScreenshotDelegate } from './Screenshotter'; -import { Browser, BrowserContext } from './Browser'; +import { Browser } from './Browser'; +import { BrowserContext } from '../browserContext'; import { Interception } from './features/interception'; import { Accessibility } from './features/accessibility'; import * as network from '../network'; @@ -58,14 +59,14 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate, readonly rawKeyboard: RawKeyboardImpl; readonly screenshotterDelegate: FFScreenshotDelegate; readonly _session: JugglerSession; - readonly _page: Page; + 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, browserContext: BrowserContext) { + constructor(session: JugglerSession, browserContext: BrowserContext) { super(); this._session = session; this.rawKeyboard = new RawKeyboardImpl(session); diff --git a/src/firefox/api.ts b/src/firefox/api.ts index 63375e11d8..466d1fcb38 100644 --- a/src/firefox/api.ts +++ b/src/firefox/api.ts @@ -3,7 +3,8 @@ export { TimeoutError } from '../Errors'; export { Keyboard, Mouse } from '../input'; -export { Browser, BrowserContext } from './Browser'; +export { Browser } from './Browser'; +export { BrowserContext } from '../browserContext'; export { BrowserFetcher } from '../browserFetcher'; export { Dialog } from '../dialog'; export { ExecutionContext, JSHandle } from '../javascript'; diff --git a/src/page.ts b/src/page.ts index 8d57336867..fd7729446f 100644 --- a/src/page.ts +++ b/src/page.ts @@ -27,6 +27,7 @@ import { Screenshotter, ScreenshotterDelegate } from './screenshotter'; import { TimeoutSettings } from './TimeoutSettings'; import * as types from './types'; import { Events } from './events'; +import { BrowserContext } from './browserContext'; export interface PageDelegate { readonly rawMouse: input.RawMouse; @@ -52,10 +53,6 @@ export interface PageDelegate { setCacheEnabled(enabled: boolean): Promise; } -interface BrowserContextInterface { - browser(): Browser; -} - type PageState = { viewport: types.Viewport | null; userAgent: string | null; @@ -72,14 +69,14 @@ export type FileChooser = { multiple: boolean }; -export class Page> extends EventEmitter { +export class Page extends EventEmitter { private _closed = false; private _closedCallback: () => void; private _closedPromise: Promise; private _disconnected = false; private _disconnectedCallback: (e: Error) => void; readonly _disconnectedPromise: Promise; - private _browserContext: BrowserContext; + private _browserContext: BrowserContext; readonly keyboard: input.Keyboard; readonly mouse: input.Mouse; readonly _timeoutSettings: TimeoutSettings; @@ -89,7 +86,7 @@ export class Page void>(); - constructor(delegate: PageDelegate, browserContext: BrowserContext) { + constructor(delegate: PageDelegate, browserContext: BrowserContext) { super(); this._delegate = delegate; this._closedPromise = new Promise(f => this._closedCallback = f); @@ -156,7 +153,7 @@ export class Page { return this._browserContext; } diff --git a/src/webkit/Browser.ts b/src/webkit/Browser.ts index c3df18b6d0..502a19fd38 100644 --- a/src/webkit/Browser.ts +++ b/src/webkit/Browser.ts @@ -17,22 +17,23 @@ import * as childProcess from 'child_process'; import { EventEmitter } from 'events'; -import { assert, helper, RegisteredListener, debugError } from '../helper'; -import { filterCookies, NetworkCookie, rewriteCookies, SetNetworkCookieParam } from '../network'; +import { helper, RegisteredListener, debugError } from '../helper'; +import * as network from '../network'; import { Connection, ConnectionEvents, TargetSession } from './Connection'; import { Page } from '../page'; import { Target } from './Target'; import { Protocol } from './protocol'; import * as types from '../types'; import { Events } from '../events'; +import { BrowserContext } from '../browserContext'; export class Browser extends EventEmitter { readonly _defaultViewport: types.Viewport; private readonly _process: childProcess.ChildProcess; readonly _connection: Connection; private _closeCallback: () => Promise; - private readonly _defaultContext: BrowserContext; - private _contexts = new Map(); + private readonly _defaultContext: BrowserContext; + private _contexts = new Map>(); _targets = new Map(); private _eventListeners: RegisteredListener[]; private _privateEvents = new EventEmitter(); @@ -51,7 +52,7 @@ export class Browser extends EventEmitter { /** @type {!Map} */ this._targets = new Map(); - this._defaultContext = new BrowserContext(this); + this._defaultContext = this._createBrowserContext(undefined); /** @type {!Map} */ this._contexts = new Map(); @@ -85,34 +86,26 @@ export class Browser extends EventEmitter { return this._process; } - async createIncognitoBrowserContext(): Promise { + async createIncognitoBrowserContext(): Promise> { const {browserContextId} = await this._connection.send('Browser.createContext'); - const context = new BrowserContext(this, browserContextId); + const context = this._createBrowserContext(browserContextId); this._contexts.set(browserContextId, context); return context; } - browserContexts(): BrowserContext[] { + browserContexts(): BrowserContext[] { return [this._defaultContext, ...Array.from(this._contexts.values())]; } - defaultBrowserContext(): BrowserContext { + defaultBrowserContext(): BrowserContext { return this._defaultContext; } async _disposeContext(browserContextId: string | null) { - await this._connection.send('Browser.deleteContext', {browserContextId}); - this._contexts.delete(browserContextId); } - async newPage(): Promise> { - return this._createPageInContext(this._defaultContext._id); - } - - async _createPageInContext(browserContextId?: string): Promise> { - const { targetId } = await this._connection.send('Browser.createPage', { browserContextId }); - const target = this._targets.get(targetId); - return await target.page(); + async newPage(): Promise> { + return this._defaultContext.newPage(); } targets(): Target[] { @@ -143,7 +136,7 @@ export class Browser extends EventEmitter { } } - async pages(): Promise[]> { + async pages(): Promise[]> { const contextPages = await Promise.all(this.browserContexts().map(context => context.pages())); // Flatten array. return contextPages.reduce((acc, x) => acc.concat(x), []); @@ -188,19 +181,13 @@ export class Browser extends EventEmitter { target._didClose(); } - _closePage(page: Page) { + _closePage(page: Page) { this._connection.send('Target.close', { targetId: Target.fromPage(page)._targetId }).catch(debugError); } - async _pages(context: BrowserContext): Promise[]> { - const targets = this.targets().filter(target => target._browserContext === context && target._type === 'page'); - const pages = await Promise.all(targets.map(target => target.page())); - return pages.filter(page => !!page); - } - - async _activatePage(page: Page): Promise { + async _activatePage(page: Page): Promise { await this._connection.send('Target.activate', { targetId: Target.fromPage(page)._targetId }); } @@ -222,54 +209,45 @@ export class Browser extends EventEmitter { helper.removeEventListeners(this._eventListeners); await this._closeCallback.call(null); } -} -export class BrowserContext { - private _browser: Browser; - _id: string; + _createBrowserContext(browserContextId: string | undefined): BrowserContext { + const isIncognito = !!browserContextId; + const context = new BrowserContext({ + contextPages: async (): Promise[]> => { + const targets = this.targets().filter(target => target._browserContext === context && target._type === 'page'); + const pages = await Promise.all(targets.map(target => target.page())); + return pages.filter(page => !!page); + }, - constructor(browser: Browser, contextId?: string) { - this._browser = browser; - this._id = contextId; - } + createPageInContext: async (): Promise> => { + const { targetId } = await this._connection.send('Browser.createPage', { browserContextId }); + const target = this._targets.get(targetId); + return await target.page(); + }, - pages(): Promise[]> { - return this._browser._pages(this); - } + closeContext: async (): Promise => { + await this._connection.send('Browser.deleteContext', { browserContextId }); + this._contexts.delete(browserContextId); + }, - isIncognito(): boolean { - return !!this._id; - } + getContextCookies: async (): Promise => { + const { cookies } = await this._connection.send('Browser.getAllCookies', { browserContextId }); + return cookies.map((c: network.NetworkCookie) => ({ + ...c, + expires: c.expires === 0 ? -1 : c.expires + })); + }, - newPage(): Promise> { - return this._browser._createPageInContext(this._id); - } + clearContextCookies: async (): Promise => { + await this._connection.send('Browser.deleteAllCookies', { browserContextId }); + }, - browser(): Browser { - return this._browser; - } - - async close() { - assert(this._id, 'Non-incognito profiles cannot be closed!'); - await this._browser._disposeContext(this._id); - } - - async cookies(...urls: string[]): Promise { - const { cookies } = await this._browser._connection.send('Browser.getAllCookies', { browserContextId: this._id }); - return filterCookies(cookies.map((c: NetworkCookie) => ({ - ...c, - expires: c.expires === 0 ? -1 : c.expires - })), urls); - } - - async setCookies(cookies: SetNetworkCookieParam[]) { - cookies = rewriteCookies(cookies); - const cc = cookies.map(c => ({ ...c, session: c.expires === -1 || c.expires === undefined })) as Protocol.Browser.SetCookieParam[]; - await this._browser._connection.send('Browser.setCookies', { cookies: cc, browserContextId: this._id }); - } - - async clearCookies() { - await this._browser._connection.send('Browser.deleteAllCookies', { browserContextId: this._id }); + setContextCookies: async (cookies: network.SetNetworkCookieParam[]): Promise => { + const cc = cookies.map(c => ({ ...c, session: c.expires === -1 || c.expires === undefined })) as Protocol.Browser.SetCookieParam[]; + await this._connection.send('Browser.setCookies', { cookies: cc, browserContextId }); + }, + }, this, isIncognito); + return context; } } diff --git a/src/webkit/Connection.ts b/src/webkit/Connection.ts index bb635d66f6..d3a93c88d0 100644 --- a/src/webkit/Connection.ts +++ b/src/webkit/Connection.ts @@ -20,7 +20,6 @@ import * as debug from 'debug'; import {EventEmitter} from 'events'; import { ConnectionTransport } from '../types'; import { Protocol } from './protocol'; -import { throws } from 'assert'; const debugProtocol = debug('playwright:protocol'); const debugWrappedMessage = require('debug')('wrapped'); @@ -96,7 +95,7 @@ export class Connection extends EventEmitter { const delay = this._delay || 0; this._dispatchTimerId = setTimeout(() => { this._dispatchTimerId = undefined; - this._dispatchOneMessageFromQueue() + this._dispatchOneMessageFromQueue(); }, delay); } diff --git a/src/webkit/FrameManager.ts b/src/webkit/FrameManager.ts index e7e238f3b9..8a5e489d6b 100644 --- a/src/webkit/FrameManager.ts +++ b/src/webkit/FrameManager.ts @@ -18,11 +18,11 @@ import * as EventEmitter from 'events'; import { TimeoutError } from '../Errors'; import * as frames from '../frames'; -import { assert, debugError, helper, RegisteredListener } from '../helper'; +import { assert, helper, RegisteredListener } from '../helper'; import * as js from '../javascript'; import * as dom from '../dom'; import * as network from '../network'; -import { TargetSession, TargetSessionEvents } from './Connection'; +import { TargetSession } from './Connection'; import { Events } from '../events'; import { ExecutionContextDelegate, EVALUATION_SCRIPT_URL } from './ExecutionContext'; import { NetworkManager, NetworkManagerEvents } from './NetworkManager'; @@ -30,7 +30,8 @@ import { Page, PageDelegate } from '../page'; import { Protocol } from './protocol'; import { DOMWorldDelegate } from './JSHandle'; import * as dialog from '../dialog'; -import { Browser, BrowserContext } from './Browser'; +import { Browser } from './Browser'; +import { BrowserContext } from '../browserContext'; import { RawMouseImpl, RawKeyboardImpl } from './Input'; import { WKScreenshotDelegate } from './Screenshotter'; import * as input from '../input'; @@ -57,7 +58,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate, readonly rawKeyboard: RawKeyboardImpl; readonly screenshotterDelegate: WKScreenshotDelegate; _session: TargetSession; - readonly _page: Page; + readonly _page: Page; private readonly _networkManager: NetworkManager; private readonly _frames: Map; private readonly _contextIdToContext: Map; @@ -66,7 +67,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate, private _mainFrame: frames.Frame; private readonly _bootstrapScripts: string[] = []; - constructor(browserContext: BrowserContext) { + constructor(browserContext: BrowserContext) { super(); this.rawKeyboard = new RawKeyboardImpl(); this.rawMouse = new RawMouseImpl(); @@ -95,7 +96,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate, } // This method is called for provisional targets as well. The session passed as the parameter - // may be different from the current session and may be destroyed without becoming current. + // may be different from the current session and may be destroyed without becoming current. async _initializeSession(session: TargetSession) { const promises : Promise[] = [ // Page agent must be enabled before Runtime. @@ -109,7 +110,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate, ]; if (!session.isProvisional()) { // FIXME: move dialog agent to web process. - // Dialog agent resides in the UI process and should not be re-enabled on navigation. + // Dialog agent resides in the UI process and should not be re-enabled on navigation. promises.push(session.send('Dialog.enable')); } if (this._page._state.userAgent !== null) @@ -193,7 +194,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate, this._handleFrameTree(child); } - page(): Page { + page(): Page { return this._page; } diff --git a/src/webkit/Target.ts b/src/webkit/Target.ts index 3ce0af0d49..3149a311ed 100644 --- a/src/webkit/Target.ts +++ b/src/webkit/Target.ts @@ -15,7 +15,8 @@ * limitations under the License. */ -import { BrowserContext, Browser } from './Browser'; +import { Browser } from './Browser'; +import { BrowserContext } from '../browserContext'; import { Page } from '../page'; import { Protocol } from './protocol'; import { isSwappedOutError, TargetSession, TargetSessionEvents } from './Connection'; @@ -24,18 +25,18 @@ import { FrameManager } from './FrameManager'; const targetSymbol = Symbol('target'); export class Target { - readonly _browserContext: BrowserContext; + readonly _browserContext: BrowserContext; readonly _targetId: string; readonly _type: 'page' | 'service-worker' | 'worker'; private readonly _session: TargetSession; - private _pagePromise: Promise> | null = null; - _page: Page | null = null; + private _pagePromise: Promise> | null = null; + _page: Page | null = null; - static fromPage(page: Page): Target { + static fromPage(page: Page): Target { return (page as any)[targetSymbol]; } - constructor(session: TargetSession, targetInfo: Protocol.Target.TargetInfo, browserContext: BrowserContext) { + constructor(session: TargetSession, targetInfo: Protocol.Target.TargetInfo, browserContext: BrowserContext) { const {targetId, type} = targetInfo; this._session = session; this._browserContext = browserContext; @@ -83,7 +84,7 @@ export class Target { (this._page._delegate as FrameManager).setSession(this._session); } - async page(): Promise> { + async page(): Promise> { if (this._type === 'page' && !this._pagePromise) { const browser = this._browserContext.browser(); // Reference local page variable as _page may be diff --git a/src/webkit/api.ts b/src/webkit/api.ts index 0f014a9b54..42df78a5a3 100644 --- a/src/webkit/api.ts +++ b/src/webkit/api.ts @@ -2,7 +2,8 @@ // Licensed under the MIT license. export { TimeoutError } from '../Errors'; -export { Browser, BrowserContext } from './Browser'; +export { Browser } from './Browser'; +export { BrowserContext } from '../browserContext'; export { BrowserFetcher } from '../browserFetcher'; export { ExecutionContext, JSHandle } from '../javascript'; export { ElementHandle } from '../dom'; diff --git a/test/chromium/coverage.spec.js b/test/chromium/coverage.spec.js index 97fd005de3..33fa891e04 100644 --- a/test/chromium/coverage.spec.js +++ b/test/chromium/coverage.spec.js @@ -22,7 +22,7 @@ module.exports.addTests = function({testRunner, expect, FFOX, CHROME, WEBKIT}) { describe('JSCoverage', function() { it('should work', async function({page, server}) { await page.coverage.startJSCoverage(); - await page.goto(server.PREFIX + '/jscoverage/simple.html', {waitUntil: 'networkidle0'}); + await page.goto(server.PREFIX + '/jscoverage/simple.html', { waitUntil: 'load' }); const coverage = await page.coverage.stopJSCoverage(); expect(coverage.length).toBe(1); expect(coverage[0].url).toContain('/jscoverage/simple.html');