Repository: mostjs/core
Pull request #43 (2017-02-17 14:12:46)

Port top level tests
Port all the tests in the top-level test dir.  Some of these should be moved into combinator, etc.  I think we should do that in another PR, since this one is already pretty big.

I also removed buster, and the entire test-buster-old dir.

TylorS: :ship: 

Patch:
diff --git a/.gitignore b/.gitignore
index decc335..94b737e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,5 @@
 node_modules/
-/.idea/
+.idea/
 *.iml
 .DS_Store
 experiments/
diff --git a/packages/core/package.json b/packages/core/package.json
index 77aa2a1..1a079f8 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -12,7 +12,7 @@
   ],
   "scripts": {
     "test": "npm run test:lint && npm run test:unit && npm run test:flow",
-    "test:unit": "buster-test -c test-buster-old/buster.js && mocha -r buba/register --reporter dot --recursive 'test/**/*-test.js'",
+    "test:unit": "mocha -r buba/register --reporter dot --recursive 'test/**/*-test.js'",
     "test:lint": "eslint src test",
     "test:flow": "flow check",
     "build": "npm run build:dist && npm run build:min && npm run build:flow",
diff --git a/packages/core/src/combinator/timeslice.js b/packages/core/src/combinator/timeslice.js
index b192003..2a07462 100644
--- a/packages/core/src/combinator/timeslice.js
+++ b/packages/core/src/combinator/timeslice.js
@@ -7,16 +7,16 @@ import Pipe from '../sink/Pipe'
 import * as dispose from '../disposable/dispose'
 import { join } from './chain'
 
-export function takeUntil (signal, stream) {
+export function until (signal, stream) {
   return new Stream(new Until(signal.source, stream.source))
 }
 
-export function skipUntil (signal, stream) {
+export function since (signal, stream) {
   return new Stream(new Since(signal.source, stream.source))
 }
 
 export function during (timeWindow, stream) {
-  return takeUntil(join(timeWindow), skipUntil(timeWindow, stream))
+  return until(join(timeWindow), since(timeWindow, stream))
 }
 
 function Until (maxSignal, source) {
diff --git a/packages/core/src/index.js b/packages/core/src/index.js
index 02f19ce..bbe6a93 100644
--- a/packages/core/src/index.js
+++ b/packages/core/src/index.js
@@ -133,10 +133,10 @@ export const skipWhile = curry2(_skipWhile)
 // -----------------------------------------------------------------------
 // Time slicing
 
-import { takeUntil as _takeUntil, skipUntil as _skipUntil, during as _during } from './combinator/timeslice'
+import { until as _until, since as _since, during as _during } from './combinator/timeslice'
 
-export const until = curry2(_takeUntil)
-export const since = curry2(_skipUntil)
+export const until = curry2(_until)
+export const since = curry2(_since)
 export const during = curry2(_during)
 
 // -----------------------------------------------------------------------


Test patch:
diff --git a/packages/core/test-buster-old/.jshintrc b/packages/core/test-buster-old/.jshintrc
deleted file mode 100644
index 0185dd7..0000000
--- a/packages/core/test-buster-old/.jshintrc
+++ /dev/null
@@ -1,8 +0,0 @@
-{
-  "globals": {
-    "describe": true,
-    "it": true
-  },
-  "predef": [ "-Promise" ],
-  "expr": true
-}
\ No newline at end of file
diff --git a/packages/core/test-buster-old/Stream-test.js b/packages/core/test-buster-old/Stream-test.js
deleted file mode 100644
index 3987145..0000000
--- a/packages/core/test-buster-old/Stream-test.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import { spec, expect } from 'buster'
-const { describe, it } = spec
-import { default as Stream } from '../src/Stream'
-
-var sentinel = { value: 'sentinel' }
-
-describe('Stream', function () {
-  it('should have expected source', function () {
-    var s = new Stream(sentinel)
-    expect(s.source).toBe(sentinel)
-  })
-})
diff --git a/packages/core/test-buster-old/buster.js b/packages/core/test-buster-old/buster.js
deleted file mode 100644
index 38d4554..0000000
--- a/packages/core/test-buster-old/buster.js
+++ /dev/null
@@ -1,5 +0,0 @@
-require('buba/register')
-exports.all = {
-  environment: 'node',
-  tests: ['**/*-test.js']
-}
diff --git a/packages/core/test-buster-old/combine-test.js b/packages/core/test-buster-old/combine-test.js
deleted file mode 100644
index c39a892..0000000
--- a/packages/core/test-buster-old/combine-test.js
+++ /dev/null
@@ -1,94 +0,0 @@
-import { spec, expect } from 'buster'
-const { describe, it } = spec
-import * as combine from '../src/combinator/combine'
-import { map } from '../src/combinator/transform'
-import { take } from '../src/combinator/slice'
-import { delay } from '../src/combinator/delay'
-import { periodic } from '../src/source/periodic'
-import { just as streamOf } from '../src/source/core'
-
-import { ticks, collectEvents } from './helper/testEnv'
-
-var sentinel = { value: 'sentinel' }
-
-describe('combine', function () {
-  it('should yield initial only after all inputs yield', function () {
-    var s1 = streamOf(1)
-    var s2 = streamOf(sentinel)
-
-    var sc = combine.combine(Array, s1, delay(1, s2))
-
-    return collectEvents(sc, ticks(2)).then(function (events) {
-      expect(events.length).toBe(1)
-      expect(events[0].value).toEqual([1, sentinel])
-    })
-  })
-
-  it('should yield when any input stream yields', function () {
-    var a1 = [0, 1, 2, 3]
-    var s1 = map(function () {
-      return a1.shift()
-    }, periodic(2))
-
-    var a2 = ['a', 'b', 'c', 'd']
-    var s2 = delay(1, map(function () {
-      return a2.shift()
-    }, periodic(2)))
-
-    var sc = combine.combine(Array, take(a1.length, s1), take(a2.length, s2))
-
-    return collectEvents(sc, ticks(a1.length + a2.length))
-      .then(function (events) {
-        expect(events).toEqual([
-          { time: 1, value: [ 0, 'a' ] },
-          { time: 2, value: [ 1, 'a' ] },
-          { time: 3, value: [ 1, 'b' ] },
-          { time: 4, value: [ 2, 'b' ] },
-          { time: 5, value: [ 2, 'c' ] },
-          { time: 6, value: [ 3, 'c' ] },
-          { time: 7, value: [ 3, 'd' ] }
-        ])
-      })
-  })
-})
-
-describe('combineArray', function () {
-  it('should yield initial only after all inputs yield', function () {
-    var s1 = streamOf(1)
-    var s2 = streamOf(sentinel)
-
-    var sc = combine.combineArray(Array, [s1, delay(1, s2)])
-
-    return collectEvents(sc, ticks(2)).then(function (events) {
-      expect(events.length).toBe(1)
-      expect(events[0].value).toEqual([1, sentinel])
-    })
-  })
-
-  it('should yield when any input stream yields', function () {
-    var a1 = [0, 1, 2, 3]
-    var s1 = map(function () {
-      return a1.shift()
-    }, periodic(2))
-
-    var a2 = ['a', 'b', 'c', 'd']
-    var s2 = delay(1, map(function () {
-      return a2.shift()
-    }, periodic(2)))
-
-    var sc = combine.combineArray(Array, [take(a1.length, s1), take(a2.length, s2)])
-
-    return collectEvents(sc, ticks(a1.length + a2.length))
-      .then(function (events) {
-        expect(events).toEqual([
-          { time: 1, value: [ 0, 'a' ] },
-          { time: 2, value: [ 1, 'a' ] },
-          { time: 3, value: [ 1, 'b' ] },
-          { time: 4, value: [ 2, 'b' ] },
-          { time: 5, value: [ 2, 'c' ] },
-          { time: 6, value: [ 3, 'c' ] },
-          { time: 7, value: [ 3, 'd' ] }
-        ])
-      })
-  })
-})
diff --git a/packages/core/test-buster-old/delay-test.js b/packages/core/test-buster-old/delay-test.js
deleted file mode 100644
index 5d7180d..0000000
--- a/packages/core/test-buster-old/delay-test.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import { spec, expect } from 'buster'
-const { describe, it } = spec
-import { delay } from '../src/combinator/delay'
-import { just as streamOf } from '../src/source/core'
-
-import { ticks, collectEvents } from './helper/testEnv'
-
-var sentinel = { value: 'sentinel' }
-
-describe('delay', function () {
-  it('should delay events by delayTime', function () {
-    var dt = 1
-    var s = delay(dt, streamOf(sentinel))
-
-    return collectEvents(s, ticks(dt + 1)).then(function (events) {
-      expect(events.length).toBe(1)
-      expect(events[0].time).toBe(dt)
-      expect(events[0].value).toBe(sentinel)
-    })
-  })
-})
diff --git a/packages/core/test-buster-old/filter-test.js b/packages/core/test-buster-old/filter-test.js
deleted file mode 100644
index 7365eac..0000000
--- a/packages/core/test-buster-old/filter-test.js
+++ /dev/null
@@ -1,56 +0,0 @@
-import { spec, referee } from 'buster'
-const { describe, it } = spec
-const { assert } = referee
-
-import { filter, skipRepeats, skipRepeatsWith } from '../src/combinator/filter'
-import { fromArray } from '../src/source/fromArray'
-
-import { ticks, collectEvents } from './helper/testEnv'
-
-const sentinel = { value: 'sentinel' }
-const other = { value: 'other' }
-
-describe('filter', function () {
-  it('should return a stream containing only allowed items', function () {
-    let a = [sentinel, other, sentinel, other]
-    let p = x => x === sentinel
-    const s = filter(p, fromArray(a))
-
-    return collectEvents(s, ticks(1))
-      .then(events => {
-        const expected = a.filter(p).map(value => ({ time: 0, value }))
-        assert.equals(expected, events)
-      })
-  })
-})
-
-describe('skipRepeats', function () {
-  it('should return a stream with repeated events removed', function () {
-    const s = skipRepeats(fromArray([1, 2, 2, 3, 4, 4]))
-
-    return collectEvents(s, ticks(1))
-      .then(events =>
-        assert.equals([
-          { time: 0, value: 1 },
-          { time: 0, value: 2 },
-          { time: 0, value: 3 },
-          { time: 0, value: 4 }],
-          events))
-  })
-})
-
-describe('skipRepeatsWith', function () {
-  it('should use provided comparator to remove repeated events', function () {
-    const eq = (a, b) => a.toLowerCase() === b.toLowerCase()
-    const s = skipRepeatsWith(eq, fromArray(['a', 'b', 'B', 'c', 'D', 'd']))
-
-    return collectEvents(s, ticks(1))
-      .then(events =>
-        assert.equals([
-          { time: 0, value: 'a' },
-          { time: 0, value: 'b' },
-          { time: 0, value: 'c' },
-          { time: 0, value: 'D' }],
-          events))
-  })
-})
diff --git a/packages/core/test-buster-old/helper/FakeDisposeSource.js b/packages/core/test-buster-old/helper/FakeDisposeSource.js
deleted file mode 100644
index c934678..0000000
--- a/packages/core/test-buster-old/helper/FakeDisposeSource.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import * as dispose from '../../src/disposable/dispose'
-
-export default FakeDisposeSource
-
-FakeDisposeSource.from = function (disposer, stream) {
-  return new FakeDisposeSource(disposer, stream.source)
-}
-
-function FakeDisposeSource (disposer, source) {
-  this.source = source
-  this.disposable = dispose.create(disposer)
-}
-
-FakeDisposeSource.prototype.run = function (sink, scheduler) {
-  return dispose.all([this.source.run(sink, scheduler), this.disposable])
-}
diff --git a/packages/core/test-buster-old/helper/VirtualTimer.js b/packages/core/test-buster-old/helper/VirtualTimer.js
deleted file mode 100644
index c412fbe..0000000
--- a/packages/core/test-buster-old/helper/VirtualTimer.js
+++ /dev/null
@@ -1,92 +0,0 @@
-/** @license MIT License (c) copyright 2010-2015 original author or authors */
-/** @author Brian Cavalier */
-/** @author John Hann */
-
-/*global setTimeout, clearTimeout*/
-
-export default VirtualTimer
-
-function VirtualTimer () {
-  this._now = this._targetNow = 0
-  this._time = Infinity
-  this._task = void 0
-  this._timer = null
-  this._active = false
-  this._running = false
-  this._key = {}
-}
-
-VirtualTimer.prototype.now = function () {
-  return this._now
-}
-
-VirtualTimer.prototype.setTimer = function (f, dt) {
-  if (this._task !== void 0) {
-    throw new Error('VirtualTimer: Only supports one in-flight timer')
-  }
-
-  this._task = f
-  this._time = this._now + Math.max(0, dt)
-  if (this._active) {
-    this._run(f)
-  }
-  return this._key
-}
-
-VirtualTimer.prototype.clearTimer = function (t) {
-  if (t !== this._key) {
-    return
-  }
-
-  this._cancel()
-  this._time = Infinity
-  this._task = void 0
-}
-
-VirtualTimer.prototype.tick = function (dt) {
-  if (dt <= 0) {
-    return
-  }
-
-  this._targetNow = this._targetNow + dt
-  this._run()
-}
-
-VirtualTimer.prototype._run = function () {
-  if (this._running) {
-    return
-  }
-
-  this._active = true
-  this._running = true
-  this._step()
-}
-
-VirtualTimer.prototype._step = function () {
-  this._timer = setTimeout(stepTimer, 0, this)
-}
-
-VirtualTimer.prototype._cancel = function () {
-  clearTimeout(this._timer)
-  this._timer = null
-}
-
-function stepTimer (vt) {
-  if (vt._now >= vt._targetNow) {
-    vt._now = vt._targetNow
-    vt._time = Infinity
-    vt._running = false
-    return
-  }
-
-  var task = vt._task
-  vt._task = void 0
-  vt._now = vt._time
-  vt._time = Infinity
-
-  if (typeof task === 'function') {
-    task()
-  }
-
-  vt._step()
-}
diff --git a/packages/core/test-buster-old/helper/stream-helper.js b/packages/core/test-buster-old/helper/stream-helper.js
deleted file mode 100644
index 29f88c7..0000000
--- a/packages/core/test-buster-old/helper/stream-helper.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import { expect } from 'buster'
-import { reduce } from '../../src/combinator/reduce'
-
-export function assertSame (s1, s2) {
-  return Promise.all([toArray(s1), toArray(s2)]).then(arrayEquals)
-}
-
-export function expectArray (array, s) {
-  return toArray(s).then(function (result) {
-    expect(result).toEqual(array)
-  })
-}
-
-function toArray (s) {
-  return reduce(function (a, x) {
-    a.push(x)
-    return a
-  }, [], s)
-}
-
-function arrayEquals (ss) {
-  expect(ss[0]).toEqual(ss[1])
-}
diff --git a/packages/core/test-buster-old/helper/testEnv.js b/packages/core/test-buster-old/helper/testEnv.js
deleted file mode 100644
index 0652d5b..0000000
--- a/packages/core/test-buster-old/helper/testEnv.js
+++ /dev/null
@@ -1,69 +0,0 @@
-/** @license MIT License (c) copyright 2010-2015 original author or authors */
-/** @author Brian Cavalier */
-/** @author John Hann */
-
-import Stream from '../../src/Stream'
-import { propagateEventTask, propagateEndTask } from '../../src/scheduler/PropagateTask'
-import Scheduler from '../../src/scheduler/Scheduler'
-import Timeline from '../../src/scheduler/Timeline'
-import VirtualTimer from './VirtualTimer'
-import { runEffects } from '../../src/runEffects'
-import { tap } from '../../src/combinator/transform'
-import { create as createDispose, empty as emptyDispose } from '../../src/disposable/dispose'
-
-export function newEnv () {
-  const timer = new VirtualTimer()
-  return { tick: n => timer.tick(n), scheduler: new Scheduler(timer, new Timeline()) }
-}
-
-export function ticks (dt) {
-  const { tick, scheduler } = newEnv()
-  tick(dt)
-  return scheduler
-}
-
-export function collectEvents (stream, scheduler) {
-  const into = []
-  const s = tap(x => into.push({ time: scheduler.now(), value: x }), stream)
-  return runEffects(s, scheduler).then(() => into)
-}
-
-export const collectEventsFor = (nticks, stream) =>
-  collectEvents(stream, ticks(nticks))
-
-export const makeEventsFromArray = (dt, a) =>
-  atTimes(a.map((value, i) => ({ time: i * dt, value })))
-
-export const makeEvents = (dt, n) =>
-  makeEventsFromArray(dt, Array.from(Array(n), (_, i) => i))
-
-export const atTime = (time, value) => atTimes([{ time, value }])
-
-export const atTimes = array => new Stream(new AtTimes(array))
-
-class AtTimes {
-  constructor (array) {
-    this.events = array
-  }
-
-  run (sink, scheduler) {
-    return this.events.length === 0
-      ? emptyDispose()
-      : runEvents(this.events, sink, scheduler)
-  }
-}
-
-const runEvents = (events, sink, scheduler) => {
-  const s = events.reduce(appendEvent(sink, scheduler), { tasks: [], time: 0 })
-  const end = scheduler.delay(s.time, propagateEndTask(undefined, sink))
-  return createDispose(cancelAll, s.tasks.concat(end))
-}
-
-const appendEvent = (sink, scheduler) => (s, event) => {
-  const task = scheduler.delay(event.time, propagateEventTask(event.value, sink))
-  return { tasks: s.tasks.concat(task), time: Math.max(s.time, event.time) }
-}
-
-const cancelAll = tasks => Promise.all(tasks.map(cancelOne))
-
-const cancelOne = task => task.dispose()
diff --git a/packages/core/test-buster-old/limit-test.js b/packages/core/test-buster-old/limit-test.js
deleted file mode 100644
index 89ed71a..0000000
--- a/packages/core/test-buster-old/limit-test.js
+++ /dev/null
@@ -1,163 +0,0 @@
-import { spec, expect } from 'buster'
-const { describe, it } = spec
-import * as limit from '../src/combinator/limit'
-import { periodic } from '../src/source/periodic'
-import { zip } from '../src/combinator/zip'
-import { map } from '../src/combinator/transform'
-import { take } from '../src/combinator/slice'
-import { fromArray } from '../src/source/fromArray'
-import { empty, just as streamOf } from '../src/source/core'
-import { default as Map } from '../src/fusion/Map'
-
-import { ticks, atTimes, collectEvents } from './helper/testEnv'
-import { assertSame } from './helper/stream-helper'
-
-var sentinel = { value: 'sentinel' }
-
-describe('debounce', function () {
-  describe('when events always occur less frequently than debounce period', function () {
-    it('should be identity', function () {
-      var n = 5
-      var period = 2
-      var s = take(n, periodic(period, sentinel))
-
-      var debounced = limit.debounce(1, s)
-
-      return collectEvents(debounced, ticks(n * period))
-        .then(function (events) {
-          expect(events.length).toBe(5)
-        })
-    })
-  })
-
-  describe('when events always occur more frequently than debounce period', function () {
-    it('should be empty when source is empty', function () {
-      var s = limit.debounce(1, empty())
-
-      return collectEvents(s, ticks(1))
-        .then(function (events) {
-          expect(events.length).toBe(0)
-        })
-    })
-
-    it('should be identity when source is singleton', function () {
-      var s = limit.debounce(1, streamOf(sentinel))
-
-      return collectEvents(s, ticks(2))
-        .then(function (events) {
-          expect(events.length).toBe(1)
-          expect(events[0].time).toBe(0)
-          expect(events[0].value).toBe(sentinel)
-        })
-    })
-
-    it('should contain last event when source has many', function () {
-      var a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
-      var n = a.length
-      var expected = a[n - 1]
-
-      var s = take(10, map(function () {
-        return a.shift()
-      }, periodic(1)))
-
-      var debounced = limit.debounce(n, s)
-
-      return collectEvents(debounced, ticks(n))
-        .then(function (events) {
-          expect(events).toEqual([{ time: 9, value: expected }])
-        })
-    })
-  })
-
-  it('should allow events that occur less frequently than debounce period', function () {
-    var s = atTimes([
-      { time: 0, value: 0 },
-      { time: 1, value: 0 },
-      { time: 2, value: 1 },
-      // leave enough time for debounce period
-      { time: 5, value: 0 },
-      { time: 6, value: 0 },
-      { time: 7, value: 2 }
-    ])
-
-    return collectEvents(limit.debounce(2, s), ticks(8))
-      .then(function (events) {
-        expect(events).toEqual([
-          { time: 4, value: 1 },
-          { time: 7, value: 2 }
-        ])
-      })
-  })
-})
-
-describe('throttle', function () {
-  describe('fusion', function () {
-    it('should use max', function () {
-      var s1 = limit.throttle(2, limit.throttle(1, atTimes([])))
-      var s2 = limit.throttle(1, limit.throttle(2, atTimes([])))
-      expect(s1.source.period).toBe(s2.source.period)
-      expect(s1.source.period).toBe(2)
-    })
-
-    it('should commute map', function () {
-      function id (x) {
-        return x
-      }
-      var s = limit.throttle(1, map(id, fromArray([1, 2, 3, 4])))
-
-      expect(s.source instanceof Map).toBe(true)
-      expect(s.source.f).toBe(id)
-      expect(s.source.source.period).toBe(1)
-    })
-  })
-
-  it('should exclude items that are too frequent', function () {
-    var s = atTimes([
-      { time: 0, value: 0 },
-      { time: 1, value: 1 },
-      { time: 2, value: 2 },
-      { time: 3, value: 3 },
-      { time: 4, value: 4 }
-    ])
-    var throttled = limit.throttle(2, s)
-
-    return collectEvents(throttled, ticks(5))
-      .then(function (events) {
-        expect(events).toEqual([
-          { time: 0, value: 0 },
-          { time: 2, value: 2 },
-          { time: 4, value: 4 }
-        ])
-      })
-  })
-
-  it('should be identity when period === 0 and all items are simultaneous', function () {
-    var a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
-    var s = take(10, fromArray(a))
-    return assertSame(s, limit.throttle(0, s))
-  })
-
-  it('should be identity when throttle period >= input period', function () {
-    var a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
-    var p = take(a.length, periodic(1))
-
-    var i1 = 0
-    var s1 = map(function () {
-      return a[i1++]
-    }, p)
-
-    var i2 = 0
-    var s2 = limit.throttle(1, map(function () {
-      return a[i2++]
-    }, p))
-
-    var zipped = zip(Array, s1, s2)
-
-    return collectEvents(zipped, ticks(a.length))
-      .then(function (pairs) {
-        pairs.forEach(function (pair) {
-          expect(pair[0]).toEqual(pair[1])
-        })
-      })
-  })
-})
diff --git a/packages/core/test-buster-old/slice-test.js b/packages/core/test-buster-old/slice-test.js
deleted file mode 100644
index 7e3dfd5..0000000
--- a/packages/core/test-buster-old/slice-test.js
+++ /dev/null
@@ -1,77 +0,0 @@
-import { spec, expect } from 'buster'
-const { describe, it } = spec
-import * as slice from '../src/combinator/slice'
-import { map } from '../src/combinator/transform'
-import { default as Map } from '../src/fusion/Map'
-import { fromArray } from '../src/source/fromArray'
-import { expectArray } from './helper/stream-helper'
-
-describe('slice', function () {
-  describe('fusion', function () {
-    it('should narrow when second slice is smaller', function () {
-      var s = slice.slice(1, 5, slice.slice(1, 10, fromArray([1])))
-      expect(s.source.min).toBe(2)
-      expect(s.source.max).toBe(6)
-    })
-
-    it('should narrow when second slice is larger', function () {
-      var s = slice.slice(1, 10, slice.slice(1, 5, fromArray([1])))
-      expect(s.source.min).toBe(2)
-      expect(s.source.max).toBe(5)
-    })
-
-    it('should commute map', function () {
-      function id (x) {
-        return x
-      }
-      var s = slice.slice(0, 3, map(id, fromArray([1, 2, 3, 4])))
-
-      expect(s.source instanceof Map).toBe(true)
-      expect(s.source.f).toBe(id)
-      return expectArray([1, 2, 3], s)
-    })
-  })
-
-  it('should retain only sliced range', function () {
-    var a = [1, 2, 3, 4, 5, 6, 7, 8, 9]
-    var s = slice.slice(2, a.length - 2, fromArray(a))
-    return expectArray(a.slice(2, a.length - 2), s)
-  })
-})
-
-describe('take', function () {
-  it('should take first n elements', function () {
-    var s = slice.take(2, fromArray([1, 1, 1, 1, 1, 1, 1]))
-    return expectArray([1, 1], s)
-  })
-})
-
-describe('skip', function () {
-  it('should skip first n elements', function () {
-    var a = [1, 1, 1, 1, 1, 1, 1]
-    var s = slice.skip(2, fromArray(a))
-    return expectArray(a.slice(2), s)
-  })
-})
-
-describe('takeWhile', function () {
-  it('should take elements until condition becomes false', function () {
-    var a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
-    var s = slice.takeWhile(function (x) {
-      return x < 5
-    }, fromArray(a))
-
-    return expectArray([0, 1, 2, 3, 4], s)
-  })
-})
-
-describe('skipWhile', function () {
-  it('should skip elements until condition becomes false', function () {
-    var a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
-    var s = slice.skipWhile(function (x) {
-      return x < 5
-    }, fromArray(a))
-
-    return expectArray([5, 6, 7, 8, 9], s)
-  })
-})
diff --git a/packages/core/test-buster-old/timeslice-test.js b/packages/core/test-buster-old/timeslice-test.js
deleted file mode 100644
index b6ac707..0000000
--- a/packages/core/test-buster-old/timeslice-test.js
+++ /dev/null
@@ -1,170 +0,0 @@
-import { spec, expect } from 'buster'
-const { describe, it } = spec
-import * as timeslice from '../src/combinator/timeslice'
-import { take } from '../src/combinator/slice'
-import { periodic } from '../src/source/periodic'
-import { just as streamOf, never } from '../src/source/core'
-import { delay } from '../src/combinator/delay'
-import { default as Stream } from '../src/Stream'
-
-import { runEffects } from '../src/runEffects'
-import { ticks, collectEvents } from './helper/testEnv'
-import FakeDisposeSource from './helper/FakeDisposeSource'
-import { endWith } from './helper/endWith'
-
-var sentinel = { value: 'sentinel' }
-
-describe('during', function () {
-  it('should contain events at or later than min and earlier than max', function () {
-    var stream = periodic(1)
-    var timespan = delay(1, streamOf(delay(5, streamOf())))
-
-    var s = timeslice.during(timespan, stream)
-    return collectEvents(s, ticks(7))
-      .then(function (events) {
-        var len = events.length
-        expect(len).toBe(5)
-        expect(events[0].time).toBe(1)
-        expect(events[len - 1].time).toBe(5)
-      })
-  })
-
-  it('should dispose source stream', function () {
-    var dispose = this.spy()
-    var stream = new Stream(FakeDisposeSource.from(dispose, periodic(1)))
-    var timespan = delay(1, streamOf(delay(5, streamOf())))
-
-    var s = timeslice.during(timespan, stream)
-    return collectEvents(s, ticks(6))
-      .then(function () {
-        expect(dispose).toHaveBeenCalledOnce()
-      })
-  })
-
-  it('should dispose signals', function () {
-    var dispose = this.spy()
-
-    var stream = periodic(1)
-    var timespan = delay(1, streamOf(delay(5, streamOf())))
-    var dt = new Stream(FakeDisposeSource.from(dispose, timespan))
-
-    var s = timeslice.during(dt, stream)
-    return collectEvents(s, ticks(6))
-      .then(function (events) {
-        expect(events.length).toBe(5)
-        expect(dispose).toHaveBeenCalledOnce()
-      })
-  })
-})
-
-describe('takeUntil', function () {
-  it('should only contain events earlier than signal', function () {
-    var stream = periodic(1)
-    var signal = delay(3, streamOf())
-
-    var s = timeslice.takeUntil(signal, stream)
-    return collectEvents(s, ticks(5))
-      .then(function (events) {
-        expect(events.length).toBe(3)
-      })
-  })
-
-  it('should dispose source stream', function () {
-    var dispose = this.spy()
-    var stream = new Stream(FakeDisposeSource.from(dispose, periodic(1)))
-    var signal = delay(3, streamOf())
-
-    var s = timeslice.takeUntil(signal, stream)
-    return collectEvents(s, ticks(5))
-      .then(function () {
-        expect(dispose).toHaveBeenCalledOnce()
-      })
-  })
-
-  it('should end immediately on signal', function () {
-    var dispose = this.spy()
-    var stream = new Stream(FakeDisposeSource.from(dispose, never()))
-    var signal = streamOf()
-
-    var s = timeslice.takeUntil(signal, stream)
-    return collectEvents(s, ticks(1))
-      .then(function (events) {
-        expect(events.length).toBe(0)
-        expect(dispose).toHaveBeenCalledOnce()
-      })
-  })
-
-  it('should dispose signal', function () {
-    var dispose = this.spy()
-    var stream = periodic(1)
-    var signal = new Stream(FakeDisposeSource.from(dispose, delay(3, streamOf())))
-
-    var s = timeslice.takeUntil(signal, stream)
-    return collectEvents(s, ticks(5))
-      .then(function (events) {
-        expect(events.length).toBe(3)
-        expect(dispose).toHaveBeenCalledOnce()
-      })
-  })
-
-  it('should use takeUntil value as end value', function () {
-    var stream = periodic(1)
-    var end = delay(3, streamOf(sentinel))
-
-    var s = timeslice.takeUntil(end, stream)
-    return runEffects(s, ticks(5))
-      .then(function (x) {
-        expect(x).toBe(sentinel)
-      })
-  })
-})
-
-describe('skipUntil', function () {
-  it('should only contain events at or later than signal', function () {
-    var n = 10
-    var stream = take(n, periodic(1))
-    var signal = delay(3, streamOf())
-
-    var s = timeslice.skipUntil(signal, stream)
-    return collectEvents(s, ticks(n))
-      .then(function (events) {
-        expect(events.length).toBe(7)
-      })
-  })
-
-  it('should dispose signal', function () {
-    var dispose = this.spy()
-    var stream = take(10, periodic(1))
-    var signal = new Stream(FakeDisposeSource.from(dispose, delay(3, streamOf())))
-
-    var s = timeslice.skipUntil(signal, stream)
-    return collectEvents(s, ticks(10))
-      .then(function (events) {
-        expect(events.length).toBe(7)
-        expect(dispose).toHaveBeenCalledOnce()
-      })
-  })
-
-  it('should preserve end value', function () {
-    var stream = endWith(sentinel, take(3, periodic(1)))
-    var start = delay(3, streamOf())
-
-    var s = timeslice.skipUntil(start, stream)
-    return runEffects(s, ticks(3))
-      .then(function (x) {
-        expect(x).toBe(sentinel)
-      })
-  })
-
-  it('should allow end before start signal', function () {
-    var stream = take(3, periodic(1))
-    var start = delay(3, streamOf())
-    var end = delay(1, streamOf(sentinel))
-
-    var s = timeslice.skipUntil(start, timeslice.takeUntil(end, stream))
-    return runEffects(s, ticks(3))
-      .then(function (x) {
-        expect(x).toBe(sentinel)
-      })
-  })
-})
diff --git a/packages/core/test-buster-old/zip-test.js b/packages/core/test-buster-old/zip-test.js
deleted file mode 100644
index be2cd57..0000000
--- a/packages/core/test-buster-old/zip-test.js
+++ /dev/null
@@ -1,37 +0,0 @@
-import { spec, expect } from 'buster'
-const { describe, it } = spec
-import { take } from '../src/combinator/slice'
-import { zip } from '../src/combinator/zip'
-import { delay } from '../src/combinator/delay'
-import { fromArray } from '../src/source/fromArray'
-
-import { ticks, collectEvents } from './helper/testEnv'
-
-describe('zip', function () {
-  it('should invoke f for each tuple', function () {
-    var a = [1, 2, 3]
-    var b = [4, 5, 6]
-    var s = zip(Array, delay(1, fromArray(a)), delay(0, fromArray(b)))
-
-    return collectEvents(s, ticks(1))
-      .then(function (events) {
-        expect(events).toEqual([
-          { time: 1, value: [1, 4] },
-          { time: 1, value: [2, 5] },
-          { time: 1, value: [3, 6] }
-        ])
-      })
-  })
-
-  it('should end when shortest stream ends', function () {
-    var s = fromArray([1, 2, 3, 4])
-
-    var a = take(2, s)
-    var b = take(3, s)
-
-    return collectEvents(zip(Array, a, b), ticks(2))
-      .then(function (events) {
-        expect(events.length).toBe(2)
-      })
-  })
-})
diff --git a/packages/core/test/Stream-test.js b/packages/core/test/Stream-test.js
new file mode 100644
index 0000000..55980ea
--- /dev/null
+++ b/packages/core/test/Stream-test.js
@@ -0,0 +1,12 @@
+import { describe, it } from 'mocha'
+import { is } from '@briancavalier/assert'
+
+import { default as Stream } from '../src/Stream'
+
+describe('Stream', function () {
+  it('should have expected source', function () {
+    const source = { run: () => ({ dispose: () => {} }) }
+    const s = new Stream(source)
+    is(source, s.source)
+  })
+})
diff --git a/packages/core/test-buster-old/build-test.js b/packages/core/test/build-test.js
similarity index 58%
rename from packages/core/test-buster-old/build-test.js
rename to packages/core/test/build-test.js
index 48419c8..feacc34 100644
--- a/packages/core/test-buster-old/build-test.js
+++ b/packages/core/test/build-test.js
@@ -1,19 +1,17 @@
-import { spec, referee } from 'buster'
-const { describe, it } = spec
-const { assert } = referee
+import { describe, it } from 'mocha'
+import { eq } from '@briancavalier/assert'
 
 import { startWith, concat } from '../src/combinator/build'
 import { just, empty } from '../src/source/core'
-import { fromArray } from '../src/source/fromArray'
 import { delay } from '../src/combinator/delay'
 
-import { ticks, collectEvents } from './helper/testEnv'
+import { collectEventsFor, makeEventsFromArray } from './helper/testEnv'
 
 const sentinel = { value: 'sentinel' }
 
 const assertSingleEvent = value => events => {
-  assert.same(1, events.length)
-  assert.equals({ time: 0, value }, events[0])
+  eq(1, events.length)
+  eq({ time: 0, value }, events[0])
 }
 
 describe('build', function () {
@@ -21,7 +19,7 @@ describe('build', function () {
     it('should return a stream containing item as head', function () {
       const s = startWith(sentinel, empty())
 
-      return collectEvents(s, ticks(1))
+      return collectEventsFor(1, s)
         .then(assertSingleEvent(sentinel))
     })
   })
@@ -29,30 +27,29 @@ describe('build', function () {
   describe('concat', function () {
     it('should return a stream containing items from both streams in correct order', function () {
       const dt = 1
-      const s1 = delay(dt, fromArray([1, 2]))
-      const s2 = fromArray([3, 4])
+      const s1 = delay(dt, makeEventsFromArray(1, [1, 2]))
+      const s2 = makeEventsFromArray(1, [3, 4])
 
-      return collectEvents(concat(s1, s2), ticks(dt + 1))
-        .then(events =>
-          assert.equals([
+      return collectEventsFor(dt + 3, concat(s1, s2))
+        .then(eq([
             { time: 1, value: 1 },
-            { time: 1, value: 2 },
-            { time: 1, value: 3 },
-            { time: 1, value: 4 }],
-            events))
+            { time: 2, value: 2 },
+            { time: 2, value: 3 },
+            { time: 3, value: 4 }]
+        ))
     })
 
     it('should satisfy left identity', function () {
       const s = concat(just(sentinel), empty())
 
-      return collectEvents(s, ticks(1))
+      return collectEventsFor(1, s)
         .then(assertSingleEvent(sentinel))
     })
 
     it('should satisfy right identity', function () {
       const s = concat(empty(), just(sentinel))
 
-      return collectEvents(s, ticks(1))
+      return collectEventsFor(1, s)
         .then(assertSingleEvent(sentinel))
     })
   })
diff --git a/packages/core/test/combine-test.js b/packages/core/test/combine-test.js
new file mode 100644
index 0000000..680de4c
--- /dev/null
+++ b/packages/core/test/combine-test.js
@@ -0,0 +1,70 @@
+import { describe, it } from 'mocha'
+import { eq } from '@briancavalier/assert'
+
+import { combine, combineArray } from '../src/combinator/combine'
+import { delay } from '../src/combinator/delay'
+import { just } from '../src/source/core'
+
+import { collectEventsFor, makeEvents, makeEventsFromArray } from './helper/testEnv'
+
+const sentinel = { value: 'sentinel' }
+
+describe('combine', function () {
+  it('should yield initial only after all inputs yield', function () {
+    const s1 = just(1)
+    const s2 = just(sentinel)
+
+    const sc = combine(Array, s1, delay(1, s2))
+
+    return collectEventsFor(2, sc)
+      .then(eq([{ time: 1, value: [1, sentinel] }]))
+  })
+
+  it('should yield when any input stream yields', function () {
+    const a = ['a', 'b', 'c', 'd']
+    const s1 = makeEvents(2, a.length)
+    const s2 = delay(1, makeEventsFromArray(2, a))
+
+    const sc = combine(Array, s1, s2)
+
+    return collectEventsFor(2 * a.length, sc)
+      .then(eq([
+        { time: 1, value: [ 0, 'a' ] },
+        { time: 2, value: [ 1, 'a' ] },
+        { time: 3, value: [ 1, 'b' ] },
+        { time: 4, value: [ 2, 'b' ] },
+        { time: 5, value: [ 2, 'c' ] },
+        { time: 6, value: [ 3, 'c' ] },
+        { time: 7, value: [ 3, 'd' ] }]))
+  })
+})
+
+describe('combineArray', function () {
+  it('should yield initial only after all inputs yield', function () {
+    const s1 = just(1)
+    const s2 = just(sentinel)
+
+    const sc = combineArray(Array, [s1, delay(1, s2)])
+
+    return collectEventsFor(2, sc)
+      .then(eq([{ time: 1, value: [1, sentinel] }]))
+  })
+
+  it('should yield when any input stream yields', function () {
+    const a = ['a', 'b', 'c', 'd']
+    const s1 = makeEvents(2, a.length)
+    const s2 = delay(1, makeEventsFromArray(2, a))
+
+    const sc = combineArray(Array, [s1, s2])
+
+    return collectEventsFor(2 * a.length, sc)
+      .then(eq([
+        { time: 1, value: [ 0, 'a' ] },
+        { time: 2, value: [ 1, 'a' ] },
+        { time: 3, value: [ 1, 'b' ] },
+        { time: 4, value: [ 2, 'b' ] },
+        { time: 5, value: [ 2, 'c' ] },
+        { time: 6, value: [ 3, 'c' ] },
+        { time: 7, value: [ 3, 'd' ] }]))
+  })
+})
diff --git a/packages/core/test/delay-test.js b/packages/core/test/delay-test.js
new file mode 100644
index 0000000..1c224d2
--- /dev/null
+++ b/packages/core/test/delay-test.js
@@ -0,0 +1,18 @@
+import { describe, it } from 'mocha'
+import { eq } from '@briancavalier/assert'
+
+import { delay } from '../src/combinator/delay'
+import { just } from '../src/source/core'
+
+import { collectEventsFor } from './helper/testEnv'
+
+describe('delay', function () {
+  it('should delay events by delayTime', function () {
+    const time = 1
+    const value = Math.random()
+    const s = delay(time, just(value))
+
+    return collectEventsFor(time + 1, s)
+      .then(eq([{ time, value }]))
+  })
+})
diff --git a/packages/core/test/filter-test.js b/packages/core/test/filter-test.js
new file mode 100644
index 0000000..20b4b9b
--- /dev/null
+++ b/packages/core/test/filter-test.js
@@ -0,0 +1,54 @@
+import { describe, it } from 'mocha'
+import { eq } from '@briancavalier/assert'
+
+import { filter, skipRepeats, skipRepeatsWith } from '../src/combinator/filter'
+
+import { collectEventsFor, makeEventsFromArray } from './helper/testEnv'
+
+const sentinel = { value: 'sentinel' }
+const other = { value: 'other' }
+
+describe('filter', function () {
+  it('should return a stream containing only allowed items', function () {
+    const a = [sentinel, other, sentinel, other]
+    const p = x => x === sentinel
+    const s = filter(p, makeEventsFromArray(1, a))
+
+    return collectEventsFor(a.length, s)
+      .then(eq([
+        { time: 0, value: sentinel },
+        { time: 2, value: sentinel }
+      ]))
+  })
+})
+
+describe('skipRepeats', function () {
+  it('should return a stream with repeated events removed', function () {
+    const a = [1, 2, 2, 3, 4, 4]
+    const s = skipRepeats(makeEventsFromArray(1, a))
+
+    return collectEventsFor(a.length, s)
+      .then(eq([
+        { time: 0, value: 1 },
+        { time: 1, value: 2 },
+        { time: 3, value: 3 },
+        { time: 4, value: 4 }
+      ]))
+  })
+})
+
+describe('skipRepeatsWith', function () {
+  it('should use provided comparator to remove repeated events', function () {
+    const compare = (a, b) => a.toLowerCase() === b.toLowerCase()
+    const a = ['a', 'b', 'B', 'c', 'D', 'd']
+    const s = skipRepeatsWith(compare, makeEventsFromArray(1, a))
+
+    return collectEventsFor(a.length, s)
+      .then(eq([
+        { time: 0, value: 'a' },
+        { time: 1, value: 'b' },
+        { time: 3, value: 'c' },
+        { time: 4, value: 'D' }
+      ]))
+  })
+})
diff --git a/packages/core/test-buster-old/helper/ArrayIterable.js b/packages/core/test/helper/ArrayIterable.js
similarity index 100%
rename from packages/core/test-buster-old/helper/ArrayIterable.js
rename to packages/core/test/helper/ArrayIterable.js
diff --git a/packages/core/test-buster-old/helper/FakeEventEmitter.js b/packages/core/test/helper/FakeEventEmitter.js
similarity index 100%
rename from packages/core/test-buster-old/helper/FakeEventEmitter.js
rename to packages/core/test/helper/FakeEventEmitter.js
diff --git a/packages/core/test-buster-old/helper/FakeEventTarget.js b/packages/core/test/helper/FakeEventTarget.js
similarity index 100%
rename from packages/core/test-buster-old/helper/FakeEventTarget.js
rename to packages/core/test/helper/FakeEventTarget.js
diff --git a/packages/core/test-buster-old/helper/Iteration.js b/packages/core/test/helper/Iteration.js
similarity index 100%
rename from packages/core/test-buster-old/helper/Iteration.js
rename to packages/core/test/helper/Iteration.js
diff --git a/packages/core/test-buster-old/helper/delayPromise.js b/packages/core/test/helper/delayPromise.js
similarity index 100%
rename from packages/core/test-buster-old/helper/delayPromise.js
rename to packages/core/test/helper/delayPromise.js
diff --git a/packages/core/test-buster-old/helper/endWith.js b/packages/core/test/helper/endWith.js
similarity index 100%
rename from packages/core/test-buster-old/helper/endWith.js
rename to packages/core/test/helper/endWith.js
diff --git a/packages/core/test/limit-test.js b/packages/core/test/limit-test.js
new file mode 100644
index 0000000..d182241
--- /dev/null
+++ b/packages/core/test/limit-test.js
@@ -0,0 +1,136 @@
+import { describe, it } from 'mocha'
+import { eq, is, assert } from '@briancavalier/assert'
+
+import { debounce, throttle } from '../src/combinator/limit'
+import { zip } from '../src/combinator/zip'
+import { map } from '../src/combinator/transform'
+import { take } from '../src/combinator/slice'
+import { empty, just } from '../src/source/core'
+import { default as Map } from '../src/fusion/Map'
+
+import { atTimes, collectEventsFor, makeEventsFromArray, makeEvents } from './helper/testEnv'
+import { assertSame } from './helper/stream-helper'
+
+const sentinel = { value: 'sentinel' }
+
+describe('debounce', function () {
+  describe('when events always occur less frequently than debounce period', function () {
+    it('should be identity', function () {
+      const n = 5
+      const period = 2
+      const s = makeEvents(period, n)
+
+      const debounced = debounce(1, s)
+
+      return collectEventsFor(n * period, debounced)
+        .then(eq([
+          { time: 1, value: 0 },
+          { time: 3, value: 1 },
+          { time: 5, value: 2 },
+          { time: 7, value: 3 },
+          { time: 8, value: 4 } // stream ends, last event emitted immediately
+        ]))
+    })
+  })
+
+  describe('when events always occur more frequently than debounce period', function () {
+    it('should be empty when source is empty', function () {
+      const s = debounce(1, empty())
+      return collectEventsFor(1, s)
+        .then(eq([]))
+    })
+
+    it('should be identity when source is singleton', function () {
+      const s = debounce(1, just(sentinel))
+      return collectEventsFor(2, s)
+        .then(eq([{ time: 0, value: sentinel }]))
+    })
+
+    it('should contain last event when source has many', function () {
+      const a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
+      const n = a.length
+      const expected = a[n - 1]
+
+      const debounced = debounce(n, makeEventsFromArray(1, a))
+
+      return collectEventsFor(n, debounced)
+        .then(eq([{ time: 9, value: expected }]))
+    })
+  })
+
+  it('should allow events that occur less frequently than debounce period', function () {
+    const s = atTimes([
+      { time: 0, value: 0 },
+      { time: 1, value: 0 },
+      { time: 2, value: 1 },
+      // leave enough time for debounce period
+      { time: 5, value: 0 },
+      { time: 6, value: 0 },
+      { time: 7, value: 2 }
+    ])
+
+    const debounced = debounce(2, s)
+
+    return collectEventsFor(8, debounced)
+      .then(eq([
+        { time: 4, value: 1 },
+        { time: 7, value: 2 }
+      ]))
+  })
+})
+
+describe('throttle', function () {
+  describe('fusion', function () {
+    it('should use max', function () {
+      const s1 = throttle(2, throttle(1, atTimes([])))
+      const s2 = throttle(1, throttle(2, atTimes([])))
+      eq(2, eq(s1.source.period, s2.source.period))
+    })
+
+    it('should commute map', function () {
+      const id = x => x
+      const s = throttle(1, map(id, just()))
+
+      assert(s.source instanceof Map)
+      is(id, s.source.f)
+      eq(1, s.source.source.period)
+    })
+  })
+
+  it('should exclude items that are too frequent', function () {
+    const s = atTimes([
+      { time: 0, value: 0 },
+      { time: 1, value: 1 },
+      { time: 2, value: 2 },
+      { time: 3, value: 3 },
+      { time: 4, value: 4 }
+    ])
+
+    const throttled = throttle(2, s)
+
+    return collectEventsFor(5, throttled)
+      .then(eq([
+        { time: 0, value: 0 },
+        { time: 2, value: 2 },
+        { time: 4, value: 4 }
+      ]))
+  })
+
+  it('should be identity when period === 0 and all items are simultaneous', function () {
+    const a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
+    const s = take(10, makeEventsFromArray(0, a))
+    return assertSame(s, throttle(0, s))
+  })
+
+  it('should be identity when throttle period >= input period', function () {
+    const a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
+
+    const s1 = makeEventsFromArray(1, a)
+    const s2 = throttle(1, makeEventsFromArray(1, a))
+
+    const zipped = zip(Array, s1, s2)
+
+    return collectEventsFor(a.length, zipped)
+      .then(pairs => pairs.forEach(pair => eq(pair[0], pair[1])))
+  })
+})
diff --git a/packages/core/test/slice-test.js b/packages/core/test/slice-test.js
new file mode 100644
index 0000000..c14d690
--- /dev/null
+++ b/packages/core/test/slice-test.js
@@ -0,0 +1,101 @@
+import { describe, it } from 'mocha'
+import { eq, is, assert } from '@briancavalier/assert'
+
+import { slice, take, skip, takeWhile, skipWhile } from '../src/combinator/slice'
+import { map } from '../src/combinator/transform'
+import { default as Map } from '../src/fusion/Map'
+
+import { makeEventsFromArray, collectEventsFor, makeEvents } from './helper/testEnv'
+
+describe('slice', function () {
+  describe('fusion', function () {
+    it('should narrow when second slice is smaller', function () {
+      const s = slice(1, 5, slice(1, 10, makeEvents(1, 1)))
+      eq(2, s.source.min)
+      eq(6, s.source.max)
+    })
+
+    it('should narrow when second slice is larger', function () {
+      const s = slice(1, 10, slice(1, 5, makeEvents(1, 1)))
+      eq(2, s.source.min)
+      eq(5, s.source.max)
+    })
+
+    it('should commute map', function () {
+      const id = x => x
+      const s = slice(0, 3, map(id, makeEventsFromArray(1, [0, 1, 2, 3])))
+
+      assert(s.source instanceof Map)
+      is(id, s.source.f)
+      return collectEventsFor(3, s)
+        .then(eq([
+          { time: 0, value: 0 },
+          { time: 1, value: 1 },
+          { time: 2, value: 2 }
+        ]))
+    })
+  })
+
+  it('should retain only sliced range', function () {
+    const a = [0, 1, 2, 3, 4, 5, 6, 7, 8]
+    const s = slice(2, a.length - 2, makeEventsFromArray(1, a))
+    return collectEventsFor(a.length - 2, s)
+      .then(eq([
+        { time: 2, value: 2 },
+        { time: 3, value: 3 },
+        { time: 4, value: 4 },
+        { time: 5, value: 5 },
+        { time: 6, value: 6 }
+      ]))
+  })
+})
+
+describe('take', function () {
+  it('should take first n elements', function () {
+    const n = 2
+    const s = take(n, makeEvents(1, 10))
+    return collectEventsFor(n, s)
+      .then(eq([
+        { time: 0, value: 0 },
+        { time: 1, value: 1 }
+      ]))
+  })
+})
+
+describe('skip', function () {
+  it('should skip first n elements', function () {
+    const n = 4
+    const s = skip(2, makeEvents(1, n))
+    return collectEventsFor(n, s)
+      .then(eq([
+        { time: 2, value: 2 },
+        { time: 3, value: 3 }
+      ]))
+  })
+})
+
+describe('takeWhile', function () {
+  it('should take elements until condition becomes false', function () {
+    const n = 2
+    const p = x => x < n
+    const s = takeWhile(p, makeEvents(1, 10))
+    return collectEventsFor(n, s)
+      .then(eq([
+        { time: 0, value: 0 },
+        { time: 1, value: 1 }
+      ]))
+  })
+})
+
+describe('skipWhile', function () {
+  it('should skip elements until condition becomes false', function () {
+    const n = 4
+    const p = x => x < 2
+    const s = skipWhile(p, makeEvents(1, n))
+    return collectEventsFor(n, s)
+      .then(eq([
+        { time: 2, value: 2 },
+        { time: 3, value: 3 }
+      ]))
+  })
+})
diff --git a/packages/core/test/source/core-test.js b/packages/core/test/source/core-test.js
index ebcd663..97a880c 100644
--- a/packages/core/test/source/core-test.js
+++ b/packages/core/test/source/core-test.js
@@ -3,7 +3,7 @@ import { eq } from '@briancavalier/assert'
 
 import { just, empty } from '../../src/source/core'
 
-import { collectEventsFor } from '../../test-buster-old/helper/testEnv'
+import { collectEventsFor } from '../helper/testEnv'
 
 describe('just', function () {
   it('should contain one item', function () {
diff --git a/packages/core/test/source/fromArray-test.js b/packages/core/test/source/fromArray-test.js
index b60ceab..16957c5 100644
--- a/packages/core/test/source/fromArray-test.js
+++ b/packages/core/test/source/fromArray-test.js
@@ -4,7 +4,7 @@ import { map } from '@most/prelude'
 
 import { fromArray } from '../../src/source/fromArray'
 
-import { collectEventsFor } from '../../test-buster-old/helper/testEnv'
+import { collectEventsFor } from '../helper/testEnv'
 
 describe('fromArray', function () {
   it('should contain array items', function () {
diff --git a/packages/core/test/source/fromIterable-test.js b/packages/core/test/source/fromIterable-test.js
index ab2e1e3..041228a 100644
--- a/packages/core/test/source/fromIterable-test.js
+++ b/packages/core/test/source/fromIterable-test.js
@@ -3,8 +3,8 @@ import { eq } from '@briancavalier/assert'
 
 import { fromIterable } from '../../src/source/fromIterable'
 
-import ArrayIterable from '../../test-buster-old/helper/ArrayIterable'
-import { collectEventsFor } from '../../test-buster-old/helper/testEnv'
+import ArrayIterable from '../helper/ArrayIterable'
+import { collectEventsFor } from '../helper/testEnv'
 
 describe('fromIterable', function () {
   it('should contain iterable items', function () {
diff --git a/packages/core/test/source/generate-test.js b/packages/core/test/source/generate-test.js
index 6c061c1..5e92606 100644
--- a/packages/core/test/source/generate-test.js
+++ b/packages/core/test/source/generate-test.js
@@ -4,8 +4,8 @@ import { eq } from '@briancavalier/assert'
 import { generate } from '../../src/source/generate'
 import { reduce } from '../../src/combinator/reduce'
 import { getIterator } from '../../src/iterable'
-import ArrayIterable from '../../test-buster-old/helper/ArrayIterable'
-import delayPromise from '../../test-buster-old/helper/delayPromise'
+import ArrayIterable from '../helper/ArrayIterable'
+import delayPromise from '../helper/delayPromise'
 
 function makeAsyncIterator (ms, n) {
   const a = new Array(n)
diff --git a/packages/core/test/source/iterate-test.js b/packages/core/test/source/iterate-test.js
index 0555ca4..180fb06 100644
--- a/packages/core/test/source/iterate-test.js
+++ b/packages/core/test/source/iterate-test.js
@@ -5,7 +5,7 @@ import { iterate } from '../../src/source/iterate'
 import { take } from '../../src/combinator/slice'
 import { observe } from '../../src/combinator/observe'
 
-import { collectEventsFor } from '../../test-buster-old/helper/testEnv'
+import { collectEventsFor } from '../helper/testEnv'
 
 const sentinel = { value: 'sentinel' }
 const other = { value: 'other' }
diff --git a/packages/core/test/source/periodic-test.js b/packages/core/test/source/periodic-test.js
index c4964d6..8dea180 100644
--- a/packages/core/test/source/periodic-test.js
+++ b/packages/core/test/source/periodic-test.js
@@ -4,7 +4,7 @@ import { eq } from '@briancavalier/assert'
 import { periodic } from '../../src/source/periodic'
 import { take } from '../../src/combinator/slice'
 
-import { collectEventsFor } from '../../test-buster-old/helper/testEnv'
+import { collectEventsFor } from '../helper/testEnv'
 
 describe('periodic', function () {
   it('should emit value at tick periods', function () {
diff --git a/packages/core/test/source/unfold-test.js b/packages/core/test/source/unfold-test.js
index 193b914..893fc21 100644
--- a/packages/core/test/source/unfold-test.js
+++ b/packages/core/test/source/unfold-test.js
@@ -5,7 +5,7 @@ import { unfold } from '../../src/source/unfold'
 import { take } from '../../src/combinator/slice'
 import { observe } from '../../src/combinator/observe'
 
-import { collectEventsFor } from '../../test-buster-old/helper/testEnv'
+import { collectEventsFor } from '../helper/testEnv'
 
 const sentinel = { value: 'sentinel' }
 const other = { value: 'other' }
diff --git a/packages/core/test-buster-old/switch-test.js b/packages/core/test/switch-test.js
similarity index 57%
rename from packages/core/test-buster-old/switch-test.js
rename to packages/core/test/switch-test.js
index 5ea958c..efcd7dd 100644
--- a/packages/core/test-buster-old/switch-test.js
+++ b/packages/core/test/switch-test.js
@@ -1,24 +1,20 @@
-import { spec, referee } from 'buster'
-const { describe, it } = spec
-const { assert, refute } = referee
+import { describe, it } from 'mocha'
+import { eq, fail } from '@briancavalier/assert'
 
 import { switchLatest } from '../src/combinator/switch'
-import { observe } from '../src/combinator/observe'
 import { take } from '../src/combinator/slice'
 import { constant, map, tap } from '../src/combinator/transform'
 import { periodic } from '../src/source/periodic'
 import { fromArray } from '../src/source/fromArray'
 import { empty, just } from '../src/source/core'
-
-import { ticks, collectEvents } from './helper/testEnv'
+import { ticks, collectEventsFor } from './helper/testEnv'
+import { runEffects } from '../src/runEffects'
 
 describe('switch', () => {
   describe('when input is empty', () => {
     // need this.spy, can't use arrow function
     it('should return empty', function () {
-      const spy = this.spy()
-      return observe(spy, switchLatest(empty()))
-        .then(() => refute(spy.called))
+      return runEffects(tap(fail, switchLatest(empty())), ticks(1))
     })
   })
 
@@ -33,10 +29,8 @@ describe('switch', () => {
       : empty()
 
     const s = switchLatest(map(toInner, fromArray([0, 1])))
-    return collectEvents(s, ticks(10))
-      .then(evs => {
-        assert.equals(evs, events)
-      })
+    return collectEventsFor(10, s)
+      .then(eq(events))
   })
 
   describe('when input contains a single stream', () => {
@@ -44,14 +38,12 @@ describe('switch', () => {
       const expected = [1, 2, 3]
       const s = just(fromArray(expected))
 
-      return collectEvents(switchLatest(s), ticks(1))
-        .then(events => {
-          assert.equals(events, [
-            { time: 0, value: 1 },
-            { time: 0, value: 2 },
-            { time: 0, value: 3 }
-          ])
-        })
+      return collectEventsFor(1, switchLatest(s))
+        .then(eq([
+          { time: 0, value: 1 },
+          { time: 0, value: 2 },
+          { time: 0, value: 3 }
+        ]))
     })
   })
 
@@ -64,14 +56,12 @@ describe('switch', () => {
           fromArray(expected)
         ])
 
-        return collectEvents(switchLatest(s), ticks(1))
-          .then(events => {
-            assert.equals(events, [
-              { time: 0, value: 1 },
-              { time: 0, value: 2 },
-              { time: 0, value: 3 }
-            ])
-          })
+        return collectEventsFor(1, switchLatest(s))
+          .then(eq([
+            { time: 0, value: 1 },
+            { time: 0, value: 2 },
+            { time: 0, value: 3 }
+          ]))
       })
     })
 
@@ -79,21 +69,19 @@ describe('switch', () => {
       let i = 0
       const s = map(() => constant(++i, periodic(1)), periodic(3))
 
-      return collectEvents(take(10, switchLatest(s)), ticks(250))
-        .then(events => {
-          assert.equals(events, [
-            { time: 0, value: 1 },
-            { time: 1, value: 1 },
-            { time: 2, value: 1 },
-            { time: 3, value: 2 },
-            { time: 4, value: 2 },
-            { time: 5, value: 2 },
-            { time: 6, value: 3 },
-            { time: 7, value: 3 },
-            { time: 8, value: 3 },
-            { time: 9, value: 4 }
-          ])
-        })
+      return collectEventsFor(250, take(10, switchLatest(s)))
+        .then(eq([
+          { time: 0, value: 1 },
+          { time: 1, value: 1 },
+          { time: 2, value: 1 },
+          { time: 3, value: 2 },
+          { time: 4, value: 2 },
+          { time: 5, value: 2 },
+          { time: 6, value: 3 },
+          { time: 7, value: 3 },
+          { time: 8, value: 3 },
+          { time: 9, value: 4 }
+        ]))
     })
   })
 })
diff --git a/packages/core/test/timeslice-test.js b/packages/core/test/timeslice-test.js
new file mode 100644
index 0000000..e213a56
--- /dev/null
+++ b/packages/core/test/timeslice-test.js
@@ -0,0 +1,169 @@
+import { describe, it } from 'mocha'
+import { eq, is, assert } from '@briancavalier/assert'
+
+import { until, since, during } from '../src/combinator/timeslice'
+import { take } from '../src/combinator/slice'
+import { periodic } from '../src/source/periodic'
+import { just, never } from '../src/source/core'
+import { delay } from '../src/combinator/delay'
+import { default as Stream } from '../src/Stream'
+
+import { runEffects } from '../src/runEffects'
+import { ticks, collectEvents, collectEventsFor, makeEvents } from './helper/testEnv'
+import FakeDisposeSource from './helper/FakeDisposeSource'
+import { endWith } from './helper/endWith'
+
+import { spy } from 'sinon'
+
+const sentinel = { value: 'sentinel' }
+
+describe('during', function () {
+  it('should contain events at or later than min and earlier than max', function () {
+    const stream = take(10, periodic(1))
+    const timespan = delay(1, just(delay(5, just())))
+
+    const s = during(timespan, stream)
+    return collectEventsFor(10, s)
+      .then(eq([
+        { time: 1, value: undefined },
+        { time: 2, value: undefined },
+        { time: 3, value: undefined },
+        { time: 4, value: undefined },
+        { time: 5, value: undefined }
+      ]))
+  })
+
+  it('should dispose source stream', function () {
+    const dispose = spy()
+    const stream = new Stream(FakeDisposeSource.from(dispose, periodic(1)))
+    const timespan = delay(1, just(delay(5, just())))
+
+    const s = during(timespan, stream)
+    return collectEvents(s, ticks(6))
+      .then(() => assert(dispose.calledOnce))
+  })
+
+  it('should dispose signals', function () {
+    const dispose = spy()
+
+    const stream = periodic(1)
+    const timespan = delay(1, just(delay(5, just())))
+    const dt = new Stream(FakeDisposeSource.from(dispose, timespan))
+
+    const s = during(dt, stream)
+    return collectEvents(s, ticks(6))
+      .then(events => {
+        eq(5, events.length)
+        assert(dispose.calledOnce)
+      })
+  })
+})
+
+describe('until', function () {
+  it('should only contain events earlier than signal', function () {
+    const stream = makeEvents(1, 10)
+    const signal = delay(3, just())
+
+    const s = until(signal, stream)
+    return collectEventsFor(10, s)
+      .then(eq([
+        { time: 0, value: 0 },
+        { time: 1, value: 1 },
+        { time: 2, value: 2 },
+        { time: 3, value: 3 }
+      ]))
+  })
+
+  it('should dispose source stream', function () {
+    const dispose = spy()
+    const stream = new Stream(FakeDisposeSource.from(dispose, periodic(1)))
+    const signal = delay(3, just())
+
+    const s = until(signal, stream)
+    return collectEventsFor(5, s)
+      .then(() => assert(dispose.calledOnce))
+  })
+
+  it('should end immediately on signal', function () {
+    const dispose = spy()
+    const stream = new Stream(FakeDisposeSource.from(dispose, never()))
+    const signal = just()
+
+    const s = until(signal, stream)
+    return collectEventsFor(1, s)
+      .then(events => {
+        eq([], events)
+        assert(dispose.calledOnce)
+      })
+  })
+
+  it('should dispose signal', function () {
+    const dispose = spy()
+    const stream = periodic(1)
+    const signal = new Stream(FakeDisposeSource.from(dispose, delay(3, just())))
+
+    const s = until(signal, stream)
+    return collectEventsFor(5, s)
+      .then(events => {
+        eq([
+          { time: 0, value: undefined },
+          { time: 1, value: undefined },
+          { time: 2, value: undefined }
+        ], events)
+        assert(dispose.calledOnce)
+      })
+  })
+
+  it('should use until value as end value', function () {
+    const stream = periodic(1)
+    const end = delay(3, just(sentinel))
+
+    const s = until(end, stream)
+    return runEffects(s, ticks(5))
+      .then(is(sentinel))
+  })
+})
+
+describe('since', function () {
+  it('should only contain events at or later than signal', function () {
+    const n = 5
+    const stream = take(n, periodic(1))
+    const signal = delay(3, just())
+
+    const s = since(signal, stream)
+    return collectEventsFor(n, s)
+      .then(eq([
+        { time: 3, value: undefined },
+        { time: 4, value: undefined }
+      ]))
+  })
+
+  it('should dispose signal', function () {
+    const dispose = spy()
+    const stream = take(5, periodic(1))
+    const signal = new Stream(FakeDisposeSource.from(dispose, delay(3, just())))
+
+    const s = since(signal, stream)
+    return collectEventsFor(5, s)
+      .then(() => assert(dispose.calledOnce))
+  })
+
+  it('should preserve end value', function () {
+    const stream = endWith(sentinel, take(3, periodic(1)))
+    const start = delay(3, just())
+
+    const s = since(start, stream)
+    return runEffects(s, ticks(3))
+      .then(is(sentinel))
+  })
+
+  it('should preserve end value if end signal occurs before start signal', function () {
+    const stream = take(3, periodic(1))
+    const start = delay(3, just())
+    const end = delay(1, just(sentinel))
+
+    const s = since(start, until(end, stream))
+    return runEffects(s, ticks(3))
+      .then(is(sentinel))
+  })
+})
diff --git a/packages/core/test-buster-old/transform-test.js b/packages/core/test/transform-test.js
similarity index 50%
rename from packages/core/test-buster-old/transform-test.js
rename to packages/core/test/transform-test.js
index 8346eb7..544b816 100644
--- a/packages/core/test-buster-old/transform-test.js
+++ b/packages/core/test/transform-test.js
@@ -1,29 +1,28 @@
-import { spec, expect } from 'buster'
-const { describe, it } = spec
+import { describe, it } from 'mocha'
+import { eq } from '@briancavalier/assert'
+
 import { assertSame } from './helper/stream-helper'
 import { map, tap, constant } from '../src/combinator/transform'
-import { observe } from '../src/combinator/observe'
-import { just as streamOf } from '../src/source/core'
+import { just } from '../src/source/core'
 
-var sentinel = { value: 'sentinel' }
-var other = { value: 'other' }
+import { collectEventsFor } from './helper/testEnv'
 
 describe('map', function () {
   it('should satisfy identity', function () {
     // u.map(function(a) { return a; })) ~= u
-    var u = streamOf(sentinel)
-    return assertSame(map(function (x) { return x }, u), u)
+    const u = just(Math.random())
+    return assertSame(map(x => x, u), u)
   })
 
   it('should satisfy composition', function () {
     // u.map(function(x) { return f(g(x)); }) ~= u.map(g).map(f)
-    function f (x) { return x + 'f' }
-    function g (x) { return x + 'g' }
+    const f = x => x + 'f'
+    const g = x => x + 'g'
 
-    var u = streamOf('e')
+    const u = just('e')
 
     return assertSame(
-      map(function (x) { return f(g(x)) }, u),
+      map(x => f(g(x)), u),
       map(f, map(g, u))
     )
   })
@@ -32,9 +31,9 @@ describe('map', function () {
 describe('constant', function () {
   it('should satisfy identity', function () {
     // u.constant(x) ~= u.map(function(){return x;})
-    var u = streamOf('e')
-    var x = 1
-    function f () { return x }
+    const u = just('e')
+    const x = 1
+    const f = () => x
     return assertSame(
       constant(x, u),
       map(f, u)
@@ -44,12 +43,10 @@ describe('constant', function () {
 
 describe('tap', function () {
   it('should not transform stream items', function () {
-    var s = tap(function () {
-      return other
-    }, streamOf(sentinel))
+    const expected = Math.random()
+    const s = tap(() => -1, just(expected))
 
-    return observe(function (x) {
-      expect(x).toBe(sentinel)
-    }, s)
+    return collectEventsFor(1, s)
+      .then(eq([{ time: 0, value: expected }]))
   })
 })
diff --git a/packages/core/test/zip-test.js b/packages/core/test/zip-test.js
new file mode 100644
index 0000000..f3d601e
--- /dev/null
+++ b/packages/core/test/zip-test.js
@@ -0,0 +1,33 @@
+import { describe, it } from 'mocha'
+import { eq } from '@briancavalier/assert'
+
+import { zip } from '../src/combinator/zip'
+
+import { makeEvents, collectEventsFor } from './helper/testEnv'
+
+describe('zip', function () {
+  it('should invoke f for each tuple', function () {
+    const n = 3
+    const s = zip(Array, makeEvents(1, n), makeEvents(2, n))
+
+    return collectEventsFor(n * 2, s)
+      .then(eq([
+        { time: 0, value: [0, 0] },
+        { time: 2, value: [1, 1] },
+        { time: 4, value: [2, 2] }
+      ]))
+  })
+
+  it('should end when shortest stream ends', function () {
+    const n = 3
+    const a = makeEvents(1, n)
+    const b = makeEvents(1, n + 1)
+
+    return collectEventsFor(n, zip(Array, a, b))
+      .then(eq([
+        { time: 0, value: [0, 0] },
+        { time: 1, value: [1, 1] },
+        { time: 2, value: [2, 2] }
+      ]))
+  })
+})