diff --git a/src/chromium/crCoverage.ts b/src/chromium/crCoverage.ts index ba0ecf61a1..53e8d74b6d 100644 --- a/src/chromium/crCoverage.ts +++ b/src/chromium/crCoverage.ts @@ -21,30 +21,6 @@ import { Protocol } from './protocol'; import * as types from '../types'; import * as sourceMap from '../utils/sourceMap'; -type JSRange = { - startOffset: number, - endOffset: number, - count: number -} - -type CSSCoverageEntry = { - url: string, - text?: string, - ranges: { - start: number, - end: number - }[] -}; - -type JSCoverageEntry = { - url: string, - source?: string, - functions: { - functionName: string, - ranges: JSRange[] - }[] -}; - export class CRCoverage { private _jsCoverage: JSCoverage; private _cssCoverage: CSSCoverage; @@ -58,7 +34,7 @@ export class CRCoverage { return await this._jsCoverage.start(options); } - async stopJSCoverage(): Promise { + async stopJSCoverage(): Promise { return await this._jsCoverage.stop(); } @@ -66,7 +42,7 @@ export class CRCoverage { return await this._cssCoverage.start(options); } - async stopCSSCoverage(): Promise { + async stopCSSCoverage(): Promise { return await this._cssCoverage.stop(); } } @@ -134,7 +110,7 @@ class JSCoverage { this._scriptSources.set(event.scriptId, response.scriptSource); } - async stop(): Promise { + async stop(): Promise { assert(this._enabled, 'JSCoverage is not enabled'); this._enabled = false; const [profileResponse] = await Promise.all([ @@ -145,7 +121,7 @@ class JSCoverage { ] as const); helper.removeEventListeners(this._eventListeners); - const coverage: JSCoverageEntry[] = []; + const coverage: types.JSCoverageEntry[] = []; for (const entry of profileResponse.result) { if (!this._scriptIds.has(entry.scriptId)) continue; @@ -216,7 +192,7 @@ class CSSCoverage { } } - async stop(): Promise { + async stop(): Promise { assert(this._enabled, 'CSSCoverage is not enabled'); this._enabled = false; const ruleTrackingResponse = await this._client.send('CSS.stopRuleUsageTracking'); @@ -241,7 +217,7 @@ class CSSCoverage { }); } - const coverage: CSSCoverageEntry[] = []; + const coverage: types.CSSCoverageEntry[] = []; for (const styleSheetId of this._stylesheetURLs.keys()) { const url = this._stylesheetURLs.get(styleSheetId)!; const text = this._stylesheetSources.get(styleSheetId)!; diff --git a/src/rpc/channels.ts b/src/rpc/channels.ts index 96fbb82c4c..8899a2d533 100644 --- a/src/rpc/channels.ts +++ b/src/rpc/channels.ts @@ -64,6 +64,8 @@ export interface BrowserChannel extends Channel { newContext(params: types.BrowserContextOptions): Promise; crNewBrowserCDPSession(): Promise; + crStartTracing(params: { page?: PageChannel, path?: string, screenshots?: boolean, categories?: string[] }): Promise; + crStopTracing(): Promise; } export type BrowserInitializer = {}; @@ -154,9 +156,13 @@ export interface PageChannel extends Channel { mouseUp(params: { button?: types.MouseButton, clickCount?: number }): Promise; mouseClick(params: { x: number, y: number, delay?: number, button?: types.MouseButton, clickCount?: number }): Promise; - // A11Y accessibilitySnapshot(params: { interestingOnly?: boolean, root?: ElementHandleChannel }): Promise; pdf: (params: types.PDFOptions) => Promise; + + crStartJSCoverage(params: types.JSCoverageOptions): Promise; + crStopJSCoverage(): Promise; + crStartCSSCoverage(params: types.CSSCoverageOptions): Promise; + crStopCSSCoverage(): Promise; } export type PageInitializer = { diff --git a/src/rpc/client/browser.ts b/src/rpc/client/browser.ts index f6211105d0..e0ff406646 100644 --- a/src/rpc/client/browser.ts +++ b/src/rpc/client/browser.ts @@ -27,7 +27,7 @@ export class Browser extends ChannelOwner { readonly _contexts = new Set(); private _isConnected = true; private _isClosedOrClosing = false; - + private _closedPromise: Promise; static from(browser: BrowserChannel): Browser { return (browser as any)._object; @@ -45,6 +45,7 @@ export class Browser extends ChannelOwner { this._isClosedOrClosing = true; this._scope.dispose(); }); + this._closedPromise = new Promise(f => this.once(Events.Browser.Disconnected, f)); } async newContext(options: types.BrowserContextOptions = {}): Promise { @@ -73,13 +74,22 @@ export class Browser extends ChannelOwner { } async close(): Promise { - if (this._isClosedOrClosing) - return; - this._isClosedOrClosing = true; - await this._channel.close(); + if (!this._isClosedOrClosing) { + this._isClosedOrClosing = true; + await this._channel.close(); + } + await this._closedPromise; } async newBrowserCDPSession(): Promise { return CDPSession.from(await this._channel.crNewBrowserCDPSession()); } + + async startTracing(page?: Page, options: { path?: string; screenshots?: boolean; categories?: string[]; } = {}) { + await this._channel.crStartTracing({ ...options, page: page ? page._channel : undefined }); + } + + async stopTracing(): Promise { + return Buffer.from(await this._channel.crStopTracing(), 'base64'); + } } diff --git a/src/rpc/client/browserContext.ts b/src/rpc/client/browserContext.ts index ff45633fc0..13c3adfe1f 100644 --- a/src/rpc/client/browserContext.ts +++ b/src/rpc/client/browserContext.ts @@ -40,6 +40,8 @@ export class BrowserContext extends ChannelOwner void, string>(); _timeoutSettings = new TimeoutSettings(); _ownerPage: Page | undefined; + private _isClosedOrClosing = false; + private _closedPromise: Promise; static from(context: BrowserContextChannel): BrowserContext { return (context as any)._object; @@ -60,6 +62,7 @@ export class BrowserContext extends ChannelOwner this._onClose()); this._channel.on('page', page => this._onPage(Page.from(page))); this._channel.on('route', ({ route, request }) => this._onRoute(network.Route.from(route), network.Request.from(request))); + this._closedPromise = new Promise(f => this.once(Events.BrowserContext.Close, f)); initializer.crBackgroundPages.forEach(p => { const page = Page.from(p); @@ -211,6 +214,7 @@ export class BrowserContext extends ChannelOwner { - await this._channel.close(); + if (!this._isClosedOrClosing) { + this._isClosedOrClosing = true; + await this._channel.close(); + } + await this._closedPromise; } async newCDPSession(page: Page): Promise { diff --git a/src/rpc/client/coverage.ts b/src/rpc/client/coverage.ts new file mode 100644 index 0000000000..393acf45d2 --- /dev/null +++ b/src/rpc/client/coverage.ts @@ -0,0 +1,42 @@ +/** + * 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 * as types from '../../types'; +import { PageChannel } from '../channels'; + +export class Coverage { + private _channel: PageChannel; + + constructor(channel: PageChannel) { + this._channel = channel; + } + + async startJSCoverage(options: types.JSCoverageOptions = {}) { + await this._channel.crStartJSCoverage(options); + } + + async stopJSCoverage(): Promise { + return await this._channel.crStopJSCoverage(); + } + + async startCSSCoverage(options: types.CSSCoverageOptions = {}) { + await this._channel.crStartCSSCoverage(options); + } + + async stopCSSCoverage(): Promise { + return await this._channel.crStopCSSCoverage(); + } +} diff --git a/src/rpc/client/page.ts b/src/rpc/client/page.ts index eb3331ba4d..a1ec0def94 100644 --- a/src/rpc/client/page.ts +++ b/src/rpc/client/page.ts @@ -38,6 +38,7 @@ import { Func1, FuncOn, SmartHandle } from './jsHandle'; import { Request, Response, Route, RouteHandler } from './network'; import { FileChooser } from './fileChooser'; import { Buffer } from 'buffer'; +import { Coverage } from './coverage'; export class Page extends ChannelOwner { @@ -54,6 +55,7 @@ export class Page extends ChannelOwner { readonly accessibility: Accessibility; readonly keyboard: Keyboard; readonly mouse: Mouse; + readonly coverage: Coverage; readonly _bindings = new Map(); private _pendingWaitForEvents = new Map<(error: Error) => void, string>(); private _timeoutSettings = new TimeoutSettings(); @@ -72,6 +74,7 @@ export class Page extends ChannelOwner { this.accessibility = new Accessibility(this._channel); this.keyboard = new Keyboard(this._channel); this.mouse = new Mouse(this._channel); + this.coverage = new Coverage(this._channel); this._mainFrame = Frame.from(initializer.mainFrame); this._mainFrame._page = this; diff --git a/src/rpc/server/browserDispatcher.ts b/src/rpc/server/browserDispatcher.ts index b8f1177eff..24b6b8b7d8 100644 --- a/src/rpc/server/browserDispatcher.ts +++ b/src/rpc/server/browserDispatcher.ts @@ -18,11 +18,12 @@ import { Browser, BrowserBase } from '../../browser'; import { BrowserContextBase } from '../../browserContext'; import { Events } from '../../events'; import * as types from '../../types'; -import { BrowserChannel, BrowserContextChannel, BrowserInitializer, CDPSessionChannel } from '../channels'; +import { BrowserChannel, BrowserContextChannel, BrowserInitializer, CDPSessionChannel, Binary } from '../channels'; import { BrowserContextDispatcher } from './browserContextDispatcher'; import { CDPSessionDispatcher } from './cdpSessionDispatcher'; import { Dispatcher, DispatcherScope } from './dispatcher'; import { CRBrowser } from '../../chromium/crBrowser'; +import { PageDispatcher } from './pageDispatcher'; export class BrowserDispatcher extends Dispatcher implements BrowserChannel { constructor(scope: DispatcherScope, browser: BrowserBase) { @@ -45,4 +46,15 @@ export class BrowserDispatcher extends Dispatcher i const crBrowser = this._object as CRBrowser; return new CDPSessionDispatcher(this._scope, await crBrowser.newBrowserCDPSession()); } + + async crStartTracing(params: { page?: PageDispatcher, path?: string, screenshots?: boolean, categories?: string[] }): Promise { + const crBrowser = this._object as CRBrowser; + await crBrowser.startTracing(params.page ? params.page._object : undefined, params); + } + + async crStopTracing(): Promise { + const crBrowser = this._object as CRBrowser; + const buffer = await crBrowser.stopTracing(); + return buffer.toString('base64'); + } } diff --git a/src/rpc/server/pageDispatcher.ts b/src/rpc/server/pageDispatcher.ts index d51f1e1516..256f53fab2 100644 --- a/src/rpc/server/pageDispatcher.ts +++ b/src/rpc/server/pageDispatcher.ts @@ -31,6 +31,7 @@ import { RequestDispatcher, ResponseDispatcher, RouteDispatcher } from './networ import { serializeResult, parseArgument } from './jsHandleDispatcher'; import { ElementHandleDispatcher, createHandle } from './elementHandlerDispatcher'; import { FileChooser } from '../../fileChooser'; +import { CRCoverage } from '../../chromium/crCoverage'; export class PageDispatcher extends Dispatcher implements PageChannel { private _page: Page; @@ -125,7 +126,7 @@ export class PageDispatcher extends Dispatcher implements }); } - async screenshot(params: types.ScreenshotOptions): Promise { + async screenshot(params: types.ScreenshotOptions): Promise { return (await this._page.screenshot(params)).toString('base64'); } @@ -190,6 +191,26 @@ export class PageDispatcher extends Dispatcher implements return binary.toString('base64'); } + async crStartJSCoverage(params: types.JSCoverageOptions): Promise { + const coverage = this._page.coverage as CRCoverage; + await coverage.startJSCoverage(params); + } + + async crStopJSCoverage(): Promise { + const coverage = this._page.coverage as CRCoverage; + return await coverage.stopJSCoverage(); + } + + async crStartCSSCoverage(params: types.CSSCoverageOptions): Promise { + const coverage = this._page.coverage as CRCoverage; + await coverage.startCSSCoverage(params); + } + + async crStopCSSCoverage(): Promise { + const coverage = this._page.coverage as CRCoverage; + return await coverage.stopCSSCoverage(); + } + _onFrameAttached(frame: Frame) { this._dispatchEvent('frameAttached', FrameDispatcher.from(this._scope, frame)); } diff --git a/src/types.ts b/src/types.ts index 03813af2b3..0645f8ab72 100644 --- a/src/types.ts +++ b/src/types.ts @@ -123,12 +123,6 @@ export type PDFOptions = { path?: string, } -export type CoverageEntry = { - url: string, - text: string, - ranges: {start: number, end: number}[] -}; - export type CSSCoverageOptions = { resetOnNavigation?: boolean, }; @@ -138,6 +132,30 @@ export type JSCoverageOptions = { reportAnonymousScripts?: boolean, }; +export type JSRange = { + startOffset: number, + endOffset: number, + count: number +}; + +export type CSSCoverageEntry = { + url: string, + text?: string, + ranges: { + start: number, + end: number + }[] +}; + +export type JSCoverageEntry = { + url: string, + source?: string, + functions: { + functionName: string, + ranges: JSRange[] + }[] +}; + export type InjectedScriptProgress = { aborted: boolean, log: (message: string) => void, diff --git a/test/browsercontext.spec.js b/test/browsercontext.spec.js index 5d8625f837..b8b648f7bd 100644 --- a/test/browsercontext.spec.js +++ b/test/browsercontext.spec.js @@ -121,7 +121,7 @@ describe('BrowserContext', function() { let error = await promise; expect(error.message).toContain('Context closed'); }); - it.fail(CHANNEL)('close() should be callable twice', async({browser}) => { + it('close() should be callable twice', async({browser}) => { const context = await browser.newContext(); await Promise.all([ context.close(), diff --git a/test/chromium/coverage.spec.js b/test/chromium/coverage.spec.js index 004986525e..52a21facfc 100644 --- a/test/chromium/coverage.spec.js +++ b/test/chromium/coverage.spec.js @@ -16,7 +16,7 @@ const {FFOX, CHROMIUM, WEBKIT, CHANNEL} = require('../utils').testOptions(browserType); -describe.skip(CHANNEL)('JSCoverage', function() { +describe('JSCoverage', function() { it('should work', async function({page, server}) { await page.coverage.startJSCoverage(); await page.goto(server.PREFIX + '/jscoverage/simple.html', { waitUntil: 'load' }); @@ -88,7 +88,7 @@ describe.skip(CHANNEL)('JSCoverage', function() { }); }); -describe.skip(CHANNEL)('CSSCoverage', function() { +describe('CSSCoverage', function() { it('should work', async function({page, server}) { await page.coverage.startCSSCoverage(); await page.goto(server.PREFIX + '/csscoverage/simple.html'); diff --git a/test/chromium/session.spec.js b/test/chromium/session.spec.js index bda12a04ea..c7562e6b73 100644 --- a/test/chromium/session.spec.js +++ b/test/chromium/session.spec.js @@ -35,7 +35,7 @@ describe('ChromiumBrowserContext.createSession', function() { await page.goto(server.EMPTY_PAGE); expect(events.length).toBe(1); }); - it.skip(CHANNEL)('should enable and disable domains independently', async function({page, browser, server}) { + it('should enable and disable domains independently', async function({page, browser, server}) { const client = await page.context().newCDPSession(page); await client.send('Runtime.enable'); await client.send('Debugger.enable'); diff --git a/test/chromium/tracing.spec.js b/test/chromium/tracing.spec.js index 2aa14047b7..0000734b0f 100644 --- a/test/chromium/tracing.spec.js +++ b/test/chromium/tracing.spec.js @@ -18,7 +18,7 @@ const fs = require('fs'); const path = require('path'); const {FFOX, CHROMIUM, WEBKIT, OUTPUT_DIR, CHANNEL} = require('../utils').testOptions(browserType); -describe.skip(CHANNEL)('Chromium.startTracing', function() { +describe('Chromium.startTracing', function() { beforeEach(async function(state) { state.outputFile = path.join(OUTPUT_DIR, `trace-${state.parallelIndex}.json`); state.browser = await state.browserType.launch(state.defaultBrowserOptions); diff --git a/test/downloadsPath.spec.js b/test/downloadsPath.spec.js index cd47ceb82c..c071871055 100644 --- a/test/downloadsPath.spec.js +++ b/test/downloadsPath.spec.js @@ -23,7 +23,7 @@ const removeFolder = require('rimraf'); const mkdtempAsync = util.promisify(fs.mkdtemp); const removeFolderAsync = util.promisify(removeFolder); -const {FFOX, CHROMIUM, WEBKIT} = utils.testOptions(browserType); +const {FFOX, CHROMIUM, WEBKIT, CHANNEL} = utils.testOptions(browserType); describe('browserType.launch({downloadsPath})', function() { beforeEach(async(state) => { @@ -116,7 +116,6 @@ describe('browserType.launchPersistent({acceptDownloads})', function() { expect(download.suggestedFilename()).toBe(`file.txt`); const path = await download.path(); expect(path.startsWith(downloadsPath)).toBeTruthy(); - await context.close(); }); it('should not delete downloads when the context closes', async({page, context}) => { diff --git a/test/page.spec.js b/test/page.spec.js index 5a929d2452..26def08106 100644 --- a/test/page.spec.js +++ b/test/page.spec.js @@ -154,7 +154,7 @@ describe.fail(FFOX && WIN)('Page.Events.Crash', function() { const error = await promise; expect(error.message).toContain('Navigation failed because page crashed'); }); - it.fail(CHANNEL)('should be able to close context when page crashes', async({page}) => { + it('should be able to close context when page crashes', async({page}) => { await page.setContent(`
This page should crash
`); crash(page); await page.waitForEvent('crash');