diff --git a/docs/api.md b/docs/api.md index c6682e0044..ad9772f4ca 100644 --- a/docs/api.md +++ b/docs/api.md @@ -28,9 +28,6 @@ * [browserFetcher.revisionInfo(revision)](#browserfetcherrevisioninforevision) - [class: Browser](#class-browser) * [event: 'disconnected'](#event-disconnected) - * [event: 'targetchanged'](#event-targetchanged) - * [event: 'targetcreated'](#event-targetcreated) - * [event: 'targetdestroyed'](#event-targetdestroyed) * [browser.browserContexts()](#browserbrowsercontexts) * [browser.chromium](#browserchromium) * [browser.close()](#browserclose) @@ -41,14 +38,9 @@ * [browser.newPage()](#browsernewpage) * [browser.pages()](#browserpages) * [browser.process()](#browserprocess) - * [browser.targets()](#browsertargets) * [browser.userAgent()](#browseruseragent) * [browser.version()](#browserversion) - * [browser.waitForTarget(predicate[, options])](#browserwaitfortargetpredicate-options) - [class: BrowserContext](#class-browsercontext) - * [event: 'targetchanged'](#event-targetchanged-1) - * [event: 'targetcreated'](#event-targetcreated-1) - * [event: 'targetdestroyed'](#event-targetdestroyed-1) * [browserContext.browser()](#browsercontextbrowser) * [browserContext.clearCookies()](#browsercontextclearcookies) * [browserContext.close()](#browsercontextclose) @@ -58,8 +50,6 @@ * [browserContext.pages()](#browsercontextpages) * [browserContext.permissions](#browsercontextpermissions) * [browserContext.setCookies(cookies)](#browsercontextsetcookiescookies) - * [browserContext.targets()](#browsercontexttargets) - * [browserContext.waitForTarget(predicate[, options])](#browsercontextwaitfortargetpredicate-options) - [class: Overrides](#class-overrides) * [overrides.setGeolocation(options)](#overridessetgeolocationoptions) * [overrides.setTimezone(timezoneId)](#overridessettimezonetimezoneid) @@ -129,7 +119,6 @@ * [page.setJavaScriptEnabled(enabled)](#pagesetjavascriptenabledenabled) * [page.setUserAgent(userAgent)](#pagesetuseragentuseragent) * [page.setViewport(viewport)](#pagesetviewportviewport) - * [page.target()](#pagetarget) * [page.title()](#pagetitle) * [page.tripleclick(selector[, options])](#pagetripleclickselector-options) * [page.type(selector, text[, options])](#pagetypeselector-text-options) @@ -171,11 +160,16 @@ - [class: PDF](#class-pdf) * [pdf.generate([options])](#pdfgenerateoptions) - [class: Chromium](#class-chromium) - * [chromium.createBrowserCDPSession()](#chromiumcreatebrowsercdpsession) - * [chromium.createCDPSession(target)](#chromiumcreatecdpsessiontarget) + * [event: 'targetchanged'](#event-targetchanged) + * [event: 'targetcreated'](#event-targetcreated) + * [event: 'targetdestroyed'](#event-targetdestroyed) + * [chromium.browserTarget()](#chromiumbrowsertarget) + * [chromium.pageTarget(page)](#chromiumpagetargetpage) * [chromium.serviceWorker(target)](#chromiumserviceworkertarget) * [chromium.startTracing(page, [options])](#chromiumstarttracingpage-options) * [chromium.stopTracing()](#chromiumstoptracing) + * [chromium.targets(context)](#chromiumtargetscontext) + * [chromium.waitForTarget(predicate[, options])](#chromiumwaitfortargetpredicate-options) * [chromium.wsEndpoint()](#chromiumwsendpoint) - [class: Dialog](#class-dialog) * [dialog.accept([promptText])](#dialogacceptprompttext) @@ -297,6 +291,7 @@ - [class: Target](#class-target) * [target.browser()](#targetbrowser) * [target.browserContext()](#targetbrowsercontext) + * [target.createCDPSession()](#targetcreatecdpsession) * [target.opener()](#targetopener) * [target.page()](#targetpage) * [target.type()](#targettype) @@ -353,7 +348,7 @@ const playwright = require('playwright'); `--load-extension=${pathToExtension}` ] }); - const targets = await browser.targets(); + const targets = await browser.chromium.targets(); const backgroundPageTarget = targets.find(target => target.type() === 'background_page'); const backgroundPage = await backgroundPageTarget.page(); // Test the background page as you would any other page. @@ -601,28 +596,6 @@ Emitted when Playwright gets disconnected from the Chromium instance. This might - Chromium is closed or crashed - The [`browser.disconnect`](#browserdisconnect) method was called -#### event: 'targetchanged' -- <[Target]> - -Emitted when the url of a target changes. - -> **NOTE** This includes target changes in incognito browser contexts. - - -#### event: 'targetcreated' -- <[Target]> - -Emitted when a target is created, for example when a new page is opened by [`window.open`](https://developer.mozilla.org/en-US/docs/Web/API/Window/open) or [`browser.newPage`](#browsernewpage). - -> **NOTE** This includes target creations in incognito browser contexts. - -#### event: 'targetdestroyed' -- <[Target]> - -Emitted when a target is destroyed, for example when a page is closed. - -> **NOTE** This includes target destructions in incognito browser contexts. - #### browser.browserContexts() - returns: <[Array]<[BrowserContext]>> @@ -683,12 +656,6 @@ the method will return an array with all the pages in all browser contexts. #### browser.process() - returns: Spawned browser process. Returns `null` if the browser instance was created with [`playwright.connect`](#playwrightconnectoptions) method. -#### browser.targets() -- returns: <[Array]<[Target]>> - -An array of all active targets inside the Browser. In case of multiple browser contexts, -the method will return an array with all the targets in all browser contexts. - #### browser.userAgent() - returns: <[Promise]<[string]>> Promise which resolves to the browser's original user agent. @@ -699,20 +666,6 @@ the method will return an array with all the targets in all browser contexts. > **NOTE** the format of browser.version() might change with future releases of Chromium. -#### browser.waitForTarget(predicate[, options]) -- `predicate` <[function]\([Target]\):[boolean]> A function to be run for every target -- `options` <[Object]> - - `timeout` <[number]> Maximum wait time in milliseconds. Pass `0` to disable the timeout. Defaults to 30 seconds. -- returns: <[Promise]<[Target]>> Promise which resolves to the first target found that matches the `predicate` function. - -This searches for a target in all browser contexts. - -An example of finding a target for a page opened via `window.open`: -```js -await page.evaluate(() => window.open('https://www.example.com/')); -const newWindowTarget = await browser.waitForTarget(target => target.url() === 'https://www.example.com/'); -``` - ### class: BrowserContext * extends: [EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter) @@ -737,21 +690,6 @@ await page.goto('https://example.com'); await context.close(); ``` -#### event: 'targetchanged' -- <[Target]> - -Emitted when the url of a target inside the browser context changes. - -#### event: 'targetcreated' -- <[Target]> - -Emitted when a new target is created inside the browser context, for example when a new page is opened by [`window.open`](https://developer.mozilla.org/en-US/docs/Web/API/Window/open) or [`browserContext.newPage`](#browsercontextnewpage). - -#### event: 'targetdestroyed' -- <[Target]> - -Emitted when a target inside the browser context is destroyed, for example when a page is closed. - #### browserContext.browser() - returns: <[Browser]> @@ -825,25 +763,6 @@ An array of all pages inside the browser context. await browserContext.setCookies([cookieObject1, cookieObject2]); ``` -#### browserContext.targets() -- returns: <[Array]<[Target]>> - -An array of all active targets inside the browser context. - -#### browserContext.waitForTarget(predicate[, options]) -- `predicate` <[function]\([Target]\):[boolean]> A function to be run for every target -- `options` <[Object]> - - `timeout` <[number]> Maximum wait time in milliseconds. Pass `0` to disable the timeout. Defaults to 30 seconds. -- returns: <[Promise]<[Target]>> Promise which resolves to the first target found that matches the `predicate` function. - -This searches for a target in this specific browser context. - -An example of finding a target for a page opened via `window.open`: -```js -await page.evaluate(() => window.open('https://www.example.com/')); -const newWindowTarget = await browserContext.waitForTarget(target => target.url() === 'https://www.example.com/'); -``` - ### class: Overrides #### overrides.setGeolocation(options) @@ -1708,9 +1627,6 @@ await page.setViewport({ await page.goto('https://example.com'); ``` -#### page.target() -- returns: <[Target]> a target this page was created from. - #### page.title() - returns: <[Promise]<[string]>> The page's title. @@ -2368,16 +2284,36 @@ await page.goto('https://www.google.com'); await page.chromium.stopTracing(); ``` -#### chromium.createBrowserCDPSession() -- returns: <[Promise]<[CDPSession]>> +#### event: 'targetchanged' +- <[Target]> -Creates a Chrome Devtools Protocol session attached to the browser. +Emitted when the url of a target changes. -#### chromium.createCDPSession(target) -- `target` <[Target]> Target to return CDP connection for. -- returns: <[Promise]<[CDPSession]>> +> **NOTE** This includes target changes in incognito browser contexts. -Creates a Chrome Devtools Protocol session attached to the target. + +#### event: 'targetcreated' +- <[Target]> + +Emitted when a target is created, for example when a new page is opened by [`window.open`](https://developer.mozilla.org/en-US/docs/Web/API/Window/open) or [`browser.newPage`](#browsernewpage). + +> **NOTE** This includes target creations in incognito browser contexts. + +#### event: 'targetdestroyed' +- <[Target]> + +Emitted when a target is destroyed, for example when a page is closed. + +> **NOTE** This includes target destructions in incognito browser contexts. + +#### chromium.browserTarget() +- returns: <[Target]> + +Returns browser target. + +#### chromium.pageTarget(page) +- `page` <[Page]> Page to return target for. +- returns: <[Target]> a target given page was created from. #### chromium.serviceWorker(target) - `target` <[Target]> Target to treat as a service worker @@ -2398,6 +2334,27 @@ Only one trace can be active at a time per browser. #### chromium.stopTracing() - returns: <[Promise]<[Buffer]>> Promise which resolves to buffer with trace data. +#### chromium.targets(context) +- `context` <[BrowserContext]> Optional, if specified, only targets from this context are returned. +- returns: <[Array]<[Target]>> + +An array of all active targets inside the Browser. In case of multiple browser contexts, +the method will return an array with all the targets in all browser contexts. + +#### chromium.waitForTarget(predicate[, options]) +- `predicate` <[function]\([Target]\):[boolean]> A function to be run for every target +- `options` <[Object]> + - `timeout` <[number]> Maximum wait time in milliseconds. Pass `0` to disable the timeout. Defaults to 30 seconds. +- returns: <[Promise]<[Target]>> Promise which resolves to the first target found that matches the `predicate` function. + +This searches for a target in all browser contexts. + +An example of finding a target for a page opened via `window.open`: +```js +await page.evaluate(() => window.open('https://www.example.com/')); +const newWindowTarget = await browser.chromium.waitForTarget(target => target.url() === 'https://www.example.com/'); +``` + #### chromium.wsEndpoint() - returns: <[string]> Browser websocket url. @@ -3712,6 +3669,11 @@ Get the browser the target belongs to. The browser context the target belongs to. +#### target.createCDPSession() +- returns: <[Promise]<[CDPSession]>> + +Creates a Chrome Devtools Protocol session attached to the target. + #### target.opener() - returns: @@ -3743,7 +3705,7 @@ Useful links: - Getting Started with DevTools Protocol: https://github.com/aslushnikov/getting-started-with-cdp/blob/master/README.md ```js -const client = await page.chromium.createCDPSession(target); +const client = await page.chromium.pageTarget(page).createCDPSession(); await client.send('Animation.enable'); client.on('Animation.animationCreated', () => console.log('Animation created!')); const response = await client.send('Animation.getPlaybackRate'); diff --git a/src/chromium/Browser.ts b/src/chromium/Browser.ts index 87b0c029a7..2c65fdb9c3 100644 --- a/src/chromium/Browser.ts +++ b/src/chromium/Browser.ts @@ -32,7 +32,7 @@ export class Browser extends EventEmitter { private _defaultViewport: Viewport; private _process: childProcess.ChildProcess; private _screenshotter = new Screenshotter(); - private _connection: Connection; + _connection: Connection; _client: CDPSession; private _closeCallback: () => Promise; private _defaultContext: BrowserContext; @@ -66,7 +66,7 @@ export class Browser extends EventEmitter { this._defaultViewport = defaultViewport; this._process = process; this._closeCallback = closeCallback || (() => Promise.resolve()); - this.chromium = new Chromium(this._connection, this._client); + this.chromium = new Chromium(this); this._defaultContext = new BrowserContext(this._client, this, null); for (const contextId of contextIds) @@ -111,10 +111,8 @@ export class Browser extends EventEmitter { assert(!this._targets.has(event.targetInfo.targetId), 'Target should not exist before targetCreated'); this._targets.set(event.targetInfo.targetId, target); - if (await target._initializedPromise) { - this.emit(Events.Browser.TargetCreated, target); - context.emit(Events.BrowserContext.TargetCreated, target); - } + if (await target._initializedPromise) + this.chromium.emit(Events.Chromium.TargetCreated, target); } async _targetDestroyed(event: { targetId: string; }) { @@ -122,10 +120,8 @@ export class Browser extends EventEmitter { target._initializedCallback(false); this._targets.delete(event.targetId); target._closedCallback(); - if (await target._initializedPromise) { - this.emit(Events.Browser.TargetDestroyed, target); - target.browserContext().emit(Events.BrowserContext.TargetDestroyed, target); - } + if (await target._initializedPromise) + this.chromium.emit(Events.Chromium.TargetDestroyed, target); } _targetInfoChanged(event: Protocol.Target.targetInfoChangedPayload) { @@ -134,10 +130,8 @@ export class Browser extends EventEmitter { const previousURL = target.url(); const wasInitialized = target._isInitialized; target._targetInfoChanged(event.targetInfo); - if (wasInitialized && previousURL !== target.url()) { - this.emit(Events.Browser.TargetChanged, target); - target.browserContext().emit(Events.BrowserContext.TargetChanged, target); - } + if (wasInitialized && previousURL !== target.url()) + this.chromium.emit(Events.Chromium.TargetChanged, target); } async newPage(): Promise { @@ -156,28 +150,28 @@ export class Browser extends EventEmitter { await this._client.send('Target.closeTarget', { targetId: target._targetId }); } - targets(): Target[] { + _allTargets(): Target[] { return Array.from(this._targets.values()).filter(target => target._isInitialized); } - async waitForTarget(predicate: (arg0: Target) => boolean, options: { timeout?: number; } | undefined = {}): Promise { + async _waitForTarget(predicate: (arg0: Target) => boolean, options: { timeout?: number; } | undefined = {}): Promise { const { timeout = 30000 } = options; - const existingTarget = this.targets().find(predicate); + const existingTarget = this._allTargets().find(predicate); if (existingTarget) return existingTarget; let resolve; const targetPromise = new Promise(x => resolve = x); - this.on(Events.Browser.TargetCreated, check); - this.on(Events.Browser.TargetChanged, check); + this.chromium.on(Events.Chromium.TargetCreated, check); + this.chromium.on(Events.Chromium.TargetChanged, check); try { if (!timeout) return await targetPromise; return await helper.waitWithTimeout(targetPromise, 'target', timeout); } finally { - this.removeListener(Events.Browser.TargetCreated, check); - this.removeListener(Events.Browser.TargetChanged, check); + this.chromium.removeListener(Events.Chromium.TargetCreated, check); + this.chromium.removeListener(Events.Chromium.TargetChanged, check); } function check(target: Target) { diff --git a/src/chromium/BrowserContext.ts b/src/chromium/BrowserContext.ts index a0c77788b0..7ae966dba2 100644 --- a/src/chromium/BrowserContext.ts +++ b/src/chromium/BrowserContext.ts @@ -15,7 +15,6 @@ * limitations under the License. */ -import { EventEmitter } from 'events'; import { assert } from '../helper'; import { filterCookies, NetworkCookie, rewriteCookies, SetNetworkCookieParam } from '../network'; import { Browser } from './Browser'; @@ -24,30 +23,25 @@ import { Permissions } from './features/permissions'; import { Page } from './Page'; import { Target } from './Target'; -export class BrowserContext extends EventEmitter { +export class BrowserContext { readonly permissions: Permissions; private _browser: Browser; private _id: string; constructor(client: CDPSession, browser: Browser, contextId: string | null) { - super(); this._browser = browser; this._id = contextId; this.permissions = new Permissions(client, contextId); } - targets(): Target[] { - return this._browser.targets().filter(target => target.browserContext() === this); - } - - waitForTarget(predicate: (arg0: Target) => boolean, options: { timeout?: number; } | undefined): Promise { - return this._browser.waitForTarget(target => target.browserContext() === this && predicate(target), options); + _targets(): Target[] { + return this._browser._allTargets().filter(target => target.browserContext() === this); } async pages(): Promise { const pages = await Promise.all( - this.targets() + this._targets() .filter(target => target.type() === 'page') .map(target => target.page()) ); diff --git a/src/chromium/Launcher.ts b/src/chromium/Launcher.ts index d2f7082b1c..faff28bc3b 100644 --- a/src/chromium/Launcher.ts +++ b/src/chromium/Launcher.ts @@ -184,7 +184,7 @@ export class Launcher { connection = new Connection('', transport, slowMo); } const browser = await Browser.create(connection, [], ignoreHTTPSErrors, defaultViewport, chromeProcess, gracefullyCloseChrome); - await browser.waitForTarget(t => t.type() === 'page'); + await browser._waitForTarget(t => t.type() === 'page'); return browser; } catch (e) { killChrome(); diff --git a/src/chromium/Page.ts b/src/chromium/Page.ts index 90c500eed0..dcd20a7b84 100644 --- a/src/chromium/Page.ts +++ b/src/chromium/Page.ts @@ -59,7 +59,7 @@ export type Viewport = { export class Page extends EventEmitter { private _closed = false; _client: CDPSession; - private _target: Target; + _target: Target; private _keyboard: input.Keyboard; private _mouse: input.Mouse; private _timeoutSettings: TimeoutSettings; @@ -178,10 +178,6 @@ export class Page extends EventEmitter { }); } - target(): Target { - return this._target; - } - browser(): Browser { return this._target.browser(); } diff --git a/src/chromium/Screenshotter.ts b/src/chromium/Screenshotter.ts index 1bc6d01d52..fe5666b184 100644 --- a/src/chromium/Screenshotter.ts +++ b/src/chromium/Screenshotter.ts @@ -85,7 +85,7 @@ export class Screenshotter { } private async _screenshot(page: Page, format: 'png' | 'jpeg', options: ScreenshotOptions): Promise { - await page._client.send('Target.activateTarget', {targetId: page.target()._targetId}); + await page._client.send('Target.activateTarget', {targetId: page._target._targetId}); let clip = options.clip ? processClip(options.clip) : undefined; const viewport = page.viewport(); diff --git a/src/chromium/Target.ts b/src/chromium/Target.ts index 6fba007ea1..922eacb1b2 100644 --- a/src/chromium/Target.ts +++ b/src/chromium/Target.ts @@ -28,7 +28,7 @@ export class Target { private _targetInfo: Protocol.Target.TargetInfo; private _browserContext: BrowserContext; _targetId: string; - _sessionFactory: () => Promise; + private _sessionFactory: () => Promise; private _ignoreHTTPSErrors: boolean; private _defaultViewport: Viewport; private _screenshotter: Screenshotter; @@ -118,6 +118,10 @@ export class Target { return this.browser()._targets.get(openerId); } + createCDPSession(): Promise { + return this._sessionFactory(); + } + _targetInfoChanged(targetInfo: Protocol.Target.TargetInfo) { this._targetInfo = targetInfo; diff --git a/src/chromium/api.ts b/src/chromium/api.ts index c98d47443d..6cab510735 100644 --- a/src/chromium/api.ts +++ b/src/chromium/api.ts @@ -1,26 +1,27 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +export { ConsoleMessage } from '../console'; +export { Dialog } from '../dialog'; +export { ElementHandle } from '../dom'; export { TimeoutError } from '../Errors'; +export { Frame } from '../frames'; +export { Keyboard, Mouse } from '../input'; +export { ExecutionContext, JSHandle } from '../javascript'; +export { Request, Response } from '../network'; export { Browser } from './Browser'; export { BrowserContext } from './BrowserContext'; export { BrowserFetcher } from './BrowserFetcher'; -export { Chromium } from './features/chromium'; export { CDPSession } from './Connection'; -export { Dialog } from '../dialog'; -export { ExecutionContext, JSHandle } from '../javascript'; -export { ElementHandle } from '../dom'; export { Accessibility } from './features/accessibility'; +export { Chromium } from './features/chromium'; export { Coverage } from './features/coverage'; -export { Overrides } from './features/overrides'; export { Interception } from './features/interception'; +export { Overrides } from './features/overrides'; export { PDF } from './features/pdf'; export { Permissions } from './features/permissions'; export { Worker, Workers } from './features/workers'; -export { Frame } from '../frames'; -export { Keyboard, Mouse } from '../input'; -export { Request, Response } from '../network'; export { Page } from './Page'; export { Playwright } from './Playwright'; export { Target } from './Target'; -export { ConsoleMessage } from '../console'; + diff --git a/src/chromium/events.ts b/src/chromium/events.ts index fd3a0e7c85..87f200c65a 100644 --- a/src/chromium/events.ts +++ b/src/chromium/events.ts @@ -37,13 +37,10 @@ export const Events = { }, Browser: { - TargetCreated: 'targetcreated', - TargetDestroyed: 'targetdestroyed', - TargetChanged: 'targetchanged', Disconnected: 'disconnected' }, - BrowserContext: { + Chromium: { TargetCreated: 'targetcreated', TargetDestroyed: 'targetdestroyed', TargetChanged: 'targetchanged', diff --git a/test/target.spec.js b/src/chromium/features/chromium.spec.js similarity index 55% rename from test/target.spec.js rename to src/chromium/features/chromium.spec.js index 4db5ada2cf..17ce5d9c5b 100644 --- a/test/target.spec.js +++ b/src/chromium/features/chromium.spec.js @@ -14,19 +14,32 @@ * limitations under the License. */ -const utils = require('./utils'); -const {waitEvent} = utils; +const { waitEvent } = require('../../../test/utils'); module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME, WEBKIT}) { const {describe, xdescribe, fdescribe} = testRunner; const {it, fit, xit} = testRunner; const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; - describe('Target', function() { - // FIXME(WebKit): need to enable agents in the inspected page to listen for updates. - it.skip(WEBKIT)('Browser.targets should return all of the targets', async({page, server, browser}) => { + describe('Chromium', function() { + it('should work across sessions', async function({browser, server}) { + expect(browser.browserContexts().length).toBe(2); + const context = await browser.createIncognitoBrowserContext(); + expect(browser.browserContexts().length).toBe(3); + const remoteBrowser = await playwright.connect({ + browserWSEndpoint: browser.chromium.wsEndpoint() + }); + const contexts = remoteBrowser.browserContexts(); + expect(contexts.length).toBe(3); + remoteBrowser.disconnect(); + await context.close(); + }); + }); + + describe('Target', function() { + it('Chromium.targets should return all of the targets', async({page, server, browser}) => { // The pages will be the testing page and the original newtab page - const targets = browser.targets(); + const targets = browser.chromium.targets(); expect(targets.some(target => target.type() === 'page' && target.url() === 'about:blank')).toBeTruthy('Missing blank page'); expect(targets.some(target => target.type() === 'browser')).toBeTruthy('Missing browser target'); @@ -38,8 +51,8 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME expect(allPages).toContain(page); expect(allPages[0]).not.toBe(allPages[1]); }); - it.skip(WEBKIT)('should contain browser target', async({browser}) => { - const targets = browser.targets(); + it('should contain browser target', async({browser}) => { + const targets = browser.chromium.targets(); const browserTarget = targets.find(target => target.type() === 'browser'); expect(browserTarget).toBeTruthy(); }); @@ -50,10 +63,9 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME expect(await originalPage.evaluate(() => ['Hello', 'world'].join(' '))).toBe('Hello world'); expect(await originalPage.$('body')).toBeTruthy(); }); - // FIXME(WebKit): need to enable agents in the inspected page to listen for updates. - it.skip(WEBKIT)('should report when a new page is created and closed', async({page, server, context}) => { + it('should report when a new page is created and closed', async({browser, page, server, context}) => { const [otherPage] = await Promise.all([ - context.waitForTarget(target => target.url() === server.CROSS_PROCESS_PREFIX + '/empty.html').then(target => target.page()), + browser.chromium.waitForTarget(target => target.url() === server.CROSS_PROCESS_PREFIX + '/empty.html').then(target => target.page()), page.evaluate(url => window.open(url), server.CROSS_PROCESS_PREFIX + '/empty.html'), ]); expect(otherPage.url()).toContain(server.CROSS_PROCESS_PREFIX); @@ -64,73 +76,73 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME expect(allPages).toContain(page); expect(allPages).toContain(otherPage); - const closePagePromise = new Promise(fulfill => context.once('targetdestroyed', target => fulfill(target.page()))); + const closePagePromise = new Promise(fulfill => browser.chromium.once('targetdestroyed', target => fulfill(target.page()))); await otherPage.close(); expect(await closePagePromise).toBe(otherPage); - allPages = await Promise.all(context.targets().map(target => target.page())); + allPages = await Promise.all(browser.chromium.targets().map(target => target.page())); expect(allPages).toContain(page); expect(allPages).not.toContain(otherPage); }); - it.skip(FFOX || WEBKIT)('should report when a service worker is created and destroyed', async({page, server, context}) => { + it('should report when a service worker is created and destroyed', async({browser, page, server, context}) => { await page.goto(server.EMPTY_PAGE); - const createdTarget = new Promise(fulfill => context.once('targetcreated', target => fulfill(target))); + const createdTarget = new Promise(fulfill => browser.chromium.once('targetcreated', target => fulfill(target))); await page.goto(server.PREFIX + '/serviceworkers/empty/sw.html'); expect((await createdTarget).type()).toBe('service_worker'); expect((await createdTarget).url()).toBe(server.PREFIX + '/serviceworkers/empty/sw.js'); - const destroyedTarget = new Promise(fulfill => context.once('targetdestroyed', target => fulfill(target))); + const destroyedTarget = new Promise(fulfill => browser.chromium.once('targetdestroyed', target => fulfill(target))); await page.evaluate(() => window.registrationPromise.then(registration => registration.unregister())); expect(await destroyedTarget).toBe(await createdTarget); }); - it.skip(FFOX || WEBKIT)('should create a worker from a service worker', async({browser, page, server, context}) => { + it('should create a worker from a service worker', async({browser, page, server, context}) => { await page.goto(server.PREFIX + '/serviceworkers/empty/sw.html'); - const target = await context.waitForTarget(target => target.type() === 'service_worker'); + const target = await browser.chromium.waitForTarget(target => target.type() === 'service_worker'); const worker = await browser.chromium.serviceWorker(target); expect(await worker.evaluate(() => self.toString())).toBe('[object ServiceWorkerGlobalScope]'); }); - it.skip(FFOX || WEBKIT)('should create a worker from a shared worker', async({browser, page, server, context}) => { + it('should create a worker from a shared worker', async({browser, page, server, context}) => { await page.goto(server.EMPTY_PAGE); await page.evaluate(() => { new SharedWorker('data:text/javascript,console.log("hi")'); }); - const target = await context.waitForTarget(target => target.type() === 'shared_worker'); + const target = await browser.chromium.waitForTarget(target => target.type() === 'shared_worker'); const worker = await browser.chromium.serviceWorker(target); expect(await worker.evaluate(() => self.toString())).toBe('[object SharedWorkerGlobalScope]'); }); - it.skip(WEBKIT)('should report when a target url changes', async({page, server, context}) => { + it('should report when a target url changes', async({browser, page, server, context}) => { await page.goto(server.EMPTY_PAGE); - let changedTarget = new Promise(fulfill => context.once('targetchanged', target => fulfill(target))); + let changedTarget = new Promise(fulfill => browser.chromium.once('targetchanged', target => fulfill(target))); await page.goto(server.CROSS_PROCESS_PREFIX + '/'); expect((await changedTarget).url()).toBe(server.CROSS_PROCESS_PREFIX + '/'); - changedTarget = new Promise(fulfill => context.once('targetchanged', target => fulfill(target))); + changedTarget = new Promise(fulfill => browser.chromium.once('targetchanged', target => fulfill(target))); await page.goto(server.EMPTY_PAGE); expect((await changedTarget).url()).toBe(server.EMPTY_PAGE); }); - it.skip(FFOX || WEBKIT)('should not report uninitialized pages', async({page, server, context}) => { + it('should not report uninitialized pages', async({browser, page, server, context}) => { let targetChanged = false; const listener = () => targetChanged = true; - context.on('targetchanged', listener); - const targetPromise = new Promise(fulfill => context.once('targetcreated', target => fulfill(target))); + browser.chromium.on('targetchanged', listener); + const targetPromise = new Promise(fulfill => browser.chromium.once('targetcreated', target => fulfill(target))); const newPagePromise = context.newPage(); const target = await targetPromise; expect(target.url()).toBe('about:blank'); const newPage = await newPagePromise; - const targetPromise2 = new Promise(fulfill => context.once('targetcreated', target => fulfill(target))); + const targetPromise2 = new Promise(fulfill => browser.chromium.once('targetcreated', target => fulfill(target))); const evaluatePromise = newPage.evaluate(() => window.open('about:blank')); const target2 = await targetPromise2; expect(target2.url()).toBe('about:blank'); await evaluatePromise; await newPage.close(); expect(targetChanged).toBe(false, 'target should not be reported as changed'); - context.removeListener('targetchanged', listener); + browser.chromium.removeListener('targetchanged', listener); }); - it.skip(WEBKIT)('should not crash while redirecting if original request was missed', async({page, server, context}) => { + it('should not crash while redirecting if original request was missed', async({browser, page, server, context}) => { let serverResponse = null; server.setRoute('/one-style.css', (req, res) => serverResponse = res); // Open a new page. Use window.open to connect to the page later. @@ -139,7 +151,7 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME server.waitForRequest('/one-style.css') ]); // Connect to the opened page. - const target = await context.waitForTarget(target => target.url().includes('one-style.html')); + const target = await browser.chromium.waitForTarget(target => target.url().includes('one-style.html')); const newPage = await target.page(); // Issue a redirect. serverResponse.writeHead(302, { location: '/injectedstyle.css' }); @@ -149,22 +161,40 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME // Cleanup. await newPage.close(); }); - it.skip(WEBKIT)('should have an opener', async({page, server, context}) => { + it('should have an opener', async({browser, page, server, context}) => { await page.goto(server.EMPTY_PAGE); const [createdTarget] = await Promise.all([ - new Promise(fulfill => context.once('targetcreated', target => fulfill(target))), + new Promise(fulfill => browser.chromium.once('targetcreated', target => fulfill(target))), page.goto(server.PREFIX + '/popup/window-open.html') ]); expect((await createdTarget.page()).url()).toBe(server.PREFIX + '/popup/popup.html'); - expect(createdTarget.opener()).toBe(page.target()); - expect(page.target().opener()).toBe(null); + expect(createdTarget.opener()).toBe(browser.chromium.pageTarget(page)); + expect(browser.chromium.pageTarget(page).opener()).toBe(null); }); }); - describe('Browser.waitForTarget', () => { + describe('Chromium.waitForTarget', () => { + it('should wait for a target', async function({browser, server}) { + const context = await browser.createIncognitoBrowserContext(); + let resolved = false; + const targetPromise = browser.chromium.waitForTarget(target => target.browserContext() === context && target.url() === server.EMPTY_PAGE); + targetPromise.then(() => resolved = true); + const page = await context.newPage(); + expect(resolved).toBe(false); + await page.goto(server.EMPTY_PAGE); + const target = await targetPromise; + expect(await target.page()).toBe(page); + await context.close(); + }); + it('should timeout waiting for a non-existent target', async function({browser, server}) { + const context = await browser.createIncognitoBrowserContext(); + const error = await browser.chromium.waitForTarget(target => target.browserContext() === context && target.url() === server.EMPTY_PAGE, {timeout: 1}).catch(e => e); + expect(error).toBeInstanceOf(playwright.errors.TimeoutError); + await context.close(); + }); it('should wait for a target', async function({browser, server}) { let resolved = false; - const targetPromise = browser.waitForTarget(target => target.url() === server.EMPTY_PAGE); + const targetPromise = browser.chromium.waitForTarget(target => target.url() === server.EMPTY_PAGE); targetPromise.then(() => resolved = true); const page = await browser.newPage(); expect(resolved).toBe(false); @@ -173,9 +203,9 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME expect(await target.page()).toBe(page); await page.close(); }); - it.skip(WEBKIT)('should timeout waiting for a non-existent target', async function({browser, server}) { + it('should timeout waiting for a non-existent target', async function({browser, server}) { let error = null; - await browser.waitForTarget(target => target.url() === server.EMPTY_PAGE, { + await browser.chromium.waitForTarget(target => target.url() === server.EMPTY_PAGE, { timeout: 1 }).catch(e => error = e); expect(error).toBeInstanceOf(playwright.errors.TimeoutError); diff --git a/src/chromium/features/chromium.ts b/src/chromium/features/chromium.ts index 326c386b3b..613552b3b4 100644 --- a/src/chromium/features/chromium.ts +++ b/src/chromium/features/chromium.ts @@ -14,31 +14,33 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { EventEmitter } from 'events'; import { assert } from '../../helper'; +import { Browser } from '../Browser'; +import { BrowserContext } from '../BrowserContext'; import { CDPSession, Connection } from '../Connection'; import { Page } from '../Page'; import { readProtocolStream } from '../protocolHelper'; import { Target } from '../Target'; import { Worker } from './workers'; -export class Chromium { +export class Chromium extends EventEmitter { private _connection: Connection; private _client: CDPSession; private _recording = false; private _path = ''; private _tracingClient: CDPSession | undefined; + private _browser: Browser; - constructor(connection: Connection, client: CDPSession) { - this._connection = connection; - this._client = client; + constructor(browser: Browser) { + super(); + this._connection = browser._connection; + this._client = browser._client; + this._browser = browser; } - createBrowserCDPSession(): Promise { - return this._connection.createBrowserSession(); - } - - createCDPSession(target: Target): Promise { - return target._sessionFactory(); + browserTarget(): Target { + return [...this._browser._targets.values()].find(t => t.type() === 'browser'); } serviceWorker(target: Target): Promise { @@ -84,6 +86,19 @@ export class Chromium { return contentPromise; } + targets(context?: BrowserContext): Target[] { + const targets = this._browser._allTargets(); + return context ? targets.filter(t => t.browserContext() === context) : targets; + } + + pageTarget(page: Page): Target { + return page._target; + } + + waitForTarget(predicate: (arg0: Target) => boolean, options: { timeout?: number; } | undefined = {}): Promise { + return this._browser._waitForTarget(predicate, options); + } + wsEndpoint(): string { return this._connection.url(); } diff --git a/src/chromium/features/geolocation.spec.js b/src/chromium/features/geolocation.spec.js index 7c2d0e8a40..242e691640 100644 --- a/src/chromium/features/geolocation.spec.js +++ b/src/chromium/features/geolocation.spec.js @@ -15,9 +15,6 @@ * limitations under the License. */ -const utils = require('../../../test/utils'); -const {waitEvent} = utils; - module.exports.addTests = function ({ testRunner, expect }) { const {describe, xdescribe, fdescribe} = testRunner; const {it, fit, xit} = testRunner; diff --git a/src/firefox/Browser.ts b/src/firefox/Browser.ts index a63f5092e8..852e604dbf 100644 --- a/src/firefox/Browser.ts +++ b/src/firefox/Browser.ts @@ -105,23 +105,21 @@ export class Browser extends EventEmitter { return this._process; } - async waitForTarget(predicate: (target: Target) => boolean, options: { timeout?: number; } = {}): Promise { + async _waitForTarget(predicate: (target: Target) => boolean, options: { timeout?: number; } = {}): Promise { const { timeout = 30000 } = options; - const existingTarget = this.targets().find(predicate); + const existingTarget = this._allTargets().find(predicate); if (existingTarget) return existingTarget; let resolve; const targetPromise = new Promise(x => resolve = x); - this.on(Events.Browser.TargetCreated, check); this.on('targetchanged', check); try { if (!timeout) return await targetPromise; return await helper.waitWithTimeout(targetPromise, 'target', timeout); } finally { - this.removeListener(Events.Browser.TargetCreated, check); this.removeListener('targetchanged', check); } @@ -148,7 +146,7 @@ export class Browser extends EventEmitter { return await Promise.all(pageTargets.map(target => target.page())); } - targets() { + _allTargets() { return Array.from(this._targets.values()); } @@ -163,23 +161,17 @@ export class Browser extends EventEmitter { openerPage.emit(Events.Page.Popup, popupPage); } } - this.emit(Events.Browser.TargetCreated, target); - context.emit(Events.BrowserContext.TargetCreated, target); } _onTargetDestroyed({targetId}) { const target = this._targets.get(targetId); this._targets.delete(targetId); target._closedCallback(); - this.emit(Events.Browser.TargetDestroyed, target); - target.browserContext().emit(Events.BrowserContext.TargetDestroyed, target); } _onTargetInfoChanged({targetId, url}) { const target = this._targets.get(targetId); target._url = url; - this.emit(Events.Browser.TargetChanged, target); - target.browserContext().emit(Events.BrowserContext.TargetChanged, target); } async close() { @@ -243,40 +235,32 @@ export class Target { } } -export class BrowserContext extends EventEmitter { +export class BrowserContext { _connection: Connection; _browser: Browser; _browserContextId: string; readonly permissions: Permissions; constructor(connection: Connection, browser: Browser, browserContextId: string | null) { - super(); this._connection = connection; this._browser = browser; this._browserContextId = browserContextId; this.permissions = new Permissions(connection, browserContextId); } - targets(): Array { - return this._browser.targets().filter(target => target.browserContext() === this); + _targets(): Array { + return this._browser._allTargets().filter(target => target.browserContext() === this); } - async pages(): Promise> { const pages = await Promise.all( - this.targets() + this._targets() .filter(target => target.type() === 'page') .map(target => target.page()) ); return pages.filter(page => !!page); } - - waitForTarget(predicate: (arg0: Target) => boolean, options: { timeout?: number; } | undefined): Promise { - return this._browser.waitForTarget(target => target.browserContext() === this && predicate(target), options); - } - - isIncognito(): boolean { return !!this._browserContextId; } diff --git a/src/firefox/Launcher.ts b/src/firefox/Launcher.ts index c89454f626..5017a4fcbd 100644 --- a/src/firefox/Launcher.ts +++ b/src/firefox/Launcher.ts @@ -165,7 +165,7 @@ export class Launcher { const browser = await Browser.create(connection, defaultViewport, firefoxProcess, gracefullyCloseFirefox); if (ignoreHTTPSErrors) await connection.send('Browser.setIgnoreHTTPSErrors', {enabled: true}); - await browser.waitForTarget(t => t.type() === 'page'); + await browser._waitForTarget(t => t.type() === 'page'); return browser; } catch (e) { killFirefox(); diff --git a/src/firefox/Page.ts b/src/firefox/Page.ts index 2d069fe7d3..f0de1717f3 100644 --- a/src/firefox/Page.ts +++ b/src/firefox/Page.ts @@ -291,10 +291,6 @@ export class Page extends EventEmitter { return this._target.browser(); } - target() { - return this._target; - } - url() { return this._frameManager.mainFrame().url(); } diff --git a/src/firefox/api.ts b/src/firefox/api.ts index d3ee2bc180..90d0f12cba 100644 --- a/src/firefox/api.ts +++ b/src/firefox/api.ts @@ -3,7 +3,7 @@ export { TimeoutError } from '../Errors'; export { Keyboard, Mouse } from '../input'; -export { Browser, BrowserContext, Target } from './Browser'; +export { Browser, BrowserContext } from './Browser'; export { BrowserFetcher } from './BrowserFetcher'; export { Dialog } from '../dialog'; export { ExecutionContext, JSHandle } from '../javascript'; diff --git a/src/firefox/events.ts b/src/firefox/events.ts index b9ea22b794..d39116fd65 100644 --- a/src/firefox/events.ts +++ b/src/firefox/events.ts @@ -37,15 +37,6 @@ export const Events = { }, Browser: { - TargetCreated: 'targetcreated', - TargetDestroyed: 'targetdestroyed', - TargetChanged: 'targetchanged', Disconnected: 'disconnected' }, - - BrowserContext: { - TargetCreated: 'targetcreated', - TargetDestroyed: 'targetdestroyed', - TargetChanged: 'targetchanged', - } }; diff --git a/src/webkit/Browser.ts b/src/webkit/Browser.ts index 4e40f86e08..6cc07a5012 100644 --- a/src/webkit/Browser.ts +++ b/src/webkit/Browser.ts @@ -36,17 +36,7 @@ export class Browser extends EventEmitter { private _contexts = new Map(); _targets = new Map(); private _eventListeners: RegisteredListener[]; - _waitForFirstTarget: Promise; - private _waitForFirstTargetCallback: () => void; - - static async create( - connection: Connection, - defaultViewport: Viewport | null, - process: childProcess.ChildProcess | null, - closeCallback?: (() => Promise)) { - const browser = new Browser(connection, defaultViewport, process, closeCallback); - return browser; - } + private _privateEvents = new EventEmitter(); constructor( connection: Connection, @@ -74,7 +64,6 @@ export class Browser extends EventEmitter { // Taking multiple screenshots in parallel doesn't work well, so we serialize them. this._screenshotTaskQueue = new TaskQueue(); - this._waitForFirstTarget = new Promise(f => this._waitForFirstTargetCallback = f); } async userAgent(): Promise { @@ -128,7 +117,7 @@ export class Browser extends EventEmitter { return Array.from(this._targets.values()); } - async waitForTarget(predicate: (arg0: Target) => boolean, options: { timeout?: number; } | undefined = {}): Promise { + async _waitForTarget(predicate: (arg0: Target) => boolean, options: { timeout?: number; } | undefined = {}): Promise { const { timeout = 30000 } = options; @@ -137,15 +126,13 @@ export class Browser extends EventEmitter { return existingTarget; let resolve; const targetPromise = new Promise(x => resolve = x); - this.on(Events.Browser.TargetCreated, check); - this.on(Events.Browser.TargetChanged, check); + this._privateEvents.on(BrowserEvents.TargetCreated, check); try { if (!timeout) return await targetPromise; return await helper.waitWithTimeout(targetPromise, 'target', timeout); } finally { - this.removeListener(Events.Browser.TargetCreated, check); - this.removeListener(Events.Browser.TargetChanged, check); + this._privateEvents.removeListener(BrowserEvents.TargetCreated, check); } function check(target: Target) { @@ -175,17 +162,13 @@ export class Browser extends EventEmitter { context = this._defaultContext; const target = new Target(targetInfo, context); this._targets.set(targetInfo.targetId, target); - this.emit(Events.Browser.TargetCreated, target); - context.emit(Events.BrowserContext.TargetCreated, target); - this._waitForFirstTargetCallback(); + this._privateEvents.emit(BrowserEvents.TargetCreated, target); } _onTargetDestroyed({targetId}) { const target = this._targets.get(targetId); this._targets.delete(targetId); target._closedCallback(); - this.emit(Events.Browser.TargetDestroyed, target); - target.browserContext().emit(Events.BrowserContext.TargetDestroyed, target); } async _onProvisionalTargetCommitted({oldTargetId, newTargetId}) { @@ -199,11 +182,6 @@ export class Browser extends EventEmitter { newTarget._pagePromise = oldTarget._pagePromise; } - _onTargetChanged(target: Target) { - this.emit(Events.BrowserContext.TargetChanged, target); - target.browserContext().emit(Events.BrowserContext.TargetChanged, target); - } - disconnect() { throw new Error('Unsupported operation'); } @@ -218,28 +196,22 @@ export class Browser extends EventEmitter { } } -export class BrowserContext extends EventEmitter { +export class BrowserContext { private _browser: Browser; _id: string; constructor(browser: Browser, contextId?: string) { - super(); this._browser = browser; this._id = contextId; } - targets(): Target[] { + _targets(): Target[] { return this._browser.targets().filter(target => target.browserContext() === this); } - waitForTarget(predicate: (arg0: Target) => boolean, options: { timeout?: number; } | undefined): Promise { - return this._browser.waitForTarget(target => target.browserContext() === this && predicate(target), options); - } - async pages(): Promise { - await this._browser._waitForFirstTarget; const pages = await Promise.all( - this.targets() + this._targets() .filter(target => target.type() === 'page') .map(target => target.page()) ); @@ -281,3 +253,8 @@ export class BrowserContext extends EventEmitter { await this._browser._connection.send('Browser.deleteAllCookies', { browserContextId: this._id }); } } + +const BrowserEvents = { + TargetCreated: Symbol('BrowserEvents.TargetCreated'), + TargetDestroyed: Symbol('BrowserEvents.TargetDestroyed'), +}; diff --git a/src/webkit/Launcher.ts b/src/webkit/Launcher.ts index 3b0b7516db..265b637181 100644 --- a/src/webkit/Launcher.ts +++ b/src/webkit/Launcher.ts @@ -127,6 +127,7 @@ export class Launcher { const transport = new PipeTransport(webkitProcess.stdio[3] as NodeJS.WritableStream, webkitProcess.stdio[4] as NodeJS.ReadableStream); connection = new Connection('', transport, slowMo); const browser = new Browser(connection, defaultViewport, webkitProcess, gracefullyCloseWebkit); + await browser._waitForTarget(t => t.type() === 'page'); return browser; } catch (e) { killWebKit(); diff --git a/src/webkit/Page.ts b/src/webkit/Page.ts index 73147d584c..2540d7122f 100644 --- a/src/webkit/Page.ts +++ b/src/webkit/Page.ts @@ -144,10 +144,6 @@ export class Page extends EventEmitter { await this._initialize().catch(e => debugError('failed to enable agents after swap: ' + e)); } - target(): Target { - return this._target; - } - browser(): Browser { return this._target.browser(); } @@ -255,11 +251,11 @@ export class Page extends EventEmitter { return await this._frameManager.mainFrame().content(); } - async setContent(html: string, options: { timeout?: number; waitUntil?: string | string[]; } | undefined) { + async setContent(html: string, options: { timeout?: number; waitUntil?: string | string[]; } = {}) { await this._frameManager.mainFrame().setContent(html, options); } - async goto(url: string, options: { referer?: string; timeout?: number; waitUntil?: string | string[]; } | undefined): Promise { + async goto(url: string, options: { referer?: string; timeout?: number; waitUntil?: string | string[]; } = {}): Promise { return await this._frameManager.mainFrame().goto(url, options); } @@ -494,11 +490,11 @@ export class Page extends EventEmitter { return this.mainFrame().select(selector, ...values); } - type(selector: string | types.Selector, text: string, options: { delay: (number | undefined); } | undefined) { + type(selector: string | types.Selector, text: string, options?: { delay: (number | undefined); }) { return this.mainFrame().type(selector, text, options); } - waitFor(selectorOrFunctionOrTimeout: (string | number | Function), options: { visible?: boolean; hidden?: boolean; timeout?: number; polling?: string | number; } = {}, ...args: any[]): Promise { + waitFor(selectorOrFunctionOrTimeout: (string | number | Function), options?: { visible?: boolean; hidden?: boolean; timeout?: number; polling?: string | number; }, ...args: any[]): Promise { return this.mainFrame().waitFor(selectorOrFunctionOrTimeout, options, ...args); } @@ -510,7 +506,7 @@ export class Page extends EventEmitter { return this.mainFrame().waitForXPath(xpath, options); } - waitForFunction(pageFunction: Function | string, options: types.WaitForFunctionOptions, ...args: any[]): Promise { + waitForFunction(pageFunction: Function | string, options?: types.WaitForFunctionOptions, ...args: any[]): Promise { return this.mainFrame().waitForFunction(pageFunction, options, ...args); } } diff --git a/src/webkit/Target.ts b/src/webkit/Target.ts index b494198342..15047d3d0d 100644 --- a/src/webkit/Target.ts +++ b/src/webkit/Target.ts @@ -42,25 +42,6 @@ export class Target { this._pagePromise = null; this._url = url; this._isClosedPromise = new Promise(fulfill => this._closedCallback = fulfill); - if (type === 'page') { - const session = this._browserContext.browser()._connection.session(this._targetId); - this._eventListeners = [ - // FIXME: we could use Page.frameStartedLoading if it had url info. - helper.addEventListener(session, 'Page.frameNavigated', this._onFrameNavigated.bind(this)), - ]; - } - } - - async _onFrameNavigated(params: Protocol.Page.frameNavigatedPayload) { - // Check if main frame, get url from the event. - // Skip child frames. - if (params.frame.parentId) - return; - const url = params.frame.url; - if (this._url !== url) { - this._url = url; - this.browser()._onTargetChanged(this); - } } async page(): Promise { diff --git a/src/webkit/api.ts b/src/webkit/api.ts index 7efebaffb0..5b0fd55bf2 100644 --- a/src/webkit/api.ts +++ b/src/webkit/api.ts @@ -11,6 +11,5 @@ export { Mouse, Keyboard } from '../input'; export { Request, Response } from '../network'; export { Page } from './Page'; export { Playwright } from './Playwright'; -export { Target } from './Target'; export { Dialog } from '../dialog'; export { ConsoleMessage } from '../console'; diff --git a/src/webkit/events.ts b/src/webkit/events.ts index de060389b0..325077002a 100644 --- a/src/webkit/events.ts +++ b/src/webkit/events.ts @@ -33,15 +33,6 @@ export const Events = { }, Browser: { - TargetCreated: 'targetcreated', - TargetDestroyed: 'targetdestroyed', - TargetChanged: 'targetchanged', Disconnected: 'disconnected' }, - - BrowserContext: { - TargetCreated: 'targetcreated', - TargetDestroyed: 'targetdestroyed', - TargetChanged: 'targetchanged', - }, }; diff --git a/test/CDPSession.spec.js b/test/CDPSession.spec.js index 2f333efc93..3ad52c9569 100644 --- a/test/CDPSession.spec.js +++ b/test/CDPSession.spec.js @@ -23,7 +23,7 @@ module.exports.addTests = function({testRunner, expect, FFOX, CHROME, WEBKIT}) { describe('Chromium.createCDPSession', function() { it('should work', async function({page, browser, server}) { - const client = await browser.chromium.createCDPSession(page.target()); + const client = await browser.chromium.pageTarget(page).createCDPSession(); await Promise.all([ client.send('Runtime.enable'), @@ -33,7 +33,7 @@ module.exports.addTests = function({testRunner, expect, FFOX, CHROME, WEBKIT}) { expect(foo).toBe('bar'); }); it('should send events', async function({page, browser, server}) { - const client = await browser.chromium.createCDPSession(page.target()); + const client = await browser.chromium.pageTarget(page).createCDPSession(); await client.send('Network.enable'); const events = []; client.on('Network.requestWillBeSent', event => events.push(event)); @@ -41,7 +41,7 @@ module.exports.addTests = function({testRunner, expect, FFOX, CHROME, WEBKIT}) { expect(events.length).toBe(1); }); it('should enable and disable domains independently', async function({page, browser, server}) { - const client = await browser.chromium.createCDPSession(page.target()); + const client = await browser.chromium.pageTarget(page).createCDPSession(); await client.send('Runtime.enable'); await client.send('Debugger.enable'); // JS coverage enables and then disables Debugger domain. @@ -56,7 +56,7 @@ module.exports.addTests = function({testRunner, expect, FFOX, CHROME, WEBKIT}) { expect(event.url).toBe('foo.js'); }); it('should be able to detach session', async function({page, browser, server}) { - const client = await browser.chromium.createCDPSession(page.target()); + const client = await browser.chromium.pageTarget(page).createCDPSession(); await client.send('Runtime.enable'); const evalResponse = await client.send('Runtime.evaluate', {expression: '1 + 2', returnByValue: true}); expect(evalResponse.result.value).toBe(3); @@ -70,7 +70,7 @@ module.exports.addTests = function({testRunner, expect, FFOX, CHROME, WEBKIT}) { expect(error.message).toContain('Session closed.'); }); it('should throw nice errors', async function({page, browser}) { - const client = await browser.chromium.createCDPSession(page.target()); + const client = await browser.chromium.pageTarget(page).createCDPSession(); const error = await theSourceOfTheProblems().catch(error => error); expect(error.stack).toContain('theSourceOfTheProblems'); expect(error.message).toContain('ThisCommand.DoesNotExist'); diff --git a/test/browsercontext.spec.js b/test/browsercontext.spec.js index 511156c315..a665c00ebb 100644 --- a/test/browsercontext.spec.js +++ b/test/browsercontext.spec.js @@ -56,7 +56,7 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME const page = await context.newPage(); await page.goto(server.EMPTY_PAGE); const [popupTarget] = await Promise.all([ - utils.waitEvent(browser, 'targetcreated'), + utils.waitEvent(page, 'popup'), page.evaluate(url => window.open(url), server.EMPTY_PAGE) ]); expect(popupTarget.browserContext()).toBe(context); @@ -65,9 +65,9 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME it.skip(WEBKIT)('should fire target events', async function({browser, server}) { const context = await browser.createIncognitoBrowserContext(); const events = []; - context.on('targetcreated', target => events.push('CREATED: ' + target.url())); - context.on('targetchanged', target => events.push('CHANGED: ' + target.url())); - context.on('targetdestroyed', target => events.push('DESTROYED: ' + target.url())); + browser.chromium.on('targetcreated', target => events.push('CREATED: ' + target.url())); + browser.chromium.on('targetchanged', target => events.push('CHANGED: ' + target.url())); + browser.chromium.on('targetdestroyed', target => events.push('DESTROYED: ' + target.url())); const page = await context.newPage(); await page.goto(server.EMPTY_PAGE); await page.close(); @@ -78,30 +78,12 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME ]); await context.close(); }); - it('should wait for a target', async function({browser, server}) { - const context = await browser.createIncognitoBrowserContext(); - let resolved = false; - const targetPromise = context.waitForTarget(target => target.url() === server.EMPTY_PAGE); - targetPromise.then(() => resolved = true); - const page = await context.newPage(); - expect(resolved).toBe(false); - await page.goto(server.EMPTY_PAGE); - const target = await targetPromise; - expect(await target.page()).toBe(page); - await context.close(); - }); - it('should timeout waiting for a non-existent target', async function({browser, server}) { - const context = await browser.createIncognitoBrowserContext(); - const error = await context.waitForTarget(target => target.url() === server.EMPTY_PAGE, {timeout: 1}).catch(e => e); - expect(error).toBeInstanceOf(playwright.errors.TimeoutError); - await context.close(); - }); it('should isolate localStorage and cookies', async function({browser, server}) { // Create two incognito contexts. const context1 = await browser.createIncognitoBrowserContext(); const context2 = await browser.createIncognitoBrowserContext(); - expect(context1.targets().length).toBe(0); - expect(context2.targets().length).toBe(0); + expect(browser.chromium.targets(context1).length).toBe(0); + expect(browser.chromium.targets(context2).length).toBe(0); // Create a page in first incognito context. const page1 = await context1.newPage(); @@ -111,8 +93,8 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME document.cookie = 'name=page1'; }); - expect(context1.targets().length).toBe(1); - expect(context2.targets().length).toBe(0); + expect(browser.chromium.targets(context1).length).toBe(1); + expect(browser.chromium.targets(context2).length).toBe(0); // Create a page in second incognito context. const page2 = await context2.newPage(); @@ -122,10 +104,10 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME document.cookie = 'name=page2'; }); - expect(context1.targets().length).toBe(1); - expect(context1.targets()[0]).toBe(page1.target()); - expect(context2.targets().length).toBe(1); - expect(context2.targets()[0]).toBe(page2.target()); + expect(browser.chromium.targets(context1).length).toBe(1); + expect(browser.chromium.targets(context1)[0]).toBe(browser.chromium.pageTarget(page1)); + expect(browser.chromium.targets(context2).length).toBe(1); + expect(browser.chromium.targets(context2)[0]).toBe(browser.chromium.pageTarget(page2)); // Make sure pages don't share localstorage or cookies. expect(await page1.evaluate(() => localStorage.getItem('name'))).toBe('page1'); @@ -140,17 +122,5 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME ]); expect(browser.browserContexts().length).toBe(1); }); - it.skip(WEBKIT)('should work across sessions', async function({browser, server}) { - expect(browser.browserContexts().length).toBe(1); - const context = await browser.createIncognitoBrowserContext(); - expect(browser.browserContexts().length).toBe(2); - const remoteBrowser = await playwright.connect({ - browserWSEndpoint: browser.chromium.wsEndpoint() - }); - const contexts = remoteBrowser.browserContexts(); - expect(contexts.length).toBe(2); - remoteBrowser.disconnect(); - await context.close(); - }); }); }; diff --git a/test/chromiumonly.spec.js b/test/chromiumonly.spec.js index a5e1141570..083d3e8193 100644 --- a/test/chromiumonly.spec.js +++ b/test/chromiumonly.spec.js @@ -14,6 +14,8 @@ * limitations under the License. */ +const utils = require('./utils'); + module.exports.addLauncherTests = function({testRunner, expect, defaultBrowserOptions, playwright}) { const {describe, xdescribe, fdescribe} = testRunner; const {it, fit, xit} = testRunner; @@ -94,6 +96,57 @@ module.exports.addLauncherTests = function({testRunner, expect, defaultBrowserOp }); }); }); + + describe('Browser target events', function() { + it('should work', async({server}) => { + const browser = await playwright.launch(defaultBrowserOptions); + const events = []; + browser.chromium.on('targetcreated', () => events.push('CREATED')); + browser.chromium.on('targetchanged', () => events.push('CHANGED')); + browser.chromium.on('targetdestroyed', () => events.push('DESTROYED')); + const page = await browser.newPage(); + await page.goto(server.EMPTY_PAGE); + await page.close(); + expect(events).toEqual(['CREATED', 'CHANGED', 'DESTROYED']); + await browser.close(); + }); + }); + + describe('Browser.Events.disconnected', function() { + it('should be emitted when: browser gets closed, disconnected or underlying websocket gets closed', async() => { + const originalBrowser = await playwright.launch(defaultBrowserOptions); + const browserWSEndpoint = originalBrowser.chromium.wsEndpoint(); + const remoteBrowser1 = await playwright.connect({browserWSEndpoint}); + const remoteBrowser2 = await playwright.connect({browserWSEndpoint}); + + let disconnectedOriginal = 0; + let disconnectedRemote1 = 0; + let disconnectedRemote2 = 0; + originalBrowser.on('disconnected', () => ++disconnectedOriginal); + remoteBrowser1.on('disconnected', () => ++disconnectedRemote1); + remoteBrowser2.on('disconnected', () => ++disconnectedRemote2); + + await Promise.all([ + utils.waitEvent(remoteBrowser2, 'disconnected'), + remoteBrowser2.disconnect(), + ]); + + expect(disconnectedOriginal).toBe(0); + expect(disconnectedRemote1).toBe(0); + expect(disconnectedRemote2).toBe(1); + + await Promise.all([ + utils.waitEvent(remoteBrowser1, 'disconnected'), + utils.waitEvent(originalBrowser, 'disconnected'), + originalBrowser.close(), + ]); + + expect(disconnectedOriginal).toBe(1); + expect(disconnectedRemote1).toBe(1); + expect(disconnectedRemote2).toBe(1); + }); + }); + }; module.exports.addPageTests = function({testRunner, expect}) { diff --git a/test/headful.spec.js b/test/headful.spec.js index d606fc8319..39d30b3948 100644 --- a/test/headful.spec.js +++ b/test/headful.spec.js @@ -50,14 +50,14 @@ module.exports.addTests = function({testRunner, expect, playwright, defaultBrows it('background_page target type should be available', async() => { const browserWithExtension = await playwright.launch(extensionOptions); const page = await browserWithExtension.newPage(); - const backgroundPageTarget = await browserWithExtension.waitForTarget(target => target.type() === 'background_page'); + const backgroundPageTarget = await browserWithExtension.chromium.waitForTarget(target => target.type() === 'background_page'); await page.close(); await browserWithExtension.close(); expect(backgroundPageTarget).toBeTruthy(); }); it('target.page() should return a background_page', async({}) => { const browserWithExtension = await playwright.launch(extensionOptions); - const backgroundPageTarget = await browserWithExtension.waitForTarget(target => target.type() === 'background_page'); + const backgroundPageTarget = await browserWithExtension.chromium.waitForTarget(target => target.type() === 'background_page'); const page = await backgroundPageTarget.page(); expect(await page.evaluate(() => 2 * 3)).toBe(6); expect(await page.evaluate(() => window.MAGIC)).toBe(42); @@ -123,7 +123,7 @@ module.exports.addTests = function({testRunner, expect, playwright, defaultBrows const context = await browser.createIncognitoBrowserContext(); await Promise.all([ context.newPage(), - context.waitForTarget(target => target.url().includes('devtools://')), + browser.chromium.waitForTarget(target => target.browserContext() === context && target.url().includes('devtools://')), ]); await browser.close(); }); diff --git a/test/launcher.spec.js b/test/launcher.spec.js index b8a3012a76..eee94198f2 100644 --- a/test/launcher.spec.js +++ b/test/launcher.spec.js @@ -344,7 +344,7 @@ module.exports.addTests = function({testRunner, expect, defaultBrowserOptions, p const browserOne = await playwright.launch(defaultBrowserOptions); const browserTwo = await playwright.connect({ ...defaultBrowserOptions, browserWSEndpoint: browserOne.chromium.wsEndpoint() }); const [page1, page2] = await Promise.all([ - new Promise(x => browserOne.once('targetcreated', target => x(target.page()))), + new Promise(x => browserOne.chromium.once('targetcreated', target => x(target.page()))), browserTwo.newPage(), ]); expect(await page1.evaluate(() => 7 * 8)).toBe(56); @@ -372,55 +372,4 @@ module.exports.addTests = function({testRunner, expect, defaultBrowserOptions, p expect(Devices['iPhone 6']).toBe(playwright.devices['iPhone 6']); }); }); - - describe.skip(WEBKIT)('Browser target events', function() { - it('should work', async({server}) => { - const browser = await playwright.launch(defaultBrowserOptions); - const events = []; - browser.on('targetcreated', () => events.push('CREATED')); - browser.on('targetchanged', () => events.push('CHANGED')); - browser.on('targetdestroyed', () => events.push('DESTROYED')); - const page = await browser.newPage(); - await page.goto(server.EMPTY_PAGE); - await page.close(); - expect(events).toEqual(['CREATED', 'CHANGED', 'DESTROYED']); - await browser.close(); - }); - }); - - describe.skip(WEBKIT || FFOX)('Browser.Events.disconnected', function() { - it('should be emitted when: browser gets closed, disconnected or underlying websocket gets closed', async() => { - const originalBrowser = await playwright.launch(defaultBrowserOptions); - const browserWSEndpoint = originalBrowser.chromium.wsEndpoint(); - const remoteBrowser1 = await playwright.connect({browserWSEndpoint}); - const remoteBrowser2 = await playwright.connect({browserWSEndpoint}); - - let disconnectedOriginal = 0; - let disconnectedRemote1 = 0; - let disconnectedRemote2 = 0; - originalBrowser.on('disconnected', () => ++disconnectedOriginal); - remoteBrowser1.on('disconnected', () => ++disconnectedRemote1); - remoteBrowser2.on('disconnected', () => ++disconnectedRemote2); - - await Promise.all([ - utils.waitEvent(remoteBrowser2, 'disconnected'), - remoteBrowser2.disconnect(), - ]); - - expect(disconnectedOriginal).toBe(0); - expect(disconnectedRemote1).toBe(0); - expect(disconnectedRemote2).toBe(1); - - await Promise.all([ - utils.waitEvent(remoteBrowser1, 'disconnected'), - utils.waitEvent(originalBrowser, 'disconnected'), - originalBrowser.close(), - ]); - - expect(disconnectedOriginal).toBe(1); - expect(disconnectedRemote1).toBe(1); - expect(disconnectedRemote2).toBe(1); - }); - }); - }; diff --git a/test/oopif.spec.js b/test/oopif.spec.js index c519fd7495..97ba5eff42 100644 --- a/test/oopif.spec.js +++ b/test/oopif.spec.js @@ -57,5 +57,5 @@ module.exports.addTests = function({testRunner, expect, defaultBrowserOptions, p * @param {!Playwright.BrowserContext} context */ function oopifs(context) { - return context.targets().filter(target => target._targetInfo.type === 'iframe'); + return context.browser().chromium.targets().filter(target => target._targetInfo.type === 'iframe'); } diff --git a/test/page.spec.js b/test/page.spec.js index 736794273a..a40747b2e0 100644 --- a/test/page.spec.js +++ b/test/page.spec.js @@ -289,9 +289,8 @@ module.exports.addTests = function({testRunner, expect, headless, playwright, FF // 3. After that, remove the iframe. frame.remove(); }); - const popupTarget = page.browserContext().targets().find(target => target !== page.target()); // 4. Connect to the popup and make sure it doesn't throw. - await popupTarget.page(); + await page.browserContext().pages(); }); }); @@ -1145,8 +1144,8 @@ module.exports.addTests = function({testRunner, expect, headless, playwright, FF // FIXME: WebKit shouldn't send targetDestroyed on PSON so that we could // convert target destroy events into close. describe('Page.Events.Close', function() { - it.skip(WEBKIT)('should work with window.close', async function({ page, context, server }) { - const newPagePromise = new Promise(fulfill => context.once('targetcreated', target => fulfill(target.page()))); + it.skip(WEBKIT)('should work with window.close', async function({ browser, page, context, server }) { + const newPagePromise = new Promise(fulfill => browser.chromium.once('targetcreated', target => fulfill(target.page()))); await page.evaluate(() => window['newPage'] = window.open('about:blank')); const newPage = await newPagePromise; const closedPromise = new Promise(x => newPage.on('close', x)); diff --git a/test/playwright.spec.js b/test/playwright.spec.js index 4fa68145f1..4ca4b0e5c2 100644 --- a/test/playwright.spec.js +++ b/test/playwright.spec.js @@ -153,7 +153,6 @@ module.exports.addTests = ({testRunner, product, playwrightPath}) => { // FIXME: screenshot tests will fail when running in parallel (maybe) require('./screenshot.spec.js').addTests(testOptions); require('./queryselector.spec.js').addTests(testOptions); - require('./target.spec.js').addTests(testOptions); require('./waittask.spec.js').addTests(testOptions); if (CHROME) { require('./CDPSession.spec.js').addTests(testOptions); @@ -162,6 +161,7 @@ module.exports.addTests = ({testRunner, product, playwrightPath}) => { require('./chromiumonly.spec.js').addPageTests(testOptions); require('../src/chromium/features/geolocation.spec.js').addTests(testOptions); require('../src/chromium/features/pdf.spec.js').addTests(testOptions); + require('../src/chromium/features/chromium.spec.js').addTests(testOptions); } }); diff --git a/utils/browser/test.js b/utils/browser/test.js index 757f8d6ff3..7e336807d8 100644 --- a/utils/browser/test.js +++ b/utils/browser/test.js @@ -67,7 +67,7 @@ describe('Playwright-Web', () => { }); it('should work over exposed DevTools protocol', async({browser, page, serverConfig}) => { // Expose devtools protocol binding into page. - const session = await browser.chromium.createBrowserCDPSession(); + const session = await browser.chromium.browserTarget().createCDPSession(); const pageInfo = (await session.send('Target.getTargets')).targetInfos.find(info => info.attached); await session.send('Target.exposeDevToolsProtocol', {targetId: pageInfo.targetId}); await session.detach();