diff --git a/docs/api.md b/docs/api.md
index 746e4f1ce36..661b25273e5 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -336,6 +336,15 @@
* [coverage.stopCSSCoverage()](#coveragestopcsscoverage)
* [coverage.stopJSCoverage()](#coveragestopjscoverage)
- [class: TimeoutError](#class-timeouterror)
+- [class: EventEmitter](#class-eventemitter)
+ * [eventEmitter.addListener(event, handler)](#eventemitteraddlistenerevent-handler)
+ * [eventEmitter.emit(event, [eventData])](#eventemitteremitevent-eventdata)
+ * [eventEmitter.listenerCount(event)](#eventemitterlistenercountevent)
+ * [eventEmitter.off(event, handler)](#eventemitteroffevent-handler)
+ * [eventEmitter.on(event, handler)](#eventemitteronevent-handler)
+ * [eventEmitter.once(event, handler)](#eventemitteronceevent-handler)
+ * [eventEmitter.removeAllListeners([event])](#eventemitterremovealllistenersevent)
+ * [eventEmitter.removeListener(event, handler)](#eventemitterremovelistenerevent-handler)
### Overview
@@ -657,7 +666,7 @@ The method initiates a GET request to download the revision from the host.
### class: Browser
-* extends: [EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter)
+* extends: [EventEmitter](#class-eventemitter)
A Browser is created when Puppeteer connects to a Chromium instance, either through [`puppeteer.launch`](#puppeteerlaunchoptions) or [`puppeteer.connect`](#puppeteerconnectoptions).
@@ -819,7 +828,7 @@ You can find the `webSocketDebuggerUrl` from `http://${host}:${port}/json/versio
### class: BrowserContext
-* extends: [EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter)
+* extends: [EventEmitter](#class-eventemitter)
BrowserContexts provide a way to operate multiple independent browser sessions. When a browser is launched, it has
a single BrowserContext used by default. The method `browser.newPage()` creates a page in the default browser context.
@@ -949,7 +958,7 @@ const newWindowTarget = await browserContext.waitForTarget(target => target.url(
### class: Page
-* extends: [EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter)
+* extends: [EventEmitter](#class-eventemitter)
Page provides methods to interact with a single tab or [extension background page](https://developer.chrome.com/extensions/background_pages) in Chromium. One [Browser] instance might have multiple [Page] instances.
@@ -966,14 +975,16 @@ const puppeteer = require('puppeteer');
})();
```
-The Page class emits various events (described below) which can be handled using any of Node's native [`EventEmitter`](https://nodejs.org/api/events.html#events_class_eventemitter) methods, such as `on`, `once` or `removeListener`.
+The Page class emits various events (described below) which can be handled using
+any of the [`EventEmitter`](#class-eventemitter) methods, such as `on`, `once`
+or `off`.
This example logs a message for a single page `load` event:
```js
page.once('load', () => console.log('Page loaded!'));
```
-To unsubscribe from events use the `removeListener` method:
+To unsubscribe from events use the `off` method:
```js
function logRequest(interceptedRequest) {
@@ -981,7 +992,7 @@ function logRequest(interceptedRequest) {
}
page.on('request', logRequest);
// Sometime later...
-page.removeListener('request', logRequest);
+page.off('request', logRequest);
```
#### event: 'close'
@@ -3869,7 +3880,7 @@ If the target is not of type `"service_worker"` or `"shared_worker"`, returns `n
### class: CDPSession
-* extends: [EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter)
+* extends: [EventEmitter](#class-eventemitter)
The `CDPSession` instances are used to talk raw Chrome Devtools Protocol:
- protocol methods can be called with `session.send` method.
@@ -3975,7 +3986,51 @@ reported.
TimeoutError is emitted whenever certain operations are terminated due to timeout, e.g. [page.waitForSelector(selector[, options])](#pagewaitforselectorselector-options) or [puppeteer.launch([options])](#puppeteerlaunchoptions).
+### class: EventEmitter
+A small EventEmitter class backed by [Mitt](https://github.com/developit/mitt/).
+
+#### eventEmitter.addListener(event, handler)
+- `event` <[string]|[symbol]> the event to remove the handler from.
+- `handler` <[Function]> the event listener that will be added.
+- returns: `this` so you can chain method calls
+
+This method is identical to `on` and maintained for compatibility with Node's EventEmitter. We recommend using `on` by default.
+
+#### eventEmitter.emit(event, [eventData])
+- `event` <[string]|[symbol]> the event to trigger.
+- `eventData` <[Object]> additional data to send with the event.
+- returns: `boolean`; `true` if there are any listeners for the event, `false` if there are none.
+
+#### eventEmitter.listenerCount(event)
+- `event` <[string]|[symbol]> the event to check for listeners.
+- returns: <[number]> the number of listeners for the given event.
+
+#### eventEmitter.off(event, handler)
+- `event` <[string]|[symbol]> the event to remove the handler from.
+- `handler` <[Function]> the event listener that will be removed.
+- returns: `this` so you can chain method calls
+
+#### eventEmitter.on(event, handler)
+- `event` <[string]|[symbol]> the event to add the handler to.
+- `handler` <[Function]> the event listener that will be added.
+- returns: `this` so you can chain method calls
+
+#### eventEmitter.once(event, handler)
+- `event` <[string]|[symbol]> the event to add the handler to.
+- `handler` <[Function]> the event listener that will be added.
+- returns: `this` so you can chain method calls
+
+#### eventEmitter.removeAllListeners([event])
+- `event` <[string]|[symbol]> optional argument to remove all listeners for the given event. If it's not given this method will remove all listeners for all events.
+- returns: `this` so you can chain method calls
+
+#### eventEmitter.removeListener(event, handler)
+- `event` <[string]|[symbol]> the event to remove the handler from.
+- `handler` <[Function]> the event listener that will be removed.
+- returns: `this` so you can chain method calls
+
+This method is identical to `off` and maintained for compatibility with Node's EventEmitter. We recommend using `off` by default.
[AXNode]: #accessibilitysnapshotoptions "AXNode"
[Accessibility]: #class-accessibility "Accessibility"
@@ -4024,4 +4079,5 @@ TimeoutError is emitted whenever certain operations are terminated due to timeou
[selector]: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors "selector"
[stream.Readable]: https://nodejs.org/api/stream.html#stream_class_stream_readable "stream.Readable"
[string]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type "String"
+[symbol]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Symbol_type "Symbol"
[xpath]: https://developer.mozilla.org/en-US/docs/Web/XPath "xpath"
diff --git a/new-docs/puppeteer.eventemitter.addlistener.md b/new-docs/puppeteer.eventemitter.addlistener.md
new file mode 100644
index 00000000000..5a43ce06882
--- /dev/null
+++ b/new-docs/puppeteer.eventemitter.addlistener.md
@@ -0,0 +1,30 @@
+
+
+[Home](./index.md) > [puppeteer](./puppeteer.md) > [EventEmitter](./puppeteer.eventemitter.md) > [addListener](./puppeteer.eventemitter.addlistener.md)
+
+## EventEmitter.addListener() method
+
+> Warning: This API is now obsolete.
+>
+> please use `on` instead.
+>
+
+Add an event listener.
+
+Signature:
+
+```typescript
+addListener(event: EventType, handler: Handler): EventEmitter;
+```
+
+## Parameters
+
+| Parameter | Type | Description |
+| --- | --- | --- |
+| event | EventType | |
+| handler | Handler | |
+
+Returns:
+
+[EventEmitter](./puppeteer.eventemitter.md)
+
diff --git a/new-docs/puppeteer.eventemitter.emit.md b/new-docs/puppeteer.eventemitter.emit.md
new file mode 100644
index 00000000000..7e3c274f235
--- /dev/null
+++ b/new-docs/puppeteer.eventemitter.emit.md
@@ -0,0 +1,27 @@
+
+
+[Home](./index.md) > [puppeteer](./puppeteer.md) > [EventEmitter](./puppeteer.eventemitter.md) > [emit](./puppeteer.eventemitter.emit.md)
+
+## EventEmitter.emit() method
+
+Emit an event and call any associated listeners.
+
+Signature:
+
+```typescript
+emit(event: EventType, eventData?: any): boolean;
+```
+
+## Parameters
+
+| Parameter | Type | Description |
+| --- | --- | --- |
+| event | EventType | the event you'd like to emit |
+| eventData | any | any data you'd like to emit with the event |
+
+Returns:
+
+boolean
+
+`true` if there are any listeners, `false` if there are not.
+
diff --git a/new-docs/puppeteer.eventemitter.listenercount.md b/new-docs/puppeteer.eventemitter.listenercount.md
new file mode 100644
index 00000000000..1e6c5f211ff
--- /dev/null
+++ b/new-docs/puppeteer.eventemitter.listenercount.md
@@ -0,0 +1,26 @@
+
+
+[Home](./index.md) > [puppeteer](./puppeteer.md) > [EventEmitter](./puppeteer.eventemitter.md) > [listenerCount](./puppeteer.eventemitter.listenercount.md)
+
+## EventEmitter.listenerCount() method
+
+Gets the number of listeners for a given event.
+
+Signature:
+
+```typescript
+listenerCount(event: EventType): number;
+```
+
+## Parameters
+
+| Parameter | Type | Description |
+| --- | --- | --- |
+| event | EventType | the event to get the listener count for |
+
+Returns:
+
+number
+
+the number of listeners bound to the given event
+
diff --git a/new-docs/puppeteer.eventemitter.md b/new-docs/puppeteer.eventemitter.md
new file mode 100644
index 00000000000..9937c5b2aca
--- /dev/null
+++ b/new-docs/puppeteer.eventemitter.md
@@ -0,0 +1,33 @@
+
+
+[Home](./index.md) > [puppeteer](./puppeteer.md) > [EventEmitter](./puppeteer.eventemitter.md)
+
+## EventEmitter class
+
+The EventEmitter class that many Puppeteer classes extend.
+
+Signature:
+
+```typescript
+export declare class EventEmitter implements CommonEventEmitter
+```
+
+## Remarks
+
+This allows you to listen to events that Puppeteer classes fire and act accordingly. Therefore you'll mostly use [on](./puppeteer.eventemitter.on.md) and [off](./puppeteer.eventemitter.off.md) to bind and unbind to event listeners.
+
+The constructor for this class is marked as internal. Third-party code should not call the constructor directly or create subclasses that extend the `EventEmitter` class.
+
+## Methods
+
+| Method | Modifiers | Description |
+| --- | --- | --- |
+| [addListener(event, handler)](./puppeteer.eventemitter.addlistener.md) | | Add an event listener. |
+| [emit(event, eventData)](./puppeteer.eventemitter.emit.md) | | Emit an event and call any associated listeners. |
+| [listenerCount(event)](./puppeteer.eventemitter.listenercount.md) | | Gets the number of listeners for a given event. |
+| [off(event, handler)](./puppeteer.eventemitter.off.md) | | Remove an event listener from firing. |
+| [on(event, handler)](./puppeteer.eventemitter.on.md) | | Bind an event listener to fire when an event occurs. |
+| [once(event, handler)](./puppeteer.eventemitter.once.md) | | Like on
but the listener will only be fired once and then it will be removed. |
+| [removeAllListeners(event)](./puppeteer.eventemitter.removealllisteners.md) | | Removes all listeners. If given an event argument, it will remove only listeners for that event. |
+| [removeListener(event, handler)](./puppeteer.eventemitter.removelistener.md) | | Remove an event listener. |
+
diff --git a/new-docs/puppeteer.eventemitter.off.md b/new-docs/puppeteer.eventemitter.off.md
new file mode 100644
index 00000000000..34f43801bf3
--- /dev/null
+++ b/new-docs/puppeteer.eventemitter.off.md
@@ -0,0 +1,27 @@
+
+
+[Home](./index.md) > [puppeteer](./puppeteer.md) > [EventEmitter](./puppeteer.eventemitter.md) > [off](./puppeteer.eventemitter.off.md)
+
+## EventEmitter.off() method
+
+Remove an event listener from firing.
+
+Signature:
+
+```typescript
+off(event: EventType, handler: Handler): EventEmitter;
+```
+
+## Parameters
+
+| Parameter | Type | Description |
+| --- | --- | --- |
+| event | EventType | the event type you'd like to stop listening to. |
+| handler | Handler | the function that should be removed. |
+
+Returns:
+
+[EventEmitter](./puppeteer.eventemitter.md)
+
+`this` to enable you to chain calls.
+
diff --git a/new-docs/puppeteer.eventemitter.on.md b/new-docs/puppeteer.eventemitter.on.md
new file mode 100644
index 00000000000..4dcf2fe2b17
--- /dev/null
+++ b/new-docs/puppeteer.eventemitter.on.md
@@ -0,0 +1,27 @@
+
+
+[Home](./index.md) > [puppeteer](./puppeteer.md) > [EventEmitter](./puppeteer.eventemitter.md) > [on](./puppeteer.eventemitter.on.md)
+
+## EventEmitter.on() method
+
+Bind an event listener to fire when an event occurs.
+
+Signature:
+
+```typescript
+on(event: EventType, handler: Handler): EventEmitter;
+```
+
+## Parameters
+
+| Parameter | Type | Description |
+| --- | --- | --- |
+| event | EventType | the event type you'd like to listen to. Can be a string or symbol. |
+| handler | Handler | the function to be called when the event occurs. |
+
+Returns:
+
+[EventEmitter](./puppeteer.eventemitter.md)
+
+`this` to enable you to chain calls.
+
diff --git a/new-docs/puppeteer.eventemitter.once.md b/new-docs/puppeteer.eventemitter.once.md
new file mode 100644
index 00000000000..3b57da0e314
--- /dev/null
+++ b/new-docs/puppeteer.eventemitter.once.md
@@ -0,0 +1,27 @@
+
+
+[Home](./index.md) > [puppeteer](./puppeteer.md) > [EventEmitter](./puppeteer.eventemitter.md) > [once](./puppeteer.eventemitter.once.md)
+
+## EventEmitter.once() method
+
+Like `on` but the listener will only be fired once and then it will be removed.
+
+Signature:
+
+```typescript
+once(event: EventType, handler: Handler): EventEmitter;
+```
+
+## Parameters
+
+| Parameter | Type | Description |
+| --- | --- | --- |
+| event | EventType | the event you'd like to listen to |
+| handler | Handler | the handler function to run when the event occurs |
+
+Returns:
+
+[EventEmitter](./puppeteer.eventemitter.md)
+
+`this` to enable you to chain calls.
+
diff --git a/new-docs/puppeteer.eventemitter.removealllisteners.md b/new-docs/puppeteer.eventemitter.removealllisteners.md
new file mode 100644
index 00000000000..7a371a215b4
--- /dev/null
+++ b/new-docs/puppeteer.eventemitter.removealllisteners.md
@@ -0,0 +1,26 @@
+
+
+[Home](./index.md) > [puppeteer](./puppeteer.md) > [EventEmitter](./puppeteer.eventemitter.md) > [removeAllListeners](./puppeteer.eventemitter.removealllisteners.md)
+
+## EventEmitter.removeAllListeners() method
+
+Removes all listeners. If given an event argument, it will remove only listeners for that event.
+
+Signature:
+
+```typescript
+removeAllListeners(event?: EventType): EventEmitter;
+```
+
+## Parameters
+
+| Parameter | Type | Description |
+| --- | --- | --- |
+| event | EventType | the event to remove listeners for. |
+
+Returns:
+
+[EventEmitter](./puppeteer.eventemitter.md)
+
+`this` to enable you to chain calls.
+
diff --git a/new-docs/puppeteer.eventemitter.removelistener.md b/new-docs/puppeteer.eventemitter.removelistener.md
new file mode 100644
index 00000000000..4b1608a4299
--- /dev/null
+++ b/new-docs/puppeteer.eventemitter.removelistener.md
@@ -0,0 +1,30 @@
+
+
+[Home](./index.md) > [puppeteer](./puppeteer.md) > [EventEmitter](./puppeteer.eventemitter.md) > [removeListener](./puppeteer.eventemitter.removelistener.md)
+
+## EventEmitter.removeListener() method
+
+> Warning: This API is now obsolete.
+>
+> please use `off` instead.
+>
+
+Remove an event listener.
+
+Signature:
+
+```typescript
+removeListener(event: EventType, handler: Handler): EventEmitter;
+```
+
+## Parameters
+
+| Parameter | Type | Description |
+| --- | --- | --- |
+| event | EventType | |
+| handler | Handler | |
+
+Returns:
+
+[EventEmitter](./puppeteer.eventemitter.md)
+
diff --git a/new-docs/puppeteer.md b/new-docs/puppeteer.md
index 5b797ad05f5..587bcba13b3 100644
--- a/new-docs/puppeteer.md
+++ b/new-docs/puppeteer.md
@@ -18,6 +18,7 @@
| [Coverage](./puppeteer.coverage.md) | |
| [Dialog](./puppeteer.dialog.md) | Dialog instances are dispatched by the [Page](./puppeteer.page.md) via the dialog
event. |
| [ElementHandle](./puppeteer.elementhandle.md) | |
+| [EventEmitter](./puppeteer.eventemitter.md) | The EventEmitter class that many Puppeteer classes extend. |
| [ExecutionContext](./puppeteer.executioncontext.md) | |
| [FileChooser](./puppeteer.filechooser.md) | |
| [Frame](./puppeteer.frame.md) | |
diff --git a/new-docs/puppeteer.page.md b/new-docs/puppeteer.page.md
index f5c0cea61a2..0277bffd267 100644
--- a/new-docs/puppeteer.page.md
+++ b/new-docs/puppeteer.page.md
@@ -32,7 +32,7 @@ const puppeteer = require('puppeteer');
})();
```
-The Page class emits various events which are documented in the [PageEmittedEvents](./puppeteer.pageemittedevents.md) enum.
+The Page class extends from Puppeteer's [EventEmitter](./puppeteer.eventemitter.md) class and will emit various events which are documented in the [PageEmittedEvents](./puppeteer.pageemittedevents.md) enum.
## Example 2
diff --git a/package.json b/package.json
index a022e27a697..2ec59a3ea77 100644
--- a/package.json
+++ b/package.json
@@ -51,6 +51,7 @@
"extract-zip": "^2.0.0",
"https-proxy-agent": "^4.0.0",
"mime": "^2.0.3",
+ "mitt": "^2.0.1",
"progress": "^2.0.1",
"proxy-from-env": "^1.0.0",
"rimraf": "^3.0.2",
diff --git a/src/Browser.ts b/src/Browser.ts
index 9b096b0e6ce..5ac2fec581d 100644
--- a/src/Browser.ts
+++ b/src/Browser.ts
@@ -16,7 +16,7 @@
import { helper, assert } from './helper';
import { Target } from './Target';
-import * as EventEmitter from 'events';
+import { EventEmitter } from './EventEmitter';
import { Events } from './Events';
import Protocol from './protocol';
import { Connection } from './Connection';
diff --git a/src/BrowserFetcher.ts b/src/BrowserFetcher.ts
index cd60777781f..4eacc6e7eef 100644
--- a/src/BrowserFetcher.ts
+++ b/src/BrowserFetcher.ts
@@ -22,11 +22,11 @@ import * as childProcess from 'child_process';
import * as https from 'https';
import * as http from 'http';
-import * as extractZip from 'extract-zip';
-import * as debug from 'debug';
-import * as removeRecursive from 'rimraf';
+import extractZip from 'extract-zip';
+import debug from 'debug';
+import removeRecursive from 'rimraf';
import * as URL from 'url';
-import * as ProxyAgent from 'https-proxy-agent';
+import ProxyAgent from 'https-proxy-agent';
import { getProxyForUrl } from 'proxy-from-env';
import { helper, assert } from './helper';
diff --git a/src/Connection.ts b/src/Connection.ts
index fcf33581c66..2ff5a75476f 100644
--- a/src/Connection.ts
+++ b/src/Connection.ts
@@ -15,13 +15,13 @@
*/
import { assert } from './helper';
import { Events } from './Events';
-import * as debug from 'debug';
+import debug from 'debug';
const debugProtocolSend = debug('puppeteer:protocol:SEND ►');
const debugProtocolReceive = debug('puppeteer:protocol:RECV ◀');
import Protocol from './protocol';
import { ConnectionTransport } from './ConnectionTransport';
-import * as EventEmitter from 'events';
+import { EventEmitter } from './EventEmitter';
interface ConnectionCallback {
resolve: Function;
diff --git a/src/EventEmitter.ts b/src/EventEmitter.ts
new file mode 100644
index 00000000000..73d7787cd53
--- /dev/null
+++ b/src/EventEmitter.ts
@@ -0,0 +1,140 @@
+import mitt, { Emitter, EventType, Handler } from 'mitt';
+
+/**
+ * @internal
+ */
+export interface CommonEventEmitter {
+ on(event: EventType, handler: Handler): CommonEventEmitter;
+ off(event: EventType, handler: Handler): CommonEventEmitter;
+ /* To maintain parity with the built in NodeJS event emitter which uses removeListener
+ * rather than `off`.
+ * If you're implementing new code you should use `off`.
+ */
+ addListener(event: EventType, handler: Handler): CommonEventEmitter;
+ removeListener(event: EventType, handler: Handler): CommonEventEmitter;
+ emit(event: EventType, eventData?: any): boolean;
+ once(event: EventType, handler: Handler): CommonEventEmitter;
+ listenerCount(event: string): number;
+
+ removeAllListeners(event?: EventType): CommonEventEmitter;
+}
+
+/**
+ * The EventEmitter class that many Puppeteer classes extend.
+ *
+ * @remarks
+ *
+ * This allows you to listen to events that Puppeteer classes fire and act
+ * accordingly. Therefore you'll mostly use {@link EventEmitter.on | on} and
+ * {@link EventEmitter.off | off} to bind
+ * and unbind to event listeners.
+ *
+ * @public
+ */
+export class EventEmitter implements CommonEventEmitter {
+ private emitter: Emitter;
+ private eventsMap = new Map();
+
+ /**
+ * @internal
+ */
+ constructor() {
+ this.emitter = mitt(this.eventsMap);
+ }
+
+ /**
+ * Bind an event listener to fire when an event occurs.
+ * @param event - the event type you'd like to listen to. Can be a string or symbol.
+ * @param handler - the function to be called when the event occurs.
+ * @returns `this` to enable you to chain calls.
+ */
+ on(event: EventType, handler: Handler): EventEmitter {
+ this.emitter.on(event, handler);
+ return this;
+ }
+
+ /**
+ * Remove an event listener from firing.
+ * @param event - the event type you'd like to stop listening to.
+ * @param handler - the function that should be removed.
+ * @returns `this` to enable you to chain calls.
+ */
+ off(event: EventType, handler: Handler): EventEmitter {
+ this.emitter.off(event, handler);
+ return this;
+ }
+
+ /**
+ * Remove an event listener.
+ * @deprecated please use `off` instead.
+ */
+ removeListener(event: EventType, handler: Handler): EventEmitter {
+ this.off(event, handler);
+ return this;
+ }
+
+ /**
+ * Add an event listener.
+ * @deprecated please use `on` instead.
+ */
+ addListener(event: EventType, handler: Handler): EventEmitter {
+ this.on(event, handler);
+ return this;
+ }
+
+ /**
+ * Emit an event and call any associated listeners.
+ *
+ * @param event - the event you'd like to emit
+ * @param eventData - any data you'd like to emit with the event
+ * @returns `true` if there are any listeners, `false` if there are not.
+ */
+ emit(event: EventType, eventData?: any): boolean {
+ this.emitter.emit(event, eventData);
+ return this.eventListenersCount(event) > 0;
+ }
+
+ /**
+ * Like `on` but the listener will only be fired once and then it will be removed.
+ * @param event - the event you'd like to listen to
+ * @param handler - the handler function to run when the event occurs
+ * @returns `this` to enable you to chain calls.
+ */
+ once(event: EventType, handler: Handler): EventEmitter {
+ const onceHandler: Handler = (eventData) => {
+ handler(eventData);
+ this.off(event, onceHandler);
+ };
+
+ return this.on(event, onceHandler);
+ }
+
+ /**
+ * Gets the number of listeners for a given event.
+ *
+ * @param event - the event to get the listener count for
+ * @returns the number of listeners bound to the given event
+ */
+ listenerCount(event: EventType): number {
+ return this.eventListenersCount(event);
+ }
+
+ /**
+ * Removes all listeners. If given an event argument, it will remove only
+ * listeners for that event.
+ * @param event - the event to remove listeners for.
+ * @returns `this` to enable you to chain calls.
+ */
+ removeAllListeners(event?: EventType): EventEmitter {
+ if (event) {
+ this.eventsMap.delete(event);
+ } else {
+ this.eventsMap.clear();
+ }
+ return this;
+ }
+
+ private eventListenersCount(event: EventType): number {
+ return this.eventsMap.has(event) ? this.eventsMap.get(event).length : 0;
+ }
+}
diff --git a/src/FrameManager.ts b/src/FrameManager.ts
index a66a54cd053..d16ab3698ac 100644
--- a/src/FrameManager.ts
+++ b/src/FrameManager.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import * as EventEmitter from 'events';
+import { EventEmitter } from './EventEmitter';
import { helper, assert, debugError } from './helper';
import { Events } from './Events';
import { ExecutionContext, EVALUATION_SCRIPT_URL } from './ExecutionContext';
diff --git a/src/NetworkManager.ts b/src/NetworkManager.ts
index 1cd23251074..8043744f611 100644
--- a/src/NetworkManager.ts
+++ b/src/NetworkManager.ts
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import * as EventEmitter from 'events';
+import { EventEmitter } from './EventEmitter';
import { helper, assert, debugError } from './helper';
import Protocol from './protocol';
import { Events } from './Events';
diff --git a/src/Page.ts b/src/Page.ts
index efec6bbc6f0..be1a593df63 100644
--- a/src/Page.ts
+++ b/src/Page.ts
@@ -15,7 +15,7 @@
*/
import * as fs from 'fs';
-import * as EventEmitter from 'events';
+import { EventEmitter } from './EventEmitter';
import * as mime from 'mime';
import { Events } from './Events';
import { Connection, CDPSession } from './Connection';
@@ -183,7 +183,7 @@ class ScreenshotTaskQueue {
* })();
* ```
*
- * The Page class emits various events which are documented in the {@link PageEmittedEvents} enum.
+ * The Page class extends from Puppeteer's {@link EventEmitter } class and will emit various events which are documented in the {@link PageEmittedEvents} enum.
*
* @example
* This example logs a message for a single page `load` event:
diff --git a/src/WebSocketTransport.ts b/src/WebSocketTransport.ts
index d7faf45ae43..1685edace87 100644
--- a/src/WebSocketTransport.ts
+++ b/src/WebSocketTransport.ts
@@ -13,8 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import * as NodeWebSocket from 'ws';
import { ConnectionTransport } from './ConnectionTransport';
+import NodeWebSocket from 'ws';
export class WebSocketTransport implements ConnectionTransport {
static create(url: string): Promise {
diff --git a/src/WebWorker.ts b/src/WebWorker.ts
index cf8b431eacc..42f4a692a60 100644
--- a/src/WebWorker.ts
+++ b/src/WebWorker.ts
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { EventEmitter } from 'events';
+import { EventEmitter } from './EventEmitter';
import { debugError } from './helper';
import { ExecutionContext } from './ExecutionContext';
import { JSHandle } from './JSHandle';
diff --git a/src/api-docs-entry.ts b/src/api-docs-entry.ts
index d9d3b3791bf..e114d555937 100644
--- a/src/api-docs-entry.ts
+++ b/src/api-docs-entry.ts
@@ -32,6 +32,7 @@ export * from './Coverage';
export * from './Dialog';
export * from './JSHandle';
export * from './ExecutionContext';
+export * from './EventEmitter';
export * from './FileChooser';
export * from './FrameManager';
export * from './JSHandle';
diff --git a/src/api.ts b/src/api.ts
index 781c6a5a95b..e9cd57f5f8b 100644
--- a/src/api.ts
+++ b/src/api.ts
@@ -29,6 +29,7 @@ module.exports = {
Dialog: require('./Dialog').Dialog,
ElementHandle: require('./JSHandle').ElementHandle,
ExecutionContext: require('./ExecutionContext').ExecutionContext,
+ EventEmitter: require('./EventEmitter').EventEmitter,
FileChooser: require('./FileChooser').FileChooser,
Frame: require('./FrameManager').Frame,
JSHandle: require('./JSHandle').JSHandle,
diff --git a/src/helper.ts b/src/helper.ts
index 11b50034c3a..c1103e9104f 100644
--- a/src/helper.ts
+++ b/src/helper.ts
@@ -14,11 +14,12 @@
* limitations under the License.
*/
import { TimeoutError } from './Errors';
-import * as debug from 'debug';
+import debug from 'debug';
import * as fs from 'fs';
import { CDPSession } from './Connection';
import { promisify } from 'util';
import Protocol from './protocol';
+import { CommonEventEmitter } from './EventEmitter';
const openAsync = promisify(fs.open);
const writeAsync = promisify(fs.write);
@@ -131,13 +132,13 @@ function installAsyncStackHooks(classType: AnyClass): void {
}
export interface PuppeteerEventListener {
- emitter: NodeJS.EventEmitter;
+ emitter: CommonEventEmitter;
eventName: string | symbol;
handler: (...args: any[]) => void;
}
function addEventListener(
- emitter: NodeJS.EventEmitter,
+ emitter: CommonEventEmitter,
eventName: string | symbol,
handler: (...args: any[]) => void
): PuppeteerEventListener {
@@ -147,7 +148,7 @@ function addEventListener(
function removeEventListeners(
listeners: Array<{
- emitter: NodeJS.EventEmitter;
+ emitter: CommonEventEmitter;
eventName: string | symbol;
handler: (...args: any[]) => void;
}>
@@ -166,7 +167,7 @@ function isNumber(obj: unknown): obj is number {
}
async function waitForEvent(
- emitter: NodeJS.EventEmitter,
+ emitter: CommonEventEmitter,
eventName: string | symbol,
predicate: (event: T) => boolean,
timeout: number,
diff --git a/src/launcher/BrowserRunner.ts b/src/launcher/BrowserRunner.ts
index d225de0cb5c..b6785af8ca9 100644
--- a/src/launcher/BrowserRunner.ts
+++ b/src/launcher/BrowserRunner.ts
@@ -14,9 +14,9 @@
* limitations under the License.
*/
-import * as debug from 'debug';
+import debug from 'debug';
-import * as removeFolder from 'rimraf';
+import removeFolder from 'rimraf';
import * as childProcess from 'child_process';
import { helper, assert, debugError } from '../helper';
import { LaunchOptions } from './LaunchOptions';
diff --git a/test/EventEmitter.spec.js b/test/EventEmitter.spec.js
new file mode 100644
index 00000000000..446e6db1e86
--- /dev/null
+++ b/test/EventEmitter.spec.js
@@ -0,0 +1,154 @@
+const { EventEmitter } = require('../lib/EventEmitter');
+const sinon = require('sinon');
+const expect = require('expect');
+
+describe('EventEmitter', () => {
+ let emitter;
+
+ beforeEach(() => {
+ emitter = new EventEmitter();
+ });
+
+ describe('on', () => {
+ const onTests = (methodName) => {
+ it(`${methodName}: adds an event listener that is fired when the event is emitted`, () => {
+ const listener = sinon.spy();
+ emitter[methodName]('foo', listener);
+ emitter.emit('foo');
+ expect(listener.callCount).toEqual(1);
+ });
+
+ it(`${methodName} sends the event data to the handler`, () => {
+ const listener = sinon.spy();
+ const data = {};
+ emitter[methodName]('foo', listener);
+ emitter.emit('foo', data);
+ expect(listener.callCount).toEqual(1);
+ expect(listener.firstCall.args[0]).toBe(data);
+ });
+
+ it(`${methodName}: supports chaining`, () => {
+ const listener = sinon.spy();
+ const returnValue = emitter[methodName]('foo', listener);
+ expect(returnValue).toBe(emitter);
+ });
+ };
+ onTests('on');
+ // we support addListener for legacy reasons
+ onTests('addListener');
+ });
+
+ describe('off', () => {
+ const offTests = (methodName) => {
+ it(`${methodName}: removes the listener so it is no longer called`, () => {
+ const listener = sinon.spy();
+ emitter.on('foo', listener);
+ emitter.emit('foo');
+ expect(listener.callCount).toEqual(1);
+ emitter.off('foo', listener);
+ emitter.emit('foo');
+ expect(listener.callCount).toEqual(1);
+ });
+
+ it(`${methodName}: supports chaining`, () => {
+ const listener = sinon.spy();
+ emitter.on('foo', listener);
+ const returnValue = emitter.off('foo', listener);
+ expect(returnValue).toBe(emitter);
+ });
+ };
+ offTests('off');
+ // we support removeListener for legacy reasons
+ offTests('removeListener');
+ });
+
+ describe('once', () => {
+ it('only calls the listener once and then removes it', () => {
+ const listener = sinon.spy();
+ emitter.once('foo', listener);
+ emitter.emit('foo');
+ expect(listener.callCount).toEqual(1);
+ emitter.emit('foo');
+ expect(listener.callCount).toEqual(1);
+ });
+
+ it('supports chaining', () => {
+ const listener = sinon.spy();
+ const returnValue = emitter.once('foo', listener);
+ expect(returnValue).toBe(emitter);
+ });
+ });
+
+ describe('emit', () => {
+ it('calls all the listeners for an event', () => {
+ const listener1 = sinon.spy();
+ const listener2 = sinon.spy();
+ const listener3 = sinon.spy();
+ emitter.on('foo', listener1).on('foo', listener2).on('bar', listener3);
+
+ emitter.emit('foo');
+
+ expect(listener1.callCount).toEqual(1);
+ expect(listener2.callCount).toEqual(1);
+ expect(listener3.callCount).toEqual(0);
+ });
+
+ it('passes data through to the listener', () => {
+ const listener = sinon.spy();
+ emitter.on('foo', listener);
+ const data = {};
+
+ emitter.emit('foo', data);
+ expect(listener.callCount).toEqual(1);
+ expect(listener.firstCall.args[0]).toBe(data);
+ });
+
+ it('returns true if the event has listeners', () => {
+ const listener = sinon.spy();
+ emitter.on('foo', listener);
+ expect(emitter.emit('foo')).toBe(true);
+ });
+
+ it('returns false if the event has listeners', () => {
+ const listener = sinon.spy();
+ emitter.on('foo', listener);
+ expect(emitter.emit('notFoo')).toBe(false);
+ });
+ });
+
+ describe('listenerCount', () => {
+ it('returns the number of listeners for the given event', () => {
+ emitter.on('foo', () => {});
+ emitter.on('foo', () => {});
+ emitter.on('bar', () => {});
+ expect(emitter.listenerCount('foo')).toEqual(2);
+ expect(emitter.listenerCount('bar')).toEqual(1);
+ expect(emitter.listenerCount('noListeners')).toEqual(0);
+ });
+ });
+
+ describe('removeAllListeners', () => {
+ it('removes every listener from all events by default', () => {
+ emitter.on('foo', () => {}).on('bar', () => {});
+
+ emitter.removeAllListeners();
+ expect(emitter.emit('foo')).toBe(false);
+ expect(emitter.emit('bar')).toBe(false);
+ });
+
+ it('returns the emitter for chaining', () => {
+ expect(emitter.removeAllListeners()).toBe(emitter);
+ });
+
+ it('can filter to remove only listeners for a given event name', () => {
+ emitter
+ .on('foo', () => {})
+ .on('bar', () => {})
+ .on('bar', () => {});
+
+ emitter.removeAllListeners('bar');
+ expect(emitter.emit('foo')).toBe(true);
+ expect(emitter.emit('bar')).toBe(false);
+ });
+ });
+});
diff --git a/test/page.spec.js b/test/page.spec.js
index f098df8fd45..8ee92899f8e 100644
--- a/test/page.spec.js
+++ b/test/page.spec.js
@@ -18,6 +18,7 @@ const path = require('path');
const utils = require('./utils');
const { waitEvent } = utils;
const expect = require('expect');
+const sinon = require('sinon');
const {
getTestState,
setupTestBrowserHooks,
@@ -130,6 +131,29 @@ describe('Page', function () {
});
});
+ // This test fails on Firefox on CI consistently but cannot be replicated
+ // locally. Skipping for now to unblock the Mitt release and given FF support
+ // isn't fully done yet but raising an issue to ask the FF folks to have a
+ // look at this.
+ describeFailsFirefox('removing and adding event handlers', () => {
+ it('should correctly fire event handlers as they are added and then removed', async () => {
+ const { page, server } = getTestState();
+
+ const handler = sinon.spy();
+ page.on('response', handler);
+ await page.goto(server.EMPTY_PAGE);
+ expect(handler.callCount).toBe(1);
+ page.off('response', handler);
+ await page.goto(server.EMPTY_PAGE);
+ // Still one because we removed the handler.
+ expect(handler.callCount).toBe(1);
+ page.on('response', handler);
+ await page.goto(server.EMPTY_PAGE);
+ // Two now because we added the handler back.
+ expect(handler.callCount).toBe(2);
+ });
+ });
+
describeFailsFirefox('Page.Events.error', function () {
it('should throw when page crashes', async () => {
const { page } = getTestState();
diff --git a/tsconfig.json b/tsconfig.json
index 08e9ba922ea..da1d0b244af 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -7,7 +7,8 @@
"moduleResolution": "node",
"module": "CommonJS",
"declaration": true,
- "declarationMap": true
+ "declarationMap": true,
+ "esModuleInterop": true
},
"include": [
"src"
diff --git a/utils/doclint/check_public_api/index.js b/utils/doclint/check_public_api/index.js
index 5b27ba7205c..40e597f8eb0 100644
--- a/utils/doclint/check_public_api/index.js
+++ b/utils/doclint/check_public_api/index.js
@@ -180,6 +180,27 @@ const expectedNonExistingMethods = new Map([
['Page', new Set(['emulateMedia'])],
]);
+// All the methods from our EventEmitter that we don't document for each subclass.
+const EVENT_LISTENER_METHODS = new Set([
+ 'emit',
+ 'listenerCount',
+ 'off',
+ 'on',
+ 'once',
+ 'removeListener',
+ 'addListener',
+ 'removeAllListeners',
+]);
+
+/* Methods that are defined in code but are not documented */
+const expectedNotFoundMethods = new Map([
+ ['Browser', EVENT_LISTENER_METHODS],
+ ['BrowserContext', EVENT_LISTENER_METHODS],
+ ['CDPSession', EVENT_LISTENER_METHODS],
+ ['Page', EVENT_LISTENER_METHODS],
+ ['WebWorker', EVENT_LISTENER_METHODS],
+]);
+
/**
* @param {!Documentation} actual
* @param {!Documentation} expected
@@ -205,14 +226,24 @@ function compareDocumentations(actual, expected) {
const methodDiff = diff(actualMethods, expectedMethods);
for (const methodName of methodDiff.extra) {
- const missingMethodsForClass = expectedNonExistingMethods.get(className);
- if (missingMethodsForClass && missingMethodsForClass.has(methodName))
+ const nonExistingMethodsForClass = expectedNonExistingMethods.get(
+ className
+ );
+ if (
+ nonExistingMethodsForClass &&
+ nonExistingMethodsForClass.has(methodName)
+ )
continue;
errors.push(`Non-existing method found: ${className}.${methodName}()`);
}
- for (const methodName of methodDiff.missing)
+
+ for (const methodName of methodDiff.missing) {
+ const missingMethodsForClass = expectedNotFoundMethods.get(className);
+ if (missingMethodsForClass && missingMethodsForClass.has(methodName))
+ continue;
errors.push(`Method not found: ${className}.${methodName}()`);
+ }
for (const methodName of methodDiff.equal) {
const actualMethod = actualClass.methods.get(methodName);
@@ -619,6 +650,62 @@ function compareDocumentations(actual, expected) {
expectedName: 'SnapshotOptions',
},
],
+ [
+ 'Method EventEmitter.emit() event',
+ {
+ actualName: 'string|symbol',
+ expectedName: 'Object',
+ },
+ ],
+ [
+ 'Method EventEmitter.listenerCount() event',
+ {
+ actualName: 'string|symbol',
+ expectedName: 'Object',
+ },
+ ],
+ [
+ 'Method EventEmitter.off() event',
+ {
+ actualName: 'string|symbol',
+ expectedName: 'Object',
+ },
+ ],
+ [
+ 'Method EventEmitter.on() event',
+ {
+ actualName: 'string|symbol',
+ expectedName: 'Object',
+ },
+ ],
+ [
+ 'Method EventEmitter.once() event',
+ {
+ actualName: 'string|symbol',
+ expectedName: 'Object',
+ },
+ ],
+ [
+ 'Method EventEmitter.removeListener() event',
+ {
+ actualName: 'string|symbol',
+ expectedName: 'Object',
+ },
+ ],
+ [
+ 'Method EventEmitter.addListener() event',
+ {
+ actualName: 'string|symbol',
+ expectedName: 'Object',
+ },
+ ],
+ [
+ 'Method EventEmitter.removeAllListeners() event',
+ {
+ actualName: 'string|symbol',
+ expectedName: 'Object',
+ },
+ ],
]);
const expectedForSource = expectedNamingMismatches.get(source);