@motorcycle/test
v3.0.0
Published
Testing functions for Motorcycle.ts
Downloads
223
Readme
@motorcycle/test -- 2.1.0
Testing functions for Motorcycle.ts
Get it
yarn add @motorcycle/test
# or
npm install --save @motorcycle/test
API Documentation
All functions are curried!
TestScheduler
TestScheduler
export type TestScheduler = {
readonly tick: (delay: Delay) => Promise<void>
readonly scheduler: Scheduler
}
VirtualTimer
A Timer instance with control over how time progresses.
import { VirtualTimer } from '@motorcycle/test'
const timer = new VirtualTimer()
timer.setTimer(() => console.log('Hello'), 100)
timer.tick(100)
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
)
})
}
}
collectEventsFor<A>(delay: Delay, stream: Stream<A>): Promise<ReadonlyArray<A>>
Collects events for a given amount of time.
// 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]))
})
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>>
}
createTestScheduler(timeline?: Timeline): TestScheduler
Creates a test scheduler. Using the test scheduler you are the master of time.
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)
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 }
}
run<Sources, Sinks>(Main: Component<Sources, Sinks>, IO: IOComponent<Sinks, Sources>)
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.
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)
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 }
}