Testing functions for Motorcycle.ts
yarn add @motorcycle/test
# or
npm install --save @motorcycle/test
All functions are curried!
TestScheduler
export type TestScheduler = {
readonly tick: (delay: Delay) => Promise<void>
readonly scheduler: Scheduler
}
A Timer instance with control over how time progresses.
See an example
import { VirtualTimer } from '@motorcycle/test'
const timer = new VirtualTimer()
timer.setTimer(() => console.log('Hello'), 100)
timer.tick(100)
See the code
export class VirtualTimer implements Timer {
protected time: Time = 0
protected targetTime: Time = 0
protected currentTime: Time = Infinity
protected task: (() => any) | void = void 0
protected timer: Handle
protected active: boolean = false
protected running: boolean = false
protected key: Handle = {}
protected promise: Promise<void> = Promise.resolve()
constructor() {}
public now(): Time {
return this.time
}
public setTimer(fn: () => any, delay: Delay): Handle {
if (this.task !== void 0) throw new Error('Virtualtimer: Only supports one in-flight task')
this.task = fn
this.currentTime = this.time + Math.max(0, delay)
if (this.active) this.run()
return this.key
}
public clearTimer(handle: Handle) {
if (handle !== this.key) return
clearTimeout(this.timer)
this.timer = void 0
this.currentTime = Infinity
this.task = void 0
}
public tick(delay: Delay) {
if (delay <= 0) return this.promise
this.targetTime = this.targetTime + delay
return this.run()
}
protected run() {
if (this.running) return this.promise
this.running = true
this.active = true
return new Promise<void>((resolve, reject) => {
this.timer = setTimeout(() => {
this.step()
.then(() => resolve())
.catch(reject)
}, 0)
})
}
protected step() {
return new Promise((resolve, reject) => {
if (this.time >= this.targetTime) {
this.time = this.targetTime
this.currentTime = Infinity
this.running = false
return resolve()
}
const task = this.task
this.task = void 0
this.time = this.currentTime
this.currentTime = Infinity
if (typeof task === 'function') task()
this.timer = setTimeout(
() =>
this.step()
.then(() => resolve())
.catch(reject),
0
)
})
}
}
Collects events for a given amount of time.
See an example
// Mocha style tests
it('increasing value by one', () => {
const stream = scan(x => x + 1, skip(1, periodic(10)))
return collectEventsFor(30, stream).then(events => assert.deepEqual(events, [0, 1, 2, 3]))
})
See the code
export const collectEventsFor: CollectEventsFor = curry2(function collectEventsFor<A>(
delay: Delay,
stream: Stream<A>
) {
const { tick, scheduler } = createTestScheduler()
const eventList: Array<A> = []
runEffects(tap(a => eventList.push(a), stream), scheduler)
return tick(delay).then(() => eventList.slice())
})
export interface CollectEventsFor {
<A>(delay: Delay, stream: Stream<A>): Promise<ReadonlyArray<A>>
(delay: Delay): <A>(stream: Stream<A>) => Promise<ReadonlyArray<A>>
<A>(delay: Delay): (stream: Stream<A>) => Promise<ReadonlyArray<A>>
}
Creates a test scheduler. Using the test scheduler you are the master of time.
See an example
import { createTestScheduler } from '@motorcycle/test'
import { now, runEffects } from '@motorcycle/stream'
const { tick, scheduler } createTestScheduler()
const stream = now(100)
runEffects(stream, scheduler).then(() => console.log('done!'))
// manually tick forward in time
// tick returns a Promise that resolves when all scheduled tasks have been run.
tick(100)
See the code
export function createTestScheduler(timeline: Timeline = newTimeline()): TestScheduler {
const timer = new VirtualTimer()
const tick = (delay: Delay) => timer.tick(delay)
const scheduler: Scheduler = newScheduler(timer, timeline)
return { tick, scheduler }
}
This is nearly identical to the run
found inside of @motorcycle/run
. The
only difference is that it makes use of the test scheduler to create the
application's event loop. An additional property is returned with the tick
that allows you to control how time progresses.
See an example
import { run } from '@motorcycle/test'
import { makeDomComponent, div, button, h2, query, clickEvent } from '@motorcycle/dom'
function Main(sources) {
const { dom } = sources
const click$ = clickEvent(query('button', dom))
const count$ = scan(x => x + 1, click$)
const view$ = map(view, count$)
return { view$ }
}
function view(count: number) {
return div([
h2(`Clicked ${count} times`),
button('Click Me'),
])
}
const Dom = fakeDomComponent({
'button': {
click: now(fakeEvent())
}
})
const { tick, dispose } = run(UI, Dom)
tick(500).then(dispose)
See the code
export function run<
Sources extends Readonly<Record<string, any>>,
Sinks extends Readonly<Record<string, Stream<any>>>
>(Main: Component<Sources, Sinks>, IO: IOComponent<Sinks, Sources>) {
const { stream: endSignal } = createProxy<void>()
const sinkProxies = {} as Record<keyof Sinks, ProxyStream<any>>
const proxySinks: Sinks = createProxySinks(sinkProxies, endSignal)
const sources: Sources = IO(proxySinks)
const sinks: Sinks = createDisposableSinks(Main(sources), endSignal)
const { disposable, tick } = replicateSinks(sinks, sinkProxies)
function dispose() {
endSignal.event(scheduler.currentTime(), void 0)
disposable.dispose()
disposeSources(sources)
}
return { sinks, sources, dispose, tick }
}