UNPKG

kefir-test-utils

Version:

Framework-agnostic testing tools for Kefir

257 lines (225 loc) 5.66 kB
import {install} from '@sinonjs/fake-timers' function extend(target /*, mixin1, mixin2... */) { for (let i = 1; i < arguments.length; i++) { for (const prop in arguments[i]) { target[prop] = arguments[i][prop] } } return target } const throwEventTypeError = (event) => { throw new TypeError(`Expected event object, received: ${JSON.stringify(event, null, ' ')}`) } export default function createTestHelpers(Kefir) { const END = 'end' const VALUE = 'value' const ERROR = 'error' const send = (obs, events) => { for (const event of events) { switch (event.type) { case VALUE: obs._emitValue(event.value) break case ERROR: obs._emitError(event.value) break case END: obs._emitEnd() break default: throwEventTypeError(event) } } return obs } const parseDiagram = (diagram, events) => { const frames = [] advancing: for (const frame of diagram) { switch (frame) { case '-': frames.push([]) break case '#': frames.push([error(new Error())]) break case '|': frames.push([end()]) break advancing default: const event = events[frame] ?? value(frame.match(/\d/) ? Number(frame) : frame) frames.push(Array.isArray(event) ? event : [event]) break } } return frames } const sendFrames = (obs, {frames, advance}) => { for (const frame of frames) { send(obs, frame) advance() } } const value = (val, {current = false} = {}) => ({ type: VALUE, value: val, current, }) const error = (err, {current = false} = {}) => ({ type: ERROR, value: err, current, }) const end = ({current = false} = {}) => ({ type: END, current, }) const _activateHelper = () => {} const activate = (obs) => { obs.onEnd(_activateHelper) return obs } const deactivate = (obs) => { obs.offEnd(_activateHelper) return obs } const prop = () => new Kefir.Property() const stream = () => new Kefir.Stream() const pool = () => new Kefir.Pool() // This function changes timers' IDs so "simultaneous" timers are reversed // Also sets createdAt to 0 so closk.tick will sort by ID // FIXME: // 1) Not sure how well it works with interval timers (setInterval), probably bad // 2) We need to restore (unshake) them back somehow (after calling tick) // Hopefully we'll get a native implementation, and wont have to fix those // https://github.com/sinonjs/lolex/issues/24 const shakeTimers = (clock) => { const ids = Object.keys(clock.timers) const timers = ids.map((id) => clock.timers[id]) // see https://github.com/sinonjs/lolex/blob/a93c8a9af05fb064ae5c2ad1bfc72874973167ee/src/lolex.js#L175-L209 timers.sort((a, b) => { if (a.callAt < b.callAt) { return -1 } if (a.callAt > b.callAt) { return 1 } if (a.immediate && !b.immediate) { return -1 } if (!a.immediate && b.immediate) { return 1 } // Following two cheks are reversed if (a.createdAt < b.createdAt) { return 1 } if (a.createdAt > b.createdAt) { return -1 } if (a.id < b.id) { return 1 } if (a.id > b.id) { return -1 } }) ids.sort((a, b) => a - b) timers.forEach((timer, i) => { const id = ids[i] timer.createdAt = 0 timer.id = id clock.timers[id] = timer }) } const withFakeTime = (cb, reverseSimultaneous = false) => { const clock = install({now: 1000}) const tick = (t) => { if (reverseSimultaneous && clock.timers) { shakeTimers(clock) } clock.tick(t) } let error = null try { cb(tick, clock) } catch (e) { error = e } finally { clock.uninstall() if (error) { throw error } } } const logItem = (event, current) => { switch (event.type) { case VALUE: return value(event.value, {current}) case ERROR: return error(event.value, {current}) case END: return end({current}) default: throwEventTypeError(event) } } const watch = (obs) => { const log = [] let isCurrent = true const fn = (event) => log.push(logItem(event, isCurrent)) const unwatch = () => obs.offAny(fn) obs.onAny(fn) isCurrent = false return {log, unwatch} } const watchWithTime = (obs) => { const startTime = +new Date() const log = [] let isCurrent = true const fn = (event) => log.push([+new Date() - startTime, logItem(event, isCurrent)]) const unwatch = () => obs.offAny(fn) obs.onAny(fn) isCurrent = false return {log, unwatch} } const observables = { active: [], clear: function () { this.active = [] }, } const {_onActivation, _onDeactivation} = Kefir.Observable.prototype extend(Kefir.Observable.prototype, { _onActivation() { observables.active.push(this) _onActivation.apply(this) }, _onDeactivation() { observables.active.splice(observables.active.indexOf(this), 1) _onDeactivation.apply(this) }, }) return { END, VALUE, ERROR, observables, send, parseDiagram, sendFrames, value, error, end, activate, deactivate, prop, stream, pool, shakeTimers, withFakeTime, logItem, watch, watchWithTime, } }