chore: refactor impl-side events to be per-class (#3569)

This commit is contained in:
Dmitry Gozman 2020-08-21 16:26:33 -07:00 committed by GitHub
parent d4dac04212
commit 57e8617474
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 165 additions and 244 deletions

View file

@ -19,7 +19,6 @@ import { BrowserContext } from './browserContext';
import { Page } from './page';
import { EventEmitter } from 'events';
import { Download } from './download';
import { Events } from './events';
import { ProxySettings } from './types';
import { ChildProcess } from 'child_process';
@ -40,6 +39,10 @@ export type BrowserOptions = types.UIOptions & {
};
export abstract class Browser extends EventEmitter {
static Events = {
Disconnected: 'disconnected',
};
readonly _options: BrowserOptions;
private _downloads = new Map<string, Download>();
_defaultContext: BrowserContext | null = null;
@ -87,7 +90,7 @@ export abstract class Browser extends EventEmitter {
context._browserClosed();
if (this._defaultContext)
this._defaultContext._browserClosed();
this.emit(Events.Browser.Disconnected);
this.emit(Browser.Events.Disconnected);
}
async close() {
@ -96,7 +99,7 @@ export abstract class Browser extends EventEmitter {
await this._options.browserProcess.close();
}
if (this.isConnected())
await new Promise(x => this.once(Events.Browser.Disconnected, x));
await new Promise(x => this.once(Browser.Events.Disconnected, x));
}
}

View file

@ -23,7 +23,6 @@ import { Page, PageBinding } from './page';
import { TimeoutSettings } from './timeoutSettings';
import * as frames from './frames';
import * as types from './types';
import { Events } from './events';
import { Download } from './download';
import { Browser } from './browser';
import { EventEmitter } from 'events';
@ -40,6 +39,13 @@ export class Screencast {
}
export abstract class BrowserContext extends EventEmitter {
static Events = {
Close: 'close',
Page: 'page',
ScreencastStarted: 'screencaststarted',
ScreencastStopped: 'screencaststopped',
};
readonly _timeoutSettings = new TimeoutSettings();
readonly _pageBindings = new Map<string, PageBinding>();
readonly _options: types.BrowserContextOptions;
@ -81,7 +87,7 @@ export abstract class BrowserContext extends EventEmitter {
this._closedStatus = 'closed';
this._downloads.clear();
this._closePromiseFulfill!(new Error('Context closed'));
this.emit(Events.BrowserContext.Close);
this.emit(BrowserContext.Events.Close);
}
// BrowserContext methods.
@ -160,7 +166,7 @@ export abstract class BrowserContext extends EventEmitter {
async _loadDefaultContext(progress: Progress) {
if (!this.pages().length) {
const waitForEvent = helper.waitForEvent(progress, this, Events.BrowserContext.Page);
const waitForEvent = helper.waitForEvent(progress, this, BrowserContext.Events.Page);
progress.cleanupWhenAborted(() => waitForEvent.dispose);
await waitForEvent.promise;
}

View file

@ -17,7 +17,6 @@
import { Browser, BrowserOptions } from '../browser';
import { assertBrowserContextIsNotOwned, BrowserContext, validateBrowserContextOptions, verifyGeolocation } from '../browserContext';
import { Events as CommonEvents } from '../events';
import { assert } from '../helper';
import * as network from '../network';
import { Page, PageBinding, Worker } from '../page';
@ -26,7 +25,6 @@ import * as types from '../types';
import { ConnectionEvents, CRConnection, CRSession } from './crConnection';
import { CRPage } from './crPage';
import { readProtocolStream } from './crProtocolHelper';
import { Events } from './events';
import { Protocol } from './protocol';
import { CRExecutionContext } from './crExecutionContext';
import { CRDevTools } from './crDevTools';
@ -151,7 +149,7 @@ export class CRBrowser extends Browser {
const backgroundPage = new CRPage(session, targetInfo.targetId, context, null, false);
this._backgroundPages.set(targetInfo.targetId, backgroundPage);
backgroundPage.pageOrError().then(() => {
context!.emit(Events.ChromiumBrowserContext.BackgroundPage, backgroundPage._page);
context!.emit(CRBrowserContext.CREvents.BackgroundPage, backgroundPage._page);
});
return;
}
@ -164,11 +162,11 @@ export class CRBrowser extends Browser {
const page = crPage._page;
if (pageOrError instanceof Error)
page._setIsError();
context!.emit(CommonEvents.BrowserContext.Page, page);
context!.emit(BrowserContext.Events.Page, page);
if (opener) {
opener.pageOrError().then(openerPage => {
if (openerPage instanceof Page && !openerPage.isClosed())
openerPage.emit(CommonEvents.Page.Popup, page);
openerPage.emit(Page.Events.Popup, page);
});
}
});
@ -178,7 +176,7 @@ export class CRBrowser extends Browser {
if (targetInfo.type === 'service_worker') {
const serviceWorker = new CRServiceWorker(context, session, targetInfo.url);
this._serviceWorkers.set(targetInfo.targetId, serviceWorker);
context.emit(Events.ChromiumBrowserContext.ServiceWorker, serviceWorker);
context.emit(CRBrowserContext.CREvents.ServiceWorker, serviceWorker);
return;
}
@ -202,7 +200,7 @@ export class CRBrowser extends Browser {
const serviceWorker = this._serviceWorkers.get(targetId);
if (serviceWorker) {
this._serviceWorkers.delete(targetId);
serviceWorker.emit(CommonEvents.Worker.Close);
serviceWorker.emit(Worker.Events.Close);
return;
}
}
@ -280,6 +278,11 @@ class CRServiceWorker extends Worker {
}
export class CRBrowserContext extends BrowserContext {
static CREvents = {
BackgroundPage: 'backgroundpage',
ServiceWorker: 'serviceworker',
};
readonly _browser: CRBrowser;
readonly _browserContextId: string | null;
readonly _evaluateOnNewDocumentSources: string[];
@ -432,7 +435,7 @@ export class CRBrowserContext extends BrowserContext {
// asynchronously and we get detached from them later.
// To avoid the wrong order of notifications, we manually fire
// "close" event here and forget about the serivce worker.
serviceWorker.emit(CommonEvents.Worker.Close);
serviceWorker.emit(Worker.Events.Close);
this._browser._serviceWorkers.delete(targetId);
}
}

View file

@ -24,7 +24,6 @@ import { CRExecutionContext } from './crExecutionContext';
import { CRNetworkManager } from './crNetworkManager';
import { Page, Worker, PageBinding } from '../page';
import { Protocol } from './protocol';
import { Events } from '../events';
import { toConsoleMessageLocation, exceptionToError, releaseObject } from './crProtocolHelper';
import * as dialog from '../dialog';
import { PageDelegate } from '../page';
@ -590,7 +589,7 @@ class FrameSession {
const args = event.args.map(o => worker._existingExecutionContext!.createHandle(o));
this._page._addConsoleMessage(event.type, args, toConsoleMessageLocation(event.stackTrace));
});
session.on('Runtime.exceptionThrown', exception => this._page.emit(Events.Page.PageError, exceptionToError(exception.exceptionDetails)));
session.on('Runtime.exceptionThrown', exception => this._page.emit(Page.Events.PageError, exceptionToError(exception.exceptionDetails)));
// TODO: attribute workers to the right frame.
this._networkManager.instrumentNetworkEvents(session, this._page._frameManager.frame(this._targetId)!);
}
@ -664,7 +663,7 @@ class FrameSession {
}
_onDialog(event: Protocol.Page.javascriptDialogOpeningPayload) {
this._page.emit(Events.Page.Dialog, new dialog.Dialog(
this._page.emit(Page.Events.Dialog, new dialog.Dialog(
event.type,
event.message,
async (accept: boolean, promptText?: string) => {
@ -674,7 +673,7 @@ class FrameSession {
}
_handleException(exceptionDetails: Protocol.Runtime.ExceptionDetails) {
this._page.emit(Events.Page.PageError, exceptionToError(exceptionDetails));
this._page.emit(Page.Events.PageError, exceptionToError(exceptionDetails));
}
async _onTargetCrashed() {
@ -692,7 +691,7 @@ class FrameSession {
lineNumber: lineNumber || 0,
columnNumber: 0,
};
this._page.emit(Events.Page.Console, new ConsoleMessage(level, text, [], location));
this._page.emit(Page.Events.Console, new ConsoleMessage(level, text, [], location));
}
}

View file

@ -1,23 +0,0 @@
/**
* Copyright 2019 Google Inc. All rights reserved.
* Modifications 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.
*/
export const Events = {
ChromiumBrowserContext: {
BackgroundPage: 'backgroundpage',
ServiceWorker: 'serviceworker',
}
};

View file

@ -15,7 +15,6 @@
*/
import { BrowserContext } from '../browserContext';
import { Events } from '../events';
import * as frames from '../frames';
import * as js from '../javascript';
import { Page } from '../page';
@ -23,10 +22,10 @@ import DebugScript from './injected/debugScript';
export class DebugController {
constructor(context: BrowserContext) {
context.on(Events.BrowserContext.Page, (page: Page) => {
context.on(BrowserContext.Events.Page, (page: Page) => {
for (const frame of page.frames())
this.ensureInstalledInFrame(frame);
page.on(Events.Page.FrameNavigated, frame => this.ensureInstalledInFrame(frame));
page.on(Page.Events.FrameNavigated, frame => this.ensureInstalledInFrame(frame));
});
}

View file

@ -18,7 +18,6 @@ import * as path from 'path';
import * as fs from 'fs';
import * as util from 'util';
import { Page } from './page';
import { Events } from './events';
import { Readable } from 'stream';
import { assert, mkdirIfNeeded } from './helper';
@ -47,13 +46,13 @@ export class Download {
page._browserContext._downloads.add(this);
this._acceptDownloads = !!this._page._browserContext._options.acceptDownloads;
if (suggestedFilename !== undefined)
this._page.emit(Events.Page.Download, this);
this._page.emit(Page.Events.Download, this);
}
_filenameSuggested(suggestedFilename: string) {
assert(this._suggestedFilename === undefined);
this._suggestedFilename = suggestedFilename;
this._page.emit(Events.Page.Download, this);
this._page.emit(Page.Events.Download, this);
}
url(): string {

View file

@ -1,60 +0,0 @@
/**
* Copyright 2019 Google Inc. All rights reserved.
* Modifications 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.
*/
export const Events = {
Browser: {
Disconnected: 'disconnected'
},
BrowserContext: {
Close: 'close',
Page: 'page',
ScreencastStarted: 'screencaststarted',
ScreencastStopped: 'screencaststopped',
},
BrowserServer: {
Close: 'close',
},
Page: {
Close: 'close',
Crash: 'crash',
Console: 'console',
Dialog: 'dialog',
Download: 'download',
FileChooser: 'filechooser',
DOMContentLoaded: 'domcontentloaded',
// Can't use just 'error' due to node.js special treatment of error events.
// @see https://nodejs.org/api/events.html#events_error_events
PageError: 'pageerror',
Request: 'request',
Response: 'response',
RequestFailed: 'requestfailed',
RequestFinished: 'requestfinished',
FrameAttached: 'frameattached',
FrameDetached: 'framedetached',
FrameNavigated: 'framenavigated',
Load: 'load',
Popup: 'popup',
Worker: 'worker',
},
Worker: {
Close: 'close',
},
};

View file

@ -17,7 +17,6 @@
import { Browser, BrowserOptions } from '../browser';
import { assertBrowserContextIsNotOwned, BrowserContext, validateBrowserContextOptions, verifyGeolocation } from '../browserContext';
import { Events } from '../events';
import { assert, helper, RegisteredListener } from '../helper';
import * as network from '../network';
import { Page, PageBinding } from '../page';
@ -129,12 +128,12 @@ export class FFBrowser extends Browser {
const page = ffPage._page;
if (pageOrError instanceof Error)
page._setIsError();
context.emit(Events.BrowserContext.Page, page);
context.emit(BrowserContext.Events.Page, page);
if (!opener)
return;
const openerPage = await opener.pageOrError();
if (openerPage instanceof Page && !openerPage.isClosed())
openerPage.emit(Events.Page.Popup, page);
openerPage.emit(Page.Events.Popup, page);
});
}

View file

@ -17,7 +17,6 @@
import * as dialog from '../dialog';
import * as dom from '../dom';
import { Events } from '../events';
import * as frames from '../frames';
import { assert, helper, RegisteredListener } from '../helper';
import { Page, PageBinding, PageDelegate, Worker } from '../page';
@ -32,7 +31,7 @@ import { FFNetworkManager } from './ffNetworkManager';
import { Protocol } from './protocol';
import { selectors } from '../selectors';
import { rewriteErrorMessage } from '../utils/stackTrace';
import { Screencast } from '../browserContext';
import { Screencast, BrowserContext } from '../browserContext';
const UTILITY_WORLD_NAME = '__playwright_utility_world__';
@ -62,7 +61,7 @@ export class FFPage implements PageDelegate {
this._browserContext = browserContext;
this._page = new Page(this, browserContext);
this._networkManager = new FFNetworkManager(session, this._page);
this._page.on(Events.Page.FrameDetached, frame => this._removeContextsForFrame(frame));
this._page.on(Page.Events.FrameDetached, frame => this._removeContextsForFrame(frame));
// TODO: remove Page.willOpenNewWindowAsynchronously from the protocol.
this._eventListeners = [
helper.addEventListener(this._session, 'Page.eventFired', this._onEventFired.bind(this)),
@ -179,7 +178,7 @@ export class FFPage implements PageDelegate {
const message = params.message.startsWith('Error: ') ? params.message.substring(7) : params.message;
const error = new Error(message);
error.stack = params.stack;
this._page.emit(Events.Page.PageError, error);
this._page.emit(Page.Events.PageError, error);
}
_onConsole(payload: Protocol.Runtime.consolePayload) {
@ -189,7 +188,7 @@ export class FFPage implements PageDelegate {
}
_onDialogOpened(params: Protocol.Page.dialogOpenedPayload) {
this._page.emit(Events.Page.Dialog, new dialog.Dialog(
this._page.emit(Page.Events.Dialog, new dialog.Dialog(
params.type,
params.message,
async (accept: boolean, promptText?: string) => {
@ -262,7 +261,7 @@ export class FFPage implements PageDelegate {
_onScreencastStarted(event: Protocol.Page.screencastStartedPayload) {
const screencast = new Screencast(event.file, this._page);
this._idToScreencast.set(event.uid, screencast);
this._browserContext.emit(Events.BrowserContext.ScreencastStarted, screencast);
this._browserContext.emit(BrowserContext.Events.ScreencastStarted, screencast);
}
_onScreencastStopped(event: Protocol.Page.screencastStoppedPayload) {
@ -270,7 +269,7 @@ export class FFPage implements PageDelegate {
if (!screencast)
return;
this._idToScreencast.delete(event.uid);
this._browserContext.emit(Events.BrowserContext.ScreencastStopped, screencast);
this._browserContext.emit(BrowserContext.Events.ScreencastStopped, screencast);
}
async exposeBinding(binding: PageBinding) {

View file

@ -17,7 +17,6 @@
import { ConsoleMessage } from './console';
import * as dom from './dom';
import { Events } from './events';
import { assert, helper, RegisteredListener, debugLogger } from './helper';
import * as js from './javascript';
import * as network from './network';
@ -50,7 +49,6 @@ type ConsoleTagHandler = () => void;
export type FunctionWithSource = (source: { context: BrowserContext, page: Page, frame: Frame}, ...args: any) => any;
export const kNavigationEvent = Symbol('navigation');
export type NavigationEvent = {
// New frame url after navigation.
url: string,
@ -63,8 +61,6 @@ export type NavigationEvent = {
// the navigation did not commit.
error?: Error,
};
export const kAddLifecycleEvent = Symbol('addLifecycle');
export const kRemoveLifecycleEvent = Symbol('removeLifecycle');
export class FrameManager {
private _page: Page;
@ -120,7 +116,7 @@ export class FrameManager {
assert(!this._frames.has(frameId));
const frame = new Frame(this._page, frameId, parentFrame);
this._frames.set(frameId, frame);
this._page.emit(Events.Page.FrameAttached, frame);
this._page.emit(Page.Events.FrameAttached, frame);
return frame;
}
}
@ -197,10 +193,10 @@ export class FrameManager {
frame._onClearLifecycle();
const navigationEvent: NavigationEvent = { url, name, newDocument: frame._currentDocument };
frame._eventEmitter.emit(kNavigationEvent, navigationEvent);
frame.emit(Frame.Events.Navigation, navigationEvent);
if (!initial) {
debugLogger.log('api', ` navigated to "${url}"`);
this._page.emit(Events.Page.FrameNavigated, frame);
this._page.emit(Page.Events.FrameNavigated, frame);
}
// Restore pending if any - see comments above about keepPending.
frame._pendingDocument = keepPending;
@ -212,9 +208,9 @@ export class FrameManager {
return;
frame._url = url;
const navigationEvent: NavigationEvent = { url, name: frame._name };
frame._eventEmitter.emit(kNavigationEvent, navigationEvent);
frame.emit(Frame.Events.Navigation, navigationEvent);
debugLogger.log('api', ` navigated to "${url}"`);
this._page.emit(Events.Page.FrameNavigated, frame);
this._page.emit(Page.Events.FrameNavigated, frame);
}
frameAbortedNavigation(frameId: string, errorText: string, documentId?: string) {
@ -230,7 +226,7 @@ export class FrameManager {
error: new Error(errorText),
};
frame._pendingDocument = undefined;
frame._eventEmitter.emit(kNavigationEvent, navigationEvent);
frame.emit(Frame.Events.Navigation, navigationEvent);
}
frameDetached(frameId: string) {
@ -266,13 +262,13 @@ export class FrameManager {
requestReceivedResponse(response: network.Response) {
if (!response.request()._isFavicon)
this._page.emit(Events.Page.Response, response);
this._page.emit(Page.Events.Response, response);
}
requestFinished(request: network.Request) {
this._inflightRequestFinished(request);
if (!request._isFavicon)
this._page.emit(Events.Page.RequestFinished, request);
this._page.emit(Page.Events.RequestFinished, request);
}
requestFailed(request: network.Request, canceled: boolean) {
@ -285,7 +281,7 @@ export class FrameManager {
this.frameAbortedNavigation(frame._id, errorText, frame._pendingDocument.documentId);
}
if (!request._isFavicon)
this._page.emit(Events.Page.RequestFailed, request);
this._page.emit(Page.Events.RequestFailed, request);
}
removeChildFramesRecursively(frame: Frame) {
@ -297,7 +293,7 @@ export class FrameManager {
this.removeChildFramesRecursively(frame);
frame._onDetached();
this._frames.delete(frame._id);
this._page.emit(Events.Page.FrameDetached, frame);
this._page.emit(Page.Events.FrameDetached, frame);
}
private _inflightRequestFinished(request: network.Request) {
@ -333,8 +329,13 @@ export class FrameManager {
}
}
export class Frame {
readonly _eventEmitter: EventEmitter;
export class Frame extends EventEmitter {
static Events = {
Navigation: 'navigation',
AddLifecycle: 'addlifecycle',
RemoveLifecycle: 'removelifecycle',
};
_id: string;
private _firedLifecycleEvents = new Set<types.LifecycleEvent>();
_subtreeLifecycleEvents = new Set<types.LifecycleEvent>();
@ -354,8 +355,8 @@ export class Frame {
private _detachedCallback = () => {};
constructor(page: Page, id: string, parentFrame: Frame | null) {
this._eventEmitter = new EventEmitter();
this._eventEmitter.setMaxListeners(0);
super();
this.setMaxListeners(0);
this._id = id;
this._page = page;
this._parentFrame = parentFrame;
@ -406,18 +407,18 @@ export class Frame {
for (const event of events) {
// Checking whether we have already notified about this event.
if (!this._subtreeLifecycleEvents.has(event)) {
this._eventEmitter.emit(kAddLifecycleEvent, event);
this.emit(Frame.Events.AddLifecycle, event);
if (this === mainFrame && this._url !== 'about:blank')
debugLogger.log('api', ` "${event}" event fired`);
if (this === mainFrame && event === 'load')
this._page.emit(Events.Page.Load);
this._page.emit(Page.Events.Load);
if (this === mainFrame && event === 'domcontentloaded')
this._page.emit(Events.Page.DOMContentLoaded);
this._page.emit(Page.Events.DOMContentLoaded);
}
}
for (const event of this._subtreeLifecycleEvents) {
if (!events.has(event))
this._eventEmitter.emit(kRemoveLifecycleEvent, event);
this.emit(Frame.Events.RemoveLifecycle, event);
}
this._subtreeLifecycleEvents = events;
}
@ -436,13 +437,13 @@ export class Frame {
}
url = helper.completeUserURL(url);
const sameDocument = helper.waitForEvent(progress, this._eventEmitter, kNavigationEvent, (e: NavigationEvent) => !e.newDocument);
const sameDocument = helper.waitForEvent(progress, this, Frame.Events.Navigation, (e: NavigationEvent) => !e.newDocument);
const navigateResult = await this._page._delegate.navigateFrame(this, url, referer);
let event: NavigationEvent;
if (navigateResult.newDocumentId) {
sameDocument.dispose();
event = await helper.waitForEvent(progress, this._eventEmitter, kNavigationEvent, (event: NavigationEvent) => {
event = await helper.waitForEvent(progress, this, Frame.Events.Navigation, (event: NavigationEvent) => {
// We are interested either in this specific document, or any other document that
// did commit and replaced the expected document.
return event.newDocument && (event.newDocument.documentId === navigateResult.newDocumentId || !event.error);
@ -460,7 +461,7 @@ export class Frame {
}
if (!this._subtreeLifecycleEvents.has(waitUntil))
await helper.waitForEvent(progress, this._eventEmitter, kAddLifecycleEvent, (e: types.LifecycleEvent) => e === waitUntil).promise;
await helper.waitForEvent(progress, this, Frame.Events.AddLifecycle, (e: types.LifecycleEvent) => e === waitUntil).promise;
const request = event.newDocument ? event.newDocument.request : undefined;
const response = request ? request._finalRequest().response() : null;
@ -474,7 +475,7 @@ export class Frame {
const waitUntil = verifyLifecycle('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil);
progress.log(`waiting for navigation until "${waitUntil}"`);
const navigationEvent: NavigationEvent = await helper.waitForEvent(progress, this._eventEmitter, kNavigationEvent, (event: NavigationEvent) => {
const navigationEvent: NavigationEvent = await helper.waitForEvent(progress, this, Frame.Events.Navigation, (event: NavigationEvent) => {
// Any failed navigation results in a rejection.
if (event.error)
return true;
@ -485,7 +486,7 @@ export class Frame {
throw navigationEvent.error;
if (!this._subtreeLifecycleEvents.has(waitUntil))
await helper.waitForEvent(progress, this._eventEmitter, kAddLifecycleEvent, (e: types.LifecycleEvent) => e === waitUntil).promise;
await helper.waitForEvent(progress, this, Frame.Events.AddLifecycle, (e: types.LifecycleEvent) => e === waitUntil).promise;
const request = navigationEvent.newDocument ? navigationEvent.newDocument.request : undefined;
return request ? request._finalRequest().response() : null;
@ -499,7 +500,7 @@ export class Frame {
async _waitForLoadState(progress: Progress, state: types.LifecycleEvent): Promise<void> {
const waitUntil = verifyLifecycle('state', state);
if (!this._subtreeLifecycleEvents.has(waitUntil))
await helper.waitForEvent(progress, this._eventEmitter, kAddLifecycleEvent, (e: types.LifecycleEvent) => e === waitUntil).promise;
await helper.waitForEvent(progress, this, Frame.Events.AddLifecycle, (e: types.LifecycleEvent) => e === waitUntil).promise;
}
async frameElement(): Promise<dom.ElementHandle> {
@ -752,7 +753,7 @@ export class Frame {
resolve();
});
const errorPromise = new Promise(resolve => {
listeners.push(helper.addEventListener(this._page, Events.Page.Console, (message: ConsoleMessage) => {
listeners.push(helper.addEventListener(this._page, Page.Events.Console, (message: ConsoleMessage) => {
if (message.type() === 'error' && message.text().includes('Content Security Policy')) {
cspMessage = message;
resolve();
@ -1045,7 +1046,7 @@ class SignalBarrier {
async addFrameNavigation(frame: Frame) {
this.retain();
const waiter = helper.waitForEvent(null, frame._eventEmitter, kNavigationEvent, (e: NavigationEvent) => {
const waiter = helper.waitForEvent(null, frame, Frame.Events.Navigation, (e: NavigationEvent) => {
if (!e.error && this._progress)
this._progress.log(` navigated to "${frame._url}"`);
return true;

View file

@ -24,7 +24,6 @@ import * as network from './network';
import { Screenshotter } from './screenshotter';
import { TimeoutSettings } from './timeoutSettings';
import * as types from './types';
import { Events } from './events';
import { BrowserContext } from './browserContext';
import { ConsoleMessage } from './console';
import * as accessibility from './accessibility';
@ -91,6 +90,29 @@ type PageState = {
};
export class Page extends EventEmitter {
static Events = {
Close: 'close',
Crash: 'crash',
Console: 'console',
Dialog: 'dialog',
Download: 'download',
FileChooser: 'filechooser',
DOMContentLoaded: 'domcontentloaded',
// Can't use just 'error' due to node.js special treatment of error events.
// @see https://nodejs.org/api/events.html#events_error_events
PageError: 'pageerror',
Request: 'request',
Response: 'response',
RequestFailed: 'requestfailed',
RequestFinished: 'requestfinished',
FrameAttached: 'frameattached',
FrameDetached: 'framedetached',
FrameNavigated: 'framenavigated',
Load: 'load',
Popup: 'popup',
Worker: 'worker',
};
private _closedState: 'open' | 'closing' | 'closed' = 'open';
private _closedCallback: () => void;
private _closedPromise: Promise<void>;
@ -154,13 +176,13 @@ export class Page extends EventEmitter {
this._frameManager.dispose();
assert(this._closedState !== 'closed', 'Page closed twice');
this._closedState = 'closed';
this.emit(Events.Page.Close);
this.emit(Page.Events.Close);
this._closedCallback();
}
_didCrash() {
this._frameManager.dispose();
this.emit(Events.Page.Crash);
this.emit(Page.Events.Crash);
this._crashedCallback(new Error('Page crashed'));
}
@ -179,12 +201,12 @@ export class Page extends EventEmitter {
async _onFileChooserOpened(handle: dom.ElementHandle) {
const multiple = await handle.evaluate(element => !!(element as HTMLInputElement).multiple);
if (!this.listenerCount(Events.Page.FileChooser)) {
if (!this.listenerCount(Page.Events.FileChooser)) {
handle.dispose();
return;
}
const fileChooser = new FileChooser(this, handle, multiple);
this.emit(Events.Page.FileChooser, fileChooser);
this.emit(Page.Events.FileChooser, fileChooser);
}
context(): BrowserContext {
@ -235,10 +257,10 @@ export class Page extends EventEmitter {
_addConsoleMessage(type: string, args: js.JSHandle[], location: types.ConsoleMessageLocation, text?: string) {
const message = new ConsoleMessage(type, text, args, location);
const intercepted = this._frameManager.interceptConsoleMessage(message);
if (intercepted || !this.listenerCount(Events.Page.Console))
if (intercepted || !this.listenerCount(Page.Events.Console))
args.forEach(arg => arg.dispose());
else
this.emit(Events.Page.Console, message);
this.emit(Page.Events.Console, message);
}
async reload(options?: types.NavigateOptions): Promise<network.Response | null> {
@ -315,7 +337,7 @@ export class Page extends EventEmitter {
}
_requestStarted(request: network.Request) {
this.emit(Events.Page.Request, request);
this.emit(Page.Events.Request, request);
const route = request._route();
if (!route)
return;
@ -362,20 +384,20 @@ export class Page extends EventEmitter {
_addWorker(workerId: string, worker: Worker) {
this._workers.set(workerId, worker);
this.emit(Events.Page.Worker, worker);
this.emit(Page.Events.Worker, worker);
}
_removeWorker(workerId: string) {
const worker = this._workers.get(workerId);
if (!worker)
return;
worker.emit(Events.Worker.Close, worker);
worker.emit(Worker.Events.Close, worker);
this._workers.delete(workerId);
}
_clearWorkers() {
for (const [workerId, worker] of this._workers) {
worker.emit(Events.Worker.Close, worker);
worker.emit(Worker.Events.Close, worker);
this._workers.delete(workerId);
}
}
@ -386,6 +408,10 @@ export class Page extends EventEmitter {
}
export class Worker extends EventEmitter {
static Events = {
Close: 'close',
};
private _url: string;
private _executionContextPromise: Promise<js.ExecutionContext>;
private _executionContextCallback: (value?: js.ExecutionContext) => void;

View file

@ -15,14 +15,12 @@
*/
import { BrowserContext } from '../../browserContext';
import { Events } from '../../events';
import { Dispatcher, DispatcherScope, lookupDispatcher } from './dispatcher';
import { PageDispatcher, BindingCallDispatcher, WorkerDispatcher } from './pageDispatcher';
import * as channels from '../channels';
import { RouteDispatcher, RequestDispatcher } from './networkDispatchers';
import { CRBrowserContext } from '../../chromium/crBrowser';
import { CDPSessionDispatcher } from './cdpSessionDispatcher';
import { Events as ChromiumEvents } from '../../chromium/events';
export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channels.BrowserContextInitializer> implements channels.BrowserContextChannel {
private _context: BrowserContext;
@ -33,8 +31,8 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
for (const page of context.pages())
this._dispatchEvent('page', { page: new PageDispatcher(this._scope, page) });
context.on(Events.BrowserContext.Page, page => this._dispatchEvent('page', { page: new PageDispatcher(this._scope, page) }));
context.on(Events.BrowserContext.Close, () => {
context.on(BrowserContext.Events.Page, page => this._dispatchEvent('page', { page: new PageDispatcher(this._scope, page) }));
context.on(BrowserContext.Events.Close, () => {
this._dispatchEvent('close');
this._dispose();
});
@ -42,10 +40,10 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
if (context._browser._options.name === 'chromium') {
for (const page of (context as CRBrowserContext).backgroundPages())
this._dispatchEvent('crBackgroundPage', { page: new PageDispatcher(this._scope, page) });
context.on(ChromiumEvents.ChromiumBrowserContext.BackgroundPage, page => this._dispatchEvent('crBackgroundPage', { page: new PageDispatcher(this._scope, page) }));
context.on(CRBrowserContext.CREvents.BackgroundPage, page => this._dispatchEvent('crBackgroundPage', { page: new PageDispatcher(this._scope, page) }));
for (const serviceWorker of (context as CRBrowserContext).serviceWorkers())
this._dispatchEvent('crServiceWorker', new WorkerDispatcher(this._scope, serviceWorker));
context.on(ChromiumEvents.ChromiumBrowserContext.ServiceWorker, serviceWorker => this._dispatchEvent('crServiceWorker', { worker: new WorkerDispatcher(this._scope, serviceWorker) }));
context.on(CRBrowserContext.CREvents.ServiceWorker, serviceWorker => this._dispatchEvent('crServiceWorker', { worker: new WorkerDispatcher(this._scope, serviceWorker) }));
}
}

View file

@ -15,7 +15,6 @@
*/
import { Browser } from '../../browser';
import { Events } from '../../events';
import * as channels from '../channels';
import { BrowserContextDispatcher } from './browserContextDispatcher';
import { CDPSessionDispatcher } from './cdpSessionDispatcher';
@ -26,7 +25,7 @@ import { PageDispatcher } from './pageDispatcher';
export class BrowserDispatcher extends Dispatcher<Browser, channels.BrowserInitializer> implements channels.BrowserChannel {
constructor(scope: DispatcherScope, browser: Browser, guid?: string) {
super(scope, browser, 'Browser', { version: browser.version() }, true, guid);
browser.on(Events.Browser.Disconnected, () => this._didClose());
browser.on(Browser.Events.Disconnected, () => this._didClose());
}
_didClose() {

View file

@ -15,7 +15,7 @@
*/
import { Dispatcher, DispatcherScope, lookupDispatcher } from './dispatcher';
import { Electron, ElectronApplication, ElectronEvents, ElectronPage } from '../../server/electron';
import { Electron, ElectronApplication, ElectronPage } from '../../server/electron';
import * as channels from '../channels';
import { BrowserContextDispatcher } from './browserContextDispatcher';
import { PageDispatcher } from './pageDispatcher';
@ -37,11 +37,11 @@ export class ElectronApplicationDispatcher extends Dispatcher<ElectronApplicatio
constructor(scope: DispatcherScope, electronApplication: ElectronApplication) {
super(scope, electronApplication, 'ElectronApplication', {}, true);
this._dispatchEvent('context', { context: new BrowserContextDispatcher(this._scope, electronApplication.context()) });
electronApplication.on(ElectronEvents.ElectronApplication.Close, () => {
electronApplication.on(ElectronApplication.Events.Close, () => {
this._dispatchEvent('close');
this._dispose();
});
electronApplication.on(ElectronEvents.ElectronApplication.Window, (page: ElectronPage) => {
electronApplication.on(ElectronApplication.Events.Window, (page: ElectronPage) => {
this._dispatchEvent('window', {
page: lookupDispatcher<PageDispatcher>(page),
browserWindow: createHandle(this._scope, page.browserWindow),

View file

@ -14,7 +14,7 @@
* limitations under the License.
*/
import { Frame, kAddLifecycleEvent, kRemoveLifecycleEvent, kNavigationEvent, NavigationEvent } from '../../frames';
import { Frame, NavigationEvent } from '../../frames';
import * as types from '../../types';
import * as channels from '../channels';
import { Dispatcher, DispatcherScope, lookupNullableDispatcher, existingDispatcher } from './dispatcher';
@ -38,13 +38,13 @@ export class FrameDispatcher extends Dispatcher<Frame, channels.FrameInitializer
loadStates: Array.from(frame._subtreeLifecycleEvents),
});
this._frame = frame;
frame._eventEmitter.on(kAddLifecycleEvent, (event: types.LifecycleEvent) => {
frame.on(Frame.Events.AddLifecycle, (event: types.LifecycleEvent) => {
this._dispatchEvent('loadstate', { add: event });
});
frame._eventEmitter.on(kRemoveLifecycleEvent, (event: types.LifecycleEvent) => {
frame.on(Frame.Events.RemoveLifecycle, (event: types.LifecycleEvent) => {
this._dispatchEvent('loadstate', { remove: event });
});
frame._eventEmitter.on(kNavigationEvent, (event: NavigationEvent) => {
frame.on(Frame.Events.Navigation, (event: NavigationEvent) => {
const params = { url: event.url, name: event.name, error: event.error ? event.error.message : undefined };
if (event.newDocument)
(params as any).newDocument = { request: RequestDispatcher.fromNullable(this._scope, event.newDocument.request || null) };

View file

@ -15,7 +15,6 @@
*/
import { BrowserContext } from '../../browserContext';
import { Events } from '../../events';
import { Frame } from '../../frames';
import { Request } from '../../network';
import { Page, Worker } from '../../page';
@ -44,29 +43,29 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageInitializer> i
isClosed: page.isClosed()
});
this._page = page;
page.on(Events.Page.Close, () => this._dispatchEvent('close'));
page.on(Events.Page.Console, message => this._dispatchEvent('console', { message: new ConsoleMessageDispatcher(this._scope, message) }));
page.on(Events.Page.Crash, () => this._dispatchEvent('crash'));
page.on(Events.Page.DOMContentLoaded, () => this._dispatchEvent('domcontentloaded'));
page.on(Events.Page.Dialog, dialog => this._dispatchEvent('dialog', { dialog: new DialogDispatcher(this._scope, dialog) }));
page.on(Events.Page.Download, dialog => this._dispatchEvent('download', { download: new DownloadDispatcher(this._scope, dialog) }));
this._page.on(Events.Page.FileChooser, (fileChooser: FileChooser) => this._dispatchEvent('fileChooser', {
page.on(Page.Events.Close, () => this._dispatchEvent('close'));
page.on(Page.Events.Console, message => this._dispatchEvent('console', { message: new ConsoleMessageDispatcher(this._scope, message) }));
page.on(Page.Events.Crash, () => this._dispatchEvent('crash'));
page.on(Page.Events.DOMContentLoaded, () => this._dispatchEvent('domcontentloaded'));
page.on(Page.Events.Dialog, dialog => this._dispatchEvent('dialog', { dialog: new DialogDispatcher(this._scope, dialog) }));
page.on(Page.Events.Download, dialog => this._dispatchEvent('download', { download: new DownloadDispatcher(this._scope, dialog) }));
this._page.on(Page.Events.FileChooser, (fileChooser: FileChooser) => this._dispatchEvent('fileChooser', {
element: new ElementHandleDispatcher(this._scope, fileChooser.element()),
isMultiple: fileChooser.isMultiple()
}));
page.on(Events.Page.FrameAttached, frame => this._onFrameAttached(frame));
page.on(Events.Page.FrameDetached, frame => this._onFrameDetached(frame));
page.on(Events.Page.Load, () => this._dispatchEvent('load'));
page.on(Events.Page.PageError, error => this._dispatchEvent('pageError', { error: serializeError(error) }));
page.on(Events.Page.Popup, page => this._dispatchEvent('popup', { page: lookupDispatcher<PageDispatcher>(page) }));
page.on(Events.Page.Request, request => this._dispatchEvent('request', { request: RequestDispatcher.from(this._scope, request) }));
page.on(Events.Page.RequestFailed, (request: Request) => this._dispatchEvent('requestFailed', {
page.on(Page.Events.FrameAttached, frame => this._onFrameAttached(frame));
page.on(Page.Events.FrameDetached, frame => this._onFrameDetached(frame));
page.on(Page.Events.Load, () => this._dispatchEvent('load'));
page.on(Page.Events.PageError, error => this._dispatchEvent('pageError', { error: serializeError(error) }));
page.on(Page.Events.Popup, page => this._dispatchEvent('popup', { page: lookupDispatcher<PageDispatcher>(page) }));
page.on(Page.Events.Request, request => this._dispatchEvent('request', { request: RequestDispatcher.from(this._scope, request) }));
page.on(Page.Events.RequestFailed, (request: Request) => this._dispatchEvent('requestFailed', {
request: RequestDispatcher.from(this._scope, request),
failureText: request._failureText
}));
page.on(Events.Page.RequestFinished, request => this._dispatchEvent('requestFinished', { request: RequestDispatcher.from(scope, request) }));
page.on(Events.Page.Response, response => this._dispatchEvent('response', { response: new ResponseDispatcher(this._scope, response) }));
page.on(Events.Page.Worker, worker => this._dispatchEvent('worker', { worker: new WorkerDispatcher(this._scope, worker) }));
page.on(Page.Events.RequestFinished, request => this._dispatchEvent('requestFinished', { request: RequestDispatcher.from(scope, request) }));
page.on(Page.Events.Response, response => this._dispatchEvent('response', { response: new ResponseDispatcher(this._scope, response) }));
page.on(Page.Events.Worker, worker => this._dispatchEvent('worker', { worker: new WorkerDispatcher(this._scope, worker) }));
}
async setDefaultNavigationTimeoutNoReply(params: channels.PageSetDefaultNavigationTimeoutNoReplyParams): Promise<void> {
@ -232,7 +231,7 @@ export class WorkerDispatcher extends Dispatcher<Worker, channels.WorkerInitiali
super(scope, worker, 'Worker', {
url: worker.url()
});
worker.on(Events.Worker.Close, () => this._dispatchEvent('close'));
worker.on(Worker.Events.Close, () => this._dispatchEvent('close'));
}
async evaluateExpression(params: channels.WorkerEvaluateExpressionParams): Promise<channels.WorkerEvaluateExpressionResult> {

View file

@ -18,7 +18,6 @@ import * as path from 'path';
import { CRBrowser, CRBrowserContext } from '../chromium/crBrowser';
import { CRConnection, CRSession } from '../chromium/crConnection';
import { CRExecutionContext } from '../chromium/crExecutionContext';
import { Events } from '../events';
import * as js from '../javascript';
import { Page } from '../page';
import { TimeoutSettings } from '../timeoutSettings';
@ -42,19 +41,17 @@ export type ElectronLaunchOptionsBase = {
timeout?: number,
};
export const ElectronEvents = {
ElectronApplication: {
Close: 'close',
Window: 'window',
}
};
export interface ElectronPage extends Page {
browserWindow: js.JSHandle<BrowserWindow>;
_browserWindowId: number;
}
export class ElectronApplication extends EventEmitter {
static Events = {
Close: 'close',
Window: 'window',
};
private _browserContext: CRBrowserContext;
private _nodeConnection: CRConnection;
private _nodeSession: CRSession;
@ -67,11 +64,11 @@ export class ElectronApplication extends EventEmitter {
constructor(browser: CRBrowser, nodeConnection: CRConnection) {
super();
this._browserContext = browser._defaultContext as CRBrowserContext;
this._browserContext.on(Events.BrowserContext.Close, () => {
this._browserContext.on(BrowserContext.Events.Close, () => {
// Emit application closed after context closed.
Promise.resolve().then(() => this.emit(ElectronEvents.ElectronApplication.Close));
Promise.resolve().then(() => this.emit(ElectronApplication.Events.Close));
});
this._browserContext.on(Events.BrowserContext.Page, event => this._onPage(event));
this._browserContext.on(BrowserContext.Events.Page, event => this._onPage(event));
this._nodeConnection = nodeConnection;
this._nodeSession = nodeConnection.rootSession;
}
@ -85,13 +82,13 @@ export class ElectronApplication extends EventEmitter {
return;
page.browserWindow = handle;
page._browserWindowId = windowId;
page.on(Events.Page.Close, () => {
page.on(Page.Events.Close, () => {
page.browserWindow.dispose();
this._windows.delete(page);
});
this._windows.add(page);
await page.mainFrame().waitForLoadState('domcontentloaded').catch(e => {}); // can happen after detach
this.emit(ElectronEvents.ElectronApplication.Window, page);
this.emit(ElectronApplication.Events.Window, page);
}
async newBrowserWindow(options: any): Promise<Page> {
@ -106,7 +103,7 @@ export class ElectronApplication extends EventEmitter {
return page;
}
return await this._waitForEvent(ElectronEvents.ElectronApplication.Window, (page: ElectronPage) => page._browserWindowId === windowId);
return await this._waitForEvent(ElectronApplication.Events.Window, (page: ElectronPage) => page._browserWindowId === windowId);
}
context(): BrowserContext {
@ -114,7 +111,7 @@ export class ElectronApplication extends EventEmitter {
}
async close() {
const closed = this._waitForEvent(ElectronEvents.ElectronApplication.Close);
const closed = this._waitForEvent(ElectronApplication.Events.Close);
await this._nodeElectronHandle!.evaluate(({ app }) => app.quit());
this._nodeConnection.close();
await closed;
@ -122,7 +119,7 @@ export class ElectronApplication extends EventEmitter {
private async _waitForEvent(event: string, predicate?: Function): Promise<any> {
const progressController = new ProgressController(this._timeoutSettings.timeout({}));
if (event !== ElectronEvents.ElectronApplication.Close)
if (event !== ElectronApplication.Events.Close)
this._browserContext._closePromise.then(error => progressController.abort(error));
return progressController.run(progress => helper.waitForEvent(progress, this, event, predicate).promise);
}

View file

@ -17,7 +17,6 @@
import { Browser, BrowserOptions } from '../browser';
import { assertBrowserContextIsNotOwned, BrowserContext, validateBrowserContextOptions, verifyGeolocation } from '../browserContext';
import { Events } from '../events';
import { helper, RegisteredListener, assert } from '../helper';
import * as network from '../network';
import { Page, PageBinding } from '../page';
@ -150,13 +149,13 @@ export class WKBrowser extends Browser {
const page = wkPage._page;
if (pageOrError instanceof Error)
page._setIsError();
context!.emit(Events.BrowserContext.Page, page);
context!.emit(BrowserContext.Events.Page, page);
if (!opener)
return;
await opener.pageOrError();
const openerPage = opener._page;
if (!openerPage.isClosed())
openerPage.emit(Events.Page.Popup, page);
openerPage.emit(Page.Events.Popup, page);
});
}

View file

@ -15,13 +15,12 @@
* limitations under the License.
*/
import { Screencast } from '../browserContext';
import { Screencast, BrowserContext } from '../browserContext';
import * as frames from '../frames';
import { helper, RegisteredListener, assert, debugAssert } from '../helper';
import * as dom from '../dom';
import * as network from '../network';
import { WKSession } from './wkConnection';
import { Events } from '../events';
import { WKExecutionContext } from './wkExecutionContext';
import { WKInterceptableRequest } from './wkInterceptableRequest';
import { WKWorkers } from './wkWorkers';
@ -81,7 +80,7 @@ export class WKPage implements PageDelegate {
this._workers = new WKWorkers(this._page);
this._session = undefined as any as WKSession;
this._browserContext = browserContext;
this._page.on(Events.Page.FrameDetached, (frame: frames.Frame) => this._removeContextsForFrame(frame, false));
this._page.on(Page.Events.FrameDetached, (frame: frames.Frame) => this._removeContextsForFrame(frame, false));
this._eventListeners = [
helper.addEventListener(this._pageProxySession, 'Target.targetCreated', this._onTargetCreated.bind(this)),
helper.addEventListener(this._pageProxySession, 'Target.targetDestroyed', this._onTargetDestroyed.bind(this)),
@ -474,7 +473,7 @@ export class WKPage implements PageDelegate {
} else {
error.stack = '';
}
this._page.emit(Events.Page.PageError, error);
this._page.emit(Page.Events.PageError, error);
return;
}
@ -524,7 +523,7 @@ export class WKPage implements PageDelegate {
}
_onDialog(event: Protocol.Dialog.javascriptDialogOpeningPayload) {
this._page.emit(Events.Page.Dialog, new dialog.Dialog(
this._page.emit(Page.Events.Dialog, new dialog.Dialog(
event.type as dialog.DialogType,
event.message,
async (accept: boolean, promptText?: string) => {
@ -718,7 +717,7 @@ export class WKPage implements PageDelegate {
height: options.height,
scale: options.scale,
});
this._browserContext.emit(Events.BrowserContext.ScreencastStarted, new Screencast(options.outputFile, this._initializedPage!));
this._browserContext.emit(BrowserContext.Events.ScreencastStarted, new Screencast(options.outputFile, this._initializedPage!));
} catch (e) {
this._recordingVideoFile = null;
throw e;
@ -731,7 +730,7 @@ export class WKPage implements PageDelegate {
const fileName = this._recordingVideoFile;
this._recordingVideoFile = null;
await this._pageProxySession.send('Screencast.stopVideoRecording');
this._browserContext.emit(Events.BrowserContext.ScreencastStopped, new Screencast(fileName, this._initializedPage!));
this._browserContext.emit(BrowserContext.Events.ScreencastStopped, new Screencast(fileName, this._initializedPage!));
}
async takeScreenshot(format: string, documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined): Promise<Buffer> {

View file

@ -61,32 +61,11 @@ function traceAPICoverage(apiCoverage, api, events) {
* @param {string} browserName
*/
function apiForBrowser(browserName) {
const BROWSER_CONFIGS = [
{
name: 'Firefox',
events: require('../lib/events').Events,
},
{
name: 'WebKit',
events: require('../lib/events').Events,
},
{
name: 'Chromium',
events: {
...require('../lib/events').Events,
...require('../lib/chromium/events').Events,
}
},
];
const browserConfig = BROWSER_CONFIGS.find(config => config.name.toLowerCase() === browserName);
const events = browserConfig.events;
// TODO: we should rethink our api.ts approach to ensure coverage and async stacks.
const api = {
...require('../lib/rpc/client/api'),
};
const events = require('../lib/rpc/client/events').Events;
const api = require('../lib/rpc/client/api');
const otherBrowsers = ['chromium', 'webkit', 'firefox'].filter(name => name.toLowerCase() !== browserName.toLowerCase());
const filteredKeys = Object.keys(api).filter(apiName => {
return !BROWSER_CONFIGS.some(config => apiName.startsWith(config.name)) || apiName.startsWith(browserConfig.name);
return !otherBrowsers.some(otherName => apiName.toLowerCase().startsWith(otherName));
});
const filteredAPI = {};
for (const key of filteredKeys)