diff --git a/src/emitters/SemiAsyncEmitter.test.ts b/src/emitters/SemiAsyncEmitter.test.ts index 28dc886..26180df 100644 --- a/src/emitters/SemiAsyncEmitter.test.ts +++ b/src/emitters/SemiAsyncEmitter.test.ts @@ -41,4 +41,27 @@ describe('SemiAsyncEmitter', () => { expect(listener).toHaveBeenCalledTimes(2); expect(listener).toHaveBeenCalledWith(84); }); + + describe('unhappy paths', () => { + describe('invalid type', () => { + test.each([['on'], ['once'], ['emit'], ['off']])('in emitter.%s(...)', (method) => + // @ts-expect-error TS7053 + expect(() => emitter[method]()).toThrow(/type must be a string/), + ); + }); + + describe('invalid listener', () => { + test.each([['on'], ['once'], ['off']])('in emitter.%s(...)', (method) => + // @ts-expect-error TS7053 + expect(() => emitter[method]('*')).toThrow(/listener must be a function/), + ); + }); + + describe('invalid order', () => { + test.each([['on'], ['once']])('in emitter.%s(...)', (method) => + // @ts-expect-error TS7053 + expect(() => emitter[method]('*', () => {}, '')).toThrow(/order must be a number/), + ); + }); + }); }); diff --git a/src/emitters/SemiAsyncEmitter.ts b/src/emitters/SemiAsyncEmitter.ts index bdc3314..aa403f2 100644 --- a/src/emitters/SemiAsyncEmitter.ts +++ b/src/emitters/SemiAsyncEmitter.ts @@ -1,3 +1,4 @@ +import { assertFunction, assertNumber, assertString } from '../utils'; import type { ReadonlyAsyncEmitter } from './AsyncEmitter'; import type { ReadonlyEmitter } from './Emitter'; import { SerialAsyncEmitter } from './SerialAsyncEmitter'; @@ -34,6 +35,10 @@ export class SemiAsyncEmitter listener: (event: any) => unknown, order?: number, ): this { + assertString(type, 'type'); + assertFunction(listener, 'listener'); + order !== undefined && assertNumber(order, 'order'); + return this.#invoke('on', type, listener, order); } @@ -52,6 +57,10 @@ export class SemiAsyncEmitter listener: (event: any) => unknown, order?: number, ): this { + assertString(type, 'type'); + assertFunction(listener, 'listener'); + order !== undefined && assertNumber(order, 'order'); + return this.#invoke('once', type, listener, order); } @@ -61,12 +70,17 @@ export class SemiAsyncEmitter type: K | '*', listener: (event: any) => unknown, ): this { + assertString(type, 'type'); + assertFunction(listener, 'listener'); + return this.#invoke('off', type, listener); } emit(type: K, event: SyncMap[K]): void; emit(type: K, event: AsyncMap[K]): Promise; emit(type: K, event: any): void | Promise { + assertString(type, 'type'); + return this.#syncEvents.has(type as keyof SyncMap) ? this.#syncEmitter.emit(type as keyof SyncMap, event) : this.#asyncEmitter.emit(type as keyof AsyncMap, event); diff --git a/src/utils/assertions.test.ts b/src/utils/assertions.test.ts new file mode 100644 index 0000000..ac7aa1f --- /dev/null +++ b/src/utils/assertions.test.ts @@ -0,0 +1,33 @@ +import { assertString, assertFunction, assertNumber } from './assertions'; + +describe('Tests assert functions', () => { + test('successfully validates a string', () => { + const testValue = 'test'; + expect(() => assertString(testValue, 'testValue')).not.toThrow(); + }); + + test('throws error for non-string', () => { + const testValue = 123; + expect(() => assertString(testValue, 'testValue')).toThrow(TypeError); + }); + + test('successfully validates a function', () => { + const testValue = () => {}; // dummy function + expect(() => assertFunction(testValue, 'testValue')).not.toThrow(); + }); + + test('throws error for non-function', () => { + const testValue: any = 123; + expect(() => assertFunction(testValue, 'testValue')).toThrow(TypeError); + }); + + test('successfully validates a number', () => { + const testValue = 123; + expect(() => assertNumber(testValue, 'testValue')).not.toThrow(); + }); + + test('throws error for non-number', () => { + const testValue: any = 'test'; + expect(() => assertNumber(testValue, 'testValue')).toThrow(TypeError); + }); +}); diff --git a/src/utils/assertions.ts b/src/utils/assertions.ts new file mode 100644 index 0000000..6859c8c --- /dev/null +++ b/src/utils/assertions.ts @@ -0,0 +1,21 @@ +export function assertString( + value: string | number | symbol, + name: string, +): asserts value is string { + assertType('string', value, name); +} + +// eslint-disable-next-line @typescript-eslint/ban-types +export function assertFunction(value: Function, name: string): asserts value is Function { + assertType('function', value, name); +} + +export function assertNumber(value: number, name: string): asserts value is number { + assertType('number', value, name); +} + +function assertType(type: 'string' | 'function' | 'number', value: unknown, name: string) { + if (typeof value !== type) { + throw new TypeError(`${name} must be a ${type}`); + } +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 4dc2546..9defaca 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,3 +1,4 @@ +export * from './assertions'; export * from './getHierarchy'; export * from './iterateSorted'; export { makeDeferred, Deferred } from './makeDeferred';