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)
- [chromiumBrowserContext.backgroundPages()](#chromiumbrowsercontextbackgroundpages)
- [chromiumBrowserContext.newCDPSession(page)](#chromiumbrowsercontextnewcdpsessionpage)
- [chromiumBrowserContext.serviceWorkers()](#chromiumbrowsercontextserviceworkers)
<!-- GEN:stop -->
<!-- GEN:toc-extends-BrowserContext -->
- [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.

View file

@ -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<any>[] = [];
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<CRSession> {
const targetId = CRTarget.fromPage(page)._targetId;
const rootSession = await this._browser._clientRootSession();

View file

@ -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<CRSession>;
private readonly _pagePromise: Promise<Page | Error> | null = null;
readonly _crPage: CRPage | 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 {
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<CRSession>,
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<Worker | null> {
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<Worker | Error> {
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<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' {

View file

@ -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;