Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 4 commits into from
Mar 19, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have origin on Worker? Determining who owns a service worker via its script url is unfortunate.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't have it at the moment. We could expose similar Page.serviceWorkers(), or add ServiceWorker class that would allow to get a list of its clients.


### class: ChromiumCoverage

Coverage gathers information about parts of JavaScript and CSS that were used by the page.
Expand Down
68 changes: 35 additions & 33 deletions src/chromium/crBrowser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 }),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this temporary before you land your chromium patch?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it's permanent, we don't need targetCreated/Destroyed notifications anymore, just attachedToTarget/detachedFromTarget.

];
const existingPageAttachPromises: Promise<any>[] = [];
if (isPersistent) {
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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(() => {
Expand All @@ -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;
Expand All @@ -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();
}

Expand Down Expand Up @@ -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();
Expand Down
57 changes: 32 additions & 25 deletions src/chromium/crTarget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand All @@ -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];
Expand All @@ -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);
}
}

Expand All @@ -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' {
Expand Down
17 changes: 17 additions & 0 deletions test/chromium/chromium.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down