feat(events): allow waiting for removeAllListeners (#31941)
This commit is contained in:
parent
193013c9ee
commit
3c87f217df
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]>>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
1964
packages/playwright-core/types/types.d.ts
vendored
1964
packages/playwright-core/types/types.d.ts
vendored
File diff suppressed because it is too large
Load diff
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
|||
80
tests/library/events/remove-all-listeners-wait.spec.ts
Normal file
80
tests/library/events/remove-all-listeners-wait.spec.ts
Normal 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');
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ class MyEE extends EventEmitter {
|
|||
super();
|
||||
this.once(1, cb);
|
||||
this.emit(1);
|
||||
this.removeAllListeners();
|
||||
void this.removeAllListeners();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
67
tests/page/page-listeners.spec.ts
Normal file
67
tests/page/page-listeners.spec.ts
Normal 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');
|
||||
});
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
11
utils/generate_types/overrides.d.ts
vendored
11
utils/generate_types/overrides.d.ts
vendored
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue