Skip to content

Commit

Permalink
Merge pull request #1 from Dashlane/add-tests
Browse files Browse the repository at this point in the history
Add tests
  • Loading branch information
lguychard authored Apr 10, 2018
2 parents 26c54d1 + da8a591 commit a2a22e5
Show file tree
Hide file tree
Showing 9 changed files with 464 additions and 41 deletions.
12 changes: 7 additions & 5 deletions src/Channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export interface Channel {
onData: (cb: OnMessageCallback) => void
onConnect: (cb: () => void) => void
onDisconnect: (cb: () => void) => void
onError?: (e: Error) => void
onError: (cb: (e: Error) => void) => void
}

export abstract class GenericChannel implements Channel {
Expand All @@ -16,8 +16,8 @@ export abstract class GenericChannel implements Channel {
private _onMessageCallbacks: OnMessageCallback[] = []
private _onConnectCallbacks: Function[] = []
private _onDisconnectCallbacks: Function[] = []
private _onErrorCallbacks: Function[] = []
private _ready = false
public onError: (e: Error) => void
public abstract send(message: {}): void

public onData(cb: OnMessageCallback): void {
Expand All @@ -37,14 +37,16 @@ export abstract class GenericChannel implements Channel {
this._onDisconnectCallbacks.push(cb)
}

public onError(cb: Function): void {
this._onErrorCallbacks.push(cb)
}

protected _messageReceived(message: {}) {
this._onMessageCallbacks.forEach(cb => cb(message))
}

protected _error(error: any) {
if (this.onError) {
this.onError(error)
}
this._onErrorCallbacks.forEach(cb => cb(error))
}

protected _connected() {
Expand Down
8 changes: 4 additions & 4 deletions src/Slot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,17 @@ export function slot<RequestData=void, ResponseData=void>(): Slot<RequestData, R
return FAKE_SLOT
}

export function connectSlot<T=void>(slotName: string, transports: Transport[]): Slot<T> {
export function connectSlot<T=void, T2=void>(slotName: string, transports: Transport[]): Slot<T, T2> {

// These will be all the handlers for this slot (eg. all the callbacks registered with `Slot.on()`)
const handlers = [] as Handler<any, any>[]

// For each transport we create a Promise that will be fulfilled only
// when the far-end has registered a handler.
// This prevents `triggers` from firing *before* any far-end is listeninng.
// This prevents `triggers` from firing *before* any far-end is listening.
let remoteHandlersConnected = [] as Promise<any>[]

// Signal to the transport that we will accept handlers for this slotName
// Signal to all transports that we will accept handlers for this slotName
transports.forEach(t => {

// Variable holds the promise's `resolve` function. A little hack
Expand Down Expand Up @@ -110,5 +110,5 @@ export function connectSlot<T=void>(slotName: string, transports: Transport[]):

}

return trigger as Slot<T>
return trigger as Slot<T, T2>
}
15 changes: 10 additions & 5 deletions src/Transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,13 @@ export class Transport {

// When the far end disconnects, remove all the handlers it had set
this._unregisterHandlers()
this._rejectAllPendingRequests()
this._rejectAllPendingRequests(new Error(`${ERRORS.REMOTE_CONNECTION_CLOSED}`))
})

// When an error happens on the channel, reject all pending requests
// (their integrity cannot be guaranteed since onError does not link
// the error to a requestId)
this._channel.onError(e => this._rejectAllPendingRequests(e))
}

/**
Expand All @@ -121,7 +126,8 @@ export class Transport {
slotName,
id,
data: response
}))
})
)

// If the resulting promise is rejected, send an error to the far end
.catch((error: Error) => this._channel.send({
Expand Down Expand Up @@ -165,7 +171,6 @@ export class Transport {
* to the far end, and keep references to the returned Promise's resolution
* and rejection function
*
* TODO: handle timeout
*/
private _registerRemoteHandler({ slotName }: TransportMessage): void {
const addHandler = this._remoteHandlerRegistrationCallbacks[slotName]
Expand Down Expand Up @@ -222,10 +227,10 @@ export class Transport {
})
}

private _rejectAllPendingRequests(): void {
private _rejectAllPendingRequests(e: Error): void {
Object.keys(this._pendingRequests).forEach(slotName => {
Object.keys(this._pendingRequests[slotName]).forEach(id => {
this._pendingRequests[slotName][id].reject(new Error(`${ERRORS.REMOTE_CONNECTION_CLOSED} on ${slotName}`))
this._pendingRequests[slotName][id].reject(e)
})
this._pendingRequests[slotName] = {}
})
Expand Down
40 changes: 40 additions & 0 deletions test/Channel.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import 'should'

import { TestChannel } from './TestChannel'
import * as sinon from 'sinon'

describe('GenericChannel', () => {

it('should call onConnect subscribers when its _connected method is called', () => {
const testInstance = new TestChannel()
const spy = sinon.spy()
testInstance.onConnect(spy)
testInstance.callConnected()
spy.called.should.be.True()
})

it('should call onDisconnect subscribers when its _disconnected method is called', () => {
const testInstance = new TestChannel()
const spy = sinon.spy()
testInstance.onDisconnect(spy)
testInstance.callDisconnected()
spy.called.should.be.True()
})

it('should call onData callbacks when its _messageReceived method is called', () => {
const testInstance = new TestChannel()
const spy = sinon.spy()
testInstance.onData(spy)
testInstance.callMessageReceived()
spy.called.should.be.True()
})

it('should call onError callbacks when its _error method is called', () => {
const testInstance = new TestChannel()
const spy = sinon.spy()
testInstance.onError(spy)
testInstance.callError()
spy.called.should.be.True()
})

})
93 changes: 93 additions & 0 deletions test/Event.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import 'should'

import {slot} from './../src/Slot'
import {combineEvents, createEventBus} from './../src/Events'
import {GenericChannel} from './../src/Channel'
import {TransportMessage} from './../src/Transport'
import {TestChannel} from './TestChannel'
import * as sinon from 'sinon'

describe('combineEvents()', () => {

it('should correctly combine several EventDeclarations', () => {
const combined = combineEvents(
{
hello: slot<{ name: string }>()
},
{
how: slot<{ mode: 'simple' | 'advanced'}>(),
are: slot<{ tense: number }>()
},
{
you: slot<{ reflective: boolean }>()
}
)
Object.keys(combined).should.eql(['hello', 'how', 'are', 'you'])

// Uncomment the following to check that combineEvents
// does preserve each slot's typings: they contain type errors
// combined.hello({ name: 5 })
// combined.how({ mode: 'smiple' })
// combined.are({ tense: true })
// combined.you({ reflective: 5 })
})

})

describe('createEventBus()', () => {

const events = {
numberToString: slot<number, string>()
}

it('should correctly create an event bus with no channels', async () => {

// Attempting to use the events without having
// created an event bus fails
const bad = () => events.numberToString(5)
bad.should.throw(/Slot not connected/)

// After creating an event bus, events can be
// subscribed to and triggered
const eventBus = createEventBus({ events })
eventBus.numberToString.on(num => num.toString())
const res = await eventBus.numberToString(5)
res.should.eql('5')
})

it('should connect the channels passed as argument', async () => {

const channel = new TestChannel()
const eventBus = createEventBus({
events,
channels: [ channel ]
})
channel.callConnected()

// When a handler is added locally, a message should be
// sent through the Channel to signal the registration
eventBus.numberToString.on(num => num.toString())
channel.sendSpy.calledWith({
type: 'handler_registered',
slotName: 'numberToString'
}).should.be.True()

// When a request is sent from the Channel, it should
// be treated and a message sent in response
channel.fakeReceive({
type: 'request',
slotName: 'numberToString',
id: '0',
data: 5
})

await Promise.resolve() // yied to ts-event-bus internals
channel.sendSpy.calledWith({
type: 'response',
slotName: 'numberToString',
id: '0',
data: '5'
}).should.be.True()
})

})
48 changes: 21 additions & 27 deletions test/Handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as sinon from 'sinon'
import { callHandlers } from '../src/Handler'

describe('callHandlers()', () => {

context('with no handler', () => {
it('should return a Promise', () => {
const result = callHandlers('my-data', [])
Expand All @@ -12,78 +13,71 @@ describe('callHandlers()', () => {
})

context('with one handler', () => {
it('should call this handler with the given data', (done) => {

it('should call this handler with the given data', () => {
const handlerSpy = sinon.spy()
const data = 'my-data'
const result = callHandlers(data, [handlerSpy])
return callHandlers(data, [handlerSpy])
.then(() => {
sinon.assert.calledOnce(handlerSpy)
sinon.assert.calledWith(handlerSpy, data)
done()
})
.catch(err => { throw new Error(err) })
})

context('which throw an error', () => {
it('should return a rejected promise', (done) => {

it('should return a rejected promise', () => {
const error = new Error('fail')
const handlerStub = () => {
throw error
}

callHandlers('my-data', [handlerStub])
return callHandlers('my-data', [handlerStub])
.catch(err => {
err.should.equal(error)
done()
})
})
})

context('which return a Promise', () => {
it('should return it', (done) => {
context('which returns a Promise', () => {

it('should return it', () => {
const promise = new Promise((resolve) => { resolve('toto') })
const handlerStub = sinon.stub().returns(promise)

const result = callHandlers('my-data', [handlerStub])
result.should.equal(promise)
result.then(res => {
return result.then(res => {
res.should.equal('toto')
done()
})
})

})

context('which doesnt return a Promise', () => {
it('should return a fullfilled promise with the result', (done) => {
it('should return a fullfilled promise with the result', () => {
const data = 'result'
const handlerStub = sinon.stub().returns(data)

const result = callHandlers('my-data', [handlerStub])
result.should.have.property('then').which.is.a.Function()

result
return result
.then(res => {
res.should.equal(data)
done()
})
.catch(err => { throw new Error(err) })
})
})
})

context('with multiple handlers', () => {
it('should call all of them with the same given data', (done) => {
it('should call all of them with the same given data', async () => {
const handlerSpies = [sinon.spy(), sinon.spy()]
const data = 'my-data'
const result = callHandlers(data, handlerSpies)
.then(() => {
sinon.assert.calledOnce(handlerSpies[0])
sinon.assert.calledOnce(handlerSpies[1])
sinon.assert.calledWith(handlerSpies[0], data)
sinon.assert.calledWith(handlerSpies[1], data)
done()
})
.catch(err => { throw new Error(err) })
await callHandlers(data, handlerSpies)
sinon.assert.calledOnce(handlerSpies[0])
sinon.assert.calledOnce(handlerSpies[1])
sinon.assert.calledWith(handlerSpies[0], data)
sinon.assert.calledWith(handlerSpies[1], data)
})
})
})
})
Loading

0 comments on commit a2a22e5

Please sign in to comment.