api(chromium): add ChromiumBrowserContext.serviceWorkers() (#1416)

This commit is contained in:
Yury Semikhatsky 2020-03-19 16:10:24 -07:00 committed by GitHub
parent c6696746dc
commit 3ed9970b33
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 88 additions and 58 deletions

View file

@ -3810,6 +3810,7 @@ const backgroundPage = await backroundPageTarget.page();
- [event: 'serviceworker'](#event-serviceworker) - [event: 'serviceworker'](#event-serviceworker)
- [chromiumBrowserContext.backgroundPages()](#chromiumbrowsercontextbackgroundpages) - [chromiumBrowserContext.backgroundPages()](#chromiumbrowsercontextbackgroundpages)
- [chromiumBrowserContext.newCDPSession(page)](#chromiumbrowsercontextnewcdpsessionpage) - [chromiumBrowserContext.newCDPSession(page)](#chromiumbrowsercontextnewcdpsessionpage)
- [chromiumBrowserContext.serviceWorkers()](#chromiumbrowsercontextserviceworkers)
<!-- GEN:stop --> <!-- GEN:stop -->
<!-- GEN:toc-extends-BrowserContext --> <!-- GEN:toc-extends-BrowserContext -->
- [event: 'close'](#event-close) - [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. - `page` <[Page]> Page to create new session for.
- returns: <[Promise]<[CDPSession]>> Promise that resolves to the newly created session. - 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 ### class: ChromiumCoverage
Coverage gathers information about parts of JavaScript and CSS that were used by the page. Coverage gathers information about parts of JavaScript and CSS that were used by the page.

View file

@ -20,7 +20,7 @@ import { assertBrowserContextIsNotOwned, BrowserContext, BrowserContextBase, Bro
import { Events as CommonEvents } from '../events'; import { Events as CommonEvents } from '../events';
import { assert, debugError, helper } from '../helper'; import { assert, debugError, helper } from '../helper';
import * as network from '../network'; import * as network from '../network';
import { Page, PageBinding, PageEvent } from '../page'; import { Page, PageBinding, PageEvent, Worker } from '../page';
import * as platform from '../platform'; import * as platform from '../platform';
import { ConnectionTransport, SlowMoTransport } from '../transport'; import { ConnectionTransport, SlowMoTransport } from '../transport';
import * as types from '../types'; import * as types from '../types';
@ -53,6 +53,7 @@ export class CRBrowser extends platform.EventEmitter implements Browser {
const promises = [ const promises = [
session.send('Target.setDiscoverTargets', { discover: true }), session.send('Target.setDiscoverTargets', { discover: true }),
session.send('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: true, flatten: true }), session.send('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: true, flatten: true }),
session.send('Target.setDiscoverTargets', { discover: false }),
]; ];
const existingPageAttachPromises: Promise<any>[] = []; const existingPageAttachPromises: Promise<any>[] = [];
if (isPersistent) { if (isPersistent) {
@ -82,9 +83,8 @@ export class CRBrowser extends platform.EventEmitter implements Browser {
context._browserClosed(); context._browserClosed();
this.emit(CommonEvents.Browser.Disconnected); 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.attachedToTarget', this._onAttachedToTarget.bind(this));
this._session.on('Target.detachedFromTarget', this._onDetachedFromTarget.bind(this));
this._firstPagePromise = new Promise(f => this._firstPageCallback = f); 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) { _onAttachedToTarget({targetInfo, sessionId, waitingForDebugger}: Protocol.Target.attachedToTargetPayload) {
const session = this._connection.session(sessionId)!; const session = this._connection.session(sessionId)!;
if (!CRTarget.isPageType(targetInfo.type)) { if (!CRTarget.isPageType(targetInfo.type) && targetInfo.type !== 'service_worker') {
assert(targetInfo.type === 'service_worker' || targetInfo.type === 'browser' || targetInfo.type === 'other'); assert(targetInfo.type === 'browser' || targetInfo.type === 'other');
if (waitingForDebugger) { if (waitingForDebugger) {
// Ideally, detaching should resume any target, but there is a bug in the backend. // Ideally, detaching should resume any target, but there is a bug in the backend.
session.send('Runtime.runIfWaitingForDebugger').catch(debugError).then(() => { 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); 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; return;
const pageEvent = new PageEvent(context, target.pageOrError()); }
target.pageOrError().then(async () => { assert(targetInfo.type === 'service_worker');
if (targetInfo.type === 'page') { target.serviceWorkerOrError().then(workerOrError => {
this._firstPageCallback(); if (workerOrError instanceof Worker)
context.emit(CommonEvents.BrowserContext.Page, pageEvent); context.emit(Events.CRBrowserContext.ServiceWorker, workerOrError);
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);
}
}); });
} }
async _targetCreated({targetInfo}: Protocol.Target.targetCreatedPayload) { private _createTarget(targetInfo: Protocol.Target.TargetInfo, session: CRSession) {
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) {
const {browserContextId} = targetInfo; const {browserContextId} = targetInfo;
const context = (browserContextId && this._contexts.has(browserContextId)) ? this._contexts.get(browserContextId)! : this._defaultContext; const context = (browserContextId && this._contexts.has(browserContextId)) ? this._contexts.get(browserContextId)! : this._defaultContext;
let hasInitialAboutBlank = false; let hasInitialAboutBlank = false;
@ -169,17 +167,17 @@ export class CRBrowser extends platform.EventEmitter implements Browser {
hasInitialAboutBlank = true; 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'); assert(!this._targets.has(targetInfo.targetId), 'Target should not exist before targetCreated');
this._targets.set(targetInfo.targetId, target); this._targets.set(targetInfo.targetId, target);
return { context, target }; return { context, target };
} }
async _targetDestroyed(event: { targetId: string; }) { _onDetachedFromTarget({targetId}: Protocol.Target.detachFromTargetParameters) {
const target = this._targets.get(event.targetId)!; const target = this._targets.get(targetId!)!;
if (!target) if (!target)
return; return;
this._targets.delete(event.targetId); this._targets.delete(targetId!);
target._didClose(); 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[]; 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<CRSession> { async newCDPSession(page: Page): Promise<CRSession> {
const targetId = CRTarget.fromPage(page)._targetId; const targetId = CRTarget.fromPage(page)._targetId;
const rootSession = await this._browser._clientRootSession(); const rootSession = await this._browser._clientRootSession();

View file

@ -15,13 +15,13 @@
* limitations under the License. * limitations under the License.
*/ */
import { assert, helper } from '../helper';
import { Page, Worker } from '../page';
import { CRBrowser, CRBrowserContext } from './crBrowser'; import { CRBrowser, CRBrowserContext } from './crBrowser';
import { CRSession, CRSessionEvents } from './crConnection'; 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 { CRExecutionContext } from './crExecutionContext';
import { CRPage } from './crPage';
import { Protocol } from './protocol';
const targetSymbol = Symbol('target'); const targetSymbol = Symbol('target');
@ -30,11 +30,11 @@ export class CRTarget {
private readonly _browser: CRBrowser; private readonly _browser: CRBrowser;
private readonly _browserContext: CRBrowserContext; private readonly _browserContext: CRBrowserContext;
readonly _targetId: string; readonly _targetId: string;
readonly sessionFactory: () => Promise<CRSession>;
private readonly _pagePromise: Promise<Page | Error> | null = null; private readonly _pagePromise: Promise<Page | Error> | null = null;
readonly _crPage: CRPage | null = null; readonly _crPage: CRPage | null = null;
_initializedPage: Page | null = null; _initializedPage: Page | null = null;
private _workerPromise: Promise<Worker> | null = null; private readonly _workerPromise: Promise<Worker | Error> | null = null;
_initializedWorker: Worker | null = null;
static fromPage(page: Page): CRTarget { static fromPage(page: Page): CRTarget {
return (page as any)[targetSymbol]; return (page as any)[targetSymbol];
@ -48,22 +48,23 @@ export class CRTarget {
browser: CRBrowser, browser: CRBrowser,
targetInfo: Protocol.Target.TargetInfo, targetInfo: Protocol.Target.TargetInfo,
browserContext: CRBrowserContext, browserContext: CRBrowserContext,
session: CRSession | null, session: CRSession,
sessionFactory: () => Promise<CRSession>,
hasInitialAboutBlank: boolean) { hasInitialAboutBlank: boolean) {
this._targetInfo = targetInfo; this._targetInfo = targetInfo;
this._browser = browser; this._browser = browser;
this._browserContext = browserContext; this._browserContext = browserContext;
this._targetId = targetInfo.targetId; this._targetId = targetInfo.targetId;
this.sessionFactory = sessionFactory;
if (CRTarget.isPageType(targetInfo.type)) { if (CRTarget.isPageType(targetInfo.type)) {
assert(session, 'Page target must be created with existing session');
this._crPage = new CRPage(session, this._browser, this._browserContext); this._crPage = new CRPage(session, this._browser, this._browserContext);
helper.addEventListener(session, 'Page.windowOpen', event => browser._onWindowOpen(targetInfo.targetId, event)); helper.addEventListener(session, 'Page.windowOpen', event => browser._onWindowOpen(targetInfo.targetId, event));
const page = this._crPage.page(); const page = this._crPage.page();
(page as any)[targetSymbol] = this; (page as any)[targetSymbol] = this;
session.once(CRSessionEvents.Disconnected, () => page._didDisconnect()); session.once(CRSessionEvents.Disconnected, () => page._didDisconnect());
this._pagePromise = this._crPage.initialize(hasInitialAboutBlank).then(() => this._initializedPage = page).catch(e => e); 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.'); throw new Error('Not a page.');
} }
async serviceWorker(): Promise<Worker | null> { private async _initializeServiceWorker(session: CRSession): Promise<Worker | Error> {
if (this._targetInfo.type !== 'service_worker') const worker = new Worker(this._targetInfo.url);
return null; session.once('Runtime.executionContextCreated', event => {
if (!this._workerPromise) { worker._createExecutionContext(new CRExecutionContext(session, event.context));
// TODO(einbinder): Make workers send their console logs. });
this._workerPromise = this.sessionFactory().then(session => { try {
const worker = new Worker(this._targetInfo.url); // This might fail if the target is closed before we receive all execution contexts.
session.once('Runtime.executionContextCreated', async event => { await Promise.all([
worker._createExecutionContext(new CRExecutionContext(session, event.context)); session.send('Runtime.enable', {}),
}); session.send('Runtime.runIfWaitingForDebugger'),
// This might fail if the target is closed before we receive all execution contexts. ]);
session.send('Runtime.enable', {}).catch(debugError); this._initializedWorker = worker;
return worker; return worker;
}); } catch (error) {
return error;
} }
return this._workerPromise; }
serviceWorkerOrError(): Promise<Worker | Error> {
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' { type(): 'page' | 'background_page' | 'service_worker' | 'shared_worker' | 'other' | 'browser' {

View file

@ -32,6 +32,23 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI
]); ]);
expect(await worker.evaluate(() => self.toString())).toBe('[object ServiceWorkerGlobalScope]'); 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}) => { it('should not create a worker from a shared worker', async({browser, page, server, context}) => {
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
let serviceWorkerCreated; let serviceWorkerCreated;