From 58b0c76f2068417279fdb6acc34fe8db38c19c16 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Mon, 29 Jul 2024 17:11:31 -0700 Subject: [PATCH] chore: use soft event emitter (#31868) --- .../src/client/channelOwner.ts | 2 +- .../src/client/eventEmitter.ts | 345 ++++++++++++++++++ tests/library/events/add-listeners.spec.ts | 103 ++++++ .../events/check-listener-leaks.spec.ts | 92 +++++ tests/library/events/events-list.spec.ts | 55 +++ tests/library/events/listener-count.spec.ts | 40 ++ .../events/listeners-side-effects.spec.ts | 61 ++++ tests/library/events/listeners.spec.ts | 154 ++++++++ tests/library/events/max-listeners.spec.ts | 43 +++ tests/library/events/method-names.spec.ts | 40 ++ tests/library/events/modify-in-emit.spec.ts | 93 +++++ tests/library/events/num-args.spec.ts | 62 ++++ tests/library/events/once.spec.ts | 84 +++++ tests/library/events/prepend.spec.ts | 52 +++ .../events/remove-all-listeners.spec.ts | 136 +++++++ tests/library/events/remove-listeners.spec.ts | 188 ++++++++++ .../set-max-listeners-side-effects.spec.ts | 34 ++ .../events/special-event-names.spec.ts | 46 +++ tests/library/events/subclass.spec.ts | 54 +++ tests/library/events/symbols.spec.ts | 46 +++ tests/library/events/utils.ts | 46 +++ 21 files changed, 1775 insertions(+), 1 deletion(-) create mode 100644 packages/playwright-core/src/client/eventEmitter.ts create mode 100644 tests/library/events/add-listeners.spec.ts create mode 100644 tests/library/events/check-listener-leaks.spec.ts create mode 100644 tests/library/events/events-list.spec.ts create mode 100644 tests/library/events/listener-count.spec.ts create mode 100644 tests/library/events/listeners-side-effects.spec.ts create mode 100644 tests/library/events/listeners.spec.ts create mode 100644 tests/library/events/max-listeners.spec.ts create mode 100644 tests/library/events/method-names.spec.ts create mode 100644 tests/library/events/modify-in-emit.spec.ts create mode 100644 tests/library/events/num-args.spec.ts create mode 100644 tests/library/events/once.spec.ts create mode 100644 tests/library/events/prepend.spec.ts create mode 100644 tests/library/events/remove-all-listeners.spec.ts create mode 100644 tests/library/events/remove-listeners.spec.ts create mode 100644 tests/library/events/set-max-listeners-side-effects.spec.ts create mode 100644 tests/library/events/special-event-names.spec.ts create mode 100644 tests/library/events/subclass.spec.ts create mode 100644 tests/library/events/symbols.spec.ts create mode 100644 tests/library/events/utils.ts diff --git a/packages/playwright-core/src/client/channelOwner.ts b/packages/playwright-core/src/client/channelOwner.ts index 4ebe32595c..abe1cbf254 100644 --- a/packages/playwright-core/src/client/channelOwner.ts +++ b/packages/playwright-core/src/client/channelOwner.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { EventEmitter } from 'events'; +import { EventEmitter } from './eventEmitter'; import type * as channels from '@protocol/channels'; import { maybeFindValidator, ValidationError, type ValidatorContext } from '../protocol/validator'; import { debugLogger } from '../utils/debugLogger'; diff --git a/packages/playwright-core/src/client/eventEmitter.ts b/packages/playwright-core/src/client/eventEmitter.ts new file mode 100644 index 0000000000..6de8ca3a31 --- /dev/null +++ b/packages/playwright-core/src/client/eventEmitter.ts @@ -0,0 +1,345 @@ +/** + * Copyright Joyent, Inc. and other Node contributors. + * Modifications copyright (c) Microsoft Corporation. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to permit + * persons to whom the Software is furnished to do so, subject to the + * following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN + * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE + * USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +type EventType = string | symbol; +type Listener = (...args: any[]) => void; +type EventMap = Record; +import { EventEmitter as OriginalEventEmitter } from 'events'; +import type { EventEmitter as EventEmitterType } from 'events'; +import { isUnderTest } from '../utils'; + +export class EventEmitter implements EventEmitterType { + + private _events: EventMap | undefined = undefined; + private _eventsCount = 0; + private _maxListeners: number | undefined = undefined; + + constructor() { + if (this._events === undefined || this._events === Object.getPrototypeOf(this)._events) { + this._events = Object.create(null); + this._eventsCount = 0; + } + this._maxListeners = this._maxListeners || undefined; + this.on = this.addListener; + this.off = this.removeListener; + } + + setMaxListeners(n: number): this { + if (typeof n !== 'number' || n < 0 || Number.isNaN(n)) + throw new RangeError('The value of "n" is out of range. It must be a non-negative number. Received ' + n + '.'); + this._maxListeners = n; + return this; + } + + getMaxListeners(): number { + return this._maxListeners === undefined ? OriginalEventEmitter.defaultMaxListeners : this._maxListeners; + } + + emit(type: EventType, ...args: any[]): boolean { + const events = this._events; + if (events === undefined) + return false; + + const handler = events?.[type]; + if (handler === undefined) + return false; + + if (typeof handler === 'function') { + Reflect.apply(handler, this, args); + } else { + const len = handler.length; + const listeners = handler.slice(); + for (let i = 0; i < len; ++i) + Reflect.apply(listeners[i], this, args); + } + + return true; + } + + addListener(type: EventType, listener: Listener): this { + return this._addListener(type, listener, false); + } + + on(type: EventType, listener: Listener): this { + return this._addListener(type, listener, false); + } + + private _addListener(type: EventType, listener: Listener, prepend: boolean): this { + checkListener(listener); + let events = this._events; + let existing; + if (events === undefined) { + events = this._events = Object.create(null); + this._eventsCount = 0; + } else { + // To avoid recursion in the case that type === "newListener"! Before + // adding it to the listeners, first emit "newListener". + if (events.newListener !== undefined) { + this.emit('newListener', type, unwrapListener(listener)); + + // Re-assign `events` because a newListener handler could have caused the + // this._events to be assigned to a new object + events = this._events!; + } + existing = events[type]; + } + + if (existing === undefined) { + // Optimize the case of one listener. Don't need the extra array object. + existing = events![type] = listener; + ++this._eventsCount; + } else { + if (typeof existing === 'function') { + // Adding the second element, need to change to array. + existing = events![type] = + prepend ? [listener, existing] : [existing, listener]; + // If we've already got an array, just append. + } else if (prepend) { + existing.unshift(listener); + } else { + existing.push(listener); + } + + // Check for listener leak + const m = this.getMaxListeners(); + if (m > 0 && existing.length > m && !(existing as any).warned) { + (existing as any).warned = true; + // No error code for this since it is a Warning + const w = new Error('Possible EventEmitter memory leak detected. ' + + existing.length + ' ' + String(type) + ' listeners ' + + 'added. Use emitter.setMaxListeners() to ' + + 'increase limit') as any; + w.name = 'MaxListenersExceededWarning'; + w.emitter = this; + w.type = type; + w.count = existing.length; + if (!isUnderTest()) { + // eslint-disable-next-line no-console + console.warn(w); + } + } + } + + return this; + } + + prependListener(type: EventType, listener: Listener): this { + return this._addListener(type, listener, true); + } + + once(type: EventType, listener: Listener): this { + checkListener(listener); + this.on(type, new OnceWrapper(this, type, listener).wrapperFunction); + return this; + } + + prependOnceListener(type: EventType, listener: Listener): this { + checkListener(listener); + this.prependListener(type, new OnceWrapper(this, type, listener).wrapperFunction); + return this; + } + + removeListener(type: EventType, listener: Listener): this { + checkListener(listener); + + const events = this._events; + if (events === undefined) + return this; + + const list = events[type]; + if (list === undefined) + return this; + + if (list === listener || (list as any).listener === listener) { + if (--this._eventsCount === 0) { + this._events = Object.create(null); + } else { + delete events[type]; + if (events.removeListener) + this.emit('removeListener', type, (list as any).listener ?? listener); + } + } else if (typeof list !== 'function') { + let position = -1; + let originalListener; + + for (let i = list.length - 1; i >= 0; i--) { + if (list[i] === listener || wrappedListener(list[i]) === listener) { + originalListener = wrappedListener(list[i]); + position = i; + break; + } + } + + if (position < 0) + return this; + + if (position === 0) + list.shift(); + else + list.splice(position, 1); + + if (list.length === 1) + events[type] = list[0]; + + if (events.removeListener !== undefined) + this.emit('removeListener', type, originalListener || listener); + } + + return this; + + } + + off(type: EventType, listener: Listener): this { + return this.removeListener(type, listener); + } + + removeAllListeners(type?: string): this { + const events = this._events; + if (!events) + return this; + + // not listening for removeListener, no need to emit + if (!events.removeListener) { + if (type === undefined) { + this._events = Object.create(null); + this._eventsCount = 0; + } else if (events[type] !== undefined) { + if (--this._eventsCount === 0) + this._events = Object.create(null); + else + delete events[type]; + } + return this; + } + + // emit removeListener for all listeners on all events + if (type === undefined) { + const keys = Object.keys(events); + let key; + for (let i = 0; i < keys.length; ++i) { + key = keys[i]; + if (key === 'removeListener') + continue; + this.removeAllListeners(key); + } + this.removeAllListeners('removeListener'); + this._events = Object.create(null); + this._eventsCount = 0; + return this; + } + + const listeners = events[type]; + + if (typeof listeners === 'function') { + this.removeListener(type, listeners); + } else if (listeners !== undefined) { + // LIFO order + for (let i = listeners.length - 1; i >= 0; i--) + this.removeListener(type, listeners[i]); + } + + return this; + } + + listeners(type: EventType): Listener[] { + return this._listeners(this, type, true); + } + + rawListeners(type: EventType): Listener[] { + return this._listeners(this, type, false); + } + + listenerCount(type: EventType): number { + const events = this._events; + if (events !== undefined) { + const listener = events[type]; + if (typeof listener === 'function') + return 1; + if (listener !== undefined) + return listener.length; + } + return 0; + } + + eventNames(): Array { + return this._eventsCount > 0 && this._events ? Reflect.ownKeys(this._events) : []; + } + + private _listeners(target: EventEmitter, type: EventType, unwrap: boolean): Listener[] { + const events = target._events; + + if (events === undefined) + return []; + + const listener = events[type]; + if (listener === undefined) + return []; + + if (typeof listener === 'function') + return unwrap ? [unwrapListener(listener)] : [listener]; + + return unwrap ? unwrapListeners(listener) : listener.slice(); + } +} + +function checkListener(listener: any) { + if (typeof listener !== 'function') + throw new TypeError('The "listener" argument must be of type Function. Received type ' + typeof listener); +} + +class OnceWrapper { + private _fired = false; + readonly wrapperFunction: (...args: any[]) => void; + readonly _listener: Listener; + private _eventEmitter: EventEmitter; + private _eventType: EventType; + + constructor(eventEmitter: EventEmitter, eventType: EventType, listener: Listener) { + this._eventEmitter = eventEmitter; + this._eventType = eventType; + this._listener = listener; + this.wrapperFunction = this._handle.bind(this); + (this.wrapperFunction as any).listener = listener; + } + + private _handle(...args: any[]) { + if (this._fired) + return; + this._fired = true; + this._eventEmitter.removeListener(this._eventType, this.wrapperFunction); + return this._listener.apply(this._eventEmitter, args); + } +} + +function unwrapListener(l: Listener): Listener { + return wrappedListener(l) ?? l; +} + +function unwrapListeners(arr: Listener[]): Listener[] { + return arr.map(l => wrappedListener(l) ?? l); +} + +function wrappedListener(l: Listener): Listener { + return (l as any).listener; +} diff --git a/tests/library/events/add-listeners.spec.ts b/tests/library/events/add-listeners.spec.ts new file mode 100644 index 0000000000..729b06b0c6 --- /dev/null +++ b/tests/library/events/add-listeners.spec.ts @@ -0,0 +1,103 @@ +// Copyright Joyent, Inc. and other Node contributors. +// Modifications copyright (c) by Microsoft Corporation +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +import { EventEmitter } from '../../../packages/playwright-core/lib/client/eventEmitter'; +import { test, expect } from '@playwright/test'; + +test.describe('EventEmitter tests', () => { + test('should work', () => { + const events_new_listener_emitted = []; + const listeners_new_listener_emitted = []; + + const ee = new EventEmitter(); + + ee.on('newListener', function(event, listener) { + if (event !== 'newListener') { + events_new_listener_emitted.push(event); + listeners_new_listener_emitted.push(listener); + } + }); + + const hello = (a, b) => { + expect(a).toEqual('a'); + expect(b).toEqual('b'); + }; + + ee.once('newListener', (name, listener) => { + expect(name).toEqual('hello'); + expect(listener).toEqual(hello); + + const listeners = ee.listeners('hello'); + expect(Array.isArray(listeners)).toBeTruthy(); + expect(listeners).toHaveLength(0); + }); + + ee.on('hello', hello); + ee.once('foo', () => { throw new Error('foo error'); }); + + expect(Array.isArray(events_new_listener_emitted)).toBeTruthy(); + expect(events_new_listener_emitted).toHaveLength(2); + expect(events_new_listener_emitted[0]).toEqual('hello'); + expect(events_new_listener_emitted[1]).toEqual('foo'); + + expect(Array.isArray(listeners_new_listener_emitted)).toBeTruthy(); + expect(listeners_new_listener_emitted).toHaveLength(2); + expect(listeners_new_listener_emitted[0]).toEqual(hello); + expect(listeners_new_listener_emitted[1]).toThrow(); + + ee.emit('hello', 'a', 'b'); + }); + + test('set max listeners test', () => { + const f = new EventEmitter(); + f.setMaxListeners(0); + }); + + test('Listener order', () => { + const listen1 = function() {}; + const listen2 = function() {}; + const ee = new EventEmitter(); + + ee.once('newListener', function() { + const listeners = ee.listeners('hello'); + expect(Array.isArray(listeners)).toBeTruthy(); + expect(listeners).toHaveLength(0); + ee.once('newListener', function() { + const listeners = ee.listeners('hello'); + expect(Array.isArray(listeners)).toBeTruthy(); + expect(listeners).toHaveLength(0); + }); + ee.on('hello', listen2); + }); + ee.on('hello', listen1); + const listeners = ee.listeners('hello'); + expect(Array.isArray(listeners)).toBeTruthy(); + expect(listeners).toHaveLength(2); + expect(listeners[0]).toEqual(listen2); + expect(listeners[1]).toEqual(listen1); + }); + + test('listener type check', () => { + const ee = new EventEmitter(); + expect(() => ee.on('foo', null)).toThrow('The "listener" argument must be of type Function. Received type object'); + }); +}); diff --git a/tests/library/events/check-listener-leaks.spec.ts b/tests/library/events/check-listener-leaks.spec.ts new file mode 100644 index 0000000000..d9b51f27ef --- /dev/null +++ b/tests/library/events/check-listener-leaks.spec.ts @@ -0,0 +1,92 @@ +// Copyright Joyent, Inc. and other Node contributors. +// Modifications copyright (c) by Microsoft Corporation +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// 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 { setUnderTest } from '../../../packages/playwright-core/lib/utils/debug'; +import { test, expect } from '@playwright/test'; +import * as common from './utils'; + +setUnderTest(); + +test('defaultMaxListeners', () => { + const e = new EventEmitter(); + + for (let i = 0; i < 10; i++) + e.on('default', common.mustNotCall()); + + + expect(e._events['default']).not.toHaveProperty('warned'); + e.on('default', common.mustNotCall()); + expect(e._events['default'].hasOwnProperty('warned')).toBeTruthy(); + + e.setMaxListeners(5); + for (let i = 0; i < 5; i++) + e.on('specific', common.mustNotCall()); + + expect(e._events['specific']).not.toHaveProperty('warned'); + e.on('specific', common.mustNotCall()); + expect(e._events['specific'].hasOwnProperty('warned')).toBeTruthy(); + + // only one + e.setMaxListeners(1); + e.on('only one', common.mustNotCall()); + expect(e._events['only one']).not.toHaveProperty('warned'); + e.on('only one', common.mustNotCall()); + expect(e._events['only one'].hasOwnProperty('warned')).toBeTruthy(); + + // unlimited + e.setMaxListeners(0); + for (let i = 0; i < 1000; i++) + e.on('unlimited', common.mustNotCall()); + + expect(e._events['unlimited']).not.toHaveProperty('warned'); +}); + +test('process-wide', () => { + events.EventEmitter.defaultMaxListeners = 42; + const e = new EventEmitter(); + + for (let i = 0; i < 42; ++i) + e.on('fortytwo', common.mustNotCall()); + + expect(e._events['fortytwo']).not.toHaveProperty('warned'); + e.on('fortytwo', common.mustNotCall()); + expect(e._events['fortytwo'].hasOwnProperty('warned')).toBeTruthy(); + delete e._events['fortytwo'].warned; + + events.EventEmitter.defaultMaxListeners = 44; + e.on('fortytwo', common.mustNotCall()); + expect(e._events['fortytwo']).not.toHaveProperty('warned'); + e.on('fortytwo', common.mustNotCall()); + expect(e._events['fortytwo'].hasOwnProperty('warned')).toBeTruthy(); +}); + +test('_maxListeners still has precedence over defaultMaxListeners', () => { + events.EventEmitter.defaultMaxListeners = 42; + const e = new EventEmitter(); + e.setMaxListeners(1); + e.on('uno', common.mustNotCall()); + expect(e._events['uno']).not.toHaveProperty('warned'); + e.on('uno', common.mustNotCall()); + expect(e._events['uno'].hasOwnProperty('warned')).toBeTruthy(); +}); diff --git a/tests/library/events/events-list.spec.ts b/tests/library/events/events-list.spec.ts new file mode 100644 index 0000000000..97d3c8edc3 --- /dev/null +++ b/tests/library/events/events-list.spec.ts @@ -0,0 +1,55 @@ +// Copyright Joyent, Inc. and other Node contributors. +// Modifications copyright (c) by Microsoft Corporation +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +import { EventEmitter } from '../../../packages/playwright-core/lib/client/eventEmitter'; +import { test, expect } from '@playwright/test'; + +test.describe('EventEmitter', () => { + test('should maintain event names correctly', () => { + const e = new EventEmitter(); + const m = () => {}; + e.on('foo', function() {}); + expect(e.eventNames().length).toBe(1); + expect(e.eventNames()[0]).toBe('foo'); + + e.on('bar', m); + expect(e.eventNames().length).toBe(2); + expect(e.eventNames()[0]).toBe('foo'); + expect(e.eventNames()[1]).toBe('bar'); + + e.removeListener('bar', m); + expect(e.eventNames().length).toBe(1); + expect(e.eventNames()[0]).toBe('foo'); + + if (typeof Symbol !== 'undefined') { + const s = Symbol('s'); + e.on(s, m); + expect(e.eventNames().length).toBe(2); + expect(e.eventNames()[0]).toBe('foo'); + expect(e.eventNames()[1]).toBe(s); + + e.removeListener(s, m); + expect(e.eventNames().length).toBe(1); + expect(e.eventNames()[0]).toBe('foo'); + } + }); +}); diff --git a/tests/library/events/listener-count.spec.ts b/tests/library/events/listener-count.spec.ts new file mode 100644 index 0000000000..8fae8cdfad --- /dev/null +++ b/tests/library/events/listener-count.spec.ts @@ -0,0 +1,40 @@ +// Copyright Joyent, Inc. and other Node contributors. +// Modifications copyright (c) by Microsoft Corporation +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// 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'; + +test('Listener count test', () => { + const emitter = new EventEmitter(); + emitter.on('foo', () => {}); + emitter.on('foo', () => {}); + emitter.on('baz', () => {}); + // 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); + expect(emitter.listenerCount(123)).toEqual(1); +}); diff --git a/tests/library/events/listeners-side-effects.spec.ts b/tests/library/events/listeners-side-effects.spec.ts new file mode 100644 index 0000000000..176d372dfe --- /dev/null +++ b/tests/library/events/listeners-side-effects.spec.ts @@ -0,0 +1,61 @@ +// Copyright Joyent, Inc. and other Node contributors. +// Modifications copyright (c) by Microsoft Corporation +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +import { test, expect } from '@playwright/test'; +import { EventEmitter } from '../../../packages/playwright-core/lib/client/eventEmitter'; + +test('listeners empty check', () => { + const e = new EventEmitter(); + let fl; // foo listeners + + fl = e.listeners('foo'); + + expect(Array.isArray(fl)).toBeTruthy(); + expect(fl).toHaveLength(0); + expect(e._events instanceof Object).toBeFalsy(); + expect(Object.keys(e._events)).toHaveLength(0); + + const fail = () => expect(true).toBe(false); + e.on('foo', fail); + fl = e.listeners('foo'); + + expect(e._events.foo).toBe(fail); + expect(Array.isArray(fl)).toBeTruthy(); + expect(fl).toHaveLength(1); + expect(fl[0]).toBe(fail); + + e.listeners('bar'); + + const pass = () => expect(true).toBe(true); + e.on('foo', pass); + fl = e.listeners('foo'); + + expect(Array.isArray(e._events.foo)).toBeTruthy(); + expect(e._events.foo).toHaveLength(2); + expect(e._events.foo[0]).toBe(fail); + expect(e._events.foo[1]).toBe(pass); + + expect(Array.isArray(fl)).toBeTruthy(); + expect(fl).toHaveLength(2); + expect(fl[0]).toBe(fail); + expect(fl[1]).toBe(pass); +}); diff --git a/tests/library/events/listeners.spec.ts b/tests/library/events/listeners.spec.ts new file mode 100644 index 0000000000..4553ed92aa --- /dev/null +++ b/tests/library/events/listeners.spec.ts @@ -0,0 +1,154 @@ +// Copyright Joyent, Inc. and other Node contributors. +// Modifications copyright (c) by Microsoft Corporation +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +import { EventEmitter } from '../../../packages/playwright-core/lib/client/eventEmitter'; +import { test, expect } from '@playwright/test'; + +const listener = () => {}; +const listener2 = () => {}; +const listener3 = () => { return 0; }; +const listener4 = () => { return 1; }; + +class TestStream extends EventEmitter {} + +test('EventEmitter listeners with one listener', () => { + const ee = new EventEmitter(); + ee.on('foo', listener); + const fooListeners = ee.listeners('foo'); + + const listeners = ee.listeners('foo'); + expect(Array.isArray(listeners)).toBeTruthy(); + expect(listeners).toHaveLength(1); + expect(listeners[0]).toEqual(listener); + + ee.removeAllListeners('foo'); + expect>(ee.listeners('foo')).toHaveLength(0); + + expect(Array.isArray(fooListeners)).toBeTruthy(); + expect(fooListeners).toHaveLength(1); + expect(fooListeners[0]).toEqual(listener); +}); + +test('Array copy modification does not modify orig', () => { + const ee = new EventEmitter(); + ee.on('foo', listener); + + const eeListenersCopy = ee.listeners('foo'); + expect(Array.isArray(eeListenersCopy)).toBeTruthy(); + expect(eeListenersCopy).toHaveLength(1); + expect(eeListenersCopy[0]).toEqual(listener); + + eeListenersCopy.push(listener2); + const listeners = ee.listeners('foo'); + + expect(Array.isArray(listeners)).toBeTruthy(); + expect(listeners).toHaveLength(1); + expect(listeners[0]).toEqual(listener); + + expect(eeListenersCopy).toHaveLength(2); + expect(eeListenersCopy[0]).toEqual(listener); + expect(eeListenersCopy[1]).toEqual(listener2); +}); + +test('Modify array copy after multiple adds', () => { + const ee = new EventEmitter(); + ee.on('foo', listener); + const eeListenersCopy = ee.listeners('foo'); + ee.on('foo', listener2); + + const listeners = ee.listeners('foo'); + expect(Array.isArray(listeners)).toBeTruthy(); + expect(listeners).toHaveLength(2); + expect(listeners[0]).toEqual(listener); + expect(listeners[1]).toEqual(listener2); + + expect(Array.isArray(eeListenersCopy)).toBeTruthy(); + expect(eeListenersCopy).toHaveLength(1); + expect(eeListenersCopy[0]).toEqual(listener); +}); + +test('listeners and once', () => { + const ee = new EventEmitter(); + ee.once('foo', listener); + const listeners = ee.listeners('foo'); + expect(Array.isArray(listeners)).toBeTruthy(); + expect(listeners).toHaveLength(1); + expect(listeners[0]).toEqual(listener); +}); + +test('listeners with conflicting types', () => { + const ee = new EventEmitter(); + ee.on('foo', listener); + ee.once('foo', listener2); + + const listeners = ee.listeners('foo'); + expect(Array.isArray(listeners)).toBeTruthy(); + expect(listeners).toHaveLength(2); + expect(listeners[0]).toEqual(listener); + expect(listeners[1]).toEqual(listener2); +}); + +test('EventEmitter with no members', () => { + const ee = new EventEmitter(); + ee._events = undefined; + const listeners = ee.listeners('foo'); + expect(Array.isArray(listeners)).toBeTruthy(); + expect(listeners).toHaveLength(0); +}); + +test('listeners on prototype', () => { + const s = new TestStream(); + const listeners = s.listeners('foo'); + expect(Array.isArray(listeners)).toBeTruthy(); + expect(listeners).toHaveLength(0); +}); + +test('raw listeners', () => { + const ee = new EventEmitter(); + ee.on('foo', listener); + const wrappedListener = ee.rawListeners('foo'); + expect(wrappedListener).toHaveLength(1); + expect(wrappedListener[0]).toEqual(listener); + + ee.once('foo', listener); + const wrappedListeners = ee.rawListeners('foo'); + expect(wrappedListeners).toHaveLength(2); + expect(wrappedListeners[0]).toEqual(listener); + expect(wrappedListeners[1].listener).toEqual(listener); + + ee.emit('foo'); + expect(wrappedListeners).toHaveLength(2); + expect(wrappedListeners[1].listener).toEqual(listener); +}); + +test('raw listeners order', () => { + const ee = new EventEmitter(); + ee.once('foo', listener3); + ee.on('foo', listener4); + const rawListeners = ee.rawListeners('foo'); + expect(rawListeners).toHaveLength(2); + expect(rawListeners[0]()).toEqual(0); + + const rawListener = ee.rawListeners('foo'); + expect(rawListener).toHaveLength(1); + expect(rawListener[0]()).toEqual(1); +}); diff --git a/tests/library/events/max-listeners.spec.ts b/tests/library/events/max-listeners.spec.ts new file mode 100644 index 0000000000..21e3582729 --- /dev/null +++ b/tests/library/events/max-listeners.spec.ts @@ -0,0 +1,43 @@ +// Copyright Joyent, Inc. and other Node contributors. +// Modifications copyright (c) by Microsoft Corporation +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH the SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +import { EventEmitter } from '../../../packages/playwright-core/lib/client/eventEmitter'; +import { test, expect } from '@playwright/test'; + +test('emit maxListeners on e', () => { + const e = new EventEmitter(); + e.on('maxListeners', () => {}); + + // Should not corrupt the 'maxListeners' queue. + e.setMaxListeners(42); + + const throwsObjs = [NaN, -1, 'and even this']; + const maxError = /^The value of \"n\" is out of range. It must be a non-negative number./; + + for (const obj of throwsObjs) { + expect(() => { + e.setMaxListeners(obj); + }).toThrow(maxError); + } + + e.emit('maxListeners'); +}); diff --git a/tests/library/events/method-names.spec.ts b/tests/library/events/method-names.spec.ts new file mode 100644 index 0000000000..69a0a0ece2 --- /dev/null +++ b/tests/library/events/method-names.spec.ts @@ -0,0 +1,40 @@ +// Copyright Joyent, Inc. and other Node contributors. +// Modifications copyright (c) by Microsoft Corporation +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +import { EventEmitter } from '../../../packages/playwright-core/lib/client/eventEmitter'; +import { test, expect } from '@playwright/test'; + +test('EventEmitter prototype test', () => { + const ee = new EventEmitter(); + // eslint-disable-next-line no-proto + expect((ee as any).__proto__.constructor.name).toEqual('EventEmitter'); + expect(ee.on).toEqual(ee.addListener); // Same method. + expect(ee.off).toEqual(ee.removeListener); // Same method. + + Object.getOwnPropertyNames(ee).forEach(name => { + if (name === 'constructor' || name === 'on' || name === 'off') + return; + if (typeof ee[name] !== 'function') + return; + expect(ee[name].name).toEqual(name); + }); +}); diff --git a/tests/library/events/modify-in-emit.spec.ts b/tests/library/events/modify-in-emit.spec.ts new file mode 100644 index 0000000000..1d832aa650 --- /dev/null +++ b/tests/library/events/modify-in-emit.spec.ts @@ -0,0 +1,93 @@ +// Copyright Joyent, Inc. and other Node contributors. +// Modifications copyright (c) by Microsoft Corporation +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +import { test, expect } from '@playwright/test'; +import { EventEmitter } from '../../../packages/playwright-core/lib/client/eventEmitter'; + +let callbacks_called = []; +const e = new EventEmitter(); + +const callback1 = () => { + callbacks_called.push('callback1'); + e.on('foo', callback2); + e.on('foo', callback3); + e.removeListener('foo', callback1); +}; + +const callback2 = () => { + callbacks_called.push('callback2'); + e.removeListener('foo', callback2); +}; + +const callback3 = () => { + callbacks_called.push('callback3'); + e.removeListener('foo', callback3); +}; + +test('add and remove listeners', () => { + e.on('foo', callback1); + expect(e.listeners('foo')).toHaveLength(1); + + e.emit('foo'); + expect(e.listeners('foo')).toHaveLength(2); + expect(Array.isArray(callbacks_called)).toBeTruthy(); + expect(callbacks_called).toHaveLength(1); + expect(callbacks_called[0]).toEqual('callback1'); + + e.emit('foo'); + expect(e.listeners('foo')).toHaveLength(0); + expect(Array.isArray(callbacks_called)).toBeTruthy(); + expect(callbacks_called).toHaveLength(3); + expect(callbacks_called[0]).toEqual('callback1'); + expect(callbacks_called[1]).toEqual('callback2'); + expect(callbacks_called[2]).toEqual('callback3'); + + e.emit('foo'); + expect(e.listeners('foo')).toHaveLength(0); + expect(Array.isArray(callbacks_called)).toBeTruthy(); + expect(callbacks_called).toHaveLength(3); + expect(callbacks_called[0]).toEqual('callback1'); + expect(callbacks_called[1]).toEqual('callback2'); + expect(callbacks_called[2]).toEqual('callback3'); + + e.on('foo', callback1); + e.on('foo', callback2); + expect(e.listeners('foo')).toHaveLength(2); + e.removeAllListeners('foo'); + expect(e.listeners('foo')).toHaveLength(0); +}); + +test('removing callbacks in emit', () => { + // Verify that removing callbacks while in emit allows emits to propagate to + // all listeners + callbacks_called = []; + + e.on('foo', callback2); + e.on('foo', callback3); + expect(e.listeners('foo')).toHaveLength(2); + e.emit('foo'); + expect(Array.isArray(callbacks_called)).toBeTruthy(); + expect(callbacks_called).toHaveLength(2); + expect(callbacks_called[0]).toEqual('callback2'); + expect(callbacks_called[1]).toEqual('callback3'); + expect(e.listeners('foo')).toHaveLength(0); +}); diff --git a/tests/library/events/num-args.spec.ts b/tests/library/events/num-args.spec.ts new file mode 100644 index 0000000000..cb0b569053 --- /dev/null +++ b/tests/library/events/num-args.spec.ts @@ -0,0 +1,62 @@ +// Copyright Joyent, Inc. and other Node contributors. +// Modifications copyright (c) by Microsoft Corporation +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +import { EventEmitter } from '../../../packages/playwright-core/lib/client/eventEmitter'; +import { test, expect } from '@playwright/test'; + +test('should work', () => { + const e = new EventEmitter(); + const num_args_emitted = []; + + e.on('numArgs', (...args) => { + const numArgs = args.length; + num_args_emitted.push(numArgs); + }); + + e.on('foo', function() { + num_args_emitted.push(arguments.length); + }); + + e.on('foo', function() { + num_args_emitted.push(arguments.length); + }); + + e.emit('numArgs'); + e.emit('numArgs', null); + e.emit('numArgs', null, null); + e.emit('numArgs', null, null, null); + e.emit('numArgs', null, null, null, null); + e.emit('numArgs', null, null, null, null, null); + + e.emit('foo', null, null, null, null); + + expect(Array.isArray(num_args_emitted)).toBeTruthy(); + expect(num_args_emitted).toHaveLength(8); + expect(num_args_emitted[0]).toEqual(0); + expect(num_args_emitted[1]).toEqual(1); + expect(num_args_emitted[2]).toEqual(2); + expect(num_args_emitted[3]).toEqual(3); + expect(num_args_emitted[4]).toEqual(4); + expect(num_args_emitted[5]).toEqual(5); + expect(num_args_emitted[6]).toEqual(4); + expect(num_args_emitted[6]).toEqual(4); +}); diff --git a/tests/library/events/once.spec.ts b/tests/library/events/once.spec.ts new file mode 100644 index 0000000000..124c98729b --- /dev/null +++ b/tests/library/events/once.spec.ts @@ -0,0 +1,84 @@ +// Copyright Joyent, Inc. and other Node contributors. +// Modifications copyright (c) by Microsoft Corporation +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +import { test, expect } from '@playwright/test'; +import { EventEmitter } from '../../../packages/playwright-core/lib/client/eventEmitter'; +import * as common from './utils'; + +test('should work', () => { + const e = new EventEmitter(); + + e.once('hello', common.mustCall()); + + e.emit('hello', 'a', 'b'); + e.emit('hello', 'a', 'b'); + e.emit('hello', 'a', 'b'); + e.emit('hello', 'a', 'b'); + + const remove = () => { + expect(false).toBe(true); + }; + + e.once('foo', remove); + e.removeListener('foo', remove); + e.emit('foo'); + + e.once('e', common.mustCall(() => { + e.emit('e'); + })); + + e.once('e', common.mustCall()); + + e.emit('e'); + + // Verify that the listener must be a function + expect(() => { + const ee = new EventEmitter(); + + ee.once('foo', null); + }).toThrow(TypeError); +}); + +test('once() has different code paths based on the number of arguments being emitted', () => { + // Verify that all of the cases are covered. + const maxArgs = 4; + + for (let i = 0; i <= maxArgs; i++) { + const ee = new EventEmitter(); + const args: any[] = ['foo']; + + for (let j = 0; j < i; j++) + args.push(j); + + ee.once('foo', common.mustCall((...params) => { + const restArgs = args.slice(1); + expect(Array.isArray(params)).toBeTruthy(); + expect(params).toHaveLength(restArgs.length); + for (let index = 0; index < params.length; index++) { + const param = params[index]; + expect(param).toEqual(restArgs[index]); + } + })); + + EventEmitter.prototype.emit.apply(ee, args); + } +}); diff --git a/tests/library/events/prepend.spec.ts b/tests/library/events/prepend.spec.ts new file mode 100644 index 0000000000..550c01eb3c --- /dev/null +++ b/tests/library/events/prepend.spec.ts @@ -0,0 +1,52 @@ +// Copyright Joyent, Inc. and other Node contributors. +// Modifications copyright (c) by Microsoft Corporation +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +import { EventEmitter } from '../../../packages/playwright-core/lib/client/eventEmitter'; +import { test, expect } from '@playwright/test'; + +test('EventEmitter functionality', () => { + const myEE = new EventEmitter(); + let m = 0; + // This one comes last. + myEE.on('foo', () => { + expect(m).toEqual(2); + }); + + // This one comes second. + myEE.prependListener('foo', () => { + expect(m++).toEqual(1); + }); + + // This one comes first. + myEE.prependOnceListener('foo', () => { + expect(m++).toEqual(0); + }); + + myEE.emit('foo'); +}); + +test('Verify that the listener must be a function', () => { + expect(() => { + const ee = new EventEmitter(); + ee.prependOnceListener('foo', null); + }).toThrow(TypeError); +}); diff --git a/tests/library/events/remove-all-listeners.spec.ts b/tests/library/events/remove-all-listeners.spec.ts new file mode 100644 index 0000000000..116b9e768c --- /dev/null +++ b/tests/library/events/remove-all-listeners.spec.ts @@ -0,0 +1,136 @@ +// Copyright Joyent, Inc. and other Node contributors. +// Modifications copyright (c) by Microsoft Corporation +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +import { EventEmitter } from '../../../packages/playwright-core/lib/client/eventEmitter'; +import { test, expect } from '@playwright/test'; +import * as common from './utils'; + +let wrappers: any[]; + +const expectWrapper = expected => { + const entry: any = { expected }; + wrappers.push(entry); + return name => { + entry.actual = entry.actual || []; + entry.actual.push(name); + }; +}; + +test.beforeEach(() => { + wrappers = []; +}); + +test.afterEach(() => { + for (const wrapper of wrappers) { + const sortedActual = wrapper.actual.sort(); + const sortedExpected = wrapper.expected.sort(); + expect(sortedActual).toEqual(sortedExpected); + } +}); + +test('listeners', () => { + const ee = new EventEmitter(); + const noop = () => { }; + ee.on('foo', noop); + ee.on('bar', noop); + ee.on('baz', noop); + ee.on('baz', noop); + const fooListeners = ee.listeners('foo'); + const barListeners = ee.listeners('bar'); + const bazListeners = ee.listeners('baz'); + ee.on('removeListener', expectWrapper(['bar', 'baz', 'baz'])); + ee.removeAllListeners('bar'); + ee.removeAllListeners('baz'); + + let listeners = ee.listeners('foo'); + expect(Array.isArray(listeners)).toBeTruthy(); + expect(listeners).toHaveLength(1); + expect(listeners[0]).toEqual(noop); + + listeners = ee.listeners('bar'); + expect(Array.isArray(listeners)).toBeTruthy(); + expect(listeners).toHaveLength(0); + listeners = ee.listeners('baz'); + expect(Array.isArray(listeners)).toBeTruthy(); + expect(listeners).toHaveLength(0); + + expect(fooListeners.length).toEqual(1); + expect(fooListeners[0]).toEqual(noop); + expect(barListeners.length).toEqual(1); + expect(barListeners[0]).toEqual(noop); + expect(bazListeners.length).toEqual(2); + expect(bazListeners[0]).toEqual(noop); + expect(bazListeners[1]).toEqual(noop); + + expect(ee.listeners('bar')).not.toEqual(barListeners); + expect(ee.listeners('baz')).not.toEqual(bazListeners); +}); + +test('removeAllListeners removes all listeners', () => { + const ee = new EventEmitter(); + ee.on('foo', () => { }); + ee.on('bar', () => { }); + ee.on('removeListener', expectWrapper(['foo', 'bar', 'removeListener'])); + ee.on('removeListener', expectWrapper(['foo', 'bar'])); + ee.removeAllListeners(); + + let listeners = ee.listeners('foo'); + expect(Array.isArray(listeners)).toBeTruthy(); + expect(listeners).toHaveLength(0); + listeners = ee.listeners('bar'); + expect(Array.isArray(listeners)).toBeTruthy(); + expect(listeners).toHaveLength(0); +}); + +test('removeAllListeners with no event type', () => { + const ee = new EventEmitter(); + ee.on('removeListener', common.mustNotCall()); + // Check for regression where removeAllListeners() throws when + // there exists a 'removeListener' listener, but there exists + // no listeners for the provided event type. + (ee as any).removeAllListeners(ee, 'foo'); +}); + +test('listener count after removeAllListeners', () => { + const ee = new EventEmitter(); + let expectLength = 2; + ee.on('removeListener', () => { + expect(expectLength--).toEqual(ee.listeners('baz').length); + }); + ee.on('baz', () => { }); + ee.on('baz', () => { }); + ee.on('baz', () => { }); + expect(ee.listeners('baz').length).toEqual(expectLength + 1); + ee.removeAllListeners('baz'); + expect(ee.listeners('baz').length).toEqual(0); +}); + +test('removeAllListeners returns EventEmitter', () => { + const ee = new EventEmitter(); + expect(ee).toEqual(ee.removeAllListeners()); +}); + +test('removeAllListeners on undefined _events', () => { + const ee = new EventEmitter(); + ee._events = undefined; + expect(ee).toEqual(ee.removeAllListeners()); +}); diff --git a/tests/library/events/remove-listeners.spec.ts b/tests/library/events/remove-listeners.spec.ts new file mode 100644 index 0000000000..1ea32bd598 --- /dev/null +++ b/tests/library/events/remove-listeners.spec.ts @@ -0,0 +1,188 @@ +// Copyright Joyent, Inc. and other Node contributors. +// Modifications copyright (c) by Microsoft Corporation +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +import { EventEmitter } from '../../../packages/playwright-core/lib/client/eventEmitter'; +import { test, expect } from '@playwright/test'; +import * as common from './utils'; + +const listener1 = () => {}; +const listener2 = () => {}; + +test.beforeEach(() => common.beforeEach()); +test.afterEach(() => common.afterEach()); + +test('First test', () => { + const ee = new EventEmitter(); + ee.on('hello', listener1); + ee.on('removeListener', common.mustCall((name, cb) => { + expect(name).toEqual('hello'); + expect(cb).toEqual(listener1); + })); + ee.removeListener('hello', listener1); + const listeners = ee.listeners('hello'); + expect(Array.isArray(listeners)).toBeTruthy(); + expect(listeners).toHaveLength(0); +}); + +test('Second test', () => { + const ee = new EventEmitter(); + ee.on('hello', listener1); + ee.on('removeListener', common.mustNotCall()); + ee.removeListener('hello', listener2); + + const listeners = ee.listeners('hello'); + expect(Array.isArray(listeners)).toBeTruthy(); + expect(listeners.length).toEqual(1); + expect(listeners[0]).toEqual(listener1); +}); + +test('Third test', () => { + const ee = new EventEmitter(); + ee.on('hello', listener1); + ee.on('hello', listener2); + + let listeners; + ee.once('removeListener', common.mustCall((name, cb) => { + expect(name).toEqual('hello'); + expect(cb).toEqual(listener1); + listeners = ee.listeners('hello'); + expect(Array.isArray(listeners)).toBeTruthy(); + expect(listeners.length).toEqual(1); + expect(listeners[0]).toEqual(listener2); + })); + ee.removeListener('hello', listener1); + listeners = ee.listeners('hello'); + expect(Array.isArray(listeners)).toBeTruthy(); + expect(listeners.length).toEqual(1); + expect(listeners[0]).toEqual(listener2); + ee.once('removeListener', common.mustCall((name, cb) => { + expect(name).toEqual('hello'); + expect(cb).toEqual(listener2); + listeners = ee.listeners('hello'); + expect(Array.isArray(listeners)).toBeTruthy(); + expect(listeners.length).toEqual(0); + })); + ee.removeListener('hello', listener2); + listeners = ee.listeners('hello'); + expect(Array.isArray(listeners)).toBeTruthy(); + expect(listeners.length).toEqual(0); +}); + +test('Fourth test', () => { + const ee = new EventEmitter(); + + const remove1 = () => { + throw new Error('remove1 should not have been called'); + }; + + const remove2 = () => { + throw new Error('remove2 should not have been called'); + }; + + ee.on('removeListener', common.mustCall((name, cb) => { + if (cb !== remove1) + return; + ee.removeListener('quux', remove2); + ee.emit('quux'); + }, 2)); + ee.on('quux', remove1); + ee.on('quux', remove2); + ee.removeListener('quux', remove1); +}); + +test('Fifth test', () => { + const ee = new EventEmitter(); + const listener3 = common.mustCall(() => { + ee.removeListener('hello', listener4); + }, 2); + const listener4 = common.mustCall(); + + ee.on('hello', listener3); + ee.on('hello', listener4); + + // listener4 will still be called although it is removed by listener 3. + ee.emit('hello'); + // This is so because the interal listener array at time of emit + // was [listener3,listener4] + + // Interal listener array [listener3] + ee.emit('hello'); +}); + +test('Sixth test', () => { + const ee = new EventEmitter(); + + ee.once('hello', listener1); + ee.on('removeListener', common.mustCall((eventName, listener) => { + expect(eventName).toEqual('hello'); + expect(listener).toEqual(listener1); + })); + ee.emit('hello'); +}); + +test('Seventh test', () => { + const ee = new EventEmitter(); + + expect(ee).toEqual(ee.removeListener('foo', () => {})); +}); + +// Verify that the removed listener must be a function +test('Eighth test', () => { + expect(() => { + const ee = new EventEmitter(); + + ee.removeListener('foo', null); + }).toThrow(/^The "listener" argument must be of type Function\. Received type object$/); +}); + +test('Ninth test', () => { + const ee = new EventEmitter(); + const listener = () => {}; + ee._events = undefined; + const e = ee.removeListener('foo', listener); + expect(e).toEqual(ee); +}); + +test('Tenth test', () => { + const ee = new EventEmitter(); + + ee.on('foo', listener1); + ee.on('foo', listener2); + let listeners = ee.listeners('foo'); + expect(Array.isArray(listeners)).toBeTruthy(); + expect(listeners.length).toEqual(2); + expect(listeners[0]).toEqual(listener1); + expect(listeners[1]).toEqual(listener2); + + ee.removeListener('foo', listener1); + expect(ee._events.foo).toEqual(listener2); + + ee.on('foo', listener1); + listeners = ee.listeners('foo'); + expect(Array.isArray(listeners)).toBeTruthy(); + expect(listeners.length).toEqual(2); + expect(listeners[0]).toEqual(listener2); + expect(listeners[1]).toEqual(listener1); + + ee.removeListener('foo', listener1); + expect(ee._events.foo).toEqual(listener2); +}); diff --git a/tests/library/events/set-max-listeners-side-effects.spec.ts b/tests/library/events/set-max-listeners-side-effects.spec.ts new file mode 100644 index 0000000000..c966e9cdf2 --- /dev/null +++ b/tests/library/events/set-max-listeners-side-effects.spec.ts @@ -0,0 +1,34 @@ +// Copyright Joyent, Inc. and other Node contributors. +// Modifications copyright (c) by Microsoft Corporation +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +import { EventEmitter } from '../../../packages/playwright-core/lib/client/eventEmitter'; +import { test, expect } from '@playwright/test'; + +test('set max listeners test', () => { + const e = new EventEmitter(); + + expect(!(e._events instanceof Object)).toBeTruthy(); + expect(Object.keys(e._events)).toHaveLength(0); + + e.setMaxListeners(5); + expect(Object.keys(e._events)).toHaveLength(0); +}); diff --git a/tests/library/events/special-event-names.spec.ts b/tests/library/events/special-event-names.spec.ts new file mode 100644 index 0000000000..5d4b6e8e1d --- /dev/null +++ b/tests/library/events/special-event-names.spec.ts @@ -0,0 +1,46 @@ +// Copyright Joyent, Inc. and other Node contributors. +// Modifications copyright (c) by Microsoft Corporation +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +import { test, expect } from '@playwright/test'; +import { EventEmitter } from '../../../packages/playwright-core/lib/client/eventEmitter'; + + +test('should support special event names', () => { + const ee = new EventEmitter(); + const handler = () => {}; + + expect(ee.eventNames().length).toEqual(0); + expect(ee._events.hasOwnProperty).toEqual(undefined); + expect(ee._events.toString).toEqual(undefined); + + ee.on('__defineGetter__', handler); + ee.on('toString', handler); + ee.on('__proto__', handler); + + expect(ee.eventNames()[0]).toEqual('__defineGetter__'); + expect(ee.eventNames()[1]).toEqual('toString'); + + expect(ee.listeners('__defineGetter__').length).toEqual(1); + expect(ee.listeners('__defineGetter__')[0]).toEqual(handler); + expect(ee.listeners('toString').length).toEqual(1); + expect(ee.listeners('toString')[0]).toEqual(handler); +}); diff --git a/tests/library/events/subclass.spec.ts b/tests/library/events/subclass.spec.ts new file mode 100644 index 0000000000..37b548a92a --- /dev/null +++ b/tests/library/events/subclass.spec.ts @@ -0,0 +1,54 @@ +// Copyright Joyent, Inc. and other Node contributors. +// Modifications copyright (c) by Microsoft Corporation +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +import { EventEmitter } from '../../../packages/playwright-core/lib/client/eventEmitter'; +import { test, expect } from '@playwright/test'; + +class MyEE extends EventEmitter { + constructor(cb) { + super(); + this.once(1, cb); + this.emit(1); + this.removeAllListeners(); + } +} + +test('myee instance', () => { + const myee = new MyEE(() => {}); + expect(myee._events).not.toBeInstanceOf(Object); + expect(Object.keys(myee._events).length).toBe(0); +}); + +class MyEE2 { + ee: EventEmitter; + constructor() { + this.ee = new EventEmitter(); + } +} + +test('MyEE2 instance', () => { + const ee1 = new MyEE2(); + const ee2 = new MyEE2(); + + ee1.ee.on('x', function() {}); + expect(ee2.ee.listenerCount('x')).toBe(0); +}); diff --git a/tests/library/events/symbols.spec.ts b/tests/library/events/symbols.spec.ts new file mode 100644 index 0000000000..7efb00f189 --- /dev/null +++ b/tests/library/events/symbols.spec.ts @@ -0,0 +1,46 @@ +// Copyright Joyent, Inc. and other Node contributors. +// Modifications copyright (c) by Microsoft Corporation +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +import { EventEmitter } from '../../../packages/playwright-core/lib/client/eventEmitter'; +import { test, expect } from '@playwright/test'; + +test('should support symbols', () => { + const ee = new EventEmitter(); + const foo = Symbol('foo'); + const listener = () => {}; + + ee.on(foo, listener); + expect(ee.listeners(foo).length).toEqual(1); + expect(ee.listeners(foo)[0]).toEqual(listener); + + ee.emit(foo); + + ee.removeAllListeners(); + expect(ee.listeners(foo).length).toEqual(0); + + ee.on(foo, listener); + expect(ee.listeners(foo).length).toEqual(1); + expect(ee.listeners(foo)[0]).toEqual(listener); + + ee.removeListener(foo, listener); + expect(ee.listeners(foo).length).toEqual(0); +}); diff --git a/tests/library/events/utils.ts b/tests/library/events/utils.ts new file mode 100644 index 0000000000..8c4a3b463f --- /dev/null +++ b/tests/library/events/utils.ts @@ -0,0 +1,46 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +import { expect } from '@playwright/test'; + +export const mustNotCall = (msg?: string) => { + return function mustNotCall() { + expect(false, msg || 'function should not have been called').toBeTruthy(); + }; +}; + +let count; + +export const beforeEach = () => { + count = 0; +}; + +export const afterEach = () => { + expect(count).toBe(0); +}; + +export const mustCall = (fn?: Function, exact?: number) => { + count += exact || 1; + return (...args: any[]) => { + fn?.(...args); + --count; + }; +};