diff --git a/ie8/AssertiveBaconSpec.coffee b/ie8/AssertiveBaconSpec.coffee new file mode 100644 index 000000000..b4cd1d9db --- /dev/null +++ b/ie8/AssertiveBaconSpec.coffee @@ -0,0 +1,3360 @@ +assert = require("assertive-chai").assert +Bacon = require("../src/Bacon").Bacon +Mocks = require( "./AssertiveMock") +TickScheduler = require("./AssertiveTickScheduler").TickScheduler +mock = Mocks.mock +mockFunction = Mocks.mockFunction +EventEmitter = require("events").EventEmitter +th = require("./AssertiveSpecHelper") +t = th.t +expectStreamEvents = th.expectStreamEvents +expectPropertyEvents = th.expectPropertyEvents +verifyCleanup = th.verifyCleanup +error = th.error +soon = th.soon +series = th.series +repeat = th.repeat +toValues = th.toValues +sc = TickScheduler() +Bacon.scheduler = sc +# Some streams are unstable when testing with verifySwitching2. +# Generally, all flatMap-based streams are unstable because flatMap discards +# child streams on unsubscribe. +unstable = {unstable:true} +expectError = (errorText, f) -> + assert.throws f, Error, errorText + +describe "Bacon._", -> + _ = Bacon._ + describe "head", -> + assert.equal _.head([5,2,9]), 5 + assert.equal _.head([]), undefined + assert.equal _.head(5), undefined + describe "always", -> assert.equal _.always(5)("francis"), 5 + describe "negate", -> + assert.isFalse _.negate(_.always(true))("timanttikobra") + describe "empty", -> + assert.isTrue _.empty([]) + assert.isTrue _.empty("") + assert.isFalse _.empty([1]) + assert.isFalse _.empty("1") + describe "tail", -> + assert.deepEqual _.tail([1,2,3]), [2,3] + assert.deepEqual _.tail([1]), [] + assert.deepEqual _.tail([]), [] + describe "filter", -> + assert.deepEqual _.filter(_.empty, ["","1",[],[2]]), ["",[]] + describe "map", -> + assert.deepEqual _.map(_.head, [[], [1], [2,2], [3,3,3] ]), [undefined, 1, 2, 3 ] + describe "flatMap", -> + assert.deepEqual _.flatMap(((x) -> [x, x]), [1,2,3]), [1,1,2,2,3,3] + describe "each", -> + it "provides key and value to iterator", -> + expectKeyVals = (x, expectedKeys, expectedValues) -> + keys = [] + values = [] + _.each(x, (key, value) -> + keys.push(key) + values.push(value) + ) + assert.deepEqual [keys, values], [expectedKeys, expectedValues] + expectKeyVals( + {cat:"furry",bird:"feathery"}, ["cat","bird"], ["furry","feathery"] + ) + expectKeyVals([1,2,3], ["0","1","2"], [1,2,3]) + describe "toArray", -> + assert.deepEqual _.toArray(2), [2] + it "ignores rest of arguments", -> + assert.deepEqual _.toArray(1,1,2), [1] + it "should, when given an array, return it back (not a copy)", -> + arr = [] + assert.equal _.toArray(arr), arr + describe "indexOf", -> + assert.equal _.indexOf([1,2], 1), 0 + assert.equal _.indexOf([1,2], 2), 1 + assert.equal _.indexOf([1,2], 3), -1 + describe "contains", -> + assert.isTrue _.contains("abc", "c") + assert.isFalse _.contains("abc", "x") + assert.isTrue _.contains([2,4,6], 4) + assert.isFalse _.contains([2,4,6], 3) + describe "id", -> + obj = {} + assert.equal _.id(obj), obj + describe "last", -> + assert.equal _.last([2,4]), 4 + assert.equal _.last("last"), "t" + describe "all", -> + assert.isFalse _.all([ [false,true], [true,true] ], _.head) + assert.isTrue _.all([ [true,false], [true,true] ], _.head) + it "should test truthiness if no function given", -> + assert.isFalse _.all([true, false, true]) + assert.isTrue _.all([true, true, true]) + assert.isTrue _.all([1, true, 1]) + describe "any", -> + assert.isTrue _.any([ [false,true], [true,true] ], _.head) + assert.isFalse _.any([ [false,false], [false,true] ], _.head) + it "should test truthiness if no function given", -> + assert.isFalse _.any([false, false, false]) + assert.isTrue _.any([true, false, true]) + describe "without", -> + assert.deepEqual _.without("apple", ["bacon","apple","apple","omelette"]), ["bacon","omelette"] + describe "remove", -> + assert.deepEqual _.remove("apple", ["bacon","apple","apple","omelette"]), ["apple"] + assert.deepEqual _.remove("raisin", ["bacon","apple","apple","omelette"]), undefined + describe "fold", -> + assert.equal _.fold([1,2,3,4,5], 0, (s, n) -> s + n), 15 + describe "toString", -> + it "for booleans", -> + assert.equal _.toString(true), "true" + it "for numbers", -> + assert.equal _.toString(1), "1" + assert.equal _.toString(1.1), "1.1" + it "for undefined and null", -> + assert.equal _.toString(undefined), "undefined" + assert.equal _.toString(null), "undefined" + it "for strings", -> + assert.equal _.toString("lol"), "lol" + it "for dates", -> + assert.include _.toString(new Date((new Date(0)).getTimezoneOffset() * 60 * 1000)), "1970" + it "for arrays", -> + assert.equal _.toString([1,2,3]), "[1,2,3]" + it "for objects", -> + assert.equal _.toString({a: "b"}), "{a:b}" + assert.equal _.toString({a: "b", c: "d"}), "{a:b,c:d}" + it "for circular refs", -> + obj = { name : "nasty" } + obj.self = obj + assert _.toString(obj).length < 100 + it "works even when enumerable properties throw errors on access", -> + obj = { "name": "madcow" } + + Object.defineProperty obj, "prop", + enumerable: true + get: -> + throw new Error "an error" + + assert.equal _.toString(obj), "{name:madcow,prop:Error: an error}" + + + +describe "Bacon.later", -> + describe "should send single event and end", -> + expectStreamEvents( + -> Bacon.later(t(1), "lol") + ["lol"]) + describe "supports sending an Error event as well", -> + expectStreamEvents( + -> Bacon.later(t(1), new Bacon.Error("oops")) + [error()]) + it "toString", -> + assert.equal Bacon.later(1, "wat").toString(), "Bacon.later(1,wat)" + it "inspect", -> + assert.equal Bacon.later(1, "wat").inspect(), "Bacon.later(1,wat)" + +describe "Bacon.sequentially", -> + describe "should send given events and end", -> + expectStreamEvents( + -> Bacon.sequentially(t(1), ["lol", "wut"]) + ["lol", "wut"]) + describe "include error events", -> + expectStreamEvents( + -> Bacon.sequentially(t(1), [error(), "lol"]) + [error(), "lol"]) + describe "will stop properly even when exception thrown by subscriber", -> + expectStreamEvents( + -> + s = Bacon.sequentially(t(1), ["lol", "wut"]) + s.onValue (value) -> + throw "testing" + s + ["lol", "wut"], unstable) + it "toString", -> + assert.equal Bacon.sequentially(1, [2]).toString(), "Bacon.sequentially(1,[2])" + +describe "Bacon.repeatedly", -> + describe "repeats given sequence forever", -> + expectStreamEvents( + -> Bacon.repeatedly(1, [1,2]).take(5) + [1,2,1,2,1]) + it "toString", -> + assert.equal Bacon.repeatedly(1, [1]).toString(), "Bacon.repeatedly(1,[1])" + +describe "Bacon.interval", -> + describe "repeats single element indefinitely", -> + expectStreamEvents( + -> Bacon.interval(t(1), "x").take(3) + ["x", "x", "x"]) + it "toString", -> + assert.equal Bacon.interval(1, 2).toString(), "Bacon.interval(1,2)" + +describe "Bacon.fromPoll", -> + describe "repeatedly polls given function for values", -> + expectStreamEvents( + -> Bacon.fromPoll(1, (-> "lol")).take(2) + ["lol", "lol"]) + it "toString", -> + assert.equal Bacon.fromPoll(1, (->)).toString(), "Bacon.fromPoll(1,function)" + +testLiftedCallback = (src, liftedCallback) -> + input = [ + Bacon.constant('a') + 'x' + Bacon.constant('b').toProperty() + 'y' + ] + output = ['a', 'x', 'b', 'y'] + expectStreamEvents( + -> liftedCallback(src, input...) + [output] + ) + + +describe "Bacon.fromCallback", -> + describe "makes an EventStream from function that takes a callback", -> + expectStreamEvents( + -> + src = (callback) -> callback("lol") + stream = Bacon.fromCallback(src) + ["lol"]) + describe "supports partial application", -> + expectStreamEvents( + -> + src = (param, callback) -> callback(param) + stream = Bacon.fromCallback(src, "lol") + ["lol"]) + describe "supports partial application with Observable arguments", -> + testLiftedCallback( + (values..., callback) -> callback(values) + Bacon.fromCallback + ) + describe "supports object, methodName, partial application", -> + expectStreamEvents( + -> + src = { + "go": (param, callback) -> callback(param + " " + this.name) + "name": "bob" + } + stream = Bacon.fromCallback(src, "go", "hello") + ["hello bob"]) + it "toString", -> + assert.equal Bacon.fromCallback((->), "lol").toString(), "Bacon.fromCallback(function,lol)" + +describe "Bacon.fromNodeCallback", -> + describe "makes an EventStream from function that takes a node-style callback", -> + expectStreamEvents( + -> + src = (callback) -> callback(null, "lol") + stream = Bacon.fromNodeCallback(src) + ["lol"]) + describe "handles error parameter correctly", -> + expectStreamEvents( + -> + src = (callback) -> callback('errortxt', null) + stream = Bacon.fromNodeCallback(src) + [error()]) + describe "supports partial application", -> + expectStreamEvents( + -> + src = (param, callback) -> callback(null, param) + stream = Bacon.fromNodeCallback(src, "lol") + ["lol"]) + describe "supports partial application with Observable arguments", -> + testLiftedCallback( + (values..., callback) -> callback(null, values) + Bacon.fromNodeCallback + ) + describe "supports object, methodName, partial application", -> + expectStreamEvents( + -> + src = { + "go": (param, callback) -> callback(null, param + " " + this.name) + "name": "bob" + } + stream = Bacon.fromNodeCallback(src, "go", "hello") + ["hello bob"]) + it "toString", -> + assert.equal Bacon.fromNodeCallback((->), "lol").toString(), "Bacon.fromNodeCallback(function,lol)" + +# Wrap EventEmitter as EventTarget +toEventTarget = (emitter) -> + addEventListener: (event, handler) -> + emitter.addListener(event, handler) + removeEventListener: (event, handler) -> emitter.removeListener(event, handler) + +describe "Bacon.fromEventTarget", -> + soon = (f) -> setTimeout f, 0 + describe "should create EventStream from DOM object", -> + expectStreamEvents( + -> + emitter = new EventEmitter() + emitter.on "newListener", -> + soon -> emitter.emit "click", "x" + element = toEventTarget emitter + Bacon.fromEventTarget(element, "click").take(1) + ["x"] + ) + + describe "should create EventStream from EventEmitter", -> + expectStreamEvents( + -> + emitter = new EventEmitter() + emitter.on "newListener", -> + soon -> emitter.emit "data", "x" + Bacon.fromEventTarget(emitter, "data").take(1) + ["x"] + ) + + describe "should allow a custom map function for EventStream from EventEmitter", -> + expectStreamEvents( + -> + emitter = new EventEmitter() + emitter.on "newListener", -> + soon -> emitter.emit "data", "x", "y" + Bacon.fromEventTarget(emitter, "data", (x, y) => [x, y]).take(1) + [["x", "y"]] + ) + + + it "should clean up event listeners from EventEmitter", -> + emitter = new EventEmitter() + Bacon.fromEventTarget(emitter, "data").take(1).subscribe -> + emitter.emit "data", "x" + assert.deepEqual emitter.listeners("data").length, 0 + + it "should clean up event listeners from DOM object", -> + emitter = new EventEmitter() + element = toEventTarget emitter + dispose = Bacon.fromEventTarget(element, "click").subscribe -> + dispose() + assert.deepEqual emitter.listeners("click").length, 0 + + it "should create EventStream from on/off event", -> + values = [] + mockSource = { + on: (type, callback) -> callback(type) + off: (callback) -> + } + Bacon.fromEventTarget(mockSource, "test").take(1).onValue (value) -> + values.push(value) + + it "toString", -> + assert.equal Bacon.fromEventTarget({}, "click").toString(), "Bacon.fromEventTarget({},click)" + +describe "Observable.log", -> + preservingLog = (f) -> + originalConsole = console + originalLog = console.log + try + f() + finally + global.console = originalConsole + console.log = originalLog + + it "does not crash", -> + preservingLog -> + console.log = -> + Bacon.constant(1).log() + it "does not crash in case console.log is not defined", -> + preservingLog -> + console.log = undefined + Bacon.constant(1).log() + it "logs event values as themselves (doesn't stringify)", -> + value = {} + assert.equal new Bacon.Next(value).log(), value + it "logs Error, End events as strings", -> + assert.equal new Bacon.Error("err").log(), " err" + assert.equal new Bacon.End().log(), "" + it "toString", -> + assert.equal Bacon.never().log().toString(), "Bacon.never()" + +describe "Observable.slidingWindow", -> + describe "slides the window for EventStreams", -> + expectPropertyEvents( + -> series(1, [1,2,3]).slidingWindow(2) + [[], [1], [1,2], [2,3]]) + describe "slides the window for Properties", -> + expectPropertyEvents( + -> series(1, [1,2,3]).toProperty().slidingWindow(2) + [[], [1], [1,2], [2,3]]) + describe "accepts second parameter for minimum amount of values", -> + expectPropertyEvents( + -> series(1, [1,2,3,4]).slidingWindow(3, 2) + [[1,2], [1,2,3], [2,3,4]]) + expectPropertyEvents( + -> series(1, [1,2,3,4]).toProperty(0).slidingWindow(3, 2) + [[0,1], [0, 1, 2], [1,2,3], [2,3,4]]) + it "toString", -> + assert.equal Bacon.never().slidingWindow(2).toString(), "Bacon.never().slidingWindow(2,0)" + +describe "EventStream.filter", -> + describe "should filter values", -> + expectStreamEvents( + -> series(1, [1, 2, error(), 3]).filter(lessThan(3)) + [1, 2, error()]) + describe "extracts field values", -> + expectStreamEvents( + -> series(1, [{good:true, value:"yes"}, {good:false, value:"no"}]).filter(".good").map(".value") + ["yes"]) + describe "can filter by Property value", -> + expectStreamEvents( + -> + src = series(1, [1,1,2,3,4,4,8,7]) + odd = src.map((x) -> x % 2).toProperty() + src.filter(odd) + [1,1,3,7]) + it "toString", -> + assert.equal Bacon.never().filter(false).toString(), "Bacon.never().filter(function)" + +describe "EventStream.map", -> + describe "should map with given function", -> + expectStreamEvents( + -> series(1, [1, 2, 3]).map(times, 2) + [2, 4, 6]) + describe "also accepts a constant value", -> + expectStreamEvents( + -> series(1, [1, 2, 3,]).map("lol") + ["lol", "lol", "lol"]) + describe "extracts property from value object", -> + o = { lol : "wut" } + expectStreamEvents( + -> repeat(1, [o]).take(3).map(".lol") + ["wut", "wut", "wut"]) + describe "extracts a nested property too", -> + o = { lol : { wut : "wat" } } + expectStreamEvents( + -> Bacon.once(o).map(".lol.wut") + ["wat"]) + describe "in case of a function property, calls the function with no args", -> + expectStreamEvents( + -> Bacon.once([1,2,3]).map(".length") + [3]) + describe "allows arguments for methods", -> + thing = { square: (x) -> x * x } + expectStreamEvents( + -> Bacon.once(thing).map(".square", 2) + [4]) + describe "works with method call on given object, with partial application", -> + multiplier = { multiply: (x, y) -> x * y } + expectStreamEvents( + -> series(1, [1,2,3]).map(multiplier, "multiply", 2) + [2,4,6]) + describe "can map to a Property value", -> + expectStreamEvents( + -> series(1, [1,2,3]).map(Bacon.constant(2)) + [2,2,2]) + it "preserves laziness", -> + calls = 0 + id = (x) -> + calls++ + x + Bacon.fromArray([1,2,3,4,5]).map(id).skip(4).onValue() + assert.equal calls, 1 + it "toString", -> + assert.equal Bacon.once(1).map(true).toString(), "Bacon.once(1).map(function)" + +describe "EventStream.mapError", -> + describe "should map error events with given function", -> + expectStreamEvents( + -> repeat(1, [1, error("OOPS")]).mapError(id).take(2) + [1, "OOPS"]) + describe "also accepts a constant value", -> + expectStreamEvents( + -> repeat(1, [1, error()]).mapError("ERR").take(2) + [1, "ERR"]) + it "toString", -> + assert.equal Bacon.never().mapError(true).toString(), "Bacon.never().mapError(function)" + +describe "EventStream.doAction", -> + it "calls function before sending value to listeners", -> + called = [] + bus = new Bacon.Bus() + s = bus.doAction((x) -> called.push(x)) + s.onValue(->) + s.onValue(->) + bus.push(1) + assert.deepEqual called, [1] + describe "does not alter the stream", -> + expectStreamEvents( + -> series(1, [1, 2]).doAction(->) + [1, 2]) + it "toString", -> + assert.equal Bacon.never().doAction((->)).toString(), "Bacon.never().doAction(function)" + +describe "EventStream.mapEnd", -> + describe "produces an extra element on stream end", -> + expectStreamEvents( + -> series(1, ["1", error()]).mapEnd("the end") + ["1", error(), "the end"]) + describe "accepts either a function or a constant value", -> + expectStreamEvents( + -> series(1, ["1", error()]).mapEnd(-> "the end") + ["1", error(), "the end"]) + describe "works with undefined value as well", -> + expectStreamEvents( + -> series(1, ["1", error()]).mapEnd() + ["1", error(), undefined]) + it "toString", -> + assert.equal Bacon.never().mapEnd(true).toString(), "Bacon.never().mapEnd(function)" + +describe "EventStream.take", -> + describe "takes N first elements", -> + expectStreamEvents( + -> series(1, [1,2,3,4]).take(2) + [1,2]) + describe "works with N=0", -> + expectStreamEvents( + -> series(1, [1,2,3,4]).take(0) + []) + describe "will stop properly even when exception thrown by subscriber", -> + expectStreamEvents( + -> + s = Bacon.repeatedly(t(1), ["lol", "wut"]).take(2) + s.onValue (value) -> + throw "testing" if value == "lol" + s + ["lol", "wut"], unstable) + describe "works with synchronous source", -> + expectStreamEvents( + -> Bacon.fromArray([1,2,3,4]).take(2) + [1,2]) + it "toString", -> + assert.equal Bacon.never().take(1).toString(), "Bacon.never().take(1)" + +describe "EventStream.takeWhile", -> + describe "takes while predicate is true", -> + expectStreamEvents( + -> repeat(1, [1, error("wat"), 2, 3]).takeWhile(lessThan(3)) + [1, error("wat"), 2]) + describe "extracts field values", -> + expectStreamEvents( + -> + series(1, [{good:true, value:"yes"}, {good:false, value:"no"}]) + .takeWhile(".good").map(".value") + ["yes"]) + describe "can filter by Property value", -> + expectStreamEvents( + -> + src = series(1, [1,1,2,3,4,4,8,7]) + odd = src.map((x) -> x % 2).toProperty() + src.takeWhile(odd) + [1,1]) + describe "works with synchronous source", -> + expectStreamEvents( + -> Bacon.fromArray([1, 2, 3]).takeWhile(lessThan(3)) + [1, 2]) + it "toString", -> + assert.equal Bacon.never().takeWhile(true).toString(), "Bacon.never().takeWhile(function)" + +describe "EventStream.skip", -> + describe "should skip first N items", -> + expectStreamEvents( + -> series(1, [1, error(), 2, error(), 3]).skip(1) + [error(), 2, error(), 3]) + describe "accepts N <= 0", -> + expectStreamEvents( + -> series(1, [1, 2]).skip(-1) + [1, 2]) + describe "works with synchronous source", -> + expectStreamEvents( + -> Bacon.fromArray([1, 2, 3]).skip(1) + [2, 3]) + it "toString", -> + assert.equal Bacon.never().skip(1).toString(), "Bacon.never().skip(1)" + +describe "EventStream.skipWhile", -> + describe "skips filter predicate holds true", -> + expectStreamEvents( + -> series(1, [1, error(), 2, error(), 3, 2]).skipWhile(lessThan(3)) + [error(), error(), 3, 2]) + describe "extracts field values", -> + expectStreamEvents( + -> + series(1, [{good:true, value:"yes"}, {good:false, value:"no"}]) + .skipWhile(".good").map(".value") + ["no"]) + describe "can filter by Property value", -> + expectStreamEvents( + -> + src = series(1, [1,1,2,3,4,4,8,7]) + odd = src.map((x) -> x % 2).toProperty() + src.skipWhile(odd) + [2,3,4,4,8,7]) + describe "for synchronous sources", -> + describe "skips filter predicate holds true", -> + expectStreamEvents( + -> Bacon.fromArray([1, 2, 3, 2]).skipWhile(lessThan(3)) + [3, 2]) + it "toString", -> + assert.equal Bacon.never().skipWhile(1).toString(), "Bacon.never().skipWhile(function)" + +describe "EventStream.skipUntil", -> + describe "skips events until one appears in given starter stream", -> + expectStreamEvents( + -> + src = series(3, [1,2,3]) + src.onValue(->) # to start "time" immediately instead of on subscribe + starter = series(4, ["start"]) + src.skipUntil(starter) + [2,3]) + describe "works with self-derived starter", -> + expectStreamEvents( + -> + src = series(3, [1,2,3]) + starter = src.filter((x) -> x == 3) + src.skipUntil(starter) + [3]) + describe "works with self-derived starter with an evil twist", -> + expectStreamEvents( + -> + src = series(3, [1,2,3]) + data = src.map((x) -> x) + data.onValue(->) + starter = src.filter((x) -> x == 3) + data.skipUntil(starter) + [3]) + it "toString", -> + assert.equal Bacon.never().skipUntil(Bacon.once(1)).toString(), "Bacon.never().skipUntil(Bacon.once(1))" + +describe "EventStream.skipDuplicates", -> + it "Drops duplicates with subscribers with non-overlapping subscription time (#211)", -> + b = new Bacon.Bus() + noDups = b.skipDuplicates() + round = (expected) -> + values = [] + noDups.take(1).onValue (x) -> values.push(x) + b.push 1 + assert.deepEqual values, expected + round([1]) + round([]) + round([]) + + describe "drops duplicates", -> + expectStreamEvents( + -> series(1, [1, 2, error(), 2, 3, 1]).skipDuplicates() + [1, 2, error(), 3, 1]) + + describe "allows undefined as initial value", -> + expectStreamEvents( + -> series(1, [undefined, undefined, 1, 2]).skipDuplicates() + [undefined, 1, 2]) + + describe "works with custom isEqual function", -> + a = {x: 1}; b = {x: 2}; c = {x: 2}; d = {x: 3}; e = {x: 1} + isEqual = (a, b) -> a?.x == b?.x + expectStreamEvents( + -> series(1, [a, b, error(), c, d, e]).skipDuplicates(isEqual) + [a, b, error(), d, e]) + + describe "works with synchrounous sources", -> + expectStreamEvents( + -> Bacon.fromArray([1, 2, 2, 3, 1]).skipDuplicates() + [1, 2, 3, 1], unstable) + + it "toString", -> + assert.equal Bacon.never().skipDuplicates().toString(), "Bacon.never().skipDuplicates()" + +describe "EventStream.flatMap", -> + describe "should spawn new stream for each value and collect results into a single stream", -> + expectStreamEvents( + -> series(1, [1, 2]).flatMap (value) -> + Bacon.sequentially(t(2), [value, error(), value]) + [1, 2, error(), error(), 1, 2], unstable) + describe "should pass source errors through to the result", -> + expectStreamEvents( + -> series(1, [error(), 1]).flatMap (value) -> + Bacon.later(t(1), value) + [error(), 1]) + describe "should work with a spawned stream responding synchronously", -> + expectStreamEvents( + -> series(1, [1, 2]).flatMap (value) -> + Bacon.never().concat(Bacon.once(value)) + [1, 2], unstable) + expectStreamEvents( + -> series(1, [1,2]).flatMap (value) -> + Bacon.never().concat(Bacon.once(value)).concat(Bacon.once("lol")) + [1, "lol", 2, "lol"], unstable) + describe "should work with a source stream responding synchronously", -> + expectStreamEvents( + -> Bacon.fromArray([1, 2]).flatMap (value) -> + Bacon.once(value) + [1, 2], unstable) + expectStreamEvents( + -> Bacon.fromArray([1, 2]).flatMap (value) -> + Bacon.fromArray([value, value*10]) + [1, 10, 2, 20], unstable) + expectStreamEvents( + -> Bacon.once(1).flatMap (value) -> + Bacon.later(0, value) + [1], unstable) + describe "Works also when f returns a Property instead of an EventStream", -> + expectStreamEvents( + -> series(1, [1,2]).flatMap(Bacon.constant) + [1,2], unstable) + describe "Works also when f returns a constant value instead of an EventStream", -> + expectStreamEvents( + -> series(1, [1,2]).flatMap((x) -> x) + [1,2], unstable) + describe "Works also when f returns an Error instead of an EventStream", -> + expectStreamEvents( + -> series(1, [1,2]).flatMap((x) -> new Bacon.Error(x)) + [new Bacon.Error(1), new Bacon.Error(2)], unstable) + describe "Accepts a constant EventStream/Property as an alternative to a function", -> + expectStreamEvents( + -> Bacon.once("asdf").flatMap(Bacon.constant("bacon")) + ["bacon"]) + expectStreamEvents( + -> Bacon.once("asdf").flatMap(Bacon.once("bacon")) + ["bacon"]) + describe "works with a complex setup (fix #363)", -> + it "case 1 (samplee has no subscribers)", -> + result = "" + prop = Bacon.combineTemplate(faq: Bacon.later(1).toProperty("default value")) + Bacon.once().flatMap(-> + problem = prop.sampledBy(Bacon.once()) + problem.onValue (x) -> + result = x + ).onValue -> + assert.deepEqual result, {faq: "default value"} + it "case 2 (samplee has subscriber)", -> + result = "" + prop = Bacon.combineTemplate(faq: Bacon.later(1).toProperty("default value")) + prop.onValue -> + Bacon.once().flatMap(-> + problem = prop.sampledBy(Bacon.once()) + problem.onValue (x) -> + result = x + ).onValue -> + assert.deepEqual result, {faq: "default value"} + it "case 3 (original fiddle)", -> + result = "" + input = new Bacon.Bus() + prop = Bacon.combineTemplate(faq: input.toProperty("default value")) + events = new Bacon.Bus() + events.flatMapLatest(-> + problem = prop.sampledBy(Bacon.once()) + problem.onValue (x) -> + result = x + ).onValue -> + events.push() + assert.deepEqual result, {faq: "default value"} + describe "Respects function construction rules", -> + expectStreamEvents( + -> Bacon.once({ bacon: Bacon.once("sir francis")}).flatMap(".bacon") + ["sir francis"], unstable) + expectStreamEvents( + -> Bacon.once({ bacon: "sir francis"}).flatMap(".bacon") + ["sir francis"], unstable) + expectStreamEvents( + -> + glorify = (x, y) -> Bacon.fromArray([x, y]) + Bacon.once("francis").flatMap(glorify, "sir") + ["sir", "francis"], unstable) + it "toString", -> + assert.equal Bacon.never().flatMap(->).toString(), "Bacon.never().flatMap(function)" + +describe "EventStream.flatMapWithConcurrencyLimit", -> + describe "limits the number of concurrently active spawned streams by queuing", -> + expectStreamEvents( + -> series(1, [1, 2]).flatMapWithConcurrencyLimit(1, (value) -> + Bacon.sequentially(t(2), [value, error(), value])) + [1, error(), 1, 2, error(), 2], unstable) + describe "works with n=2", -> + expectStreamEvents( + -> series(1, [1,2,3]).flatMapWithConcurrencyLimit(2, (value) -> + Bacon.sequentially(t(2), [value, value])) + [1, 2, 1, 2, 3, 3]) + describe "Respects function construction rules", -> + expectStreamEvents( + -> Bacon.once({ bacon: Bacon.once("sir francis")}).flatMapWithConcurrencyLimit(1, ".bacon") + ["sir francis"]) + it "toString", -> + assert.equal Bacon.once(1).flatMapWithConcurrencyLimit(2, ->).toString(), "Bacon.once(1).flatMapWithConcurrencyLimit(2,function)" + +describe "EventStream.flatMapConcat", -> + describe "is like flatMapWithConcurrencyLimit(1)", -> + expectStreamEvents( + -> series(1, [1, 2]).flatMapConcat((value) -> + Bacon.sequentially(t(2), [value, error(), value])) + [1, error(), 1, 2, error(), 2], unstable) + describe "Respects function construction rules", -> + expectStreamEvents( + -> Bacon.once({ bacon: Bacon.once("sir francis")}).flatMapConcat(".bacon") + ["sir francis"]) + it "toString", -> + assert.equal Bacon.once(1).flatMapConcat(->).toString(), "Bacon.once(1).flatMapConcat(function)" + + +describe "Property.flatMap", -> + describe "should spawn new stream for all events including Init", -> + expectStreamEvents( + -> + once = (x) -> Bacon.once(x) + series(1, [1, 2]).toProperty(0).flatMap(once) + [0, 1, 2], unstable) + describe "Works also when f returns a Property instead of an EventStream", -> + expectStreamEvents( + -> series(1, [1,2]).toProperty().flatMap(Bacon.constant) + [1,2], unstable) + expectPropertyEvents( + -> series(1, [1,2]).toProperty().flatMap(Bacon.constant).toProperty() + [1,2], unstable) + describe "works for synchronous source", -> + expectStreamEvents( + -> + once = (x) -> Bacon.once(x) + Bacon.fromArray([1, 2]).toProperty(0).flatMap(once) + [0, 1, 2], unstable) + describe "works in a complex scenario #338", -> + expectStreamEvents( + -> + a = activate(series(2, ["a", "A"])) + b = activate(series(2, ["b", "B"])).delay(1).toProperty() + a.flatMapLatest((a) -> b.map((b) -> a + b)) + ["ab", "Ab", "AB"], unstable) + it "toString", -> + assert.equal Bacon.constant(1).flatMap(->).toString(), "Bacon.constant(1).flatMap(function)" + +describe "EventStream.flatMapLatest", -> + describe "spawns new streams but collects values from the latest spawned stream only", -> + expectStreamEvents( + -> series(3, [1, 2]).flatMapLatest (value) -> + Bacon.sequentially(t(2), [value, error(), value]) + [1, 2, error(), 2], unstable) + describe "Accepts a constant EventStream/Property as an alternative to a function", -> + expectStreamEvents( + -> Bacon.once("asdf").flatMapLatest(Bacon.constant("bacon")) + ["bacon"], unstable) + describe "Accepts a field extractor string instead of function", -> + expectStreamEvents( + -> Bacon.once({ bacon: Bacon.once("sir francis")}).flatMapLatest(".bacon") + ["sir francis"]) + expectStreamEvents( + -> Bacon.once({ bacon: "sir francis"}).flatMapLatest(".bacon") + ["sir francis"]) + it "toString", -> + assert.equal Bacon.never().flatMapLatest(->).toString(), "Bacon.never().flatMapLatest(function)" + describe "No glitches in a complex scenario", -> + expectPropertyEvents( + -> + changes = series(1, [{a:0,b:0},{a:1,b:1}]) + + a = changes.map '.a' + b = changes.map '.b' + + ab = Bacon.combineAsArray a, b + + f = ab.flatMapLatest (values) -> + Bacon.once 'f' + values + + Bacon.combineAsArray(f, b).map(".0") + ["f0,0","f1,1"]) + +describe "Property.flatMapLatest", -> + describe "spawns new streams but collects values from the latest spawned stream only", -> + expectStreamEvents( + -> series(3, [1, 2]).toProperty(0).flatMapLatest (value) -> + Bacon.sequentially(t(2), [value, value]) + [0, 1, 2, 2], unstable) + describe "Accepts a constant EventStream/Property as an alternative to a function", -> + expectStreamEvents( + -> Bacon.constant("asdf").flatMapLatest(Bacon.constant("bacon")) + ["bacon"], unstable) + it "toString", -> + assert.equal Bacon.constant(1).flatMapLatest(->).toString(), "Bacon.constant(1).flatMapLatest(function)" + +describe "EventStream.flatMapFirst", -> + describe "spawns new streams and ignores source events until current spawned stream has ended", -> + expectStreamEvents( + -> series(2, [2, 4, 6, 8]).flatMapFirst (value) -> + series(1, ["a" + value, "b" + value, "c" + value]) + ["a2", "b2", "c2", "a6", "b6", "c6"], unstable) + describe "Accepts a field extractor string instead of function", -> + expectStreamEvents( + -> Bacon.once({ bacon: Bacon.once("sir francis")}).flatMapFirst(".bacon") + ["sir francis"]) + expectStreamEvents( + -> Bacon.once({ bacon: "sir francis"}).flatMapFirst(".bacon") + ["sir francis"]) + it "toString", -> + assert.equal Bacon.never().flatMapFirst(->).toString(), "Bacon.never().flatMapFirst(function)" + +describe "EventStream.flatMapError", -> + describe "allows spawning a new stream from an error", -> + expectStreamEvents( + -> + source = Bacon.fromArray [ + error() + error(value: true, data: 1) + error() + error(value: true, data: 2) + ] + source.flatMapError (err) -> + if err?.value + Bacon.once(err.data) + else + error() + + [error(), 1, error(), 2] + ) + describe "has no effect on values", -> + expectStreamEvents( + -> Bacon.fromArray([1, 2]).flatMapError(-> Bacon.once("omg")) + [1, 2]) + it "toString", -> + assert.equal Bacon.once(1).flatMapError(->).toString(), "Bacon.once(1).flatMapError(function)" + +describe "EventStream.merge", -> + describe "merges two streams and ends when both are exhausted", -> + expectStreamEvents( + -> + left = series(1, [1, error(), 2, 3]) + right = series(1, [4, 5, 6]).delay(t(4)) + left.merge(right) + [1, error(), 2, 3, 4, 5, 6], unstable) + describe "respects subscriber return value", -> + expectStreamEvents( + -> + left = repeat(2, [1, 3]).take(3) + right = repeat(3, [2]).take(3) + left.merge(right).takeWhile(lessThan(2)) + [1]) + describe "does not duplicate same error from two streams", -> + expectStreamEvents( + -> + src = series(1, [1, error(), 2, error(), 3]) + left = src.map((x) -> x) + right = src.map((x) -> x * 2) + left.merge(right) + [1, 2, error(), 2, 4, error(), 3, 6], unstable) + describe "works with synchronous sources", -> + expectStreamEvents( + -> Bacon.fromArray([1,2]).merge(Bacon.fromArray([3,4])) + [1,2,3,4]) + it "toString", -> + assert.equal Bacon.once(1).merge(Bacon.once(2)).toString(), "Bacon.once(1).merge(Bacon.once(2))" + +describe "EventStream.delay", -> + describe "delays all events (except errors) by given delay in milliseconds", -> + expectStreamEvents( + -> + left = series(2, [1, 2, 3]) + right = series(1, [error(), 4, 5, 6]).delay(t(6)) + left.merge(right) + [error(), 1, 2, 3, 4, 5, 6], unstable) + describe "works with synchronous streams", -> + expectStreamEvents( + -> + left = Bacon.fromArray([1, 2, 3]) + right = Bacon.fromArray([4, 5, 6]).delay(t(6)) + left.merge(right) + [1, 2, 3, 4, 5, 6], unstable) + it "toString", -> + assert.equal Bacon.never().delay(1).toString(), "Bacon.never().delay(1)" + +describe "EventStream.debounce", -> + describe "throttles input by given delay, passing-through errors", -> + expectStreamEvents( + -> series(2, [1, error(), 2]).debounce(t(7)) + [error(), 2]) + describe "waits for a quiet period before outputing anything", -> + th.expectStreamTimings( + -> series(2, [1, 2, 3, 4]).debounce(t(3)) + [[11, 4]]) + describe "works with synchronous source", -> + expectStreamEvents( + -> Bacon.fromArray([1, 2, 3, 4]).debounce(t(3)) + [4]) + describe "works in combination with scan", -> + count = 0 + expectPropertyEvents( + -> series(2, [1,2,3]).debounce(1).scan(0, (x,y) -> count++; x + y) + [0, 1, 3, 6] + {extraCheck: -> it "calls function once per value", -> assert.equal count, 3} + ) + it "toString", -> + assert.equal Bacon.never().debounce(1).toString(), "Bacon.never().debounce(1)" + + +describe "EventStream.debounceImmediate(delay)", -> + describe "outputs first event immediately, then ignores events for given amount of milliseconds", -> + th.expectStreamTimings( + -> series(2, [1, 2, 3, 4]).debounceImmediate(t(3)) + [[2, 1], [6, 3]], unstable) + describe "works with synchronous source", -> + expectStreamEvents( + -> Bacon.fromArray([1, 2, 3, 4]).debounceImmediate(t(3)) + [1]) + it "toString", -> + assert.equal Bacon.never().debounceImmediate(1).toString(), "Bacon.never().debounceImmediate(1)" + +describe "EventStream.throttle(delay)", -> + describe "outputs at steady intervals, without waiting for quiet period", -> + th.expectStreamTimings( + -> series(2, [1, 2, 3]).throttle(t(3)) + [[5, 2], [8, 3]]) + describe "works with synchronous source", -> + expectStreamEvents( + -> Bacon.fromArray([1, 2, 3]).throttle(t(3)) + [3]) + it "toString", -> + assert.equal Bacon.never().throttle(1).toString(), "Bacon.never().throttle(1)" + +describe "EventStream.bufferingThrottle(minimumInterval)", -> + describe "limits throughput but includes all events", -> + th.expectStreamTimings( + -> series(1, [1,2,3]).bufferingThrottle(t(3)) + [[1,1], [4,2], [7,3]]) + it "toString", -> + assert.equal Bacon.once(1).bufferingThrottle(2).toString(), "Bacon.once(1).bufferingThrottle(2)" + +describe "Property.bufferingThrottle(delay)", -> + describe "limits throughput but includes all events", -> + th.expectStreamTimings( + -> series(1, [1,2,3]).toProperty().bufferingThrottle(t(3)).changes() + [[1,1], [4,2], [7,3]]) + +describe "EventStream.holdWhen", -> + describe "Keeps events on hold when a property is true", -> + th.expectStreamTimings( + -> + src = series(2, [1,2,3,4]) + valve = series(2, [true, false, true, false]).delay(1).toProperty() + src.holdWhen(valve) + [[2, 1], [5, 2], [6, 3], [9, 4]]) + describe "Holds forever when the property ends with truthy value", -> + th.expectStreamTimings( + -> + src = series(2, [1,2,3,4]) + valve = series(2, [true, false, true]).delay(1).toProperty() + src.holdWhen(valve) + [[2, 1], [5, 2], [6, 3]]) + describe "Supports truthy init value for property", -> + th.expectStreamTimings( + -> + src = series(2, [1,2]) + valve = series(2, [false]).delay(1).toProperty(true) + src.holdWhen(valve) + [[3, 1], [4, 2]]) + describe.skip "Doesn't crash when flushing huge buffers", -> + count = 6000 + expectPropertyEvents( + -> + source = series(1, [1..count]) + flag = source.map((x) -> x != count-1).toProperty(true) + source.holdWhen(flag).fold(0, ((x,y) -> x+1), { eager: true}) + [count-1]) + + it "toString", -> + assert.equal Bacon.once(1).holdWhen(Bacon.constant(true)).toString(), "Bacon.once(1).holdWhen(Bacon.constant(true))" + +describe "EventStream.bufferWithTime", -> + describe "returns events in bursts, passing through errors", -> + expectStreamEvents( + -> series(2, [error(), 1, 2, 3, 4, 5, 6, 7]).bufferWithTime(t(7)) + [error(), [1, 2, 3, 4], [5, 6, 7]]) + describe "keeps constant output rate even when input is sporadical", -> + th.expectStreamTimings( + -> th.atGivenTimes([[0, "a"], [3, "b"], [5, "c"]]).bufferWithTime(t(2)) + [[2, ["a"]], [4, ["b"]], [6, ["c"]]] + unstable + ) + describe "works with empty stream", -> + expectStreamEvents( + -> Bacon.never().bufferWithTime(t(1)) + []) + describe "allows custom defer-function", -> + fast = (f) -> sc.setTimeout(f, 0) + th.expectStreamTimings( + -> th.atGivenTimes([[0, "a"], [2, "b"]]).bufferWithTime(fast) + [[0, ["a"]], [2, ["b"]]]) + describe "works with synchronous defer-function", -> + sync = (f) -> f() + th.expectStreamTimings( + -> th.atGivenTimes([[0, "a"], [2, "b"]]).bufferWithTime(sync) + [[0, ["a"]], [2, ["b"]]]) + describe "works with synchronous source", -> + expectStreamEvents( + -> series(2, [1,2,3]).bufferWithTime(t(7)) + [[1,2,3]]) + it "toString", -> + assert.equal Bacon.never().bufferWithTime(1).toString(), "Bacon.never().bufferWithTime(1)" + +describe "EventStream.bufferWithCount", -> + describe "returns events in chunks of fixed size, passing through errors", -> + expectStreamEvents( + -> series(1, [1, 2, 3, error(), 4, 5]).bufferWithCount(2) + [[1, 2], error(), [3, 4], [5]]) + describe "works with synchronous source", -> + expectStreamEvents( + -> Bacon.fromArray([1,2,3,4,5]).bufferWithCount(2) + [[1, 2], [3, 4], [5]]) + it "toString", -> + assert.equal Bacon.never().bufferWithCount(1).toString(), "Bacon.never().bufferWithCount(1)" + +describe "EventStream.bufferWithTimeOrCount", -> + describe "flushes on count", -> + expectStreamEvents( + -> series(1, [1, 2, 3, error(), 4, 5]).bufferWithTimeOrCount(t(10), 2) + [[1, 2], error(), [3, 4], [5]]) + describe "flushes on timeout", -> + expectStreamEvents( + -> series(2, [error(), 1, 2, 3, 4, 5, 6, 7]).bufferWithTimeOrCount(t(7), 10) + [error(), [1, 2, 3, 4], [5, 6, 7]]) + it "toString", -> + assert.equal Bacon.never().bufferWithTimeOrCount(1, 2).toString(), "Bacon.never().bufferWithTimeOrCount(1,2)" + +describe "EventStream.takeUntil", -> + describe "takes elements from source until an event appears in the other stream", -> + expectStreamEvents( + -> + src = repeat(3, [1, 2, 3]) + stopper = repeat(7, ["stop!"]) + src.takeUntil(stopper) + [1, 2], unstable) + describe "works on self-derived stopper", -> + expectStreamEvents( + -> + src = repeat(3, [3, 2, 1]) + stopper = src.filter(lessThan(3)) + src.takeUntil(stopper) + [3]) + describe "works on self-derived stopper with an evil twist", -> + expectStreamEvents( + -> + src = repeat(3, [3, 2, 1]) + data = src.map((x) -> x) + data.take(3).onValue(->) + stopper = src.filter(lessThan(3)) + data.takeUntil(stopper) + [3]) + describe "includes source errors, ignores stopper errors", -> + expectStreamEvents( + -> + src = repeat(2, [1, error(), 2, 3]) + stopper = repeat(7, ["stop!"]).merge(repeat(1, [error()])) + src.takeUntil(stopper) + [1, error(), 2], unstable) + describe "works with Property as stopper", -> + expectStreamEvents( + -> + src = repeat(3, [1, 2, 3]) + stopper = repeat(7, ["stop!"]).toProperty() + src.takeUntil(stopper) + [1, 2], unstable) + describe "considers Property init value as stopper", -> + expectStreamEvents( + -> + src = repeat(3, [1, 2, 3]) + stopper = Bacon.constant("stop") + src.takeUntil(stopper) + []) + describe "ends immediately with synchronous stopper", -> + expectStreamEvents( + -> + src = repeat(3, [1, 2, 3]) + stopper = Bacon.once("stop") + src.takeUntil(stopper) + []) + describe "ends properly with a never-ending stopper", -> + expectStreamEvents( + -> + src = series(1, [1,2,3]) + stopper = new Bacon.Bus() + src.takeUntil(stopper) + [1,2,3]) + describe "ends properly with a never-ending stopper and synchronous source", -> + expectStreamEvents( + -> + src = Bacon.fromArray([1,2,3]).mapEnd("finito") + stopper = new Bacon.Bus() + src.takeUntil(stopper) + [1,2,3, "finito"]) + describe "unsubscribes its source as soon as possible", -> + expectStreamEvents( + -> + startTick = sc.now() + Bacon.later(20) + .onUnsub(-> + assert.equal sc.now(), startTick + 1) + .takeUntil Bacon.later(1) + []) + describe "it should unsubscribe its stopper on end", -> + expectStreamEvents( + -> + startTick = sc.now() + Bacon.later(1,'x').takeUntil(Bacon.later(20).onUnsub(-> + assert.equal sc.now(), startTick + 1)) + ['x']) + describe "it should unsubscribe its stopper on no more", -> + expectStreamEvents( + -> + startTick = sc.now() + Bacon.later(1,'x').takeUntil(Bacon.later(20).onUnsub(-> + assert.equal sc.now(), startTick + 1)) + ['x']) + ### TODO does not pass + describe "works with synchronous self-derived sources", -> + expectStreamEvents( + -> + a = Bacon.fromArray [1,2] + b = a.filter((x) -> x >= 2) + a.takeUntil b + [1]) + ### + it "toString", -> + assert.equal Bacon.later(1, "a").takeUntil(Bacon.later(2, "b")).toString(), "Bacon.later(1,a).takeUntil(Bacon.later(2,b))" + +describe "EventStream.awaiting(other)", -> + describe "indicates whether s1 has produced output after s2 (or only the former has output so far)", -> + expectPropertyEvents( + -> series(2, [1, 1]).awaiting(series(3, [2])) + [false, true, false, true]) + describe "supports Properties", -> + expectPropertyEvents( + -> series(2, [1, 1]).awaiting(series(3, [2]).toProperty()) + [false, true, false, true]) + describe "supports simultaneouts events", -> + expectPropertyEvents( + -> + src = Bacon.later(1, 1) + src.awaiting(src.map(->)) + [false]) + expectPropertyEvents( + -> + src = Bacon.later(1, 1) + src.map(->).awaiting(src) + [false]) + it "toString", -> + assert.equal Bacon.never().awaiting(Bacon.once(1)).toString(), "Bacon.never().awaiting(Bacon.once(1))" + +describe "EventStream.endOnError", -> + describe "terminates on error", -> + expectStreamEvents( + -> repeat(1, [1, 2, error(), 3]).endOnError() + [1, 2, error()]) + describe "accepts predicate function", -> + expectStreamEvents( + -> series(1, [1, 2, error(), 3, new Bacon.Error({serious:true}), 4]).endOnError((e) -> e?.serious) + [1,2,error(),3,error()]) + describe "accepts extractor string", -> + expectStreamEvents( + -> series(1, [1, 2, error(), 3, new Bacon.Error({serious:true}), 4]).endOnError(".serious") + [1,2,error(),3,error()]) + describe "works with synchronous source", -> + expectStreamEvents( + -> Bacon.fromArray([1, 2, error(), 3]).endOnError() + [1, 2, error()]) + it "toString", -> + assert.equal Bacon.never().endOnError().toString(), "Bacon.never().endOnError()" + +describe "Bacon.constant", -> + describe "creates a constant property", -> + expectPropertyEvents( + -> Bacon.constant("lol") + ["lol"]) + it "ignores unsubscribe", -> + Bacon.constant("lol").onValue(=>)() + describe "provides same value to all listeners", -> + c = Bacon.constant("lol") + expectPropertyEvents((-> c), ["lol"]) + it "check check", -> + f = mockFunction() + c.onValue(f) + f.verify("lol") + it "provides same value to all listeners, when mapped (bug fix)", -> + c = Bacon.constant("lol").map(id) + f = mockFunction() + c.onValue(f) + f.verify("lol") + c.onValue(f) + f.verify("lol") + it "toString", -> + assert.equal Bacon.constant(1).toString(), "Bacon.constant(1)" + +describe "Bacon.never", -> + describe "should send just end", -> + expectStreamEvents( + -> Bacon.never() + []) + +describe "Bacon.once", -> + describe "should send single event and end", -> + expectStreamEvents( + -> Bacon.once("pow") + ["pow"]) + describe "accepts an Error event as parameter", -> + expectStreamEvents( + -> Bacon.once(new Bacon.Error("oop")) + [error()]) + describe "Allows wrapped events, for instance, Bacon.Error", -> + expectStreamEvents( + -> Bacon.once(error()) + [error()]) + it "Responds synchronously", -> + values = [] + s = Bacon.once(1) + s.onValue(values.push.bind(values)) + assert.deepEqual values, [1] + s.onValue(values.push.bind(values)) + assert.deepEqual values, [1] + +describe "Bacon.fromArray", -> + describe "Turns an empty array into an EventStream", -> + expectStreamEvents( + -> Bacon.fromArray([]) + []) + describe "Turns a single-element array into an EventStream", -> + expectStreamEvents( + -> Bacon.fromArray([1]) + [1]) + describe "Turns a longer array into an EventStream", -> + expectStreamEvents( + -> Bacon.fromArray([1, 2, 3]) + [1, 2, 3]) + describe "Allows wrapped events, for instance, Bacon.Error", -> + expectStreamEvents( + -> Bacon.fromArray([error(), 1]) + [error(), 1]) + it "toString", -> + assert.equal Bacon.fromArray([1,2]).toString(), "Bacon.fromArray([1,2])" + it "doesn't mutate the given array, toString works after subscribe (bug fix)", -> + array = [1,2] + s = Bacon.fromArray(array) + s.onValue(->) + assert.equal s.toString(), "Bacon.fromArray([1,2])" + assert.deepEqual array, [1,2] + +describe "EventStream.concat", -> + describe "provides values from streams in given order and ends when both are exhausted", -> + expectStreamEvents( + -> + left = series(2, [1, error(), 2, 3]) + right = series(1, [4, 5, 6]) + left.concat(right) + [1, error(), 2, 3, 4, 5, 6], unstable) + describe "respects subscriber return value when providing events from left stream", -> + expectStreamEvents( + -> + left = repeat(3, [1, 3]).take(3) + right = repeat(2, [1]).take(3) + left.concat(right).takeWhile(lessThan(2)) + [1]) + describe "respects subscriber return value when providing events from right stream", -> + expectStreamEvents( + -> + left = series(3, [1, 2]) + right = series(2, [2, 4, 6]) + left.concat(right).takeWhile(lessThan(4)) + [1, 2, 2]) + describe "works with Bacon.never()", -> + expectStreamEvents( + -> Bacon.never().concat(Bacon.never()) + []) + describe "works with Bacon.once()", -> + expectStreamEvents( + -> Bacon.once(2).concat(Bacon.once(1)) + [2, 1]) + describe "works with Bacon.once() and Bacon.never()", -> + expectStreamEvents( + -> Bacon.once(1).concat(Bacon.never()) + [1]) + describe "works with Bacon.never() and Bacon.once()", -> + expectStreamEvents( + -> Bacon.never().concat(Bacon.once(1)) + [1]) + describe "works with Bacon.once() and async source", -> + expectStreamEvents( + -> Bacon.once(1).concat(series(1, [2, 3])) + [1, 2, 3]) + describe "works with Bacon.once() and Bacon.fromArray()", -> + expectStreamEvents( + -> Bacon.once(1).concat(Bacon.fromArray([2, 3])) + [1, 2, 3], unstable) + describe "Works with synchronized left stream and doAction", -> + expectStreamEvents( + -> + bus = new Bacon.Bus() + stream = Bacon.fromArray([1,2]).flatMapLatest (x) -> + Bacon.once(x).concat(Bacon.later(10, x).doAction((x) -> bus.push(x); bus.end())) + stream.onValue -> + bus + [2]) + it "toString", -> + assert.equal Bacon.once(1).concat(Bacon.once(2)).toString(), "Bacon.once(1).concat(Bacon.once(2))" + +describe "EventStream.startWith", -> + describe "provides seed value, then the rest", -> + expectStreamEvents( + -> + left = series(1, [1, 2, 3]) + left.startWith('pow') + ['pow', 1, 2, 3], unstable) + describe "works with synchronous source", -> + expectStreamEvents( + -> + left = Bacon.fromArray([1, 2, 3]) + left.startWith('pow') + ['pow', 1, 2, 3], unstable) + it "toString", -> + assert.equal Bacon.never().startWith(0).toString(), "Bacon.never().startWith(0)" + +describe "Property.startWith", -> + describe "starts with given value if the Property doesn't have an initial value", -> + expectPropertyEvents( + -> + left = series(1, [1, 2, 3]).toProperty() + left.startWith('pow') + ['pow', 1, 2, 3], unstable) + describe "works with synchronous source", -> + expectPropertyEvents( + -> + left = Bacon.fromArray([1, 2, 3]).toProperty() + left.startWith('pow') + ['pow', 1, 2, 3], unstable) + describe "starts with the initial value of the Property if any", -> + expectPropertyEvents( + -> + left = series(1, [1, 2, 3]).toProperty(0) + left.startWith('pow') + [0, 1, 2, 3], unstable) + it "works with combineAsArray", -> + result = null + a = Bacon.constant("lolbal") + result = Bacon.combineAsArray([a.map(true), a.map(true)]).map("right").startWith("wrong") + result.onValue((x) -> result = x) + assert.equal result, "right" + it "toString", -> + assert.equal Bacon.constant(2).startWith(1).toString(), "Bacon.constant(2).startWith(1)" + +describe "EventStream.toProperty", -> + describe "delivers current value and changes to subscribers", -> + expectPropertyEvents( + -> + s = new Bacon.Bus() + p = s.toProperty("a") + soon -> + s.push "b" + s.end() + p + ["a", "b"]) + describe "passes through also Errors", -> + expectPropertyEvents( + -> series(1, [1, error(), 2]).toProperty() + [1, error(), 2]) + + describe "supports null as value", -> + expectPropertyEvents( + -> series(1, [null, 1, null]).toProperty(null) + [null, null, 1, null]) + + describe "does not get messed-up by a transient subscriber (bug fix)", -> + expectPropertyEvents( + -> + prop = series(1, [1,2,3]).toProperty(0) + prop.subscribe (event) => + Bacon.noMore + prop + [0, 1, 2, 3]) + describe "works with synchronous source", -> + expectPropertyEvents( + -> Bacon.fromArray([1,2,3]).toProperty() + [1,2,3]) + expectPropertyEvents( + -> Bacon.fromArray([1,2,3]).toProperty(0) + [0,1,2,3], unstable) + it "preserves laziness", -> + calls = 0 + id = (x) -> + calls++ + x + Bacon.fromArray([1,2,3,4,5]).map(id).toProperty().skip(4).onValue() + assert.equal calls, 1 + it "toString", -> + assert.equal Bacon.once(1).toProperty(0).toString(), "Bacon.once(1).toProperty(0)" + +describe "Property.toEventStream", -> + describe "creates a stream that starts with current property value", -> + expectStreamEvents( + -> series(1, [1, 2]).toProperty(0).toEventStream() + [0, 1, 2], unstable) + describe "works with synchronous source", -> + expectStreamEvents( + -> Bacon.fromArray([1, 2]).toProperty(0).toEventStream() + [0, 1, 2], unstable) + +describe "Property.toProperty", -> + describe "returns the same Property", -> + expectPropertyEvents( + -> Bacon.constant(1).toProperty() + [1]) + it "rejects arguments", -> + try + Bacon.constant(1).toProperty(0) + fail() + catch e + +describe "Property.map", -> + describe "maps property values", -> + expectPropertyEvents( + -> + s = new Bacon.Bus() + p = s.toProperty(1).map(times, 2) + soon -> + s.push 2 + s.error() + s.end() + p + [2, 4, error()]) + +describe "Property.filter", -> + describe "should filter values", -> + expectPropertyEvents( + -> series(1, [1, error(), 2, 3]).toProperty().filter(lessThan(3)) + [1, error(), 2]) + it "preserves old current value if the updated value is non-matching", -> + s = new Bacon.Bus() + p = s.toProperty().filter(lessThan(2)) + p.onValue(=>) # to ensure that property is actualy updated + s.push(1) + s.push(2) + values = [] + p.onValue((v) => values.push(v)) + assert.deepEqual values, [1] + describe "can filter by Property value", -> + expectPropertyEvents( + -> + src = series(2, [1, 2, 3, 4]).delay(t(1)).toProperty() + ok = series(2, [false, true, true, false]).toProperty() + src.filter(ok) + [2, 3]) + +describe "Property.take(1)", -> + describe "takes the Initial event", -> + expectPropertyEvents( + -> series(1, [1,2,3]).toProperty(0).take(1) + [0]) + describe "takes the first Next event, if no Initial value", -> + expectPropertyEvents( + -> series(1, [1,2,3]).toProperty().take(1) + [1]) + describe "works for constants", -> + expectPropertyEvents( + -> Bacon.constant(1) + [1]) + describe "works for never-ending Property", -> + expectPropertyEvents( + -> repeat(1, [1,2,3]).toProperty(0).take(1) + [0]) + expectPropertyEvents( + -> repeat(1, [1,2,3]).toProperty().take(1) + [1]) + +describe "Bacon.once().take(1)", -> + describe "works", -> + expectStreamEvents( + -> Bacon.once(1).take(1) + [1]) + +describe "Property.takeWhile", -> + describe "takes while predicate is true", -> + expectPropertyEvents( + -> + series(1, [1, error("wat"), 2, 3]) + .toProperty().takeWhile(lessThan(3)) + [1, error("wat"), 2]) + describe "extracts field values", -> + expectPropertyEvents( + -> + series(1, [{good:true, value:"yes"}, {good:false, value:"no"}]) + .toProperty().takeWhile(".good").map(".value") + ["yes"]) + describe "can filter by Property value", -> + expectPropertyEvents( + -> + src = series(1, [1,1,2,3,4,4,8,7]).toProperty() + odd = src.map((x) -> x % 2) + src.takeWhile(odd) + [1,1]) + describe "works with never-ending Property", -> + expectPropertyEvents( + -> + repeat(1, [1, error("wat"), 2, 3]) + .toProperty().takeWhile(lessThan(3)) + [1, error("wat"), 2]) + +describe "Property.takeUntil", -> + describe "takes elements from source until an event appears in the other stream", -> + expectPropertyEvents( + -> series(2, [1,2,3]).toProperty().takeUntil(Bacon.later(t(3))) + [1]) + describe "works with errors", -> + expectPropertyEvents( + -> + src = repeat(2, [1, error(), 3]) + stopper = repeat(5, ["stop!"]) + src.toProperty(0).takeUntil(stopper) + [0, 1, error()]) + it "works with synchronous error (fix #447)", -> + errors = [] + Bacon.once(new Bacon.Error("fail")).toProperty() + .takeUntil(Bacon.never()) + .onError((e) -> errors.push(e)) + assert.deepEqual errors, ["fail"] + it "toString", -> + assert.equal Bacon.constant(1).takeUntil(Bacon.never()).toString(), "Bacon.constant(1).takeUntil(Bacon.never())" + +describe "Property.delay", -> + describe "delivers initial value and changes", -> + expectPropertyEvents( + -> series(1, [1,2,3]).toProperty(0).delay(t(1)) + [0,1,2,3]) + describe "delays changes", -> + expectStreamEvents( + -> + series(2, [1,2,3]) + .toProperty() + .delay(t(2)).changes().takeUntil(Bacon.later(t(5))) + [1], unstable) + describe "does not delay initial value", -> + expectPropertyEvents( + -> series(3, [1]).toProperty(0).delay(1).takeUntil(Bacon.later(t(2))) + [0]) + it "toString", -> + assert.equal Bacon.constant(0).delay(1).toString(), "Bacon.constant(0).delay(1)" + +describe "Property.debounce", -> + describe "delivers initial value and changes", -> + expectPropertyEvents( + -> series(2, [1,2,3]).toProperty(0).debounce(t(1)) + [0,1,2,3]) + describe "throttles changes, but not initial value", -> + expectPropertyEvents( + -> series(1, [1,2,3]).toProperty(0).debounce(t(4)) + [0,3]) + describe "works without initial value", -> + expectPropertyEvents( + -> series(2, [1,2,3]).toProperty().debounce(t(4)) + [3]) + describe "works with Bacon.constant (bug fix)", -> + expectPropertyEvents( + -> Bacon.constant(1).debounce(1) + [1]) + it "works with Bacon.combine (bug fix)", -> + values = [] + p1 = Bacon.once(true).toProperty() + p2 = Bacon.once(true).toProperty() + + visibleP = Bacon.combineAsArray([p1, p2]).startWith(false) + + visibleP.debounce(500).onValue (val) -> + values.push(val) + + it "toString", -> + assert.equal Bacon.constant(0).debounce(1).toString(), "Bacon.constant(0).debounce(1)" +describe "Property.throttle", -> + describe "throttles changes, but not initial value", -> + expectPropertyEvents( + -> series(1, [1,2,3]).toProperty(0).throttle(t(4)) + [0,3]) + describe "works with Bacon.once (bug fix)", -> + expectPropertyEvents( + -> Bacon.once(1).toProperty().throttle(1) + [1]) + it "toString", -> + assert.equal Bacon.constant(0).throttle(1).toString(), "Bacon.constant(0).throttle(1)" + +describe "Property.endOnError", -> + describe "terminates on Error", -> + expectPropertyEvents( + -> series(2, [1, error(), 2]).toProperty().endOnError() + [1, error()]) + +describe "Property.awaiting(other)", -> + describe "indicates whether p1 has produced output after p2 (or only the former has output so far)", -> + expectPropertyEvents( + -> series(2, [1, 1]).toProperty().awaiting(series(3, [2])) + [false, true, false, true]) + +describe "Property.skipDuplicates", -> + describe "drops duplicates", -> + expectPropertyEvents( + -> series(1, [1, 2, error(), 2, 3, 1]).toProperty(0).skipDuplicates() + [0, 1, 2, error(), 3, 1]) + describe "Doesn't skip initial value (bug fix #211)", -> + b = new Bacon.Bus() + p = b.toProperty() + p.onValue -> # force property update + s = p.skipDuplicates() + b.push 'foo' + + describe "series 1", -> + expectPropertyEvents((-> s.take(1)), ["foo"]) + describe "series 2", -> + expectPropertyEvents((-> s.take(1)), ["foo"]) + describe "series 3", -> + expectPropertyEvents((-> s.take(1)), ["foo"]) + +describe "Property.changes", -> + describe "sends property change events", -> + expectStreamEvents( + -> + s = new Bacon.Bus() + p = s.toProperty("a").changes() + soon -> + s.push "b" + s.error() + s.end() + p + ["b", error()]) + describe "works with synchronous source", -> + expectStreamEvents( + -> Bacon.fromArray([1, 2, 3]).toProperty(0).changes() + [1, 2, 3]) + +describe "Property.combine", -> + describe "combines latest values of two properties, with given combinator function, passing through errors", -> + expectPropertyEvents( + -> + left = series(2, [1, error(), 2, 3]).toProperty() + right = series(2, [4, error(), 5, 6]).delay(t(1)).toProperty() + left.combine(right, add) + [5, error(), error(), 6, 7, 8, 9]) + describe "also accepts a field name instead of combinator function", -> + expectPropertyEvents( + -> + left = series(1, [[1]]).toProperty() + right = series(2, [[2]]).toProperty() + left.combine(right, ".concat") + [[1, 2]]) + + describe "combines with null values", -> + expectPropertyEvents( + -> + left = series(1, [null]).toProperty() + right = series(1, [null]).toProperty() + left.combine(right, (l, r)-> [l, r]) + [[null, null]]) + + it "unsubscribes when initial value callback returns Bacon.noMore", -> + calls = 0 + bus = new Bacon.Bus() + other = Bacon.constant(["rolfcopter"]) + bus.toProperty(["lollerskates"]).combine(other, ".concat").subscribe (e) -> + if !e.isInitial() + calls += 1 + Bacon.noMore + + bus.push(["fail whale"]) + + describe "does not duplicate same error from two streams", -> + expectPropertyEvents( + -> + src = series(1, ["same", error()]) + Bacon.combineAsArray(src, src) + [["same", "same"], error()]) + it "toString", -> + assert.equal Bacon.constant(1).combine(Bacon.constant(2), (->)).toString(), "Bacon.constant(1).combine(Bacon.constant(2),function)" + describe "with random methods on Array.prototype", -> + it "doesn't throw exceptions", -> + try + Array.prototype.foo = "bar" + events = [] + Bacon.once("a").combine(Bacon.once("b"), (a,b) -> [a,b]).onValue (v) -> + events.push(v) + assert.deepEqual events, [["a", "b"]] + finally + delete Array.prototype.foo + + + +describe "EventStream.combine", -> + describe "converts stream to Property, then combines", -> + expectPropertyEvents( + -> + left = series(2, [1, error(), 2, 3]) + right = series(2, [4, error(), 5, 6]).delay(t(1)).toProperty() + left.combine(right, add) + [5, error(), error(), 6, 7, 8, 9]) + +describe "Bacon.groupSimultaneous", -> + describe "groups simultaneous values in to arrays", -> + expectStreamEvents( + -> + src = series(1, [1,2]) + stream = src.merge(src.map((x) -> x * 2)) + Bacon.groupSimultaneous(stream) + [[[1, 2]], [[2,4]]]) + describe "groups simultaneous values from multiple sources in to arrays", -> + expectStreamEvents( + -> + src = series(1, [1,2]) + stream = src.merge(src.map((x) -> x * 2)) + stream2 = src.map (x) -> x * 4 + Bacon.groupSimultaneous(stream, stream2) + [[[1, 2], [4]], [[2,4], [8]]]) + describe "accepts an array or multiple args", -> + expectStreamEvents( + -> Bacon.groupSimultaneous([Bacon.later(1, 1), Bacon.later(2, 2)]) + [[[1],[]], [[], [2]]]) + describe "returns empty stream for zero sources", -> + expectStreamEvents( + -> Bacon.groupSimultaneous() + []) + expectStreamEvents( + -> Bacon.groupSimultaneous([]) + []) + describe "works with synchronous sources", -> + expectStreamEvents( + -> Bacon.groupSimultaneous(Bacon.fromArray([1,2])) + [[[1]], [[2]]]) + expectStreamEvents( + -> Bacon.groupSimultaneous(Bacon.fromArray([1,2]).mapEnd(3)) + [[[1]], [[2]], [[3]]]) + it "toString", -> + assert.equal Bacon.groupSimultaneous(Bacon.never()).toString(), "Bacon.groupSimultaneous(Bacon.never())" + +describe "Property update is atomic", -> + describe "in a diamond-shaped combine() network", -> + expectPropertyEvents( + -> + a = series(1, [1, 2]).toProperty() + b = a.map (x) -> x + c = a.map (x) -> x + b.combine(c, (x, y) -> x + y) + [2, 4]) + describe "in a triangle-shaped combine() network", -> + expectPropertyEvents( + -> + a = series(1, [1, 2]).toProperty() + b = a.map (x) -> x + a.combine(b, (x, y) -> x + y) + [2, 4]) + describe "when filter is involved", -> + expectPropertyEvents( + -> + a = series(1, [1, 2]).toProperty() + b = a.map((x) -> x).filter(true) + a.combine(b, (x, y) -> x + y) + [2, 4]) + describe "when root property is based on combine*", -> + expectPropertyEvents( + -> + a = series(1, [1, 2]).toProperty().combine(Bacon.constant(0), (x, y) -> x) + b = a.map (x) -> x + c = a.map (x) -> x + b.combine(c, (x, y) -> x + y) + [2, 4]) + describe "when root is not a Property", -> + expectPropertyEvents( + -> + a = series(1, [1, 2]) + b = a.map (x) -> x + c = a.map (x) -> x + b.combine(c, (x, y) -> x + y) + [2, 4]) + it "calls combinator function for valid combos only", -> + calls = 0 + results = [] + combinator = (x,y) -> + calls++ + x+y + src = new Bacon.Bus() + prop = src.toProperty() + out = prop.map((x) -> x) + .combine(prop.map((x) -> x * 2), combinator) + .doAction(->) + .combine(prop, (x,y) -> x) + out.onValue((x) -> results.push(x)) + src.push(1) + src.push(2) + assert.deepEqual results, [3,6] + assert.equal calls, 2 + describe "yet respects subscriber return values (bug fix)", -> + expectStreamEvents( + -> Bacon.repeatedly(t(1), [1, 2, 3]).toProperty().changes().take(1) + [1]) + +describe "When an Event triggers another one in the same stream, while dispatching", -> + it "Delivers triggered events correctly", -> + bus = new Bacon.Bus + values = [] + bus.take(2).onValue (v) -> + bus.push "A" + bus.push "B" + bus.onValue (v) -> + values.push(v) + bus.push "a" + bus.push "b" + assert.deepEqual values, ["a", "A", "A", "B", "B", "b"] + it "EventStream.take(1) works correctly (bug fix)", -> + bus = new Bacon.Bus + values = [] + bus.take(1).onValue (v) -> + bus.push("onValue triggers a side-effect here") + values.push(v) + bus.push("foo") + assert.deepEqual values, ["foo"] + it "complex scenario (fix #470)", -> + values = [] + bus1 = new Bacon.Bus() + bus2 = new Bacon.Bus() + p1 = bus1.toProperty("p1") + p2 = bus2.toProperty(true) + p2.filter(Bacon._.id).changes().onValue -> bus1.push "empty" + Bacon.combineAsArray(p1, p2).onValue (val) -> values.push val + + bus2.push false + bus2.push true + assert.deepEqual values, [["p1", true],["p1", false],["p1", true],["empty", true]] + +describe "observables created while dispatching", -> + verifyWhileDispatching = (name, f, expected) -> + it name + " (independent)", -> + values = [] + Bacon.once(1).onValue -> + f().onValue (value) -> + values.push(value) + assert.deepEqual values, expected + assert.deepEqual values, expected + + it name + " (dependent)", -> + values = [] + src = Bacon.combineAsArray(Bacon.once(1).toProperty(), Bacon.constant(2)) + src.onValue -> + src.flatMap(f()).onValue (value) -> + values.push(value) + assert.deepEqual values, expected + assert.deepEqual values, expected + + verifyWhileDispatching "with combineAsArray", + (-> Bacon.combineAsArray([Bacon.constant(1)])), + [[1]] + verifyWhileDispatching "with combineAsArray.startWith", + (-> + a = Bacon.constant("lolbal") + Bacon.combineAsArray([a, a]).map("right").startWith("wrong")), + ["right"] + verifyWhileDispatching "with stream.startWith", + (-> Bacon.later(1).startWith(0)), + [0] + verifyWhileDispatching "with combineAsArray.changes.startWith", + (-> + a = Bacon.constant("lolbal") + Bacon.combineAsArray([a, a]).changes().startWith("right")), + ["right"] + verifyWhileDispatching "with flatMap", (-> + a = Bacon.constant("lolbal") + a.flatMap((x) -> Bacon.once(x))), ["lolbal"] + verifyWhileDispatching "with awaiting", (-> + a = Bacon.constant(1) + s = a.awaiting(a.map(->))), [true] + verifyWhileDispatching "with concat", (-> + s = Bacon.once(1).concat(Bacon.once(2))), [1,2] + verifyWhileDispatching "with Property.delay", (-> + c = Bacon.constant(1) + Bacon.combineAsArray([c, c]).delay(1).map(".0")), [1] + +describe "when subscribing while dispatching", -> + describe "single subscriber", -> + describe "up-to-date values are used (skipped bounce)", -> + expectStreamEvents( + -> + src = series(1, [1,2]) + trigger = src.map((x) -> x) + trigger.onValue -> + value = src.toProperty() + value.onValue -> + trigger.flatMap -> + value.take(1) + [1,2]) + describe "delayed bounce", -> + expectStreamEvents( + -> + src = series(1, [1,2]) + trigger = src.map((x) -> x) + trigger.onValue -> + value = src.filter((x) -> x == 1).toProperty(0) + value.onValue -> + trigger.flatMap -> + value.take(1) + [0, 1]) + describe "multiple subscribers", -> + describe "up-to-date values are used (skipped bounce)", -> + expectStreamEvents( + -> + src = series(1, [1,2]) + trigger = src.map((x) -> x) + trigger.onValue -> + value = src.toProperty() + value.onValue -> + trigger.flatMap -> + value.onValue(->) + value.take(1) + [1,2]) + describe "delayed bounce", -> + expectStreamEvents( + -> + src = series(1, [1,2]) + trigger = src.map((x) -> x) + trigger.onValue -> + value = src.filter((x) -> x == 1).toProperty(0) + value.onValue -> + trigger.flatMap -> + value.onValue(->) + value.take(1) + [0, 1]) + describe "delayed bounce in case Property ended (bug fix)", -> + expectStreamEvents( + -> + bus = new Bacon.Bus() + root = Bacon.once(0).toProperty() + root.onValue -> + Bacon.later(1).onValue -> + root.map(-> 1).subscribe (event) -> + if event.isEnd() + bus.end() + else + bus.push(event.value()) + bus + [1]) + describe "poking for errors 2", -> + expectStreamEvents( + -> + bus = new Bacon.Bus() + root = Bacon.sequentially(1, [1,2]).toProperty() + root.subscribe (event) -> + outdatedChild = root.filter((x) -> x == 1).map((x) -> x) + outdatedChild.onValue(->) # sets value but will be outdated at value 2 + + Bacon.later(3).onValue -> + outdatedChild.subscribe (event) -> + if event.isEnd() + bus.end() + else + bus.push(event.value()) + bus + [1] + ) + +describe "Bacon.combineAsArray", -> + describe "initial value", -> + event = null + before -> + prop = Bacon.constant(1) + Bacon.combineAsArray(prop).subscribe (x) -> + event = x if x.hasValue() + it "is output as Initial event", -> + assert.equal event.isInitial(), true + describe "combines properties and latest values of streams, into a Property having arrays as values", -> + expectPropertyEvents( + -> + stream = series(1, ["a", "b"]) + Bacon.combineAsArray([Bacon.constant(1), Bacon.constant(2), stream]) + [[1, 2, "a"], [1, 2, "b"]]) + describe "Works with streams provided as a list of arguments as well as with a single array arg", -> + expectPropertyEvents( + -> + stream = series(1, ["a", "b"]) + Bacon.combineAsArray(Bacon.constant(1), Bacon.constant(2), stream) + [[1, 2, "a"], [1, 2, "b"]]) + describe "works with single property", -> + expectPropertyEvents( + -> + Bacon.combineAsArray([Bacon.constant(1)]) + [[1]]) + describe "works with single stream", -> + expectPropertyEvents( + -> + Bacon.combineAsArray([Bacon.once(1)]) + [[1]]) + describe "works with arrays as values, with first array being empty (bug fix)", -> + expectPropertyEvents( + -> + Bacon.combineAsArray([Bacon.constant([]), Bacon.constant([1])]) + ([[[], [1]]])) + describe "works with arrays as values, with first array being non-empty (bug fix)", -> + expectPropertyEvents( + -> + Bacon.combineAsArray([Bacon.constant([1]), Bacon.constant([2])]) + ([[[1], [2]]])) + describe "works with empty array", -> + expectPropertyEvents( + -> Bacon.combineAsArray([]) + [[]]) + describe "works with empty args list", -> + expectPropertyEvents( + -> Bacon.combineAsArray() + [[]]) + describe "accepts constant values instead of Observables", -> + expectPropertyEvents( + -> Bacon.combineAsArray(Bacon.constant(1), 2, 3) + [[1,2,3]]) + describe "works with synchronous sources and flatMap (#407)", -> + expectStreamEvents( + -> + Bacon + .once(123) + .flatMap -> + Bacon.combineAsArray(Bacon.once(1), Bacon.once(2), 3) + [[1,2,3]]) + it "preserves laziness", -> + calls = 0 + id = (x) -> + calls++ + x + Bacon.combineAsArray(Bacon.fromArray([1,2,3,4,5]).map(id)).skip(4).onValue() + assert.equal calls, 1 + it "toString", -> + assert.equal Bacon.combineAsArray(Bacon.never()).toString(), "Bacon.combineAsArray(Bacon.never())" + +describe "Bacon.combineWith", -> + describe "combines n properties, streams and constants using an n-ary function", -> + expectPropertyEvents( + -> + stream = series(1, [1, 2]) + f = (x, y, z) -> x + y + z + Bacon.combineWith(f, stream, Bacon.constant(10), 100) + [111, 112]) + describe "works with single input", -> + expectPropertyEvents( + -> + stream = series(1, [1, 2]) + f = (x) -> x * 2 + Bacon.combineWith(f, stream) + [2, 4]) + describe "works with 0 inputs (results to a constant)", -> + expectPropertyEvents( + -> + Bacon.combineWith(-> 1) + [1]) + it "toString", -> + assert.equal Bacon.combineWith((->), Bacon.never()).toString(), "Bacon.combineWith(function,Bacon.never())" + +describe "Boolean logic", -> + describe "combines Properties with and()", -> + expectPropertyEvents( + -> Bacon.constant(true).and(Bacon.constant(false)) + [false]) + describe "combines Properties with or()", -> + expectPropertyEvents( + -> Bacon.constant(true).or(Bacon.constant(false)) + [true]) + describe "inverts property with not()", -> + expectPropertyEvents( + -> Bacon.constant(true).not() + [false]) + describe "accepts constants instead of properties", -> + describe "true and false", -> + expectPropertyEvents( + -> Bacon.constant(true).and(false) + [false]) + describe "true and true", -> + expectPropertyEvents( + -> Bacon.constant(true).and(true) + [true]) + describe "true or false", -> + expectPropertyEvents( + -> Bacon.constant(true).or(false) + [true]) + it "toString", -> + assert.equal Bacon.constant(1).and(Bacon.constant(2).not()).or(Bacon.constant(3)).toString(), "Bacon.constant(1).and(Bacon.constant(2).not()).or(Bacon.constant(3))" + +describe "Bacon.mergeAll", -> + describe ("merges all given streams"), -> + expectStreamEvents( + -> + Bacon.mergeAll([ + series(3, [1, 2]) + series(3, [3, 4]).delay(t(1)) + series(3, [5, 6]).delay(t(2))]) + [1, 3, 5, 2, 4, 6], unstable) + describe ("supports n-ary syntax"), -> + expectStreamEvents( + -> + Bacon.mergeAll( + series(3, [1, 2]) + series(3, [3, 4]).delay(t(1)) + series(3, [5, 6]).delay(t(2))) + [1, 3, 5, 2, 4, 6], unstable) + describe "works with a single stream", -> + expectStreamEvents( + -> Bacon.mergeAll([Bacon.once(1)]) + [1]) + expectStreamEvents( + -> Bacon.mergeAll(Bacon.once(1)) + [1]) + describe "returns empty stream for zero input", -> + expectStreamEvents( + -> Bacon.mergeAll([]) + []) + expectStreamEvents( + -> Bacon.mergeAll() + []) + it "toString", -> + assert.equal Bacon.mergeAll(Bacon.never()).toString(), "Bacon.mergeAll(Bacon.never())" + +describe "Property.sampledBy(stream)", -> + describe "samples property at events, resulting to EventStream", -> + expectStreamEvents( + -> + prop = series(2, [1, 2]).toProperty() + stream = repeat(3, ["troll"]).take(4) + prop.sampledBy(stream) + [1, 2, 2, 2]) + describe "includes errors from both Property and EventStream", -> + expectStreamEvents( + -> + prop = series(2, [error(), 2]).toProperty() + stream = series(3, [error(), "troll"]) + prop.sampledBy(stream) + [error(), error(), 2]) + describe "ends when sampling stream ends", -> + expectStreamEvents( + -> + prop = repeat(2, [1, 2]).toProperty() + stream = repeat(2, [""]).delay(t(1)).take(4) + prop.sampledBy(stream) + [1, 2, 1, 2]) + describe "accepts optional combinator function f(Vp, Vs)", -> + expectStreamEvents( + -> + prop = series(2, ["a", "b"]).toProperty() + stream = series(2, ["1", "2", "1", "2"]).delay(t(1)) + prop.sampledBy(stream, add) + ["a1", "b2", "b1", "b2"]) + describe "allows method name instead of function too", -> + expectStreamEvents( + -> + Bacon.constant([1]).sampledBy(Bacon.once([2]), ".concat") + [[1, 2]]) + describe "works with same origin", -> + expectStreamEvents( + -> + src = series(2, [1, 2]) + src.toProperty().sampledBy(src) + [1, 2]) + expectStreamEvents( + -> + src = series(2, [1, 2]) + src.toProperty().sampledBy(src.map(times, 2)) + [1, 2]) + describe "uses updated property after combine", -> + latter = (a, b) -> b + expectPropertyEvents( + -> + src = series(2, ["b", "c"]).toProperty("a") + combined = Bacon.constant().combine(src, latter) + src.sampledBy(combined, add) + ["aa", "bb", "cc"]) + describe "uses updated property after combine with subscriber", -> + latter = (a, b) -> b + expectPropertyEvents( + -> + src = series(2, ["b", "c"]).toProperty("a") + combined = Bacon.constant().combine(src, latter) + combined.onValue(->) + src.sampledBy(combined, add) + ["aa", "bb", "cc"]) + describe "skips samplings that occur before the property gets its first value", -> + expectStreamEvents( + -> + p = series(5, [1]).toProperty() + p.sampledBy(series(3, [0])) + []) + expectStreamEvents( + -> + p = series(5, [1, 2]).toProperty() + p.sampledBy(series(3, [0, 0, 0, 0])) + [1, 1, 2], unstable) + expectPropertyEvents( + -> + p = series(5, [1, 2]).toProperty() + p.sampledBy(series(3, [0, 0, 0, 0]).toProperty()) + [1, 1, 2], unstable) + describe "works with stream of functions", -> + f = -> + expectStreamEvents( + -> + p = series(1, [f]).toProperty() + p.sampledBy(series(1, [1, 2, 3])) + [f, f, f]) + describe "works with synchronous sampler stream", -> + expectStreamEvents( + -> Bacon.constant(1).sampledBy(Bacon.fromArray([1,2,3])) + [1,1,1], unstable) + expectStreamEvents( + -> Bacon.later(1, 1).toProperty().sampledBy(Bacon.fromArray([1,2,3])) + []) + describe "laziness", -> + calls = 0 + before (done) -> + id = (x) -> + calls++ + x + sampler = Bacon.later(5).map(id) + property = repeat(1, [1]).toProperty().map(id) + sampled = property.sampledBy sampler + sampled.onValue() + sampled.onEnd(done) + it "preserves laziness", -> + assert.equal calls, 1 + it "toString", -> + assert.equal Bacon.constant(0).sampledBy(Bacon.never()).toString(), "Bacon.constant(0).sampledBy(Bacon.never(),function)" + describe "With circular Bus setup", -> + it "Just works (bug fix)", -> + values = [] + clicks = new Bacon.Bus() + toggleBus = new Bacon.Bus() + + shown = toggleBus.toProperty(false) + shown.changes().onValue (show) => values.push show + toggleClicks = shown.sampledBy(clicks).map (shown) => not shown + + toggleBus.plug(toggleClicks) + + clicks.push(true) + assert.deepEqual values, [true] + +describe "Property.sampledBy(property)", -> + describe "samples property at events, resulting to a Property", -> + expectPropertyEvents( + -> + prop = series(2, [1, 2]).toProperty() + sampler = repeat(3, ["troll"]).take(4).toProperty() + prop.sampledBy(sampler) + [1, 2, 2, 2]) + describe "works on an event stream by automatically converting to property", -> + expectPropertyEvents( + -> + stream = series(2, [1, 2]) + sampler = repeat(3, ["troll"]).take(4).toProperty() + stream.sampledBy(sampler) + [1, 2, 2, 2]) + describe "accepts optional combinator function f(Vp, Vs)", -> + expectPropertyEvents( + -> + prop = series(2, ["a", "b"]).toProperty() + sampler = series(2, ["1", "2", "1", "2"]).delay(t(1)).toProperty() + prop.sampledBy(sampler, add) + ["a1", "b2", "b1", "b2"]) + +describe "Property.sample", -> + describe "samples property by given interval", -> + expectStreamEvents( + -> + prop = series(2, [1, 2]).toProperty() + prop.sample(t(3)).take(4) + [1, 2, 2, 2]) + describe "includes all errors", -> + expectStreamEvents( + -> + prop = series(2, [1, error(), 2]).toProperty() + prop.sample(t(5)).take(2) + [error(), 1, 2], unstable) + describe "works with synchronous source", -> + expectStreamEvents( + -> + prop = Bacon.constant(1) + prop.sample(t(3)).take(4) + [1, 1, 1, 1]) + it "toString", -> + assert.equal Bacon.constant(0).sample(1).toString(), "Bacon.constant(0).sample(1)" + +describe "EventStream.errors", -> + describe "Includes errors only", -> + expectStreamEvents( + -> series(1, [1, error(), 2]).errors() + [error()]) + it "toString", -> + assert.equal Bacon.never().errors().toString(), "Bacon.never().errors()" + +describe "EventStream.scan", -> + describe "accumulates values with given seed and accumulator function, passing through errors", -> + expectPropertyEvents( + -> series(1, [1, 2, error(), 3]).scan(0, add) + [0, 1, 3, error(), 6]) + describe "also works with method name", -> + expectPropertyEvents( + -> series(1, [[1], [2]]).scan([], ".concat") + [[], [1], [1, 2]]) + it "yields the seed value immediately", -> + outputs = [] + bus = new Bacon.Bus() + bus.scan(0, -> 1).onValue((value) -> outputs.push(value)) + assert.deepEqual outputs, [0] + describe "yields null seed value", -> + expectPropertyEvents( + -> series(1, [1]).scan(null, ->1) + [null, 1]) + describe "works with synchronous streams", -> + expectPropertyEvents( + -> Bacon.fromArray([1,2,3]).scan(0, ((x,y)->x+y)) + [0,1,3,6], unstable) + describe "calls accumulator function once per value", -> + describe "(simple case)", -> + count = 0 + expectPropertyEvents( + -> series(2, [1,2,3]).scan(0, (x,y) -> count++; x + y) + [0, 1, 3, 6] + { extraCheck: -> it "calls accumulator once per value", -> assert.equal count, 3} + ) + it "(when pushing to Bus in accumulator)", -> + count = 0 + someBus = new Bacon.Bus() + someBus.onValue -> + src = new Bacon.Bus() + result = src.scan 0, -> + someBus.push() + count++ + result.onValue -> + result.onValue -> + src.push() + assert.equal count, 1 + + +describe "EventStream.fold", -> + describe "folds stream into a single-valued Property, passes through errors", -> + expectPropertyEvents( + -> series(1, [1, 2, error(), 3]).fold(0, add) + [error(), 6]) + describe "has reduce as synonym", -> + expectPropertyEvents( + -> series(1, [1, 2, error(), 3]).fold(0, add) + [error(), 6]) + describe "works with synchronous source", -> + expectPropertyEvents( + -> Bacon.fromArray([1, 2, error(), 3]).fold(0, add) + [error(), 6]) + describe.skip "works with really large chunks too, with { eager: true }", -> + count = 50000 + expectPropertyEvents( + -> series(1, [1..count]).fold(0, ((x,y) -> x+1), { eager: true }) + [count]) + +describe "Property.scan", -> + describe "with Init value, starts with f(seed, init)", -> + expectPropertyEvents( + -> series(1, [2,3]).toProperty(1).scan(0, add) + [1, 3, 6]) + describe "without Init value, starts with seed", -> + expectPropertyEvents( + -> series(1, [2,3]).toProperty().scan(0, add) + [0, 2, 5]) + describe "treats null seed value like any other value", -> + expectPropertyEvents( + -> series(1, [1]).toProperty().scan(null, add) + [null, 1]) + expectPropertyEvents( + -> series(1, [2]).toProperty(1).scan(null, add) + [1, 3]) + describe "for synchronous source", -> + describe "with Init value, starts with f(seed, init)", -> + expectPropertyEvents( + -> Bacon.fromArray([2,3]).toProperty(1).scan(0, add) + [1, 3, 6], unstable) + describe "without Init value, starts with seed", -> + expectPropertyEvents( + -> Bacon.fromArray([2,3]).toProperty().scan(0, add) + [0, 2, 5], unstable) + describe "works with synchronously responding empty source", -> + expectPropertyEvents( + -> Bacon.never().toProperty(1).scan(0, add) + [1]) + +describe "EventStream.withStateMachine", -> + f = (sum, event) -> + if event.hasValue() + [sum + event.value(), []] + else if event.isEnd() + [sum, [new Bacon.Next(-> sum), event]] + else + [sum, [event]] + describe "runs state machine on the stream", -> + expectStreamEvents( + -> Bacon.fromArray([1,2,3]).withStateMachine(0, f) + [6]) + +describe "Property.withStateMachine", -> + describe "runs state machine on the stream", -> + expectPropertyEvents( + -> Bacon.fromArray([1,2,3]).toProperty().withStateMachine(0, (sum, event) -> + if event.hasValue() + [sum + event.value(), []] + else if event.isEnd() + [sum, [new Bacon.Next(-> sum), event]] + else + [sum, [event]]) + [6]) + +describe "Property.fold", -> + describe "Folds Property into a single-valued one", -> + expectPropertyEvents( + -> series(1, [2,3]).toProperty(1).fold(0, add) + [6]) + +describe "EventStream.diff", -> + describe "apply diff function to previous and current values, passing through errors", -> + expectPropertyEvents( + -> series(1, [1, 2, error(), 3]).diff(0, add) + [1, 3, error(), 5]) + describe "also works with method name", -> + expectPropertyEvents( + -> series(1, [[1], [2]]).diff([0], ".concat") + [[0, 1], [1, 2]]) + it "does not yields the start value immediately", -> + outputs = [] + bus = new Bacon.Bus() + bus.diff(0, -> 1).onValue((value) -> outputs.push(value)) + assert.deepEqual outputs, [] + it "toString", -> + assert.equal Bacon.once(1).diff(0, (->)).toString(), "Bacon.once(1).diff(0,function)" + +describe "Property.diff", -> + describe "with Init value, starts with f(start, init)", -> + expectPropertyEvents( + -> series(1, [2,3]).toProperty(1).diff(0, add) + [1, 3, 5]) + describe "without Init value, waits for the first value", -> + expectPropertyEvents( + -> series(1, [2,3]).toProperty().diff(0, add) + [2, 5]) + describe "treats null start value like any other value", -> + expectPropertyEvents( + -> series(1, [1]).toProperty().diff(null, add) + [1]) + expectPropertyEvents( + -> series(1, [2]).toProperty(1).diff(null, add) + [1, 3]) + +describe "EventStream.zip", -> + describe "pairwise combines values from two streams", -> + expectStreamEvents( + -> series(1, [1, 2, 3]).zip(series(1, ['a', 'b', 'c'])) + [[1, 'a'], [2, 'b'], [3, 'c']]) + describe "passes through errors", -> + expectStreamEvents( + -> series(2, [1, error(), 2]).zip(series(2, ['a', 'b']).delay(1)) + [[1, 'a'], error(), [2, 'b']]) + describe "completes as soon as possible", -> + expectStreamEvents( + -> series(1, [1]).zip(series(1, ['a', 'b', 'c'])) + [[1, 'a']]) + describe "can zip an observable with itself", -> + expectStreamEvents( + -> + obs = series(1, ['a', 'b', 'c']) + obs.zip(obs.skip(1)) + [['a', 'b'], ['b', 'c']]) + it "toString", -> + assert.equal Bacon.never().zip(Bacon.once(1)).toString(), "Bacon.never().zip(Bacon.once(1))" + +describe "Property.zip", -> + describe "pairwise combines values from two properties", -> + expectStreamEvents( + -> series(1, [1, 2, 3]).toProperty().zip(series(1, ['a', 'b', 'c']).toProperty()) + [[1, 'a'], [2, 'b'], [3, 'c']], { unstable }) + +describe "Bacon.zipAsArray", -> + describe "zips an array of streams into a stream of arrays", -> + expectStreamEvents( + -> + obs = series(1, [1, 2, 3, 4]) + Bacon.zipAsArray([obs, obs.skip(1), obs.skip(2)]) + [[1 , 2 , 3], [2 , 3 , 4]]) + describe "supports n-ary syntax", -> + expectStreamEvents( + -> + obs = series(1, [1, 2, 3, 4]) + Bacon.zipAsArray(obs, obs.skip(1)) + [[1 , 2], [2 , 3], [3, 4]]) + describe "accepts Properties as well as EventStreams", -> + expectStreamEvents( + -> + obs = series(1, [1, 2, 3, 4]) + Bacon.zipAsArray(obs, obs.skip(1), Bacon.constant(5)) + [[1 , 2, 5]]) + describe "works with single stream", -> + expectStreamEvents( + -> + obs = series(1, [1, 2]) + Bacon.zipAsArray([obs]) + [[1], [2]]) + expectStreamEvents( + -> + obs = series(1, [1, 2]) + Bacon.zipAsArray(obs) + [[1], [2]]) + describe "works with 0 streams (=Bacon.never())", -> + expectStreamEvents( + -> Bacon.zipAsArray([]) + []) + expectStreamEvents( + -> Bacon.zipAsArray() + []) + it "toString", -> + assert.equal Bacon.zipAsArray(Bacon.never(), Bacon.never()).toString(), "Bacon.zipAsArray(Bacon.never(),Bacon.never())" + +describe "Bacon.zipWith", -> + describe "zips an array of streams with given function", -> + expectStreamEvents( + -> + obs = series(1, [1, 2, 3, 4]) + Bacon.zipWith([obs, obs.skip(1), obs.skip(2)], ((x,y,z) -> (x + y + z))) + [1 + 2 + 3, 2 + 3 + 4]) + describe "supports n-ary syntax", -> + expectStreamEvents( + -> + obs = series(1, [1, 2, 3, 4]) + f = ((x,y,z) -> (x + y + z)) + Bacon.zipWith(f, obs, obs.skip(1), obs.skip(2)) + [1 + 2 + 3, 2 + 3 + 4]) + describe "works with single stream", -> + expectStreamEvents( + -> + obs = series(1, [1,2]) + f = (x) -> x * 2 + Bacon.zipWith(f, obs) + [1 * 2, 2 * 2]) + describe "works with 0 streams (=Bacon.never())", -> + expectStreamEvents( + -> + Bacon.zipWith([], ->) + []) + expectStreamEvents( + -> + Bacon.zipWith(->) + []) + it "toString", -> + assert.equal Bacon.zipWith((->), Bacon.never()).toString(), "Bacon.zipWith(function,Bacon.never())" + +describe "Bacon.when", -> + describe "synchronizes on join patterns", -> + expectStreamEvents( + -> + [a,b,_] = ['a','b','_'] + as = series(1, [a, _, a, a, _, a, _, _, a, a]).filter((x) -> x == a) + bs = series(1, [_, b, _, _, b, _, b, b, _, _]).filter((x) -> x == b) + Bacon.when( + [as, bs], (a,b) -> a + b, + [as], (a) -> a) + ['a', 'ab', 'a', 'ab', 'ab', 'ab'], unstable) + describe "consider the join patterns from top to bottom", -> + expectStreamEvents( + -> + [a,b,_] = ['a','b','_'] + as = series(1, [a, _, a, a, _, a, _, _, a, a]).filter((x) -> x == a) + bs = series(1, [_, b, _, _, b, _, b, b, _, _]).filter((x) -> x == b) + Bacon.when( + [as], (a) -> a, + [as, bs], (a,b) -> a + b) + ['a', 'a', 'a', 'a', 'a', 'a']) + describe "handles any number of join patterns", -> + expectStreamEvents( + -> + [a,b,c,_] = ['a','b','c','_'] + as = series(1, [a, _, a, _, a, _, a, _, _, _, a, a]).filter((x) -> x == a) + bs = series(1, [_, b, _, _, _, b, _, b, _, b, _, _]).filter((x) -> x == b) + cs = series(1, [_, _, _, c, _, _, _, _, c, _, _, _]).filter((x) -> x == c) + Bacon.when( + [as, bs, cs], (a,b,c) -> a + b + c, + [as, bs], (a,b) -> a + b, + [as], (a) -> a) + ['a', 'ab', 'a', 'abc', 'abc', 'ab'], unstable) + describe "does'nt synchronize on properties", -> + expectStreamEvents( + -> + p = repeat(1, ["p"]).take(100).toProperty() + s = series(3, ["1", "2", "3"]) + Bacon.when( + [p,s], (p, s) -> p + s) + ["p1", "p2", "p3"]) + expectStreamEvents( + -> + p = series(3, ["p"]).toProperty() + s = series(1, ["1"]) + Bacon.when( + [p,s], (p, s) -> p + s) + []) + expectStreamEvents( + -> + [a,b,c,_] = ['a','b','c','_'] + as = series(1, [a, _, a, _, a, _, a, _, _, _, a, _, a]).filter((x) -> x == a) + bs = series(1, [_, b, _, _, _, b, _, b, _, b, _, _, _]).filter((x) -> x == b) + cs = series(1, [_, _, _, c, _, _, _, _, c, _, _, c, _]).filter((x) -> x == c).map(1).scan 0, ((x,y) -> x + y) + Bacon.when( + [as, bs, cs], (a,b,c) -> a + b + c, + [as], (a) -> a) + ['a', 'ab0', 'a', 'ab1', 'ab2', 'ab3'], unstable) + it "Rejects patterns with Properties only", -> expectError("At least one EventStream required", -> + Bacon.when([Bacon.constant()], ->)) + describe "doesn't output before properties have values", -> + expectStreamEvents( + -> + p = series(2, ["p"]) + s = series(1, ["s"]) + Bacon.when( + [s, p], (s, p) -> s + p) + ["sp"]) + describe "returns Bacon.never() on the empty list of patterns", -> + expectStreamEvents( + -> + Bacon.when() + []) + describe "returns Bacon.never() when all patterns are zero-length", -> + expectStreamEvents( + -> + Bacon.when([], ->) + []) + describe "works with empty patterns", -> + expectStreamEvents( + -> Bacon.when( + [Bacon.once(1)], (x) -> x, + [], ->) + [1]) + describe "works with empty patterns (2)", -> + expectStreamEvents( + -> Bacon.when( + [], ->, + [Bacon.once(1)], (x) -> x) + [1]) + describe "works with single stream", -> + expectStreamEvents( + -> Bacon.when([Bacon.once(1)], (x) -> x) + [1]) + describe "works with multiples of streams", -> + expectStreamEvents( + -> + [h,o,c,_] = ['h','o','c','_'] + hs = series(1, [h, _, h, _, h, _, h, _, _, _, h, _, h]).filter((x) -> x == h) + os = series(1, [_, o, _, _, _, o, _, o, _, o, _, _, _]).filter((x) -> x == o) + cs = series(1, [_, _, _, c, _, _, _, _, c, _, _, c, _]).filter((x) -> x == c) + Bacon.when( + [hs, hs, os], (h1,h2,o) -> [h1,h2,o], + [cs, os], (c,o) -> [c,o]) + [['h', 'h', 'o'], ['c', 'o'], ['h', 'h', 'o'], ['c', 'o']], unstable) + describe "works with multiples of properties", -> + expectStreamEvents( + -> + c = Bacon.constant("c") + Bacon.when( + [c, c, Bacon.once(1)], (c1, c2, _) -> c1 + c2) + ["cc"]) + describe "accepts constants instead of functions too", -> + expectStreamEvents( + -> Bacon.when(Bacon.once(1), 2) + [2]) + describe "works with synchronous sources", -> + expectStreamEvents( + -> + xs = Bacon.once "x" + ys = Bacon.once "y" + Bacon.when( + [xs, ys], (x, y) -> x + y + ) + ["xy"]) + it "toString", -> + assert.equal Bacon.when([Bacon.never()], (->)).toString(), "Bacon.when([Bacon.never()],function)" + +describe "Bacon.update", -> + describe "works like Bacon.when, but produces a property, and can be defined in terms of a current value", -> + expectPropertyEvents( + -> + [r,i,_] = ['r','i',0] + incr = series(1, [1, _, 1, _, 2, _, 1, _, _, _, 2, _, 1]).filter((x) -> x != _) + reset = series(1, [_, r, _, _, _, r, _, r, _, r, _, _, _]).filter((x) -> x == r) + Bacon.update( + 0, + [reset], 0, + [incr], (i,c) -> i+c) + [0, 1, 0, 1, 3, 0, 1, 0, 0, 2, 3]) + + describe "Correctly handles multiple arguments in parameter list, and synchronous sources", -> + expectPropertyEvents( + -> + one = Bacon.once(1) + two = Bacon.once(2) + Bacon.update( + 0, + [one, two], (i, a, b) -> [i,a,b]) + [0, [0,1,2]], unstable) + it "Rejects patterns with Properties only", -> expectError("At least one EventStream required", -> + Bacon.update(0, [Bacon.constant()], ->)) + it "toString", -> + assert.equal Bacon.update(0, [Bacon.never()], (->)).toString(), "Bacon.update(0,[Bacon.never()],function)" + +describe "combineTemplate", -> + describe "combines streams and properties according to a template object", -> + expectPropertyEvents( + -> + name = Bacon.constant({first:"jack", last:"bauer"}) + stuff = Bacon.later(1, { key: "value" }) + Bacon.combineTemplate({ name, stuff }) + [{ name: { first:"jack", last:"bauer"}, stuff: {key:"value"}}]) + describe "combines properties according to a template object", -> + expectPropertyEvents( + -> + firstName = Bacon.constant("juha") + lastName = Bacon.constant("paananen") + userName = Bacon.constant("mr.bacon") + Bacon.combineTemplate({ userName: userName, password: "*****", fullName: { firstName: firstName, lastName: lastName }}) + [{ userName: "mr.bacon", password: "*****", fullName: { firstName: "juha", lastName: "paananen" } }]) + describe "works with a single-stream template", -> + expectPropertyEvents( + -> + bacon = Bacon.constant("bacon") + Bacon.combineTemplate({ favoriteFood: bacon }) + [{ favoriteFood: "bacon" }]) + describe "works when dynamic part is not the last part (bug fix)", -> + expectPropertyEvents( + -> + username = Bacon.constant("raimohanska") + password = Bacon.constant("easy") + Bacon.combineTemplate({url: "/user/login", + data: { username: username, password: password }, type: "post"}) + [url: "/user/login", data: {username: "raimohanska", password: "easy"}, type: "post"]) + describe "works with arrays as data (bug fix)", -> + expectPropertyEvents( + -> Bacon.combineTemplate( { x : Bacon.constant([]), y : Bacon.constant([[]]), z : Bacon.constant(["z"])}) + [{ x : [], y : [[]], z : ["z"]}]) + describe "supports empty object", -> + expectPropertyEvents( + -> Bacon.combineTemplate({}) + [{}]) + it "supports arrays", -> + value = {key: [{ x: 1 }, { x: 2 }]} + Bacon.combineTemplate(value).onValue (x) -> + assert.deepEqual x, value + assert.deepEqual x.key instanceof Array, true # seems that the former passes even if x is not an array + value = [{ x: 1 }, { x: 2 }] + Bacon.combineTemplate(value).onValue (x) -> + assert.deepEqual x, value + assert.deepEqual x instanceof Array, true + value = {key: [{ x: 1 }, { x: 2 }], key2: {}} + Bacon.combineTemplate(value).onValue (x) -> + assert.deepEqual x, value + assert.deepEqual x.key instanceof Array, true + value = {key: [{ x: 1 }, { x: Bacon.constant(2) }]} + Bacon.combineTemplate(value).onValue (x) -> + assert.deepEqual x, {key: [{ x: 1 }, { x: 2 }]} + assert.deepEqual x.key instanceof Array, true # seems that the former passes even if x is not an array + it "supports nulls", -> + value = {key: null} + Bacon.combineTemplate(value).onValue (x) -> + assert.deepEqual x, value + it "supports NaNs", -> + value = {key: NaN} + Bacon.combineTemplate(value).onValue (x) -> + assert.deepEqual isNaN(x.key), true + it "supports dates", -> + value = {key: new Date()} + Bacon.combineTemplate(value).onValue (x) -> + assert.deepEqual x, value + it "supports regexps", -> + value = {key: /[0-0]/i} + Bacon.combineTemplate(value).onValue (x) -> + assert.deepEqual x, value + it "supports functions", -> + value = {key: ->} + Bacon.combineTemplate(value).onValue (x) -> + assert.deepEqual x, value + it "toString", -> + assert.equal Bacon.combineTemplate({ thing: Bacon.never(), const: "a" }).toString(), "Bacon.combineTemplate({thing:Bacon.never(),const:a})" + +describe "Bacon.retry", -> + describe "does not retry after value", -> + expectStreamEvents( + -> + calls = 0 + source = -> + calls += 1 + Bacon.once({calls}) + Bacon.retry({source, retries: 2}) + [calls: 1]) + describe "retries to run the source stream given number of times until it yields a value", -> + expectStreamEvents( + -> + calls = 0 + source = -> + calls += 1 + if calls < 3 + Bacon.once(new Bacon.Error()) + else + Bacon.once({calls}) + Bacon.retry({source, retries: 5}) + [calls: 3]) + describe "does not change source stream characteristics", -> + expectStreamEvents( + -> Bacon.retry(source: -> Bacon.fromArray([3, 1, 2, 1, 3]).skipDuplicates().take(2)) + [3, 1]) + describe "retries after retryable error", -> + expectStreamEvents( + -> + calls = 0 + source = -> + calls += 1 + Bacon.once(new Bacon.Error({calls})) + isRetryable = ({calls}) -> + calls < 2 + Bacon.retry({source, isRetryable, retries: 5}) + [error(calls: 2)]) # TODO: assert error content + describe "yields error when no retries left", -> + expectStreamEvents( + -> + calls = 0 + source = -> + calls += 1 + Bacon.once(new Bacon.Error({calls})) + Bacon.retry {source, retries: 2} + [error(calls: 3)]) # TODO: assert error content + it "allows specifying delay by context for each retry", (done) -> + calls = 0 + contexts = [] + source = -> + calls += 1 + Bacon.once(new Bacon.Error({calls})) + delay = (context) -> + contexts.push(context) + 1 + Bacon.retry({source, delay, retries: 2}).onError (err) -> + + + done() + it "calls source function after delay", (done) -> + calls = 0 + source = -> + calls += 1 + Bacon.once(new Bacon.Error()) + interval = -> 100 + Bacon.retry({source, interval, retries: 1}).onValue -> # noop + + done() + it "throws exception if 'source' option is not a function", -> + + it "toString", -> + + +describe "Property.decode", -> + describe "switches between source Properties based on property value", -> + expectPropertyEvents( + -> + a = Bacon.constant("a") + b = Bacon.constant("b") + c = Bacon.constant("c") + series(1, [1,2,3]).toProperty().decode({1: a, 2: b, 3: c}) + ["a", "b", "c"]) + it "toString", -> + assert.equal Bacon.constant(1).decode({1: "lol"}).toString(), "Bacon.constant(1).decode({1:lol})" + +describe "EventStream.decode", -> + describe "switches between source Properties based on property value", -> + expectPropertyEvents( + -> + a = Bacon.constant("a") + b = Bacon.constant("b") + c = Bacon.constant("c") + series(1, [1,2,3]).decode({1: a, 2: b, 3: c}) + ["a", "b", "c"]) + +describe "Observable.onValues", -> + it "splits value array to callback arguments", -> + f = mockFunction() + Bacon.constant([1,2,3]).onValues(f) + f.verify(1,2,3) + +describe "Bacon.onValues", -> + it "is a shorthand for combineAsArray.onValues", -> + f = mockFunction() + Bacon.onValues(1, 2, 3, f) + f.verify(1,2,3) + +describe "Observable.subscribe and onValue", -> + it "returns a dispose() for unsubscribing", -> + s = new Bacon.Bus() + values = [] + dispose = s.onValue (value) -> values.push value + s.push "lol" + dispose() + s.push "wut" + assert.deepEqual values, ["lol"] + +describe "Observable.onEnd", -> + it "is called on stream end", -> + s = new Bacon.Bus() + ended = false + s.onEnd(-> ended = true) + s.push("LOL") + assert.deepEqual ended, false + s.end() + assert.deepEqual ended, true + +describe "Field value extraction", -> + describe "extracts field value", -> + expectStreamEvents( + -> Bacon.once({lol:"wut"}).map(".lol") + ["wut"]) + describe "extracts nested field value", -> + expectStreamEvents( + -> Bacon.once({lol:{wut: "wat"}}).map(".lol.wut") + ["wat"]) + describe "yields 'undefined' if any value on the path is 'undefined'", -> + expectStreamEvents( + -> Bacon.once({}).map(".lol.wut") + [undefined]) + it "if field value is method, it does a method call", -> + context = null + result = null + object = { + method: -> + context = this + "result" + } + Bacon.once(object).map(".method").onValue((x) -> result = x) + assert.deepEqual result, "result" + assert.deepEqual context, object + +testSideEffects = (wrapper, method) -> + -> + it "(f) calls function with property value", -> + f = mockFunction() + wrapper("kaboom")[method](f) + f.verify("kaboom") + it "(f, param) calls function, partially applied with param", -> + f = mockFunction() + wrapper("kaboom")[method](f, "pow") + f.verify("pow", "kaboom") + it "('.method') calls event value object method", -> + value = mock("get") + value.when().get().thenReturn("pow") + wrapper(value)[method](".get") + value.verify().get() + it "('.method', param) calls event value object method with param", -> + value = mock("get") + value.when().get("value").thenReturn("pow") + wrapper(value)[method](".get", "value") + value.verify().get("value") + it "(object, method) calls object method with property value", -> + target = mock("pow") + wrapper("kaboom")[method](target, "pow") + target.verify().pow("kaboom") + it "(object, method, param) partially applies object method with param", -> + target = mock("pow") + wrapper("kaboom")[method](target, "pow", "smack") + target.verify().pow("smack", "kaboom") + it "(object, method, param1, param2) partially applies with 2 args", -> + target = mock("pow") + wrapper("kaboom")[method](target, "pow", "smack", "whack") + target.verify().pow("smack", "whack", "kaboom") + +describe "Property.onValue", testSideEffects(Bacon.constant, "onValue") +describe "Property.assign", testSideEffects(Bacon.constant, "assign") +describe "EventStream.onValue", testSideEffects(Bacon.once, "onValue") + +describe "Property.assign", -> + it "calls given objects given method with property values", -> + target = mock("pow") + Bacon.constant("kaboom").assign(target, "pow") + target.verify().pow("kaboom") + it "allows partial application of method (i.e. adding fixed args)", -> + target = mock("pow") + Bacon.constant("kaboom").assign(target, "pow", "smack") + target.verify().pow("smack", "kaboom") + it "allows partial application of method with 2 args (i.e. adding fixed args)", -> + target = mock("pow") + Bacon.constant("kaboom").assign(target, "pow", "smack", "whack") + target.verify().pow("smack", "whack", "kaboom") + +describe "Bacon.Bus", -> + it "merges plugged-in streams", -> + bus = new Bacon.Bus() + values = [] + dispose = bus.onValue (value) -> values.push value + push = new Bacon.Bus() + bus.plug(push) + push.push("lol") + assert.deepEqual values, ["lol"] + dispose() + verifyCleanup() + describe "works with looped streams", -> + expectStreamEvents( + -> + bus = new Bacon.Bus() + bus.plug(Bacon.later(t(2), "lol")) + bus.plug(bus.filter((value) => "lol" == value).map(=> "wut")) + Bacon.later(t(4)).onValue(=> bus.end()) + bus + ["lol", "wut"], unstable) + it "dispose works with looped streams", -> + bus = new Bacon.Bus() + bus.plug(Bacon.later(t(2), "lol")) + bus.plug(bus.filter((value) => "lol" == value).map(=> "wut")) + dispose = bus.onValue(=>) + dispose() + it "Removes input from input list on End event", -> + subscribed = 0 + bus = new Bacon.Bus() + input = new Bacon.Bus() + # override subscribe to increase the subscribed-count + inputSubscribe = input.dispatcher.subscribe + input.dispatcher.subscribe = (sink) -> + subscribed++ + inputSubscribe.call(input, sink) + bus.plug(input) + dispose = bus.onValue(=>) + input.end() + dispose() + bus.onValue(=>) # this latter subscription should not go to the ended source anymore + assert.deepEqual subscribed, 1 + it "unsubscribes inputs on end() call", -> + bus = new Bacon.Bus() + input = new Bacon.Bus() + events = [] + bus.plug(input) + bus.subscribe((e) => events.push(e)) + input.push("a") + bus.end() + input.push("b") + assert.deepEqual toValues(events), ["a", ""] + it "handles cold single-event streams correctly (bug fix)", -> + values = [] + bus = new Bacon.Bus() + bus.plug(Bacon.once("x")) + bus.plug(Bacon.once("y")) + bus.onValue((x) -> values.push(x)) + assert.deepEqual values, ["x", "y"] + + it "handles end() calls even when there are no subscribers", -> + bus = new Bacon.Bus() + bus.end() + + describe "delivers pushed events and errors", -> + expectStreamEvents( + -> + s = new Bacon.Bus() + s.push "pullMe" + soon -> + s.push "pushMe" + s.error() + s.end() + s + ["pushMe", error()]) + + it "does not deliver pushed events after end() call", -> + called = false + bus = new Bacon.Bus() + bus.onValue(-> called = true) + bus.end() + bus.push("LOL") + assert.deepEqual called, false + + it "does not plug after end() call", -> + plugged = false + bus = new Bacon.Bus() + bus.end() + bus.plug(new Bacon.EventStream((sink) -> plugged = true; (->))) + bus.onValue(->) + assert.deepEqual plugged, false + + it "returns unplug function from plug", -> + values = [] + bus = new Bacon.Bus() + src = new Bacon.Bus() + unplug = bus.plug(src) + bus.onValue((x) -> values.push(x)) + src.push("x") + unplug() + src.push("y") + assert.deepEqual values, ["x"] + + it "allows consumers to re-subscribe after other consumers have unsubscribed (bug fix)", -> + bus = new Bacon.Bus + otherBus = new Bacon.Bus + otherBus.plug(bus) + unsub = otherBus.onValue -> + unsub() + o = [] + otherBus.onValue (v) -> o.push(v) + bus.push("foo") + assert.deepEqual o, ["foo"] + it "toString", -> + assert.equal new Bacon.Bus().toString(), "Bacon.Bus()" + +describe "EventStream", -> + describe "works with functions as values (bug fix)", -> + expectStreamEvents( + -> Bacon.once(-> "hello").map((f) -> f()) + ["hello"]) + expectStreamEvents( + -> Bacon.once(-> "hello").flatMap(Bacon.once).map((f) -> f()) + ["hello"]) + expectPropertyEvents( + -> Bacon.constant(-> "hello").map((f) -> f()) + ["hello"]) + expectStreamEvents( + -> Bacon.constant(-> "hello").flatMap(Bacon.once).map((f) -> f()) + ["hello"]) + it "handles one subscriber added twice just like two separate subscribers (case Bacon.noMore)", -> + values = [] + bus = new Bacon.Bus() + f = (v) -> + if v.hasValue() + values.push(v.value()) + return Bacon.noMore + bus.subscribe(f) + bus.subscribe(f) + bus.push("bacon") + assert.deepEqual values, ["bacon", "bacon"] + it "handles one subscriber added twice just like two separate subscribers (case unsub)", -> + values = [] + bus = new Bacon.Bus() + f = (v) -> + if v.hasValue() + values.push(v.value()) + bus.subscribe(f) + unsub = bus.subscribe(f) + unsub() + bus.push("bacon") + assert.deepEqual values, ["bacon"] + +describe "Bacon.fromBinder", -> + describe "Provides an easier alternative to the EventStream constructor, allowing sending multiple events at a time", -> + expectStreamEvents( + -> + Bacon.fromBinder (sink) -> + sink([new Bacon.Next(1), new Bacon.End()]) + (->) + [1]) + describe "Allows sending unwrapped values as well as events", -> + expectStreamEvents( + -> + Bacon.fromBinder (sink) -> + sink([1, new Bacon.End()]) + (->) + [1]) + describe "Allows sending single value without wrapping array", -> + expectStreamEvents( + -> + Bacon.fromBinder (sink) -> + sink(1) + sink(new Bacon.End()) + (->) + [1]) + it "calls unbinder only once", -> + unbound = 0 + output = undefined + timer = Bacon.fromBinder((sink) -> + output = sink + -> unbound++ + ) + timer.take(1).log() + output "hello" + assert.equal unbound, 1 + it "toString", -> + assert.equal Bacon.fromBinder(->).toString(), "Bacon.fromBinder(function,function)" + +describe "String presentations", -> + describe "Initial(1).toString", -> + it "is 1", -> + assert.equal new Bacon.Initial(1).toString(), "1" + describe "Next({a:1i}).toString", -> + it "is {a:1}", -> + assert.equal new Bacon.Next({a:1}).toString(), "{a:1}" + describe "Error({a:1}).toString", -> + it "is {a:1}", -> + assert.equal new Bacon.Error({a:1}).toString(), " {a:1}" + describe "End.toString", -> + it "is ", -> + assert.equal new Bacon.End().toString(), "" + describe "inspect", -> + it "is the same as toString", -> + assert.equal new Bacon.Initial(1).inspect(), "1" + +describe "Observable.name", -> + it "sets return value of toString and inspect", -> + assert.equal Bacon.once(1).name("one").toString(), "one" + assert.equal Bacon.once(1).name("one").inspect(), "one" + it "modifies the stream in place", -> + obs = Bacon.once(1) + obs.name("one") + assert.equal obs.toString(), "one" + it "supports composition", -> + assert.equal Bacon.once("raimohanska").name("raimo").take(1).inspect(), "raimo.take(1)" + +describe "Observable.withDescription", -> + it "affects toString and inspect", -> + assert.equal Bacon.once(1).withDescription(Bacon, "una", "mas").inspect(), "Bacon.una(mas)" + it "affects desc", -> + description = Bacon.once(1).withDescription(Bacon, "una", "mas").desc + assert.equal description.context, Bacon + assert.equal description.method, "una" + assert.deepEqual description.args, ["mas"] + +describe "Bacon.spy", -> + testSpy = (expectedCount, f) -> + calls = 0 + spy = (obs) -> + obs.toString() + calls++ + Bacon.spy spy + f() + assert.equal calls, expectedCount + describe "calls spy function for all created Observables", -> + it "EventStream", -> + testSpy 1, -> Bacon.once(1) + it "Property", -> + testSpy 1, -> Bacon.constant(1) + it "map", -> + testSpy 2, -> Bacon.once(1).map(->) + it "combineTemplate (also called for the intermediate combineAsArray property)", -> + testSpy 5, -> Bacon.combineTemplate([Bacon.once(1), Bacon.constant(2)]) + +describe "Infinite synchronous sequences", -> + describe "Limiting length with take(n)", -> + expectStreamEvents( + -> endlessly(1,2,3).take(4) + [1,2,3,1], unstable) + expectStreamEvents( + -> endlessly(1,2,3).take(4).concat(Bacon.once(5)) + [1,2,3,1,5], unstable) + expectStreamEvents( + -> endlessly(1,2,3).take(4).concat(endlessly(5, 6).take(2)) + [1,2,3,1,5,6], unstable) + describe "With flatMap", -> + expectStreamEvents( + -> Bacon.fromArray([1,2]).flatMap((x) -> endlessly(x)).take(2) + [1,1], unstable) + expectStreamEvents( + -> endlessly(1,2).flatMap((x) -> endlessly(x)).take(2) + [1,1], unstable) + +endlessly = (values...) -> + index = 0 + Bacon.fromSynchronousGenerator -> new Bacon.Next(-> values[index++ % values.length]) + +Bacon.fromGenerator = (generator) -> + Bacon.fromBinder (sink) -> + unsubd = false + push = (events) -> + events = Bacon._.toArray(events) + for event in events + return if unsubd + reply = sink event + return if event.isEnd() or reply == Bacon.noMore + generator(push) + push [] + -> unsubd = true + +Bacon.fromSynchronousGenerator = (generator) -> + Bacon.fromGenerator (push) -> + push generator() + +lessThan = (limit) -> + (x) -> x < limit +times = (x, y) -> x * y +add = (x, y) -> x + y +id = (x) -> x +activate = (obs) -> + obs.onValue(->) + obs + + diff --git a/ie8/AssertiveMock.coffee b/ie8/AssertiveMock.coffee new file mode 100644 index 000000000..c52b8f1c9 --- /dev/null +++ b/ie8/AssertiveMock.coffee @@ -0,0 +1,54 @@ +assert = require("assertive-chai").assert + +class Mock + constructor: (methodNames...) -> + for name in methodNames + this[name] = mockFunction(name) + this.verify = () => + verifier = {} + for name in methodNames + verifier[name] = this[name].verify + verifier + this.when = () => + returner = {} + assign = (name) => + returner[name] = (args...) => + { + thenReturn: (returnValue) => + this[name].doReturn(returnValue).when(args...) + } + for name in methodNames + assign(name) + returner + +mockFunction = (name) -> + calls = [] + returns = [] + method = (args...) => + calls.push(args) + for returnCombo in returns + #console.log("check #{args} against #{name}(#{returnCombo.args})") + if eq(returnCombo.args, args) + #console.log("match => #{returnCombo.returnValue}") + return returnCombo.returnValue + method.verify = (args...) -> + if !calls + throw "not called: #{name}" + actualCall = calls[0] + calls.splice(0,1) + assert.deepEqual actualCall, args + method.doReturn = (returnValue) -> + { + when: (args...) -> + #console.log("#{name}(#{args}) => #{returnValue}") + returns.push({ args: args, returnValue: returnValue}) + } + method +eq = (xs, ys) -> + return false if (xs.length != ys.length) + for x, i in xs + return false if (x != ys[i]) + true +@mock = (methodNames...) -> new Mock(methodNames...) +@mockFunction = mockFunction + diff --git a/ie8/AssertivePromiseSpec.coffee b/ie8/AssertivePromiseSpec.coffee new file mode 100644 index 000000000..d50e3e6ec --- /dev/null +++ b/ie8/AssertivePromiseSpec.coffee @@ -0,0 +1,59 @@ +assert = require("assertive-chai").assert +Bacon = (require "../src/Bacon").Bacon + +success = undefined +fail = undefined +calls = 0 +promise = { + then: (s, f) -> + success = s + fail = f + calls = calls + 1 +} +_ = Bacon._ +nop = -> + +describe "Bacon.fromPromise", -> + it "should produce value and end on success", -> + events = [] + Bacon.fromPromise(promise).subscribe( (e) => events.push(e)) + success("a") + assert.deepEqual _.map(((e) -> e.toString()), events), ["a", ""] + + it "should produce error and end on error", -> + events = [] + Bacon.fromPromise(promise).subscribe( (e) => events.push(e)) + fail("a") + assert.deepEqual events.map((e) -> e.toString()), [" a", ""] + + it "should respect unsubscription", -> + events = [] + dispose = Bacon.fromPromise(promise).subscribe( (e) => events.push(e)) + dispose() + success("a") + assert.deepEqual events, [] + + it "should abort ajax promise on unsub, if abort flag is set", -> + isAborted = false + promise.abort = -> + isAborted = true + dispose = Bacon.fromPromise(promise, true).subscribe(nop) + dispose() + delete promise.abort + assert.deepEqual isAborted, true + + it "should not abort ajax promise on unsub, if abort flag is not set", -> + isAborted = false + promise.abort = -> + isAborted = true + dispose = Bacon.fromPromise(promise).subscribe(nop) + dispose() + delete promise.abort + assert.deepEqual isAborted, false + + it "should not abort non-ajax promise", -> + isAborted = false + dispose = Bacon.fromPromise(promise).subscribe(nop) + dispose() + assert.deepEqual isAborted, false + diff --git a/ie8/AssertiveSpecHelper.coffee b/ie8/AssertiveSpecHelper.coffee new file mode 100644 index 000000000..f9c8bff84 --- /dev/null +++ b/ie8/AssertiveSpecHelper.coffee @@ -0,0 +1,283 @@ +assert = require("assertive-chai").assert +Bacon = (require "../src/Bacon").Bacon +_ = Bacon._ + +t = @t = (time) -> time +seqs = [] +waitMs = 100 + +browser = (typeof window == "object") +if browser + console.log("Running in browser, narrowing test set") + +grep = process.env.grep +if grep + console.log("running with grep:", grep) + origDescribe = describe + match = false + global.describe = (desc, f) -> + if desc.indexOf(grep) >= 0 + match = true + origDescribe(desc, f) + match = false + else if match + origDescribe(desc, f) + +@error = (msg) -> new Bacon.Error(msg) +@soon = (f) -> setTimeout f, t(1) +@series = (interval, values) -> + Bacon.sequentially(t(interval), values) +@repeat = (interval, values) -> + source = Bacon.repeatedly(t(interval), values) + seqs.push({ values : values, source : source }) + source +@atGivenTimes = (timesAndValues) -> + streams = for tv in timesAndValues + Bacon.later(t(tv[0]), tv[1]) + Bacon.mergeAll(streams) + +@expectStreamTimings = (src, expectedEventsAndTimings, options) -> + srcWithRelativeTime = () -> + now = Bacon.scheduler.now + t0 = now() + relativeTime = () -> + Math.floor(now() - t0) + withRelativeTime = (x) -> [relativeTime(), x] + src().flatMap(withRelativeTime) + @expectStreamEvents(srcWithRelativeTime, expectedEventsAndTimings, options) + +@expectStreamEvents = (src, expectedEvents, {unstable} = {}) -> + verifySingleSubscriber src, expectedEvents + verifyLateEval src, expectedEvents + if not unstable + verifySwitching src, expectedEvents unless browser + verifySwitchingWithUnsub src, expectedEvents unless browser + verifySwitchingAggressively src, expectedEvents + +@expectPropertyEvents = (src, expectedEvents, {unstable, extraCheck} = {}) -> + assert.deepEqual expectedEvents.length > 0, true, "at least one expected event is specified" + verifyPSingleSubscriber src, expectedEvents, extraCheck + verifyPLateEval src, expectedEvents + if not unstable + verifyPSwitching src, justValues(expectedEvents) + +verifyPSingleSubscriber = (srcF, expectedEvents, extraCheck) -> + verifyPropertyWith "(single subscriber)", srcF, expectedEvents, ((src, events, done) -> + gotInitial = false + gotNext = false + sync = true + src.subscribe (event) -> + if event.isEnd() + done() + else + if event.isInitial() + if gotInitial then done(new Error "got more than one Initial event: " + toValue(event)) + if gotNext then done(new Error "got Initial event after the Next one: " + toValue(event)) + unless sync then done(new Error "got async Initial event: " + toValue(event)) + gotInitial = true + else if event.hasValue() + gotNext = true + events.push(toValue(event)) + sync = false + ), extraCheck + +verifyPLateEval = (srcF, expectedEvents) -> + verifyPropertyWith "(late eval)", srcF, expectedEvents, (src, events, done) -> + src.subscribe (event) -> + if event.isEnd() + done() + else + events.push(event) + +verifyPSwitching = (srcF, expectedEvents) -> + verifyPropertyWith "(switching subscribers)", srcF, expectedEvents, (src, events, done) -> + src.subscribe (event) -> + if event.isEnd() + done() + else + if event.hasValue() + src.subscribe (event) -> + if event.isInitial() + events.push(event.value()) + Bacon.noMore + +verifyPropertyWith = (description, srcF, expectedEvents, collectF, extraCheck) -> + describe description, -> + src = null + events = [] + before -> + src = srcF() + before (done) -> + collectF(src, events, done) + it "is a Property", -> + assert.deepEqual src instanceof Bacon.Property, true + it "outputs expected events in order", -> + assert.deepEqual toValues(events), toValues(expectedEvents) + it "has correct final state", -> + verifyFinalState(src, lastNonError(expectedEvents)) + it "cleans up observers", verifyCleanup + if (extraCheck) + extraCheck() + +verifyLateEval = (srcF, expectedEvents) -> + verifyStreamWith "(late eval)", srcF, expectedEvents, (src, events, done) -> + src.subscribe (event) -> + if event.isEnd() + done() + else + assert.deepEqual event instanceof Bacon.Initial, false, "no Initial events" + events.push(event) + + +verifySingleSubscriber = (srcF, expectedEvents) -> + verifyStreamWith "(single subscriber)", srcF, expectedEvents, (src, events, done) -> + src.subscribe (event) -> + if event.isEnd() + done() + else + assert.deepEqual event instanceof Bacon.Initial, false, "no Initial events" + events.push(toValue(event)) + +# get each event with new subscriber +verifySwitching = (srcF, expectedEvents, done) -> + verifyStreamWith "(switching subscribers)", srcF, expectedEvents, (src, events, done) -> + newSink = -> + (event) -> + if event.isEnd() + done() + else + assert.deepEqual event instanceof Bacon.Initial, false, "no Initial events" + events.push(toValue(event)) + src.subscribe(newSink()) + Bacon.noMore + src.subscribe(newSink()) + +# get each event with new subscriber. Unsub using the unsub function +# instead of Bacon.noMore +verifySwitchingWithUnsub = (srcF, expectedEvents, done) -> + verifyStreamWith "(switching subscribers with unsub)", srcF, expectedEvents, (src, events, done) -> + globalEnded = false + subNext = -> + unsub = null + newSink = -> + ended = false + noMoreExpected = false + usedUnsub = false + (event) -> + if noMoreExpected + console.log "got unexp", event.toString(), "usedUnsub", usedUnsub + if event.isEnd() + if ended + console.log("one stream, two ends") + else if globalEnded + console.log("two ends") + globalEnded = true + ended = true + done() + else + assert.deepEqual event instanceof Bacon.Initial, false, "no Initial events" + events.push(toValue(event)) + prevUnsub = unsub + noMoreExpected = true + subNext() + if unsub? + usedUnsub = true + unsub() + else + Bacon.noMore + unsub = src.subscribe(newSink()) + subNext() + +verifyStreamWith = (description, srcF, expectedEvents, collectF) -> + describe description, -> + src = null + events = [] + before -> + src = srcF() + assert.equal src instanceof Bacon.EventStream, true, "is an EventStream" + before (done) -> + collectF(src, events, done) + it "outputs expected value in order", -> + assert.deepEqual toValues(events), toValues(expectedEvents) + it "the stream is exhausted", -> + verifyExhausted src + it "cleans up observers", verifyCleanup + +verifySwitchingAggressively = (srcF, expectedEvents, done) -> + describe "(switching aggressively)", -> + src = null + events = [] + before -> + src = srcF() + assert.equal src instanceof Bacon.EventStream, true, "is an EventStream" + before (done) -> + newSink = -> + unsub = null + (event) -> + if event.isEnd() + done() + else + assert.deepEqual event instanceof Bacon.Initial, false, "no Initial events" + events.push(toValue(event)) + unsub() if unsub? + unsub = src.subscribe(newSink()) + Bacon.noMore + unsub = src.subscribe(newSink()) + it "outputs expected value in order", -> + assert.deepEqual events, toValues(expectedEvents) + it "the stream is exhausted", -> + verifyExhausted src + it "cleans up observers", verifyCleanup + +verifyExhausted = (src) -> + events = [] + src.subscribe (event) -> + if (event == undefined) + throw new Error("got undefined event") + events.push(event) + if (events.length == 0) + throw new Error("got zero events") + assert.deepEqual events[0].isEnd(), true + +lastNonError = (events) -> + _.last(_.filter(((e) -> toValue(e) != ""), events)) + +verifyFinalState = (property, value) -> + events = [] + property.subscribe (event) -> + events.push(event) + assert.deepEqual toValues(events), toValues([value, ""]) + +verifyCleanup = @verifyCleanup = -> + for seq in seqs + assert.deepEqual seq.source.dispatcher.hasSubscribers(), false + seqs = [] + +toValues = (xs) -> + values = [] + for x in xs + values.push(toValue(x)) + values +toValue = (x) -> + switch true + when !x?.isEvent?() then x + when x.isError() then "" + when x.isEnd() then "" + else x.value() + +justValues = (xs) -> + _.filter hasValue, xs +hasValue = (x) -> + toValue(x) != "" + +@toValues = toValues + +Bacon.Observable.prototype.onUnsub = (f) -> + self = this; + ended = false + return new Bacon.EventStream (sink) -> + unsub = self.subscribe sink + -> + f() + unsub() + diff --git a/ie8/AssertiveTickScheduler.coffee b/ie8/AssertiveTickScheduler.coffee new file mode 100644 index 000000000..44a33f260 --- /dev/null +++ b/ie8/AssertiveTickScheduler.coffee @@ -0,0 +1,46 @@ +Bacon = (require "../src/Bacon").Bacon +_ = Bacon._ + +exports.TickScheduler = -> + counter = 1 + currentTick = 0 + schedule = {} + toRemove = [] + nextId = -> counter++ + running = false + + add = (delay, entry) -> + tick = currentTick + delay + entry.id = nextId() if not entry.id + schedule[tick] = [] if not schedule[tick] + schedule[tick].push entry + entry.id + boot = (id) -> + if not running + running = true + setTimeout run, 0 + id + run = -> + while Object.keys(schedule).length + while schedule[currentTick]?.length + forNow = schedule[currentTick].splice(0, schedule[currentTick].length) + for entry in forNow + if _.contains(toRemove, entry.id) + _.remove(entry.id, toRemove) + else + try + entry.fn() + catch e + throw e unless e == "testing" + add entry.recur, entry if entry.recur + delete schedule[currentTick] + currentTick++ + running = false + { + setTimeout: (fn, delay) -> boot(add delay, { fn }) + setInterval: (fn, recur) -> boot(add recur, { fn, recur }) + clearTimeout: (id) -> toRemove.push(id) + clearInterval: (id) -> toRemove.push(id) + now: -> currentTick + } + diff --git a/ie8/Assertivejquery-tests.coffee b/ie8/Assertivejquery-tests.coffee new file mode 100644 index 000000000..59c770055 --- /dev/null +++ b/ie8/Assertivejquery-tests.coffee @@ -0,0 +1,40 @@ +sinon = require 'sinon' +Bacon = require '../src/Bacon' +assert = require("assertive-chai").assert +$.fn.asEventStream = Bacon.$.asEventStream + +elemName = (event) -> + event.target.localName + +describe 'asEventStream', -> + it 'supports simple format', -> + mock = sinon.spy() + $('body').asEventStream('click').map(elemName).take(1).onValue(mock) + $('body').click() + assert.equal mock.callCount, 1 + assert.equal mock.firstCall.args[0], 'body' + it 'supports jQuery live selector format', -> + mock = sinon.spy() + $('html').asEventStream('click', 'body').map(elemName).take(1).onValue(mock) + $('body').click() + assert.equal mock.callCount, 1 + assert.equal mock.firstCall.args[0], 'body' + it 'supports optional eventTransformer, with jQuery live selector', -> + mock = sinon.spy() + $('html').asEventStream('click', 'body', elemName).take(1).onValue(mock) + $('body').click() + assert.equal mock.callCount, 1 + assert.equal mock.firstCall.args[0], 'body' + it 'supports optional eventTransformer, without jQuery live selector', -> + mock = sinon.spy() + $('body').asEventStream('click', elemName).take(1).onValue(mock) + $('body').click() + assert.equal mock.callCount, 1 + assert.equal mock.firstCall.args[0], 'body' + it 'binds “this” to DOM element', -> + mock = sinon.spy() + $('body').asEventStream('click', mock).take(1).onValue(->) + $('body').click() + assert.isTrue mock.calledOn($('body')[0]) + + diff --git a/ie8/browserify.sh b/ie8/browserify.sh new file mode 100755 index 000000000..0e9baa8b2 --- /dev/null +++ b/ie8/browserify.sh @@ -0,0 +1,7 @@ +### Notes on Running tests in IE8 and modern browsers +# IE8 is not supported by "chai". To be able to run Bacon tests in IE8, +# it is required to convert the Expect api to Assert api. +# The following steps generate Assert API equivalent tests in ie8 directory +# respective Expect API (wherever possible!) and browserify these IE 8 tests. + +../node_modules/.bin/browserify --extension .coffee -t coffeeify ./mocha-harness-ie8.coffee > ../browsertest/bundle.js diff --git a/ie8/mocha-harness-ie8.coffee b/ie8/mocha-harness-ie8.coffee new file mode 100644 index 000000000..eb80406a3 --- /dev/null +++ b/ie8/mocha-harness-ie8.coffee @@ -0,0 +1,4 @@ +# insert test files here +require './AssertiveBaconSpec' +require './AssertivePromiseSpec' +require './Assertivejquery-tests' \ No newline at end of file diff --git a/ie8/mocha.runner.html b/ie8/mocha.runner.html new file mode 100644 index 000000000..20660b7ca --- /dev/null +++ b/ie8/mocha.runner.html @@ -0,0 +1,32 @@ + + + + Mocha Tests + + + + + + + + + + +
+
+
+ + diff --git a/ie8/polyfill.js b/ie8/polyfill.js new file mode 100644 index 000000000..e25c3157c --- /dev/null +++ b/ie8/polyfill.js @@ -0,0 +1,43 @@ +(function(){ + // From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys + if (!Object.keys) { + Object.keys = (function() { + 'use strict'; + var hasOwnProperty = Object.prototype.hasOwnProperty, + hasDontEnumBug = !({ toString: null }).propertyIsEnumerable('toString'), + dontEnums = [ + 'toString', + 'toLocaleString', + 'valueOf', + 'hasOwnProperty', + 'isPrototypeOf', + 'propertyIsEnumerable', + 'constructor' + ], + dontEnumsLength = dontEnums.length; + + return function(obj) { + if (typeof obj !== 'object' && (typeof obj !== 'function' || obj === null)) { + throw new TypeError('Object.keys called on non-object'); + } + + var result = [], prop, i; + + for (prop in obj) { + if (hasOwnProperty.call(obj, prop)) { + result.push(prop); + } + } + + if (hasDontEnumBug) { + for (i = 0; i < dontEnumsLength; i++) { + if (hasOwnProperty.call(obj, dontEnums[i])) { + result.push(dontEnums[i]); + } + } + } + return result; + }; + }()); + } +})(); \ No newline at end of file diff --git a/package.json b/package.json index 6476f4a5d..c6598ef27 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,9 @@ "mocha": "1.17.1", "lodash": "~2.4.1", "coffeelint": "~1.5.5", - "grunt-coffeelint": "0.0.13" + "grunt-coffeelint": "0.0.13", + "assertive-chai": "^1.0.0", + "split": "^0.3.2" }, "repository": { "type": "git",