@cycle/time
Advanced tools
Comparing version 0.4.0 to 0.5.0-rc0
import xs from 'xstream'; | ||
declare function makeAnimationFrames(addFrameCallback: any, currentTime: any): () => xs<{ | ||
export declare type Frame = { | ||
delta: number; | ||
normalizedDelta: number; | ||
time: number; | ||
}>; | ||
}; | ||
declare function makeAnimationFrames(addFrameCallback: any, currentTime: any): () => xs<Frame>; | ||
export { makeAnimationFrames }; |
"use strict"; | ||
var xstream_1 = require("xstream"); | ||
var adapt_1 = require("@cycle/run/lib/adapt"); | ||
var EXPECTED_DELTA = 1000 / 60; | ||
@@ -12,3 +13,3 @@ function makeAnimationFrames(addFrameCallback, currentTime) { | ||
var stopped = false; | ||
return xstream_1.default.create({ | ||
var frameStream = xstream_1.default.create({ | ||
start: function (listener) { | ||
@@ -35,4 +36,5 @@ frame.time = currentTime(); | ||
}); | ||
return adapt_1.adapt(frameStream); | ||
}; | ||
} | ||
exports.makeAnimationFrames = makeAnimationFrames; |
@@ -70,5 +70,5 @@ "use strict"; | ||
addAssert(assert); | ||
var actualLog$ = actual.compose(Time.record); | ||
var expectedLog$ = expected.compose(Time.record); | ||
xstream_1.default.combine(actualLog$, expectedLog$).addListener({ | ||
var actualLog$ = Time.record(actual); | ||
var expectedLog$ = Time.record(expected); | ||
xstream_1.default.combine(xstream_1.default.fromObservable(actualLog$), xstream_1.default.fromObservable(expectedLog$)).addListener({ | ||
next: function (_a) { | ||
@@ -75,0 +75,0 @@ var aLog = _a[0], bLog = _a[1]; |
"use strict"; | ||
var xstream_1 = require("xstream"); | ||
var adapt_1 = require("@cycle/run/lib/adapt"); | ||
function makeDebounceListener(schedule, currentTime, debounceInterval, listener, state) { | ||
@@ -28,6 +29,6 @@ return { | ||
var state = { scheduledEntry: null }; | ||
return xstream_1.default.create({ | ||
var debouncedStream = xstream_1.default.create({ | ||
start: function (listener) { | ||
var debounceListener = makeDebounceListener(schedule, currentTime, debounceInterval, listener, state); | ||
stream.addListener(debounceListener); | ||
xstream_1.default.fromObservable(stream).addListener(debounceListener); | ||
}, | ||
@@ -37,2 +38,3 @@ // TODO - maybe cancel the scheduled event? | ||
}); | ||
return adapt_1.adapt(debouncedStream); | ||
}; | ||
@@ -39,0 +41,0 @@ }; |
"use strict"; | ||
var xstream_1 = require("xstream"); | ||
var adapt_1 = require("@cycle/run/lib/adapt"); | ||
function makeDelayListener(schedule, currentTime, delayTime, listener) { | ||
@@ -23,7 +24,7 @@ var delayedTime = function () { return currentTime() + delayTime; }; | ||
var delayListener = makeDelayListener(schedule, currentTime, delayTime, listener); | ||
stream.addListener(delayListener); | ||
xstream_1.default.fromObservable(stream).addListener(delayListener); | ||
}, | ||
stop: function () { } | ||
}; | ||
return xstream_1.default.create(producer); | ||
return adapt_1.adapt(xstream_1.default.create(producer)); | ||
}; | ||
@@ -30,0 +31,0 @@ }; |
"use strict"; | ||
var adapt_1 = require("@cycle/run/lib/adapt"); | ||
var xstream_1 = require("xstream"); | ||
@@ -30,5 +31,5 @@ var parseIntIfDecimal = function (str) { | ||
}); | ||
return stream; | ||
return adapt_1.adapt(stream); | ||
}; | ||
} | ||
exports.makeDiagram = makeDiagram; |
@@ -1,3 +0,3 @@ | ||
import { timeDriver } from './time-driver'; | ||
import { mockTimeSource } from './mock-time-source'; | ||
export { timeDriver, mockTimeSource }; | ||
import { TimeSource, MockTimeSource } from './time-source'; | ||
export declare function mockTimeSource(args?: Object): MockTimeSource; | ||
export declare function timeDriver(_: any, adapter: any): TimeSource; |
"use strict"; | ||
var time_driver_1 = require("./time-driver"); | ||
exports.timeDriver = time_driver_1.timeDriver; | ||
var mock_time_source_1 = require("./mock-time-source"); | ||
exports.mockTimeSource = mock_time_source_1.mockTimeSource; | ||
function mockTimeSource(args) { | ||
return mock_time_source_1.mockTimeSource(args); | ||
} | ||
exports.mockTimeSource = mockTimeSource; | ||
function timeDriver(_, adapter) { | ||
return time_driver_1.timeDriver(_, adapter); | ||
} | ||
exports.timeDriver = timeDriver; |
@@ -1,27 +0,4 @@ | ||
import xs from 'xstream'; | ||
declare function mockTimeSource({interval}?: { | ||
interval?: number; | ||
}): { | ||
diagram: (diagramString: string, values?: {}) => xs<any>; | ||
record: (stream: xs<any>) => xs<any>; | ||
assertEqual: (actual: xs<any>, expected: xs<any>) => void; | ||
delay: (delayTime: number) => <T>(stream: xs<T>) => xs<T>; | ||
debounce: (debounceInterval: number) => <T>(stream: xs<T>) => xs<T>; | ||
periodic: (period: number) => xs<number>; | ||
throttle: (period: number) => <T>(stream: xs<T>) => xs<T>; | ||
animationFrames: () => xs<{ | ||
time: number; | ||
delta: number; | ||
normalizedDelta: number; | ||
}>; | ||
throttleAnimation: <T>(stream: xs<T>) => xs<T>; | ||
run(doneCallback?: (err: any) => void, timeToRunTo?: any): void; | ||
_scheduler: { | ||
_schedule: () => any[]; | ||
next(stream: any, time: any, value: any, f?: () => void): any; | ||
error(stream: any, time: any, error: any): any; | ||
completion(stream: any, time: any): any; | ||
}; | ||
_time: () => number; | ||
}; | ||
}): any; | ||
export { mockTimeSource }; |
"use strict"; | ||
var xstream_1 = require("xstream"); | ||
var adapt_1 = require("@cycle/run/lib/adapt"); | ||
function makePeriodic(schedule, currentTime) { | ||
@@ -25,5 +26,5 @@ return function periodic(period) { | ||
}; | ||
return xstream_1.default.create(producer); | ||
return adapt_1.adapt(xstream_1.default.create(producer)); | ||
}; | ||
} | ||
exports.makePeriodic = makePeriodic; |
"use strict"; | ||
var xstream_1 = require("xstream"); | ||
var adapt_1 = require("@cycle/run/lib/adapt"); | ||
function recordListener(currentTime, outListener) { | ||
@@ -25,11 +26,11 @@ var entries = []; | ||
return function record(stream) { | ||
return xstream_1.default.createWithMemory({ | ||
var recordedStream = xstream_1.default.createWithMemory({ | ||
start: function (listener) { | ||
stream.addListener(recordListener(currentTime, listener)); | ||
xstream_1.default.fromObservable(stream).addListener(recordListener(currentTime, listener)); | ||
}, | ||
stop: function () { | ||
} | ||
stop: function () { } | ||
}); | ||
return adapt_1.adapt(recordedStream); | ||
}; | ||
} | ||
exports.makeRecord = makeRecord; |
@@ -37,4 +37,4 @@ "use strict"; | ||
} | ||
processEvent(); | ||
setImmediate(processEvent); | ||
} | ||
exports.runVirtually = runVirtually; |
"use strict"; | ||
var makeAccumulator = require('sorted-immutable-list').default; | ||
var comparator = function (a) { return function (b) { | ||
if (a.time < b.time) { | ||
return -1; | ||
} | ||
if (a.time === b.time) { | ||
// In the case where a complete and next event occur in the same frame, | ||
// the next always comes before the complete | ||
if (a.stream === b.stream) { | ||
if (a.type === 'complete' && b.type === 'next') { | ||
return 1; | ||
} | ||
if (b.type === 'complete' && a.type === 'next') { | ||
return -1; | ||
} | ||
} | ||
return 0; | ||
} | ||
return 1; | ||
}; }; | ||
function makeScheduler() { | ||
@@ -9,3 +28,3 @@ var schedule = []; | ||
var addScheduleEntry = makeAccumulator({ | ||
key: function (entry) { return entry.time; }, | ||
comparator: comparator, | ||
unique: false | ||
@@ -12,0 +31,0 @@ }); |
"use strict"; | ||
var xstream_1 = require("xstream"); | ||
var adapt_1 = require("@cycle/run/lib/adapt"); | ||
function makeThrottleAnimation(timeSource, schedule, currentTime) { | ||
return function throttleAnimation(stream) { | ||
var source = timeSource(); | ||
return xstream_1.default.create({ | ||
var throttledStream = xstream_1.default.create({ | ||
start: function (listener) { | ||
var lastValue = null; | ||
var emittedLastValue = true; | ||
var frame$ = source.animationFrames(); | ||
var frame$ = xstream_1.default.fromObservable(source.animationFrames()); | ||
var animationListener = { | ||
@@ -18,3 +19,3 @@ next: function (event) { | ||
}; | ||
stream.addListener({ | ||
xstream_1.default.fromObservable(stream).addListener({ | ||
next: function (event) { | ||
@@ -36,4 +37,5 @@ lastValue = event; | ||
}); | ||
return adapt_1.adapt(throttledStream); | ||
}; | ||
} | ||
exports.makeThrottleAnimation = makeThrottleAnimation; |
"use strict"; | ||
var xstream_1 = require("xstream"); | ||
var adapt_1 = require("@cycle/run/lib/adapt"); | ||
function makeThrottleListener(schedule, currentTime, period, listener, state) { | ||
@@ -28,9 +29,10 @@ return { | ||
var state = { lastEventTime: -Infinity }; // so that the first event is always scheduled | ||
return xstream_1.default.create({ | ||
var throttledStream = xstream_1.default.create({ | ||
start: function (listener) { | ||
var throttleListener = makeThrottleListener(schedule, currentTime, period, listener, state); | ||
stream.addListener(throttleListener); | ||
xstream_1.default.fromObservable(stream).addListener(throttleListener); | ||
}, | ||
stop: function () { } | ||
}); | ||
return adapt_1.adapt(throttledStream); | ||
}; | ||
@@ -37,0 +39,0 @@ }; |
@@ -1,24 +0,2 @@ | ||
import xs from 'xstream'; | ||
declare function timeDriver(_: any, streamAdapter: any): { | ||
animationFrames: () => xs<{ | ||
delta: number; | ||
normalizedDelta: number; | ||
time: number; | ||
}>; | ||
delay: (delayTime: number) => <T>(stream: xs<T>) => xs<T>; | ||
debounce: (debounceInterval: number) => <T>(stream: xs<T>) => xs<T>; | ||
periodic: (period: number) => xs<number>; | ||
throttle: (period: number) => <T>(stream: xs<T>) => xs<T>; | ||
throttleAnimation: <T>(stream: xs<T>) => xs<T>; | ||
_time: () => number; | ||
_scheduler: { | ||
_schedule: () => any[]; | ||
next(stream: any, time: any, value: any, f?: () => void): any; | ||
error(stream: any, time: any, error: any): any; | ||
completion(stream: any, time: any): any; | ||
}; | ||
_pause: () => boolean; | ||
_resume: (time: any) => void; | ||
_runVirtually: (done: any, timeToRunTo: any) => void; | ||
}; | ||
declare function timeDriver(_: any, streamAdapter: any): any; | ||
export { timeDriver }; |
{ | ||
"name": "@cycle/time", | ||
"version": "0.4.0", | ||
"version": "0.5.0-rc0", | ||
"description": "A time driver designed to enable awesome testing and dev tooling", | ||
@@ -11,3 +11,7 @@ "main": "dist/index.js", | ||
"files": [ | ||
"dist/" | ||
"dist/", | ||
"most.js", | ||
"rxjs.js", | ||
"most.d.ts", | ||
"rxjs.d.ts" | ||
], | ||
@@ -22,3 +26,3 @@ "publishConfig": { | ||
"test/docs": "npm run compile && markdown-doctest", | ||
"compile": "tsc", | ||
"compile": "tsc && tsc rxjs.ts most.ts", | ||
"prepublish": "npm run compile", | ||
@@ -35,3 +39,3 @@ "postpublish": "greenkeeper-postpublish" | ||
"dependencies": { | ||
"@cycle/xstream-run": "^4.2.0", | ||
"@cycle/run": "^1.0.0-rc.9", | ||
"@types/core-js": "^0.9.35", | ||
@@ -47,3 +51,4 @@ "@types/node": "^7.0.0", | ||
"devDependencies": { | ||
"@cycle/dom": "^14.1.0", | ||
"@cycle/dom": "^15.0.0-rc.2", | ||
"@cycle/rxjs-run": "^4.0.0-rc.6", | ||
"@types/mocha": "^2.2.37", | ||
@@ -56,2 +61,4 @@ "browserify": "^14.0.0", | ||
"mocha": "^3.1.2", | ||
"most": "^1.2.1", | ||
"rxjs": "^5.1.0", | ||
"snabbdom-selector": "^1.1.1", | ||
@@ -58,0 +65,0 @@ "ts-node": "^2.0.0", |
303
README.md
@@ -5,21 +5,19 @@ # @cycle/time | ||
`@cycle/time` is a library that deals with all things time related in Cycle.js. It's a driver for time, providing methods like `debounce`, `delay`, `throttle` and `periodic`. It also provides tools for elegantly testing Cycle applications and any functions that use streams. | ||
`@cycle/time` is a time driver and tool for testing Cycle.js applications. It works great with `xstream`, `rxjs` and `most`. | ||
Features | ||
It allows you to write beautiful, fast, declarative tests for your Cycle.js applications using marble diagrams. This is made possible by treating time as a side effect, and wrapping it up in a driver. | ||
Influences | ||
--- | ||
`@cycle/time` is split into two parts, `timeDriver` and `mockTimeSource`. | ||
`@cycle/time` was inspired by and expands upon the approach used by the `TestScheduler` used in `RxJS`. By reimagining the scheduler as a driver, we can better meet the needs of Cycle.js users, and enable powerful test tooling across multiple stream libraries with the same tool. | ||
**Development/production** - `timeDriver` | ||
Additionally, `@cycle/time` implements the API of all the time-based operators provided by `xstream`. | ||
* Super smooth side effect free implementations of `periodic`, `delay`, `debounce` and more | ||
* Enables excellent dev tooling like hot code reloading and time travel | ||
* Powered by `requestAnimationFrame`, so your apps will be faster and smoother | ||
Why put Time in a Driver? | ||
--- | ||
**Testing** - `mockTimeSource` | ||
Time is a side effect, therefore we should interact with Time through a driver. | ||
* Write tests using marble diagram syntax, including expected output | ||
* Blazing fast! 100x faster than tests written with `xstream`'s `fromDiagram` | ||
* No more intermittent failures and timing errors. Runs in virtual time so ordering is guaranteed. | ||
* No more tests timing out when they fail, assertions works even with streams that don't complete | ||
Methods like `xs.periodic` and `Observable.delay()` use imperative calls to `setTimeout` and other browser APIs to schedule events. This is a change in state outside of the scope of our applications, so it should be treated as a side effect. | ||
@@ -29,2 +27,4 @@ Installation | ||
To install the stable version: | ||
```bash | ||
@@ -34,2 +34,47 @@ $ npm install @cycle/time --save | ||
## FAQ | ||
### Why would I want to use the time based operators provided by this library over the ones from my stream library? | ||
Stream libraries' time-based operators (`xstream`'s `periodic`, `delay`, `debounce`, `throttle`, etc) are implemented using `setTimeout`. | ||
`setTimeout` [provides no guarantee](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout#Reasons_for_delays_longer_than_specified) that it will actually fire the event precisely at the given interval. The variance in `setTimeout` has a few consequences. | ||
* It makes it impossible to consistently record streams into diagrams, which prevents asserting two streams are equal | ||
* Events might occur in different orders each time the code is run | ||
* Operators implemented using `setTimeout` cause a real delay in tests. A delay of 300ms is common for normal `fromDiagram` tests | ||
Instead, `@cycle/time` schedules events onto a central queue. In tests, they are then emitted as fast as possible, while guaranteeing the ordering. | ||
This allows incredibly fast tests for complex asynchronous behaviour. A `@cycle/time` test takes 3-5ms to run on my machine. | ||
This approach also means we can express our expected output using a diagram, which is nice. | ||
### (RxJS) Why would I want to use `@cycle/time` over the `TestScheduler` from `RxJS`? | ||
The `TestScheduler` is [an excellent piece of software](https://www.ericponto.com/blog/2017/01/08/rxjs-marble-diagram-tests-with-qunit/) and was a primary inspiration for this library, but it was not designed for Cycle.js. | ||
If you use time-based operators from `RxJS` such as `.delay(200)` or `.debounceTime(200)`, you will notice that they do not work as expected with the `TestScheduler` unless the scheduler is passed as the second argument to the operator. | ||
Aside from it being slightly sad to have to change our app code to enable testing, we now have to pass our test scheduler into our Cycle application. We then need to have logic to determine if we should use the normal scheduler for an operator in production or the test scheduler. | ||
Other people have solved this with global injection and stubbing, but that doesn't feel very in the spirit of Cycle.js to me. | ||
By using `@cycle/time` you can have testing just as good, if not better, than the `TestScheduler` while not having to change your code for tests. | ||
### (RxJS and Most) How do I use operators like `.delay` and `.debounce` without `.compose`? | ||
The equivalent to `.compose` in RxJS is `.let`. | ||
```js | ||
Observable.of('Hello World').let(Time.delay(200)); | ||
``` | ||
The equivalent to `.compose` in Most is `.thru`. | ||
```js | ||
most.of('Hello World').thru(Time.delay(200)); | ||
``` | ||
Usage (Development / Production) | ||
@@ -43,2 +88,3 @@ --- | ||
xstream: | ||
```js | ||
@@ -48,2 +94,14 @@ import {timeDriver} from '@cycle/time'; | ||
RxJS: | ||
<!-- skip-example --> | ||
```js | ||
import {timeDriver} from '@cycle/time/rxjs'; | ||
``` | ||
most.js: | ||
<!-- skip-example --> | ||
```js | ||
import {timeDriver} from '@cycle/time/most'; | ||
``` | ||
Then it needs to be added to the drivers object. | ||
@@ -71,5 +129,4 @@ | ||
``` | ||
The `timeDriver` also provides `delay`, `debounce` and `throttle` operators that can be used with `.compose` (aka RxJS `.let`, Most `.thru`). | ||
The `timeDriver` also provides `delay`, `debounce` and `throttle` operators that can be used with `.compose`. | ||
Additionally, the `timeDriver` provides support for animations. `animationFrames` can be used to build games or animations. `throttleAnimation` can be used to throttle a stream so that only one event passes through each frame. | ||
@@ -84,129 +141,7 @@ | ||
So what does testing with Cycle look like? The basic principle is to subscribe to a stream, and to make assertions about what it emits. Here's a contrived example (using mocha): | ||
Here's an example of what testing with `@cycle/time` looks like: | ||
```js | ||
import assert from 'assert'; | ||
import xs from 'xstream'; | ||
import fromDiagram from 'xstream/extra/fromDiagram'; | ||
Say we have a counter component that we want to test, defined like this: | ||
function double (i) { | ||
return i * 2; | ||
} | ||
describe('double', () => { | ||
it('doubles a number', (done) => { | ||
const input$ = fromDiagram('---1---2---3--|'); | ||
const actual$ = input$.map(double); | ||
const expectedValues = [2, 4, 6]; | ||
actual$.take(expectedValues.length).addListener({ | ||
next (value) { | ||
assert.equal(value, expectedValues.shift()); | ||
}, | ||
error: done, | ||
complete: done | ||
}) | ||
}); | ||
}); | ||
``` | ||
We make an input stream, perform an operation on it, and then make assertions about what comes out the other side. This approach can be used for testing Cycle apps as well. Input is passed via sources using say `mockDOMSource` or directly stubbing out the driver, and assertions are made about sink streams coming out. | ||
There are a few problems here. The first is that `xstream`'s `fromDiagram` is very slow. By default, each character in a diagram string represents 20ms. The above diagram is 15 characters long, and will take 300ms to complete. If you have 10 unit tests like that, suddenly your test suite takes 3 seconds. | ||
Additionally, and perhaps more significantly, since `setTimeout` provides no guarantees of accurate scheduling, writing tests with multiple `fromDiagram` inputs will occasionally fail due to events occurring in the wrong order. | ||
Timing is very important for Cycle applications since streams are about "when this happens, this changes". Time and testing are intertwined with Cycle.js. | ||
So where does `@cycle/time` come in? | ||
```js | ||
import {mockTimeSource} from '@cycle/time'; | ||
function double (i) { | ||
return i * 2; | ||
} | ||
describe('double', () => { | ||
it('doubles a number', (done) => { | ||
const Time = mockTimeSource(); | ||
const input$ = Time.diagram('---1---2---3--|'); | ||
const actual$ = input$.map(double); | ||
const expected$ = Time.diagram('---2---4---6--|'); | ||
Time.assertEqual(actual$, expected$); | ||
Time.run(done); | ||
}); | ||
}); | ||
``` | ||
A few things have changed here. First is that we're now creating our input streams from diagrams using `@cycle/time`. Instead of scheduling their events using `setTimeout`, which is slow and inconsistent, their events are scheduled on a central queue inside of `@cycle/time`. | ||
This queue is processed when we call `Time.run();`. Even though each character in the diagram still represents 20ms, we don't have to wait all that time. Instead, the application's time is managed by `@cycle/time`, so we can run on "virtual time". This means this test is much faster than the equivalent using `xstream` `fromDiagram`, around 100x faster. | ||
This approach is comparable to RxJS's schedulers and HistoricalScheduler approach, but works with `xstream` and potentially other libraries. | ||
There's one other problem with the `fromDiagram` way of testing. | ||
Say you use an operator that performs a time based operation, like `.delay()`. | ||
```js | ||
import assert from 'assert'; | ||
import xs from 'xstream'; | ||
import fromDiagram from 'xstream/extra/fromDiagram'; | ||
import delay from 'xstream/extra/delay'; | ||
describe('xstream delay', () => { | ||
it('slows our test down by 200ms', (done) => { | ||
const input$ = fromDiagram('-1--------2---|'); | ||
const actual$ = input$.compose(delay(200)); | ||
const expectedValues = [1, 2]; | ||
actual$.take(expectedValues.length).addListener({ | ||
next (value) { | ||
assert.equal(value, expectedValues.shift()); | ||
}, | ||
error: done, | ||
complete: done | ||
}); | ||
}); | ||
}); | ||
``` | ||
This test will take at least 200ms to run, because once again `delay` is implemented using `setTimeout`. This is also subject to timing problems, which stops us from expressing our expected output using a marble diagram. Here's the same test written with `@cycle/time`. | ||
```js | ||
import {mockTimeSource} from '@cycle/time'; | ||
describe('@cycle/time delay', () => { | ||
it('is super quick because of virtual time', (done) => { | ||
const Time = mockTimeSource(); | ||
const input$ = Time.diagram('-1--------2---|'); | ||
const actual$ = input$.compose(Time.delay(200)); | ||
const expected$ = Time.diagram('-----------1--------2---|'); | ||
Time.assertEqual(actual$, expected$); | ||
Time.run(done); | ||
}); | ||
}); | ||
``` | ||
Notice that we are now using `Time.delay` instead of the `xstream` equivalent. Like `Time.diagram`, `Time.delay` is implemented by scheduling onto a central queue, and in tests is processed in "virtual time". This means that we no longer have to wait 200ms, but the `.delay` will function exactly as it did before. | ||
Usage (testing Cycle applications) | ||
--- | ||
Say we have a counter, defined like this: | ||
```js | ||
function Counter ({DOM}) { | ||
@@ -239,8 +174,25 @@ const add$ = DOM | ||
We can test this counter using `mockDOMSource`, `snabddom-selector` and `@cycle/time`. | ||
In the test file, firstly import `mockTimeSource`. | ||
xstream: | ||
```js | ||
import {mockTimeSource} from '@cycle/time'; | ||
``` | ||
RxJS: | ||
<!-- skip-example --> | ||
```js | ||
import {mockTimeSource} from '@cycle/time/rxjs'; | ||
``` | ||
most.js: | ||
<!-- skip-example --> | ||
```js | ||
import {mockTimeSource} from '@cycle/time/most'; | ||
``` | ||
For testing components that use `@cycle/dom` we will also want `mockDOMSource` and potentially `snabbdom-selector`. | ||
```js | ||
import {mockDOMSource} from '@cycle/dom'; | ||
import xsAdapter from '@cycle/xstream-adapter'; | ||
import {select} from 'snabbdom-selector' | ||
@@ -252,14 +204,15 @@ | ||
it('increments and decrements in response to clicks', (done) => { | ||
const addClick = `---x--x-------x--x--|`; | ||
const subtractClick = `---------x----------|`; | ||
const expectedCount = `0--1--2--1----2--3--|`; | ||
const Time = mockTimeSource(); | ||
const Time = mockTimeSource(); | ||
const DOM = mockDOMSource(xsAdapter, { | ||
const addClick$ = Time.diagram(`---x--x-------x--x--|`); | ||
const subtractClick$ = Time.diagram(`---------x----------|`); | ||
const expectedCount$ = Time.diagram(`0--1--2--1----2--3--|`); | ||
const DOM = mockDOMSource({ | ||
'.add': { | ||
'click': Time.diagram(addClick) | ||
click: addClick$ | ||
}, | ||
'.subtract': { | ||
'click': Time.diagram(subtractClick) | ||
click: subtractClick$ | ||
}, | ||
@@ -272,4 +225,2 @@ }); | ||
const expectedCount$ = Time.diagram(expectedCount); | ||
Time.assertEqual(count$, expectedCount$) | ||
@@ -282,22 +233,46 @@ | ||
If you want to see more examples of tests using `@cycle/time`, check out the test directory. | ||
**Marble Syntax** | ||
## FAQ | ||
The diagrams in the above test are called [marble diagrams](https://github.com/ReactiveX/rxjs/blob/master/doc/writing-marble-tests.md). | ||
### Why would I want to use the time based operators provided by this library over the ones from `xstream`? | ||
The diagram syntax is inspired by xstream's [fromDiagram](Vhttps://github.com/staltz/xstream/blob/master/EXTRA_DOCS.md#-fromdiagramdiagram-options) and RxJS's [marble diagrams](Vhttps://github.com/ReactiveX/rxjs/blob/master/doc/writing-marble-tests.md). | ||
xstream's time-based operators (`periodic`, `delay`, `debounce`, `throttle`, etc) are implemented using `setTimeout`. | ||
* `-` the passage of time without any events, by default 20 virtual milliseconds | ||
* `1` numbers 0-9 are treated as literal numeric values | ||
* `a` other literal values are strings | ||
* `|` completion of the stream | ||
* `#` an error | ||
`setTimeout` provides no guarantee that it will actually fire the event precisely at the given interval. The variance in `setTimeout` has a few consequences. | ||
We make input streams, run our app, and then make assertions about what comes out the other side. | ||
* It makes it impossible to consistently record streams into diagrams, which prevents asserting two streams are equal | ||
* Events might occur in different orders each time the code is run | ||
* Operators implemented using `setTimeout` cause a real delay in tests. A delay of 300ms is common for normal `fromDiagram` tests | ||
When we call `Time.diagram()`, the events in our diagram are placed on a central queue inside of `Time`. This is called the `schedule`. | ||
Instead, `@cycle/time` schedules events onto a central queue. In tests, they are then emitted as fast as possible, while guaranteeing the ordering. | ||
This queue is processed when we call `Time.run();`, one event at a time. Even though each character in the diagram still represents 20ms, we don't have to wait all that time. Instead, the application's time is managed by `@cycle/time`, so we can run on "virtual time". This means this test is much faster than the equivalent using `xstream` `fromDiagram`, around 100x faster. | ||
This allows incredibly fast tests for complex asynchronous behaviour. A `@cycle/time` test takes 3-5ms to run on my machine. | ||
This approach is comparable to RxJS's schedulers and `TestScheduler` approach, but works across `xstream`, `rxjs` and `most`. | ||
This approach also means we can express our expected output using a diagram, which is nice. | ||
We can also use `@cycle/time` to declaratively test time based operators such as `delay` and `debounce`. | ||
```js | ||
import {mockTimeSource} from '@cycle/time'; | ||
describe('@cycle/time delay', () => { | ||
it('is super quick because of virtual time', (done) => { | ||
const Time = mockTimeSource(); | ||
const input$ = Time.diagram('-1--------2---|'); | ||
const actual$ = input$.compose(Time.delay(200)); | ||
const expected$ = Time.diagram('-----------1--------2---|'); | ||
Time.assertEqual(actual$, expected$); | ||
Time.run(done); | ||
}); | ||
}); | ||
``` | ||
Notice that we are now using `Time.delay` instead of the `xstream` equivalent. Like `Time.diagram`, `Time.delay` is implemented by scheduling onto a central queue, and in tests is processed in "virtual time". This means that we no longer have to wait 200ms, but the `.delay` will function exactly as it did before. | ||
If you want to see more examples of tests using `@cycle/time`, check out the test directory. | ||
## API | ||
@@ -417,10 +392,4 @@ | ||
The diagram syntax is inspired by xstream's [fromDiagram](Vhttps://github.com/staltz/xstream/blob/master/EXTRA_DOCS.md#-fromdiagramdiagram-options) and RxJS's [marble diagrams](Vhttps://github.com/ReactiveX/rxjs/blob/master/doc/writing-marble-tests.md). | ||
Each event in the diagram will be scheduled based on their index in the string and the interval provided to `mockTimeSource`. | ||
* `-` the passage of time without any events, by default 20 virtual millseconds (can be changed by passing an argument to `mockTimeSource`) | ||
* `1` numbers 0-9 are treated as literal numeric values | ||
* `a` other literal values are strings | ||
* `|` completion of the stream | ||
* `#` an error | ||
The stream returned by diagram will only emit events once `Time.run()` is called. | ||
@@ -427,0 +396,0 @@ |
48440
35
829
15
437
+ Added@cycle/run@^1.0.0-rc.9
+ Added@cycle/run@1.0.0(transitive)
+ Addedsymbol-observable@1.2.0(transitive)
+ Addedxstream@10.9.0(transitive)
- Removed@cycle/xstream-run@^4.2.0
- Removed@cycle/base@4.2.0(transitive)
- Removed@cycle/xstream-adapter@3.1.0(transitive)
- Removed@cycle/xstream-run@4.2.0(transitive)