Skip to content

Commit

Permalink
feat(test): implement @motorcycle/test
Browse files Browse the repository at this point in the history
MVP of what @motorcycle/test can be.
  • Loading branch information
TylorS committed Jul 20, 2017
1 parent cf4bb92 commit 487f67d
Show file tree
Hide file tree
Showing 24 changed files with 1,111 additions and 0 deletions.
320 changes: 320 additions & 0 deletions packages/test/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,320 @@
# @motorcycle/test -- 0.0.0

Testing functions for Motorcycle.ts

## Get it
```sh
yarn add @motorcycle/test
# or
npm install --save @motorcycle/test
```

## API Documentation

All functions are curried!

#### VirtualTimer

<p>

A Timer instance with control over how time progresses.

</p>


<details>
<summary>See an example</summary>

```typescript
import { VirtualTimer } from '@motorcycle/test'

const timer = new VirtualTimer()

timer.setTimer(() => console.log('Hello'), 100)

timer.tick(100)
```

</details>

<details>
<summary>See the code</summary>

```typescript

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)
})
}
}

```

</details>

<hr />


#### collectEventsFor\<A\>(delay: Delay, stream: Stream\<A\>): Promise\<ReadonlyArray\<A\>\>

<p>

Collects events for a given amount of time.

</p>


<details>
<summary>See an example</summary>

```typescript
// 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]))
})
```

</details>

<details>
<summary>See the code</summary>

```typescript

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>>
}

```

</details>

<hr />


#### createTestScheduler(timeline?: Timeline): TestScheduler

<p>

Creates a test scheduler. Using the test scheduler you are the master of time.

</p>


<details>
<summary>See an example</summary>

```typescript
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)
```

</details>

<details>
<summary>See the code</summary>

```typescript

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 }
}

```

</details>

<hr />


#### run\<Sources, Sinks\>(UI: Component\<Sources, Sinks\>, Application: EffectfulComponent\<Sinks, Sources\>)

<p>

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.

</p>


<details>
<summary>See an example</summary>

```typescript
import { run } from '@motorcycle/test'
import { makeDomComponent, div, button, h2, query, clickEvent } from '@motorcycle/dom'

function UI(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)
```

</details>

<details>
<summary>See the code</summary>

```typescript

export function run<
Sources extends Readonly<Record<string, any>>,
Sinks extends Readonly<Record<string, Stream<any>>>
>(UI: Component<Sources, Sinks>, Application: EffectfulComponent<Sinks, Sources>) {
const { stream: endSignal } = createProxy<void>()

const sinkProxies = {} as Record<keyof Sinks, ProxyStream<any>>
const proxySinks: Sinks = createProxySinks(sinkProxies, endSignal)
const sources: Sources = Application(proxySinks)
const sinks: Sinks = createDisposableSinks(UI(sources), endSignal)

const { disposable, tick } = replicateSinks(sinks, sinkProxies)

function dispose() {
endSignal.event(scheduler.now(), void 0)
disposable.dispose()
disposeSources(sources)
}

return { sinks, sources, dispose, tick }
}

```

</details>

<hr />
43 changes: 43 additions & 0 deletions packages/test/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"name": "@motorcycle/test",
"version": "0.0.0",
"description": "Testing functions for Motorcycle.ts",
"main": "lib/index.js",
"module": "lib.es2015/index.js",
"jsnext:main": "lib.es2015/index.js",
"typings": "lib/index.d.ts",
"types": "lib/index.d.ts",
"scripts": {
"build": "cd ../../ && node ./tools/build.js --only test",
"test": "yarn test:lint && yarn test:unit",
"test:unit": "../../node_modules/.bin/typed-test 'src/*.test.ts' 'src/**/*.test.ts'",
"test:lint": "../../node_modules/.bin/prettier --write --print-width 100 --tab-width 2 --no-semi --single-quote --trailing-comma es5 --parser typescript src/**/*.ts"
},
"repository": {
"type": "git",
"url": "git+https://github.com/motorcyclets/motorcycle.git"
},
"keywords": [
"test",
"typescript",
"motorcycle",
"most",
"functional",
"reactive",
"streams"
],
"author": "Tylor Steinberger <[email protected]>",
"license": "MIT",
"bugs": {
"url": "https://github.com/motorcyclets/motorcycle/issues"
},
"homepage": "https://github.com/motorcyclets/motorcycle#readme",
"dependencies": {
"@most/scheduler": "0.11.0",
"@motorcycle/stream": "1.1.0",
"@motorcycle/types": "1.1.0"
},
"devDependencies": {
"@most/core": "0.11.2"
}
}
14 changes: 14 additions & 0 deletions packages/test/src/collectEventsFor/collectEventsFor.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Test, describe, given, it } from '@typed/test'
import { periodic, scan, skip } from '@motorcycle/stream'

import { collectEventsFor } from './collectEventsFor'

export const test: Test = describe(`collectEvents`, [
given(`a delay and a Stream`, [
it(`returns Promise of Streams events up to that delay time`, ({ equal }) => {
const stream = scan(x => x + 1, 0, skip(1, periodic(10)))

return collectEventsFor(30, stream).then(equal([0, 1, 2, 3]))
}),
]),
])
Loading

0 comments on commit 487f67d

Please sign in to comment.