Big refactoring + testing

This commit is contained in:
Kai Röder 2018-12-16 15:33:20 +01:00
parent 22c2139dd9
commit f67bf03cd2
2 changed files with 202 additions and 127 deletions

View File

@ -40,120 +40,193 @@ describe('Channel', () => {
describe('method:addListener', () => {
it('should create one listener', () => {
const keyValue = 'stringAsKey';
const eventName = 'event1';
channel.addListener(keyValue, jest.fn());
expect(channel.listeners(keyValue).length).toBe(1);
});
});
describe('method:emit', () => {
// todo check if [] or string is returned
it('should emit the added listener', () => {
const keyValue = 'stringAsKey';
const mockListener: Listener = (data: string) => {
return data;
};
const mockReturnValue = ['string1', 'string2', 'string3'];
channel.addListener(keyValue, mockListener);
channel.emit<string>(keyValue, ...mockReturnValue);
expect(mockListener).toReturnWith(mockReturnValue);
});
});
describe('method:addPeerListener', () => {});
// todo before, addListener was called with numbers; is this still the correct test?
describe('method:addListener', () => {
it('should call channel.on with args', () => {
const testFn = jest.fn();
channel.on = jest.fn();
channel.addListener('A', testFn);
expect(channel.on).toHaveBeenCalled();
expect(channel.on).toHaveBeenCalledWith('A', testFn);
});
});
describe('method:emit', () => {
it('should call transport.send', () => {
transport.send = jest.fn();
const type = 'test-type';
const args = [1, 2, 3];
const expected = { type, args };
channel.emit(type, ...args);
expect(transport.send).toHaveBeenCalled();
const event: ChannelEvent = transport.send.mock.calls[0][0];
expect(typeof event.from).toEqual('string');
delete event.from;
expect(event).toEqual(expected);
});
it('should be type safe', () => {
transport.send = jest.fn();
const type = 'test-type';
const args = [1, 2, 3];
const expected = { type, args };
// todo check if generic argument typing works
expect(true).toBe(false);
});
it('should call handle async option', () => {
transport.send = jest.fn();
const type = 'test-type';
const args = [1, 2, 3];
channel = new Channel({ async: true, transport });
channel.emit(type, ...args);
expect(transport.send).not.toHaveBeenCalled();
jest.runAllImmediates();
expect(transport.send).toHaveBeenCalled();
});
});
describe('method:eventNames', () => {
it('should return an array of strings', () => {
channel.on('type-1', jest.fn());
channel.on('type-2', jest.fn());
channel.on('type-2', jest.fn());
const expected = ['type-1', 'type-2'];
expect(channel.eventNames()).toEqual(expected);
});
});
describe('method:listenerCount', () => {
it('should return the correct count', () => {
channel.on('type-1', jest.fn());
channel.on('type-2', jest.fn());
channel.on('type-2', jest.fn());
expect(channel.listenerCount('type-1')).toEqual(1);
expect(channel.listenerCount('type-2')).toEqual(2);
});
});
describe('method:listeners', () => {
const fn1 = jest.fn();
const fn2 = jest.fn();
const fn3 = jest.fn();
it('should return an array of listeners', () => {
channel.on('type-1', fn1);
channel.on('type-2', fn2);
channel.on('type-2', fn3);
expect(channel.listeners('type-1')).toEqual([fn1]);
expect(channel.listeners('type-2')).toEqual([fn2, fn3]);
channel.addListener(eventName, jest.fn());
expect(channel.listeners(eventName).length).toBe(1);
});
});
describe('method:on', () => {
const fn1 = jest.fn();
const fn2 = jest.fn();
const fn3 = jest.fn();
it('should do the same as addListener', () => {
const eventName = 'event1';
channel.addListener(eventName, jest.fn());
expect(channel.listeners(eventName).length).toBe(1);
});
});
describe('method:emit', () => {
it('should execute the callback fn of a listener', () => {
const eventName = 'event1';
const listenerInputData = ['string1', 'string2', 'string3'];
let listenerOutputData: string[] = null;
const mockListener: Listener<string[]> = data => {
listenerOutputData = data;
};
channel.addListener(eventName, mockListener);
channel.emit<string[]>(eventName, listenerInputData);
expect(listenerOutputData).toBe(listenerInputData);
});
it('should be callable with a spread operator as event arguments', () => {
const eventName = 'event1';
const listenerInputData = ['string1', 'string2', 'string3'];
let listenerOutputData: string[] = null;
channel.addListener<string>(eventName, (...data) => {
listenerOutputData = data;
});
channel.emit<string>(eventName, ...listenerInputData);
expect(listenerOutputData).toEqual(listenerInputData);
});
it('should use setImmediate if async is true', () => {
channel = new Channel({ async: true, transport });
channel.addListener('event1', jest.fn());
});
});
describe('method:addPeerListener', () => {
it('should add a listener and set ignorePeer to true', () => {
const eventName = 'event1';
channel.addPeerListener(eventName, jest.fn());
expect(channel.listeners(eventName)[0].ignorePeer).toBe(true);
});
});
describe('method:eventNames', () => {
it('should return a list of all registered events', () => {
const eventNames = ['event1', 'event2', 'event3'];
eventNames.forEach(eventName => channel.addListener(eventName, jest.fn()));
expect(channel.eventNames()).toEqual(eventNames);
});
});
describe('method:listenerCount', () => {
it('should return a list of all registered events', () => {
const events = [
{ eventName: 'event1', listeners: [jest.fn(), jest.fn(), jest.fn()], listenerCount: 0 },
{ eventName: 'event2', listeners: [jest.fn()], listenerCount: 0 },
];
events.forEach(event => {
event.listeners.forEach(listener => {
channel.addListener(event.eventName, listener);
event.listenerCount++;
});
});
events.forEach(event => {
expect(channel.listenerCount(event.eventName)).toBe(event.listenerCount);
});
});
});
describe('method:once', () => {
it('should execute a listener once and remove it afterwards', () => {
const eventName = 'event1';
channel.once(eventName, jest.fn());
channel.emit(eventName);
expect(channel.listenerCount(eventName)).toBe(0);
});
it('should pass all event arguments correctly to the listener', () => {
const eventName = 'event1';
const listenerInputData = ['string1', 'string2', 'string3'];
let listenerOutputData = null;
const mockListener: Listener<string[]> = (data: string[]) => {
listenerOutputData = data;
};
channel.once<string[]>(eventName, args => mockListener(args));
channel.emit<string[]>(eventName, listenerInputData);
expect(listenerOutputData).toEqual(listenerInputData);
});
it('should be removable', () => {
const eventName = 'event1';
const listenerToBeRemoved = jest.fn();
channel.once(eventName, listenerToBeRemoved);
channel.removeListener(eventName, listenerToBeRemoved);
});
});
describe('method:prependListener', () => {
it('should prepend listener', () => {
const eventName = 'event1';
const prependFn = jest.fn();
channel.addListener(eventName, jest.fn());
channel.prependListener(eventName, prependFn);
expect(channel.listeners(eventName)[0]).toBe(prependFn);
});
});
describe('method:prependOnceListener', () => {
it('should prepend listener and remove it after one execution', () => {
const eventName = 'event1';
const prependFn = jest.fn();
const otherFns = [jest.fn(), jest.fn(), jest.fn()];
otherFns.forEach(fn => channel.addListener(eventName, fn));
channel.prependOnceListener(eventName, prependFn);
channel.emit(eventName);
otherFns.forEach(listener => {
expect(listener).toBe(
channel.listeners(eventName).find(_listener => _listener === listener)
);
});
});
});
describe('method:removeAllListeners', () => {
it('should remove all listeners', () => {
const eventName1 = 'event1';
const eventName2 = 'event2';
const listeners1 = [jest.fn(), jest.fn(), jest.fn()];
const listeners2 = [jest.fn()];
listeners1.forEach(fn => channel.addListener(eventName1, fn));
listeners2.forEach(fn => channel.addListener(eventName2, fn));
channel.removeAllListeners();
expect(channel.listenerCount(eventName1)).toBe(0);
expect(channel.listenerCount(eventName2)).toBe(0);
});
it('should remove all listeners of a certain event', () => {
const eventName = 'event1';
const listeners = [jest.fn(), jest.fn(), jest.fn()];
listeners.forEach(fn => channel.addListener(eventName, fn));
expect(channel.listenerCount(eventName)).toBe(listeners.length);
channel.removeAllListeners(eventName);
expect(channel.listenerCount(eventName)).toBe(0);
});
});
describe('method:removeListener', () => {
it('should remove one listener', () => {
const eventName = 'event1';
const listenerToBeRemoved = jest.fn();
const listeners = [jest.fn(), jest.fn()];
const findListener = (listener: Listener) =>
channel.listeners(eventName).find(_listener => _listener === listener);
listeners.forEach(fn => channel.addListener(eventName, fn));
channel.addListener(eventName, listenerToBeRemoved);
expect(findListener(listenerToBeRemoved)).toBe(listenerToBeRemoved);
channel.removeListener(eventName, listenerToBeRemoved);
expect(findListener(listenerToBeRemoved)).toBeUndefined();
});
});
});

View File

@ -4,9 +4,9 @@ export interface ChannelTransport {
}
export interface ChannelEvent<TEventArgs = any> {
type: string; // todo deprecate in favor of prop name eventName? type totally confused me after I saw eventNames()
type: string; // eventName
from: string;
args: TEventArgs[];
args: TEventArgs;
}
export interface Listener<TEventArgs = any> {
@ -49,12 +49,12 @@ export class Channel {
return !!this._transport;
}
addListener(eventName: string, listener: Listener) {
addListener<TEventArgs = any>(eventName: string, listener: Listener<TEventArgs>) {
this._events[eventName] = this._events[eventName] || [];
this._events[eventName].push(listener);
}
addPeerListener(eventName: string, listener: Listener) {
addPeerListener<TEventArgs = any>(eventName: string, listener: Listener<TEventArgs[]>) {
const peerListener = listener;
peerListener.ignorePeer = true;
this.addListener(eventName, peerListener);
@ -71,6 +71,7 @@ export class Channel {
};
if (this.isAsync) {
// todo I'm not sure how to test this
setImmediate(handler);
} else {
handler();
@ -91,22 +92,23 @@ export class Channel {
return listeners ? listeners : undefined;
}
once(eventName: string, listener: Listener) {
const onceListener = this._onceListener(eventName, listener);
this.addListener(eventName, onceListener);
once<TEventArgs = any>(eventName: string, listener: Listener<TEventArgs>) {
const onceListener: Listener = this._onceListener<TEventArgs>(eventName, listener);
this.addListener<TEventArgs>(eventName, onceListener);
}
prependListener(eventName: string, listener: Listener) {
prependListener<TEventArgs = any>(eventName: string, listener: Listener<TEventArgs>) {
this._events[eventName] = this._events[eventName] || [];
this._events[eventName].unshift(listener);
}
prependOnceListener(eventName: string, listener: Listener) {
const onceListener = this._onceListener(eventName, listener);
// todo 'listener' is getting mutated by _onceListener, therefore: Input fn() !== Output fn(). This makes testing more difficult
prependOnceListener<TEventArgs = any>(eventName: string, listener: Listener<TEventArgs>) {
const onceListener: Listener = this._onceListener<TEventArgs>(eventName, listener);
this.prependListener(eventName, onceListener);
}
removeAllListeners(eventName: string) {
removeAllListeners(eventName?: string) {
if (!eventName) {
this._events = {};
} else if (this._events[eventName]) {
@ -115,7 +117,7 @@ export class Channel {
}
removeListener(eventName: string, listener: Listener) {
const listeners = this._events[eventName];
const listeners = this.listeners(eventName);
if (listeners) {
this._events[eventName] = listeners.filter(l => l !== listener);
}
@ -124,19 +126,19 @@ export class Channel {
/**
* @deprecated use addListener
*/
on(eventName: string, listener: Listener) {
this.addListener(eventName, listener);
on<TEventArgs = any>(eventName: string, listener: Listener<TEventArgs>) {
this.addListener<TEventArgs>(eventName, listener);
}
private _handleEvent(event: ChannelEvent, isPeer = false) {
private _handleEvent<TEventArgs = any>(event: ChannelEvent<TEventArgs[]>, isPeer = false) {
const listeners = this._events[event.type];
if (listeners && (isPeer || event.from !== this._sender)) {
listeners.forEach(fn => !(isPeer && fn.ignorePeer) && fn(...event.args));
}
}
private _onceListener(eventName: string, listener: Listener) {
const onceListener = (...args: any[]) => {
private _onceListener<TEventArgs>(eventName: string, listener: Listener<TEventArgs>) {
const onceListener: Listener<TEventArgs> = (...args: TEventArgs[]) => {
this.removeListener(eventName, onceListener);
return listener(...args);
};