import { symbolToken } from '@servicetitan/react-ioc'; import { EventBus } from '../event-bus'; jest.mock('../event-bus', () => { return { EventBus: jest.fn().mockImplementation(() => { return { job: { emit: jest.fn(), on: jest.fn(), off: jest.fn(), }, emit: jest.fn(), on: jest.fn(), off: jest.fn(), }; }), }; }); describe(`[web-components] ${EventBus.name}`, () => { describe('type definitions', () => { interface InterfaceDataType { value: string; } describe('EventBus', () => { test('rejects invalid data type', () => { // @ts-expect-error Types of property 'foo' are incompatible. const eventBus = EventBus<{ foo: () => void }>; expect(eventBus).toBeDefined(); }); }); describe('when eventBus is not typed', () => { const eventBus = new EventBus(); describe('emit', () => { test('accepts valid event with valid data', () => { eventBus.emit('foo', ''); eventBus.emit('foo', 123); eventBus.emit('foo', true); eventBus.emit('bar', { value: 1 }); eventBus.emit('foo', ['']); eventBus.emit('foo', [123]); eventBus.emit('foo', [true]); eventBus.emit('bar', [{ value: 1 }]); eventBus.emit('bar', [{ value: [1] }]); eventBus.emit(123, ''); eventBus.emit(Symbol(), ''); }); test('accepts event with no data', () => { eventBus.emit('foo'); }); test('accepts data declared as interface', () => { const data: InterfaceDataType = { value: '' }; eventBus.emit('foo', data); }); test('rejects invalid event', () => { // @ts-expect-error Argument of type '() => void' is not assignable to parameter of type 'string | number | symbol | SymbolToken'. eventBus.emit(() => {}, ''); // @ts-expect-error Argument of type 'Date' is not assignable to parameter of type 'string | number | symbol | SymbolToken'. eventBus.emit(new Date(), ''); // @ts-expect-error Argument of type 'boolean' is not assignable to parameter of type 'string | number | symbol | SymbolToken'. eventBus.emit(true, ''); }); test('rejects invalid data', () => { /* * Note: This case isn't caught * @ts-expect-error Argument of type '() => void' is not assignable to parameter of type 'JSON | JSONPrimitive' * eventBus.emit('foo', () => {}); */ // @ts-expect-error Type '() => void' is not assignable to type 'never'. eventBus.emit('foo', { fn: () => {} }); // @ts-expect-error Type 'Date' is not assignable to type 'never'. eventBus.emit('foo', { date: new Date() }); // @ts-expect-error Type 'Map' is not assignable to type 'never'. eventBus.emit('foo', { map: new Map() }); // @ts-expect-error Type 'Set' is not assignable to type 'never'. eventBus.emit('foo', { set: new Set() }); // @ts-expect-error Type 'symbol' is not assignable to type 'never'. eventBus.emit('foo', { symbol: Symbol() }); }); }); describe('on', () => { test('accepts valid event with any handler', () => { eventBus.on('foo', () => {}); eventBus.on('bar', (_value: string) => {}); eventBus.on('baz', ({ value: _value }: { value: Date }) => {}); eventBus.on(Symbol(), () => {}); eventBus.on(123, () => {}); }); test('rejects invalid event', () => { // @ts-expect-error Argument of type 'Date' is not assignable to parameter of type 'JSON | JSONPrimitive'. eventBus.on(new Date(), () => {}); }); }); describe('off', () => { test('accepts valid event with any handler', () => { eventBus.off('foo', () => {}); eventBus.off('bar', (_value: string) => {}); eventBus.off('baz', ({ value: _value }: { value: Date }) => {}); eventBus.off(Symbol(), () => {}); eventBus.off(123, () => {}); }); test('rejects invalid event', () => { // @ts-expect-error Argument of type 'Date' is not assignable to parameter of type 'JSON | JSONPrimitive'. eventBus.off(new Date(), () => {}); }); }); describe('job.emit', () => { test('accepts valid event', () => { eventBus.job.emit('foo'); }); test('rejects invalid event', () => { // @ts-expect-error Argument of type 'Date' is not assignable to parameter of type 'JSON | JSONPrimitive'. eventBus.job.emit(new Date()); }); }); describe('job.on', () => { test('accepts valid event with valid handler', () => { eventBus.job.on('foo', () => {}); eventBus.job.on('foo', ({ resolve }) => resolve({ value: '' })); eventBus.job.on('foo', ({ reject }) => reject(new Error('Oops!'))); eventBus.job.on('foo', ({ reject }) => reject('Oops!')); eventBus.job.on(Symbol(), () => {}); eventBus.job.on(123, () => {}); }); test('rejects invalid event', () => { // @ts-expect-error Argument of type 'Date' is not assignable to parameter of type 'JSON | JSONPrimitive'. eventBus.job.on(new Date(), () => {}); }); test('rejects invalid handler', () => { eventBus.job.on('foo', ({ reject }) => { // @ts-expect-error Argument of type '123' is not assignable to parameter of type 'string | Error | undefined'. reject(123); }); }); }); describe('job.off', () => { test('accepts valid event with valid handler', () => { eventBus.job.off('foo', () => {}); eventBus.job.off('foo', ({ resolve }) => resolve({ value: '' })); eventBus.job.off('foo', ({ reject }) => reject(new Error('Oops!'))); eventBus.job.off('foo', ({ reject }) => reject('Oops!')); eventBus.job.off(Symbol(), () => {}); eventBus.job.off(123, () => {}); }); test('rejects invalid event', () => { // @ts-expect-error Argument of type 'Date' is not assignable to parameter of type 'JSON | JSONPrimitive'. eventBus.job.off(new Date(), () => {}); }); test('rejects invalid handler', () => { eventBus.job.off('foo', ({ reject }) => { // @ts-expect-error Argument of type '123' is not assignable to parameter of type 'string | Error | undefined'. reject(123); }); }); }); describe('when event is symbolToken', () => { const token = symbolToken<{ value: number }>('token'); const untypedToken = symbolToken('untyped-token'); const optionalPayloadToken = symbolToken<{ value: number } | undefined>( 'optionalToken' ); describe('emit', () => { test('accepts valid data', () => { eventBus.emit(token, { value: 1 }); eventBus.emit(optionalPayloadToken, { value: 1 }); eventBus.emit(optionalPayloadToken); eventBus.emit(untypedToken); }); test('rejects invalid data', () => { // @ts-expect-error Type 'string' is not assignable to type 'number' eventBus.emit(token, { value: '' }); }); test('rejects missing data', () => { // @ts-expect-error Expected 2 arguments, but got 1 eventBus.emit(token); }); test('rejects invalid data type', () => { const token = symbolToken<{ value: () => void }>('token'); // @ts-expect-error Argument of type 'SymbolToken<{ value: () => void; }>' is not assignable to parameter of type ... eventBus.emit(token, { value: () => {} }); }); }); describe('on', () => { test('accepts valid handler', () => { eventBus.on(token, () => {}); eventBus.on(token, data => data.value++); eventBus.on(untypedToken, data => data.value++); }); test('rejects invalid handler', () => { // @ts-expect-error Argument of type '(_: number) => void' is not assignable to parameter of type ... eventBus.on(token, (_: number) => {}); // @ts-expect-error Argument of type '(data: { value: string; }) => number' is not assignable to parameter of type ... eventBus.on(token, (data: { value: string }) => data.value.split('')); }); }); describe('off', () => { test('accepts valid handler', () => { eventBus.off(token, () => {}); eventBus.off(token, (data: { value: number }) => data.value++); }); test('rejects invalid handler', () => { // @ts-expect-error Argument of type '(_: number) => void' is not assignable to parameter of type ... eventBus.off(token, (_: number) => {}); // @ts-expect-error Argument of type '(data: { value: string; }) => number' is not assignable to parameter of type ... eventBus.off(token, (data: { value: string }) => data.value.split('')); }); }); describe('job.emit', () => { test('accepts token', () => { eventBus.job.emit(token); }); }); describe('job.on', () => { test('accepts valid handler', () => { eventBus.job.on(token, ({ resolve }) => resolve({ value: 1 })); eventBus.job.on(token, ({ reject }) => reject(new Error('Oops!'))); eventBus.job.on(token, ({ reject }) => reject('Oops!')); eventBus.job.on(optionalPayloadToken, ({ resolve }) => resolve({ value: 1 }) ); eventBus.job.on(optionalPayloadToken, ({ resolve }) => resolve()); }); test('rejects invalid handler', () => { eventBus.job.on(token, ({ resolve }) => { // @ts-expect-error Type 'string' is not assignable to type 'number'.ts(2322) resolve({ value: '' }); }); eventBus.job.on(token, ({ reject }) => { // @ts-expect-error Argument of type '123' is not assignable to parameter of type 'string | Error | undefined'. reject(123); }); }); }); describe('job.off', () => { test('accepts valid handler', () => { eventBus.job.off(token, ({ resolve }) => resolve({ value: 1 })); eventBus.job.off(token, ({ reject }) => reject(new Error('Oops!'))); eventBus.job.off(token, ({ reject }) => reject('Oops!')); }); test('rejects invalid handler', () => { eventBus.job.off(token, ({ resolve }) => { // @ts-expect-error Type 'string' is not assignable to type 'number'.ts(2322) resolve({ value: '' }); }); eventBus.job.off(token, ({ reject }) => { // @ts-expect-error Argument of type '123' is not assignable to parameter of type 'string | Error | undefined'. reject(123); }); }); }); }); describe('with legacy type declaration', () => { const numberToken = symbolToken('number-token'); const untypedToken = symbolToken('untyped-token'); describe('emit', () => { test('accepts valid event with valid data', () => { eventBus.emit('foo', 123); eventBus.emit(numberToken, 123); eventBus.emit(untypedToken, 123); eventBus.emit('foo', { value: '' }); }); test('rejects invalid data type', () => { // @ts-expect-error Type 'Function' does not satisfy the constraint 'Serializable'. eventBus.emit('foo', () => {}); // @ts-expect-error Type '{ date: Date; }' does not satisfy the constraint 'Serializable<{ date: Date; }>'. eventBus.emit<{ date: Date }>('foo', { date: new Date() }); }); test('rejects invalid data', () => { // @ts-expect-error Type 'string' is not assignable to type 'number'. eventBus.emit('foo', ''); // @ts-expect-error Type 'string' is not assignable to type 'number'. eventBus.emit(numberToken, ''); // @ts-expect-error Type 'string' is not assignable to type 'number'. eventBus.emit(untypedToken, ''); }); }); describe('on', () => { test('accepts valid handler', () => { eventBus.on('foo', (_: number) => {}); eventBus.on(numberToken, (_: number) => {}); eventBus.on(untypedToken, (_: number) => {}); eventBus.on('foo', (_: InterfaceDataType) => {}); }); test('rejects invalid data type', () => { // @ts-expect-error Type 'Function' does not satisfy the constraint 'Serializable'. eventBus.on('foo', () => {}); // @ts-expect-error Type 'Function' does not satisfy the constraint 'Serializable'. eventBus.on<{ date: Date }>('foo', () => {}); }); test('rejects invalid handler', () => { // @ts-expect-error Argument of type '(_: string) => void' is not assignable to parameter of type ... eventBus.on('foo', (_: string) => {}); // @ts-expect-error Argument of type '(_: string) => void' is not assignable to parameter of type ... eventBus.on(numberToken, (_: string) => {}); // @ts-expect-error Argument of type '(_: string) => void' is not assignable to parameter of type ... eventBus.on(untypedToken, (_: string) => {}); }); }); describe('off', () => { test('accepts valid handler', () => { eventBus.off('foo', (_: number) => {}); eventBus.off(numberToken, (_: number) => {}); eventBus.off(untypedToken, (_: number) => {}); eventBus.off('foo', (_: InterfaceDataType) => {}); }); test('rejects invalid data type', () => { // @ts-expect-error Type 'Function' does not satisfy the constraint 'Serializable'. eventBus.off('foo', () => {}); // @ts-expect-error Type 'Function' does not satisfy the constraint 'Serializable'. eventBus.off<{ date: Date }>('foo', () => {}); }); test('rejects invalid handler', () => { // @ts-expect-error Argument of type '(_: number) => void' is not assignable to parameter of type ... eventBus.off('foo', (_: number) => {}); // @ts-expect-error Argument of type '(_: string) => void' is not assignable to parameter of type ... eventBus.off(numberToken, (_: string) => {}); // @ts-expect-error Argument of type '(_: string) => void' is not assignable to parameter of type ... eventBus.off(untypedToken, (_: string) => {}); }); }); }); }); describe('when eventBus is typed', () => { interface TestEvents { 'number-event': { value: number }; 'string-event': { value: string }; 'array-event': [string, { value: string }]; 'optional-data-event': { value: number } | undefined; 'interface-data-event': InterfaceDataType; } const eventBus = new EventBus(); describe('emit', () => { test('accepts valid events and data', () => { eventBus.emit('number-event', { value: 1 }); eventBus.emit('string-event', { value: '' }); eventBus.emit('optional-data-event', { value: 1 }); eventBus.emit('optional-data-event'); const data: InterfaceDataType = { value: '' }; eventBus.emit('interface-data-event', data); }); test('rejects invalid event', () => { // @ts-expect-error '"foo"' is not assignable to parameter of type 'keyof TestEvents' eventBus.emit('foo', { value: 1 }); }); test('rejects invalid data', () => { // @ts-expect-error Type 'string' is not assignable to type 'number' eventBus.emit('number-event', { value: '' }); // @ts-expect-error 'foo' does not exist in type '{ value: number; }' eventBus.emit('number-event', { foo: 1 }); }); test('rejects missing data', () => { // @ts-expect-error Expected 2 arguments, but got 1 eventBus.emit('number-event'); }); test('accepts event with no data', () => { new EventBus<{ foo: never }>().emit('foo'); new EventBus<{ foo: undefined }>().emit('foo'); }); }); describe('on', () => { test('accepts valid event and handler', () => { eventBus.on('number-event', () => {}); eventBus.on('number-event', data => data.value++); eventBus.on('string-event', data => data.value.split('')); }); test('rejects invalid event', () => { // @ts-expect-error Argument of type '"foo"' is not assignable to parameter of type 'keyof TestEvents' eventBus.on('foo', () => {}); }); test('rejects invalid handler', () => { // @ts-expect-error Property 'split' does not exist on type 'number'. eventBus.on('number-event', data => data.value.split('')); // @ts-expect-error An arithmetic operand must be of type 'any', 'number', 'bigint' or an enum type. eventBus.on('string-event', data => data.value++); }); }); describe('off', () => { test('accepts valid event and handler', () => { eventBus.off('number-event', () => {}); eventBus.off('number-event', data => data.value++); eventBus.off('string-event', data => data.value.split('')); }); test('rejects invalid event', () => { // @ts-expect-error Argument of type '"foo"' is not assignable to parameter of type 'keyof TestEvents' eventBus.off('foo', () => {}); }); test('rejects invalid handler', () => { // @ts-expect-error Property 'split' does not exist on type 'number'. eventBus.off('number-event', data => data.value.split('')); // @ts-expect-error An arithmetic operand must be of type 'any', 'number', 'bigint' or an enum type. eventBus.off('string-event', data => data.value++); }); }); describe('job.emit', () => { test('accepts valid event', () => { eventBus.job.emit('number-event'); eventBus.job.emit('string-event'); }); test('rejects invalid event', () => { // @ts-expect-error Argument of type '"foo"' is not assignable to parameter of type 'keyof TestEvents'. eventBus.job.emit('foo'); }); }); describe('job.on', () => { test('accepts valid event and handler', () => { eventBus.job.on('number-event', () => {}); eventBus.job.on('number-event', ({ resolve }) => resolve({ foo: 1 })); eventBus.job.on('string-event', ({ resolve }) => resolve({ foo: 1 })); eventBus.job.on('string-event', ({ reject }) => reject(new Error('Oops!'))); eventBus.job.on('number-event', ({ reject }) => reject('Oops!')); }); test('rejects invalid event', () => { // @ts-expect-error Argument of type '"foo"' is not assignable to parameter of type 'keyof TestEvents'. eventBus.job.on('foo', () => {}); }); test('rejects invalid handler', () => { // @ts-expect-error Argument of type '123' is not assignable to parameter of type 'string | Error | undefined' eventBus.job.on('number-event', ({ reject }) => reject(123)); }); }); describe('job.off', () => { test('accepts valid event and handler', () => { eventBus.job.off('number-event', () => {}); eventBus.job.off('number-event', ({ resolve }) => resolve({ foo: 1 })); eventBus.job.off('string-event', ({ resolve }) => resolve({ foo: 1 })); eventBus.job.off('string-event', ({ reject }) => reject(new Error('Oops!'))); eventBus.job.off('number-event', ({ reject }) => reject('Oops!')); }); test('rejects invalid event', () => { // @ts-expect-error Argument of type '"foo"' is not assignable to parameter of type 'keyof TestEvents'. eventBus.job.off('foo', () => {}); }); test('rejects invalid handler', () => { // @ts-expect-error Argument of type '123' is not assignable to parameter of type 'string | Error | undefined' eventBus.job.off('number-event', ({ reject }) => reject(123)); }); }); describe('when event is symbolToken', () => { const token = symbolToken<{ value: number }>('token'); const optionalPayloadToken = symbolToken<{ value: number } | undefined>( 'optionalToken' ); const eventBus = new EventBus(); describe('emit', () => { test('accepts valid data', () => { eventBus.emit(token, { value: 1 }); eventBus.emit(optionalPayloadToken, { value: 1 }); eventBus.emit(optionalPayloadToken); }); test('rejects invalid data', () => { // @ts-expect-error Type 'string' is not assignable to type 'number' eventBus.emit(token, { value: '' }); }); test('rejects missing value', () => { // @ts-expect-error Expected 2 arguments, but got 1 eventBus.emit(token); }); }); describe('on', () => { test('accepts valid handler', () => { eventBus.on(token, () => {}); eventBus.on(token, data => data.value++); }); test('rejects invalid handler', () => { // @ts-expect-error Argument of type '(_: number) => void' is not assignable to parameter of type ... eventBus.on(token, (_: number) => {}); // @ts-expect-error Argument of type '(data: { value: string; }) => number' is not assignable to parameter of type ... eventBus.on(token, (data: { value: string }) => data.value.split('')); }); }); describe('off', () => { test('accepts valid handler', () => { eventBus.off(token, () => {}); eventBus.off(token, data => data.value++); }); test('rejects invalid handler', () => { // @ts-expect-error Argument of type '(_: number) => void' is not assignable to parameter of type ... eventBus.off(token, (_: number) => {}); // @ts-expect-error Argument of type '(data: { value: string; }) => number' is not assignable to parameter of type ... eventBus.off(token, (data: { value: string }) => data.value.split('')); }); }); describe('job.emit', () => { test('accepts token', () => { eventBus.job.emit(token); }); }); describe('job.on', () => { test('accepts valid handler', () => { eventBus.job.on(token, ({ resolve }) => resolve({ value: 1 })); eventBus.job.on(token, ({ reject }) => reject(new Error('Oops!'))); eventBus.job.on(token, ({ reject }) => reject('Oops!')); eventBus.job.on(optionalPayloadToken, ({ resolve }) => resolve()); }); test('rejects invalid handler', () => { eventBus.job.on(token, ({ resolve }) => { // @ts-expect-error Type 'string' is not assignable to type 'number'.ts(2322) resolve({ value: '' }); }); eventBus.job.on(token, ({ reject }) => { // @ts-expect-error Argument of type '123' is not assignable to parameter of type 'string | Error | undefined'. reject(123); }); }); }); describe('job.off', () => { test('accepts valid handler', () => { eventBus.job.off(token, ({ resolve }) => resolve({ value: 1 })); eventBus.job.off(token, ({ reject }) => reject(new Error('Oops!'))); eventBus.job.off(token, ({ reject }) => reject('Oops!')); }); test('rejects invalid handler', () => { eventBus.job.off(token, ({ resolve }) => { // @ts-expect-error Type 'string' is not assignable to type 'number'.ts(2322) resolve({ value: '' }); }); eventBus.job.off(token, ({ reject }) => { // @ts-expect-error Argument of type '123' is not assignable to parameter of type 'string | Error | undefined'. reject(123); }); }); }); }); }); }); });