diff --git a/docs/api.md b/docs/api.md index ab67f7b5c8..6fd657bf24 100644 --- a/docs/api.md +++ b/docs/api.md @@ -3810,6 +3810,7 @@ const backgroundPage = await backroundPageTarget.page(); - [event: 'serviceworker'](#event-serviceworker) - [chromiumBrowserContext.backgroundPages()](#chromiumbrowsercontextbackgroundpages) - [chromiumBrowserContext.newCDPSession(page)](#chromiumbrowsercontextnewcdpsessionpage) +- [chromiumBrowserContext.serviceWorkers()](#chromiumbrowsercontextserviceworkers) - [event: 'close'](#event-close) @@ -3853,6 +3854,9 @@ Emitted when new service worker is created in the context. - `page` <[Page]> Page to create new session for. - returns: <[Promise]<[CDPSession]>> Promise that resolves to the newly created session. +#### chromiumBrowserContext.serviceWorkers() +- returns: <[Array]<[Worker]>> All existing service workers in the context. + ### class: ChromiumCoverage Coverage gathers information about parts of JavaScript and CSS that were used by the page. diff --git a/src/chromium/crBrowser.ts b/src/chromium/crBrowser.ts index 2574fd9ce3..61d6f43d91 100644 --- a/src/chromium/crBrowser.ts +++ b/src/chromium/crBrowser.ts @@ -20,7 +20,7 @@ import { assertBrowserContextIsNotOwned, BrowserContext, BrowserContextBase, Bro import { Events as CommonEvents } from '../events'; import { assert, debugError, helper } from '../helper'; import * as network from '../network'; -import { Page, PageBinding, PageEvent } from '../page'; +import { Page, PageBinding, PageEvent, Worker } from '../page'; import * as platform from '../platform'; import { ConnectionTransport, SlowMoTransport } from '../transport'; import * as types from '../types'; @@ -53,6 +53,7 @@ export class CRBrowser extends platform.EventEmitter implements Browser { const promises = [ session.send('Target.setDiscoverTargets', { discover: true }), session.send('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: true, flatten: true }), + session.send('Target.setDiscoverTargets', { discover: false }), ]; const existingPageAttachPromises: Promise[] = []; if (isPersistent) { @@ -82,9 +83,8 @@ export class CRBrowser extends platform.EventEmitter implements Browser { context._browserClosed(); this.emit(CommonEvents.Browser.Disconnected); }); - this._session.on('Target.targetCreated', this._targetCreated.bind(this)); - this._session.on('Target.targetDestroyed', this._targetDestroyed.bind(this)); this._session.on('Target.attachedToTarget', this._onAttachedToTarget.bind(this)); + this._session.on('Target.detachedFromTarget', this._onDetachedFromTarget.bind(this)); this._firstPagePromise = new Promise(f => this._firstPageCallback = f); } @@ -116,8 +116,8 @@ export class CRBrowser extends platform.EventEmitter implements Browser { _onAttachedToTarget({targetInfo, sessionId, waitingForDebugger}: Protocol.Target.attachedToTargetPayload) { const session = this._connection.session(sessionId)!; - if (!CRTarget.isPageType(targetInfo.type)) { - assert(targetInfo.type === 'service_worker' || targetInfo.type === 'browser' || targetInfo.type === 'other'); + if (!CRTarget.isPageType(targetInfo.type) && targetInfo.type !== 'service_worker') { + assert(targetInfo.type === 'browser' || targetInfo.type === 'other'); if (waitingForDebugger) { // Ideally, detaching should resume any target, but there is a bug in the backend. session.send('Runtime.runIfWaitingForDebugger').catch(debugError).then(() => { @@ -128,34 +128,32 @@ export class CRBrowser extends platform.EventEmitter implements Browser { } const { context, target } = this._createTarget(targetInfo, session); - if (!CRTarget.isPageType(targetInfo.type)) + if (CRTarget.isPageType(targetInfo.type)) { + const pageEvent = new PageEvent(context, target.pageOrError()); + target.pageOrError().then(async () => { + if (targetInfo.type === 'page') { + this._firstPageCallback(); + context.emit(CommonEvents.BrowserContext.Page, pageEvent); + const opener = target.opener(); + if (!opener) + return; + const openerPage = await opener.pageOrError(); + if (openerPage instanceof Page && !openerPage.isClosed()) + openerPage.emit(CommonEvents.Page.Popup, pageEvent); + } else if (targetInfo.type === 'background_page') { + context.emit(Events.CRBrowserContext.BackgroundPage, pageEvent); + } + }); return; - const pageEvent = new PageEvent(context, target.pageOrError()); - target.pageOrError().then(async () => { - if (targetInfo.type === 'page') { - this._firstPageCallback(); - context.emit(CommonEvents.BrowserContext.Page, pageEvent); - const opener = target.opener(); - if (!opener) - return; - const openerPage = await opener.pageOrError(); - if (openerPage instanceof Page && !openerPage.isClosed()) - openerPage.emit(CommonEvents.Page.Popup, pageEvent); - } else if (targetInfo.type === 'background_page') { - context.emit(Events.CRBrowserContext.BackgroundPage, pageEvent); - } + } + assert(targetInfo.type === 'service_worker'); + target.serviceWorkerOrError().then(workerOrError => { + if (workerOrError instanceof Worker) + context.emit(Events.CRBrowserContext.ServiceWorker, workerOrError); }); } - async _targetCreated({targetInfo}: Protocol.Target.targetCreatedPayload) { - if (targetInfo.type !== 'service_worker') - return; - const { context, target } = this._createTarget(targetInfo, null); - const serviceWorker = await target.serviceWorker(); - context.emit(Events.CRBrowserContext.ServiceWorker, serviceWorker); - } - - private _createTarget(targetInfo: Protocol.Target.TargetInfo, session: CRSession | null) { + private _createTarget(targetInfo: Protocol.Target.TargetInfo, session: CRSession) { const {browserContextId} = targetInfo; const context = (browserContextId && this._contexts.has(browserContextId)) ? this._contexts.get(browserContextId)! : this._defaultContext; let hasInitialAboutBlank = false; @@ -169,17 +167,17 @@ export class CRBrowser extends platform.EventEmitter implements Browser { hasInitialAboutBlank = true; } } - const target = new CRTarget(this, targetInfo, context, session, () => this._connection.createSession(targetInfo), hasInitialAboutBlank); + const target = new CRTarget(this, targetInfo, context, session, hasInitialAboutBlank); assert(!this._targets.has(targetInfo.targetId), 'Target should not exist before targetCreated'); this._targets.set(targetInfo.targetId, target); return { context, target }; } - async _targetDestroyed(event: { targetId: string; }) { - const target = this._targets.get(event.targetId)!; + _onDetachedFromTarget({targetId}: Protocol.Target.detachFromTargetParameters) { + const target = this._targets.get(targetId!)!; if (!target) return; - this._targets.delete(event.targetId); + this._targets.delete(targetId!); target._didClose(); } @@ -417,6 +415,10 @@ export class CRBrowserContext extends BrowserContextBase { return this._targets().filter(target => target.type() === 'background_page').map(target => target._initializedPage).filter(pageOrNull => !!pageOrNull) as Page[]; } + serviceWorkers(): Worker[] { + return this._targets().filter(target => target.type() === 'service_worker').map(target => target._initializedWorker).filter(workerOrNull => !!workerOrNull) as any as Worker[]; + } + async newCDPSession(page: Page): Promise { const targetId = CRTarget.fromPage(page)._targetId; const rootSession = await this._browser._clientRootSession(); diff --git a/src/chromium/crTarget.ts b/src/chromium/crTarget.ts index fac521c1a5..19183b890c 100644 --- a/src/chromium/crTarget.ts +++ b/src/chromium/crTarget.ts @@ -15,13 +15,13 @@ * limitations under the License. */ +import { assert, helper } from '../helper'; +import { Page, Worker } from '../page'; import { CRBrowser, CRBrowserContext } from './crBrowser'; import { CRSession, CRSessionEvents } from './crConnection'; -import { Page, Worker } from '../page'; -import { Protocol } from './protocol'; -import { debugError, assert, helper } from '../helper'; -import { CRPage } from './crPage'; import { CRExecutionContext } from './crExecutionContext'; +import { CRPage } from './crPage'; +import { Protocol } from './protocol'; const targetSymbol = Symbol('target'); @@ -30,11 +30,11 @@ export class CRTarget { private readonly _browser: CRBrowser; private readonly _browserContext: CRBrowserContext; readonly _targetId: string; - readonly sessionFactory: () => Promise; private readonly _pagePromise: Promise | null = null; readonly _crPage: CRPage | null = null; _initializedPage: Page | null = null; - private _workerPromise: Promise | null = null; + private readonly _workerPromise: Promise | null = null; + _initializedWorker: Worker | null = null; static fromPage(page: Page): CRTarget { return (page as any)[targetSymbol]; @@ -48,22 +48,23 @@ export class CRTarget { browser: CRBrowser, targetInfo: Protocol.Target.TargetInfo, browserContext: CRBrowserContext, - session: CRSession | null, - sessionFactory: () => Promise, + session: CRSession, hasInitialAboutBlank: boolean) { this._targetInfo = targetInfo; this._browser = browser; this._browserContext = browserContext; this._targetId = targetInfo.targetId; - this.sessionFactory = sessionFactory; if (CRTarget.isPageType(targetInfo.type)) { - assert(session, 'Page target must be created with existing session'); this._crPage = new CRPage(session, this._browser, this._browserContext); helper.addEventListener(session, 'Page.windowOpen', event => browser._onWindowOpen(targetInfo.targetId, event)); const page = this._crPage.page(); (page as any)[targetSymbol] = this; session.once(CRSessionEvents.Disconnected, () => page._didDisconnect()); this._pagePromise = this._crPage.initialize(hasInitialAboutBlank).then(() => this._initializedPage = page).catch(e => e); + } else if (targetInfo.type === 'service_worker') { + this._workerPromise = this._initializeServiceWorker(session); + } else { + assert(false, 'Unsupported target type: ' + targetInfo.type); } } @@ -78,22 +79,28 @@ export class CRTarget { throw new Error('Not a page.'); } - async serviceWorker(): Promise { - if (this._targetInfo.type !== 'service_worker') - return null; - if (!this._workerPromise) { - // TODO(einbinder): Make workers send their console logs. - this._workerPromise = this.sessionFactory().then(session => { - const worker = new Worker(this._targetInfo.url); - session.once('Runtime.executionContextCreated', async event => { - worker._createExecutionContext(new CRExecutionContext(session, event.context)); - }); - // This might fail if the target is closed before we receive all execution contexts. - session.send('Runtime.enable', {}).catch(debugError); - return worker; - }); + private async _initializeServiceWorker(session: CRSession): Promise { + const worker = new Worker(this._targetInfo.url); + session.once('Runtime.executionContextCreated', event => { + worker._createExecutionContext(new CRExecutionContext(session, event.context)); + }); + try { + // This might fail if the target is closed before we receive all execution contexts. + await Promise.all([ + session.send('Runtime.enable', {}), + session.send('Runtime.runIfWaitingForDebugger'), + ]); + this._initializedWorker = worker; + return worker; + } catch (error) { + return error; } - return this._workerPromise; + } + + serviceWorkerOrError(): Promise { + if (this.type() === 'service_worker') + return this._workerPromise!; + throw new Error('Not a service worker.'); } type(): 'page' | 'background_page' | 'service_worker' | 'shared_worker' | 'other' | 'browser' { diff --git a/test/chromium/chromium.spec.js b/test/chromium/chromium.spec.js index 1f215792f8..f8940ff889 100644 --- a/test/chromium/chromium.spec.js +++ b/test/chromium/chromium.spec.js @@ -32,6 +32,23 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI ]); expect(await worker.evaluate(() => self.toString())).toBe('[object ServiceWorkerGlobalScope]'); }); + it('serviceWorkers() should return current workers', async({browser, page, server, context}) => { + const [worker1] = await Promise.all([ + context.waitForEvent('serviceworker'), + page.goto(server.PREFIX + '/serviceworkers/empty/sw.html') + ]); + let workers = context.serviceWorkers(); + expect(workers.length).toBe(1); + + const [worker2] = await Promise.all([ + context.waitForEvent('serviceworker'), + page.goto(server.CROSS_PROCESS_PREFIX + '/serviceworkers/empty/sw.html') + ]); + workers = context.serviceWorkers(); + expect(workers.length).toBe(2); + expect(workers).toContain(worker1); + expect(workers).toContain(worker2); + }); it('should not create a worker from a shared worker', async({browser, page, server, context}) => { await page.goto(server.EMPTY_PAGE); let serviceWorkerCreated;