feat(events): allow waiting for removeAllListeners (#31941)

This commit is contained in:
Pavel Feldman 2024-08-05 21:14:35 -07:00 committed by GitHub
parent 193013c9ee
commit 3c87f217df
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 1280 additions and 987 deletions

View file

@ -296,6 +296,18 @@ testing frameworks should explicitly create [`method: Browser.newContext`] follo
### option: Browser.newPage.storageStatePath = %%-csharp-java-context-option-storage-state-path-%%
* since: v1.9
## async method: Browser.removeAllListeners
* since: v1.47
Removes all the listeners of the given type if the type is given. Otherwise removes all the listeners.
### param: Browser.removeAllListeners.type
* since: v1.47
- `type` ?<[string]>
### option: Browser.removeAllListeners.behavior = %%-remove-all-listeners-options-behavior-%%
* since: v1.47
## async method: Browser.startTracing
* since: v1.11
* langs: java, js, python

View file

@ -1016,6 +1016,18 @@ Creates a new page in the browser context.
Returns all open pages in the context.
## async method: BrowserContext.removeAllListeners
* since: v1.47
Removes all the listeners of the given type if the type is given. Otherwise removes all the listeners.
### param: BrowserContext.removeAllListeners.type
* since: v1.47
- `type` ?<[string]>
### option: BrowserContext.removeAllListeners.behavior = %%-remove-all-listeners-options-behavior-%%
* since: v1.47
## property: BrowserContext.request
* since: v1.16
* langs:

View file

@ -3340,6 +3340,17 @@ Specifies the maximum number of times this handler should be called. Unlimited b
By default, after calling the handler Playwright will wait until the overlay becomes hidden, and only then Playwright will continue with the action/assertion that triggered the handler. This option allows to opt-out of this behavior, so that overlay can stay visible after the handler has run.
## async method: Page.removeAllListeners
* since: v1.47
Removes all the listeners of the given type if the type is given. Otherwise removes all the listeners.
### param: Page.removeAllListeners.type
* since: v1.47
- `type` ?<[string]>
### option: Page.removeAllListeners.behavior = %%-remove-all-listeners-options-behavior-%%
* since: v1.47
## async method: Page.removeLocatorHandler
* since: v1.44

View file

@ -781,6 +781,16 @@ Whether to allow sites to register Service workers. Defaults to `'allow'`.
* `'allow'`: [Service Workers](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) can be registered.
* `'block'`: Playwright will block all registration of Service Workers.
## remove-all-listeners-options-behavior
* langs: js
* since: v1.47
- `behavior` <[RemoveAllListenersBehavior]<"wait"|"ignoreErrors"|"default">>
Specifies whether to wait for already running listeners and what to do if they throw errors:
* `'default'` - do not wait for current listener calls (if any) to finish, if the listener throws, it may result in unhandled error
* `'wait'` - wait for current listener calls (if any) to finish
* `'ignoreErrors'` - do not wait for current listener calls (if any) to finish, all errors thrown by the listeners after removal are silently caught
## unroute-all-options-behavior
* langs: js, csharp, python
* since: v1.41
@ -791,6 +801,7 @@ Specifies whether to wait for already running handlers and what to do if they th
* `'wait'` - wait for current handler calls (if any) to finish
* `'ignoreErrors'` - do not wait for current handler calls (if any) to finish, all errors thrown by the handlers after unrouting are silently caught
## select-options-values
* langs: java, js, csharp
- `values` <[null]|[string]|[ElementHandle]|[Array]<[string]>|[Object]|[Array]<[ElementHandle]>|[Array]<[Object]>>

View file

@ -23,7 +23,7 @@
*/
type EventType = string | symbol;
type Listener = (...args: any[]) => void;
type Listener = (...args: any[]) => any;
type EventMap = Record<EventType, Listener | Listener[]>;
import { EventEmitter as OriginalEventEmitter } from 'events';
import type { EventEmitter as EventEmitterType } from 'events';
@ -34,6 +34,8 @@ export class EventEmitter implements EventEmitterType {
private _events: EventMap | undefined = undefined;
private _eventsCount = 0;
private _maxListeners: number | undefined = undefined;
readonly _pendingHandlers = new Map<EventType, Set<Promise<void>>>();
private _rejectionHandler: ((error: Error) => void) | undefined;
constructor() {
if (this._events === undefined || this._events === Object.getPrototypeOf(this)._events) {
@ -66,15 +68,32 @@ export class EventEmitter implements EventEmitterType {
return false;
if (typeof handler === 'function') {
Reflect.apply(handler, this, args);
this._callHandler(type, handler, args);
} else {
const len = handler.length;
const listeners = handler.slice();
for (let i = 0; i < len; ++i)
Reflect.apply(listeners[i], this, args);
this._callHandler(type, listeners[i], args);
}
return true;
}
return true;
private _callHandler(type: EventType, handler: Listener, args: any[]): void {
const promise = Reflect.apply(handler, this, args);
if (!(promise instanceof Promise))
return;
let set = this._pendingHandlers.get(type);
if (!set) {
set = new Set();
this._pendingHandlers.set(type, set);
}
set.add(promise);
promise.catch(e => {
if (this._rejectionHandler)
this._rejectionHandler(e);
else
throw e;
}).finally(() => set.delete(promise));
}
addListener(type: EventType, listener: Listener): this {
@ -214,10 +233,34 @@ export class EventEmitter implements EventEmitterType {
return this.removeListener(type, listener);
}
removeAllListeners(type?: string): this {
removeAllListeners(type?: EventType): this;
removeAllListeners(type: EventType | undefined, options: { behavior?: 'wait'|'ignoreErrors'|'default' }): Promise<void>;
removeAllListeners(type?: string, options?: { behavior?: 'wait'|'ignoreErrors'|'default' }): this | Promise<void> {
this._removeAllListeners(type);
if (!options)
return this;
if (options.behavior === 'wait') {
const errors: Error[] = [];
this._rejectionHandler = error => errors.push(error);
// eslint-disable-next-line internal-playwright/await-promise-in-class-returns
return this._waitFor(type).then(() => {
if (errors.length)
throw errors[0];
});
}
if (options.behavior === 'ignoreErrors')
this._rejectionHandler = () => {};
// eslint-disable-next-line internal-playwright/await-promise-in-class-returns
return Promise.resolve();
}
private _removeAllListeners(type?: string) {
const events = this._events;
if (!events)
return this;
return;
// not listening for removeListener, no need to emit
if (!events.removeListener) {
@ -230,7 +273,7 @@ export class EventEmitter implements EventEmitterType {
else
delete events[type];
}
return this;
return;
}
// emit removeListener for all listeners on all events
@ -241,12 +284,12 @@ export class EventEmitter implements EventEmitterType {
key = keys[i];
if (key === 'removeListener')
continue;
this.removeAllListeners(key);
this._removeAllListeners(key);
}
this.removeAllListeners('removeListener');
this._removeAllListeners('removeListener');
this._events = Object.create(null);
this._eventsCount = 0;
return this;
return;
}
const listeners = events[type];
@ -258,8 +301,6 @@ export class EventEmitter implements EventEmitterType {
for (let i = listeners.length - 1; i >= 0; i--)
this.removeListener(type, listeners[i]);
}
return this;
}
listeners(type: EventType): Listener[] {
@ -286,6 +327,18 @@ export class EventEmitter implements EventEmitterType {
return this._eventsCount > 0 && this._events ? Reflect.ownKeys(this._events) : [];
}
private async _waitFor(type?: EventType) {
let promises: Promise<void>[] = [];
if (type) {
promises = [...(this._pendingHandlers.get(type) || [])];
} else {
promises = [];
for (const [, pending] of this._pendingHandlers)
promises.push(...pending);
}
await Promise.all(promises);
}
private _listeners(target: EventEmitter, type: EventType, unwrap: boolean): Listener[] {
const events = target._events;
@ -310,7 +363,7 @@ function checkListener(listener: any) {
class OnceWrapper {
private _fired = false;
readonly wrapperFunction: (...args: any[]) => void;
readonly wrapperFunction: (...args: any[]) => Promise<void> | void;
readonly _listener: Listener;
private _eventEmitter: EventEmitter;
private _eventType: EventType;

File diff suppressed because it is too large Load diff

View file

@ -20,7 +20,6 @@
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
import events from 'events';
import { EventEmitter } from '../../../packages/playwright-core/lib/client/eventEmitter';
import { test, expect } from '@playwright/test';
@ -32,7 +31,6 @@ test('Listener count test', () => {
// Allow any type
emitter.on(123, () => {});
expect(events.listenerCount(emitter, 'foo')).toEqual(2);
expect(emitter.listenerCount('foo')).toEqual(2);
expect(emitter.listenerCount('bar')).toEqual(0);
expect(emitter.listenerCount('baz')).toEqual(1);

View file

@ -40,7 +40,7 @@ test('EventEmitter listeners with one listener', () => {
expect(listeners).toHaveLength(1);
expect(listeners[0]).toEqual(listener);
ee.removeAllListeners('foo');
void ee.removeAllListeners('foo');
expect<Array<any>>(ee.listeners('foo')).toHaveLength(0);
expect(Array.isArray(fooListeners)).toBeTruthy();

View file

@ -72,7 +72,7 @@ test('add and remove listeners', () => {
e.on('foo', callback1);
e.on('foo', callback2);
expect(e.listeners('foo')).toHaveLength(2);
e.removeAllListeners('foo');
void e.removeAllListeners('foo');
expect(e.listeners('foo')).toHaveLength(0);
});

View file

@ -0,0 +1,80 @@
/**
* Copyright 2018 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.
*/
import { ManualPromise } from '../../../packages/playwright-core/lib/utils/manualPromise';
import { EventEmitter } from '../../../packages/playwright-core/lib/client/eventEmitter';
import { test, expect } from '@playwright/test';
test('should not throw with ignoreErrors', async () => {
const ee = new EventEmitter();
const releaseHandler = new ManualPromise();
ee.on('console', async () => {
await releaseHandler;
throw new Error('Error in console handler');
});
ee.emit('console');
await ee.removeAllListeners('console', { behavior: 'ignoreErrors' });
releaseHandler.resolve();
});
test('should wait', async () => {
const ee = new EventEmitter();
const releaseHandler = new ManualPromise();
let value = 0;
ee.on('console', async () => {
await releaseHandler;
value = 42;
});
ee.emit('console');
const removePromise = ee.removeAllListeners('console', { behavior: 'wait' });
releaseHandler.resolve();
await removePromise;
expect(value).toBe(42);
});
test('should wait all', async () => {
const ee = new EventEmitter();
const releaseHandler = new ManualPromise();
const values = [];
ee.on('a', async () => {
await releaseHandler;
values.push(42);
});
ee.on('b', async () => {
await releaseHandler;
values.push(43);
});
ee.emit('a');
ee.emit('b');
const removePromise = ee.removeAllListeners(undefined, { behavior: 'wait' });
releaseHandler.resolve();
await removePromise;
expect(values).toEqual([42, 43]);
});
test('wait should throw', async () => {
const ee = new EventEmitter();
const releaseHandler = new ManualPromise();
ee.on('console', async () => {
await releaseHandler;
throw new Error('Error in handler');
});
ee.emit('console');
const removePromise = ee.removeAllListeners('console', { behavior: 'wait' });
releaseHandler.resolve();
await expect(removePromise).rejects.toThrow('Error in handler');
});

View file

@ -58,8 +58,8 @@ test('listeners', () => {
const barListeners = ee.listeners('bar');
const bazListeners = ee.listeners('baz');
ee.on('removeListener', expectWrapper(['bar', 'baz', 'baz']));
ee.removeAllListeners('bar');
ee.removeAllListeners('baz');
void ee.removeAllListeners('bar');
void ee.removeAllListeners('baz');
let listeners = ee.listeners('foo');
expect(Array.isArray(listeners)).toBeTruthy();
@ -91,7 +91,7 @@ test('removeAllListeners removes all listeners', () => {
ee.on('bar', () => { });
ee.on('removeListener', expectWrapper(['foo', 'bar', 'removeListener']));
ee.on('removeListener', expectWrapper(['foo', 'bar']));
ee.removeAllListeners();
void ee.removeAllListeners();
let listeners = ee.listeners('foo');
expect(Array.isArray(listeners)).toBeTruthy();
@ -120,7 +120,7 @@ test('listener count after removeAllListeners', () => {
ee.on('baz', () => { });
ee.on('baz', () => { });
expect(ee.listeners('baz').length).toEqual(expectLength + 1);
ee.removeAllListeners('baz');
void ee.removeAllListeners('baz');
expect(ee.listeners('baz').length).toEqual(0);
});

View file

@ -28,7 +28,7 @@ class MyEE extends EventEmitter {
super();
this.once(1, cb);
this.emit(1);
this.removeAllListeners();
void this.removeAllListeners();
}
}

View file

@ -34,7 +34,7 @@ test('should support symbols', () => {
ee.emit(foo);
ee.removeAllListeners();
void ee.removeAllListeners();
expect(ee.listeners(foo).length).toEqual(0);
ee.on(foo, listener);

View file

@ -0,0 +1,67 @@
/**
* Copyright 2018 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.
*/
import { ManualPromise } from '../../packages/playwright-core/lib/utils/manualPromise';
import { test as it, expect } from './pageTest';
// This test is mostly for type checking, the actual tests are in the library/events.
it('should not throw with ignoreErrors', async ({ page }) => {
const reachedHandler = new ManualPromise();
const releaseHandler = new ManualPromise();
page.on('console', async () => {
reachedHandler.resolve();
await releaseHandler;
throw new Error('Error in console handler');
});
await page.evaluate('console.log(1)');
await reachedHandler;
await page.removeAllListeners('console', { behavior: 'ignoreErrors' });
releaseHandler.resolve();
await page.waitForTimeout(1000);
});
it('should wait', async ({ page }) => {
const reachedHandler = new ManualPromise();
const releaseHandler = new ManualPromise();
let value = 0;
page.on('console', async () => {
reachedHandler.resolve();
value = 42;
});
await page.evaluate('console.log(1)');
await reachedHandler;
const removePromise = page.removeAllListeners('console', { behavior: 'wait' });
releaseHandler.resolve();
await removePromise;
expect(value).toBe(42);
});
it('wait should throw', async ({ page }) => {
const reachedHandler = new ManualPromise();
const releaseHandler = new ManualPromise();
page.on('console', async () => {
reachedHandler.resolve();
await releaseHandler;
throw new Error('Error in handler');
});
await page.evaluate('console.log(1)');
await reachedHandler;
const removePromise = page.removeAllListeners('console', { behavior: 'wait' });
releaseHandler.resolve();
await expect(removePromise).rejects.toThrow('Error in handler');
});

View file

@ -56,7 +56,7 @@ module.exports = function lint(documentation, jsSources, apiFileName) {
continue;
}
for (const member of cls.membersArray) {
if (member.kind === 'event')
if (member.kind === 'event' || member.alias === 'removeAllListeners')
continue;
const params = methods.get(member.alias);
if (!params) {

View file

@ -62,6 +62,9 @@ export interface Page {
exposeBinding(name: string, playwrightBinding: (source: BindingSource, arg: JSHandle) => any, options: { handle: true }): Promise<void>;
exposeBinding(name: string, playwrightBinding: (source: BindingSource, ...args: any[]) => any, options?: { handle?: boolean }): Promise<void>;
removeAllListeners(type?: string): this;
removeAllListeners(type: string | undefined, options: { behavior?: 'wait'|'ignoreErrors'|'default' }): Promise<void>;
}
export interface Frame {
@ -101,6 +104,14 @@ export interface BrowserContext {
exposeBinding(name: string, playwrightBinding: (source: BindingSource, ...args: any[]) => any, options?: { handle?: boolean }): Promise<void>;
addInitScript<Arg>(script: PageFunction<Arg, any> | { path?: string, content?: string }, arg?: Arg): Promise<void>;
removeAllListeners(type?: string): this;
removeAllListeners(type: string | undefined, options: { behavior?: 'wait'|'ignoreErrors'|'default' }): Promise<void>;
}
export interface Browser {
removeAllListeners(type?: string): this;
removeAllListeners(type: string | undefined, options: { behavior?: 'wait'|'ignoreErrors'|'default' }): Promise<void>;
}
export interface Worker {