UNPKG

rpd

Version:

RPD is a minimal framework for building Node-Based User Interfaces, powered by Reactive Programming

1,194 lines (891 loc) 41.1 kB
describe('registration: node type', function() { it('could be registered with an empty object', function() { expect(function() { Rpd.nodetype('spec/foo', {}); }).not.toThrow(); }); it('creates specified inlets for the node instance', function() { Rpd.nodetype('spec/foo', { inlets: { 'a': { type: 'spec/any' }, 'b': { type: 'spec/any' } } }); withNewPatch(function(patch, updateSpy) { var node = patch.addNode('spec/foo'); expect(updateSpy).toHaveBeenCalledWith( jasmine.objectContaining( { type: 'node/add-inlet', inlet: jasmine.objectContaining({ alias: 'a', type: 'spec/any' }) }) ); expect(updateSpy).toHaveBeenCalledWith( jasmine.objectContaining( { type: 'node/add-inlet', inlet: jasmine.objectContaining({ alias: 'b', type: 'spec/any' }) }) ); }); }); it('creates specified outlets for the node instance', function() { Rpd.nodetype('spec/foo', { outlets: { 'a': { type: 'spec/any' }, 'b': { type: 'spec/any' } } }); withNewPatch(function(patch, updateSpy) { var node = patch.addNode('spec/foo'); expect(updateSpy).toHaveBeenCalledWith( jasmine.objectContaining( { type: 'node/add-outlet', outlet: jasmine.objectContaining({ alias: 'a', type: 'spec/any' }) }) ); expect(updateSpy).toHaveBeenCalledWith( jasmine.objectContaining( { type: 'node/add-outlet', outlet: jasmine.objectContaining({ alias: 'b', type: 'spec/any' }) }) ); }); }); it('informs it is ready when all inlets and outlets are ready', function() { Rpd.nodetype('spec/foo', { inlets: { 'a': { type: 'spec/any' }, 'b': { type: 'spec/any' } }, outlets: { 'c': { type: 'spec/any' }, 'd': { type: 'spec/any' } } }); withNewPatch(function(patch, updateSpy) { var node = patch.addNode('spec/foo'); expect(updateSpy).toHaveBeenOrderlyCalledWith([ [ jasmine.objectContaining({ type: 'node/add-inlet' }) ], [ jasmine.objectContaining({ type: 'node/add-outlet' }) ], [ jasmine.objectContaining({ type: 'node/is-ready' }) ] ]); }); }); it('informs it is ready only once', function() { Rpd.nodetype('spec/foo', { inlets: { 'a': { type: 'spec/any' } }, outlets: { 'b': { type: 'spec/any' } } }); withNewPatch(function(patch, updateSpy) { var node = patch.addNode('spec/foo'); updateSpy.calls.reset(); node.addInlet('spec/any', 'foo'); expect(updateSpy).not.toHaveBeenCalledWith( jasmine.objectContaining({ type: 'node/is-ready' })); }); }); it('provides access to inlets or outlets defined in type', function() { Rpd.nodetype('spec/foo', { inlets: { 'a': { type: 'spec/any' }, 'b': { type: 'spec/any' } }, outlets: { 'c': { type: 'spec/any' } } }); withNewPatch(function(patch, updateSpy) { var node = patch.addNode('spec/foo'); expect(node.inlets['a']).toBeDefined(); expect(node.inlets['b']).toBeDefined(); expect(node.inlets['c']).not.toBeDefined(); expect(node.outlets['c']).toBeDefined(); }); }); describe('processing function', function() { var processSpy; beforeEach(function() { processSpy = jasmine.createSpy('process'); }); it('gets node instance as context of a call', function() { Rpd.nodetype('spec/foo', { inlets: { 'a': { type: 'spec/any' } }, process: processSpy }); withNewPatch(function(patch, updateSpy) { var node = patch.addNode('spec/foo'); processSpy.and.callFake(function() { expect(this).toBe(node); }); node.inlets['a'].receive(2); expect(processSpy).toHaveBeenCalled(); }); }); it('is not called when inlets have no default values', function() { Rpd.nodetype('spec/foo', { inlets: { 'a': { type: 'spec/any' }, 'b': { type: 'spec/any' } }, process: processSpy }); withNewPatch(function(patch, updateSpy) { var node = patch.addNode('spec/foo'); expect(processSpy).not.toHaveBeenCalled(); }); }); it('is still not called when inlets have no default values and there is an outlet', function() { Rpd.nodetype('spec/foo', { inlets: { 'a': { type: 'spec/any' }, 'b': { type: 'spec/any' } }, outlets: { 'c': { type: 'spec/any' } }, process: processSpy }); withNewPatch(function(patch, updateSpy) { var node = patch.addNode('spec/foo'); expect(processSpy).not.toHaveBeenCalled(); }); }); it('is called once when single inlet has some default value', function() { Rpd.nodetype('spec/foo', { inlets: { 'a': { type: 'spec/any', 'default': 10 }, 'b': { type: 'spec/any' } }, process: processSpy }); withNewPatch(function(patch, updateSpy) { var node = patch.addNode('spec/foo'); expect(processSpy).toHaveBeenCalledWith({ 'a': 10 }, jasmine.anything()); expect(processSpy).toHaveBeenCalledOnce(); }); }); it('is called for every inlet which have a default value', function() { Rpd.nodetype('spec/foo', { inlets: { 'a': { type: 'spec/any', 'default': 10 }, 'b': { type: 'spec/any', 'default': 5 } }, process: processSpy }); withNewPatch(function(patch, updateSpy) { var ensureExecuted = handleNextCalls(processSpy, [ function() { expect(processSpy).toHaveBeenCalledWith({ 'a': 10 }, jasmine.anything()); }, function() { expect(processSpy).toHaveBeenCalledWith({ 'a': 10, 'b': 5 }, jasmine.anything()); } ]); var node = patch.addNode('spec/foo'); ensureExecuted(); }); }); it('is not affected with number of outlets', function() { Rpd.nodetype('spec/foo', { inlets: { 'a': { type: 'spec/any', 'default': 10 }, 'b': { type: 'spec/any', 'default': 5 } }, outlets: { 'c': { type: 'spec/any' } }, process: processSpy }); withNewPatch(function(patch, updateSpy) { var node = patch.addNode('spec/foo'); expect(processSpy).toHaveBeenCalledTwice(); }); }); it('gets values from inlets even when there\'s no outlets', function() { Rpd.nodetype('spec/foo', { inlets: { 'a': { type: 'spec/any' }, 'b': { type: 'spec/any' } }, process: processSpy }); withNewPatch(function(patch, updateSpy) { var node = patch.addNode('spec/foo'); node.inlets['a'].receive(2); expect(processSpy).toHaveBeenCalledWith({ a: 2 }, jasmine.anything()); node.inlets['b'].receive('abc'); expect(processSpy).toHaveBeenCalledWith({ a: 2, b: 'abc' }, jasmine.anything()); }); }); it('does not uses default values for outlets', function() { // throw an error if default value passed to outlet? Rpd.nodetype('spec/foo', { inlets: { 'a': { type: 'spec/any' }, 'b': { type: 'spec/any' } }, outlets: { 'c': { type: 'spec/any', 'default': 17 } }, process: processSpy }); withNewPatch(function(patch, updateSpy) { var node = patch.addNode('spec/foo'); var outlet = node.outlets['c']; expect(updateSpy).not.toHaveBeenCalledWith( jasmine.objectContaining({ type: 'outlet/update', outlet: outlet, value: 17 }) ); expect(processSpy).not.toHaveBeenCalled(); }); }); it('called only when node is ready', function() { var nodeReady = false; Rpd.nodetype('spec/foo', { inlets: { 'a': { type: 'spec/any', 'default': 5 }, 'b': { type: 'spec/any', 'default': [ 2, 1 ] }, 'c': { type: 'spec/any', 'default': {} }, 'd': { type: 'spec/any', 'default': '' } }, outlets: { 'e': { type: 'spec/any' }, 'f': { type: 'spec/any' } }, process: processSpy }); withNewPatch(function(patch, updateSpy) { updateSpy.and.callFake(function(update) { if (update.type === 'node/is-ready') { expect(processSpy).not.toHaveBeenCalled(); nodeReady = true; } else if (!nodeReady) { expect(processSpy).not.toHaveBeenCalled(); } }.bind(this)); var node = patch.addNode('spec/foo'); expect(updateSpy).toHaveBeenCalled(); expect(processSpy).toHaveBeenCalled(); }); }); it('when inlet is set to transfer some stream by default, gets values from this stream one by one', function() { jasmine.clock().install(); var values = [ 'a', 'b', 'c' ]; var period = 30; Rpd.nodetype('spec/foo', { inlets: { 'char': { type: 'spec/any', 'default': Kefir.sequentially(period, values) } }, process: processSpy }); withNewPatch(function(patch, updateSpy) { var ensureExecuted = handleNextCalls(processSpy, [ function() { expect(processSpy).toHaveBeenCalledWith({ char: values[0] }, jasmine.anything()); }, function() { expect(processSpy).toHaveBeenCalledWith({ char: values[1] }, jasmine.anything()); }, function() { expect(processSpy).toHaveBeenCalledWith({ char: values[2] }, jasmine.anything()); } ]); var node = patch.addNode('spec/foo'); jasmine.clock().tick(period * (values.length + 1)); ensureExecuted(); }); jasmine.clock().uninstall(); }); it('when stream was sent to the inlet, still gets values one by one', function() { jasmine.clock().install(); var values = [ 'a', 'b', 'c' ]; var period = 30; Rpd.nodetype('spec/foo', { inlets: { 'char': { type: 'spec/any' } }, process: processSpy }); withNewPatch(function(patch, updateSpy) { var node = patch.addNode('spec/foo'); var ensureExecuted = handleNextCalls(processSpy, [ function() { expect(processSpy).toHaveBeenCalledWith({ char: values[0] }, jasmine.anything()); }, function() { expect(processSpy).toHaveBeenCalledWith({ char: values[1] }, jasmine.anything()); }, function() { expect(processSpy).toHaveBeenCalledWith({ char: values[2] }, jasmine.anything()); } ]); node.inlets['char'].stream(Kefir.sequentially(period, values)); jasmine.clock().tick(period * (values.length + 1)); ensureExecuted(); }); jasmine.clock().uninstall(); }); it('passes previous values with a call', function() { Rpd.nodetype('spec/foo', { inlets: { 'char': { type: 'spec/any', 'default': 'a' } }, process: processSpy }); withNewPatch(function(patch, updateSpy) { var node = patch.addNode('spec/foo'); node.inlets['char'].receive('b'); expect(processSpy).toHaveBeenCalledWith( jasmine.objectContaining({ char: 'b' }), jasmine.objectContaining({ char: 'a' }) ); node.inlets['char'].receive('c'); expect(processSpy).toHaveBeenCalledWith( jasmine.objectContaining({ char: 'c' }), jasmine.objectContaining({ char: 'b' }) ); }); }); it('still gets values from hidden inlets', function() { Rpd.nodetype('spec/foo', { inlets: { 'char': { type: 'spec/any', hidden: true, 'default': 'a' } }, process: processSpy }); withNewPatch(function(patch, updateSpy) { var node = patch.addNode('spec/foo'); expect(processSpy).toHaveBeenCalledWith( jasmine.objectContaining({ char: 'a' }), jasmine.anything() ); node.inlets['char'].receive('b'); expect(processSpy).toHaveBeenCalledWith( jasmine.objectContaining({ char: 'b' }), jasmine.anything() ); }); }); it('still gets values from hidden inlets', function() { Rpd.nodetype('spec/foo', { inlets: { 'char': { type: 'spec/any', hidden: true, 'default': 'a' } }, process: processSpy }); withNewPatch(function(patch, updateSpy) { var node = patch.addNode('spec/foo'); expect(node.inlets['char'].def.hidden).toBeTruthy(); expect(processSpy).toHaveBeenCalledWith( jasmine.objectContaining({ char: 'a' }), jasmine.anything() ); node.inlets['char'].receive('b'); expect(processSpy).toHaveBeenCalledWith( jasmine.objectContaining({ char: 'b' }), jasmine.anything() ); }); }); it('does not react if updated inlet was cold, but keeps its value for next update', function() { Rpd.nodetype('spec/foo', { inlets: { 'foo': { type: 'spec/any', cold: true, 'default': 'a' }, 'bar': { type: 'spec/any', cold: true }, 'buz': { type: 'spec/any' } }, process: processSpy }); withNewPatch(function(patch, updateSpy) { var node = patch.addNode('spec/foo'); expect(processSpy).not.toHaveBeenCalledWith( jasmine.objectContaining({ foo: 'a' }) ); node.inlets['foo'].receive('b'); expect(processSpy).not.toHaveBeenCalledWith( jasmine.objectContaining({ foo: 'b' }) ); node.inlets['bar'].receive(12); expect(processSpy).not.toHaveBeenCalledWith( jasmine.objectContaining({ bar: 12 }) ); node.inlets['buz'].receive('jazz'); expect(processSpy).toHaveBeenCalledWith( jasmine.objectContaining({ bar: 12, foo: 'b', buz: 'jazz' }), jasmine.objectContaining({ foo: 'a' }) ); }); }); it('passes values to corresponding outlets based on default inlets values', function() { processSpy.and.callFake(function(inlets) { return { 'c': (inlets.a || 0) * (inlets.b || 1) }; }); Rpd.nodetype('spec/foo', { inlets: { 'a': { type: 'spec/any', 'default': 7 }, 'b': { type: 'spec/any', 'default': 2 } }, outlets: { 'c': { type: 'spec/any' } }, process: processSpy }); withNewPatch(function(patch, updateSpy) { var node = patch.addNode('spec/foo'); var outlet = node.outlets['c']; expect(processSpy).toHaveBeenCalled(); expect(updateSpy).toHaveBeenCalledWith( jasmine.objectContaining({ type: 'outlet/update', outlet: outlet, value: 7 * 1 }) ); expect(updateSpy).toHaveBeenCalledWith( jasmine.objectContaining({ type: 'outlet/update', outlet: outlet, value: 2 * 7 }) ); }); }); it('passes single values to corresponding outlets, even in sequences', function() { processSpy.and.callFake(function(inlets) { return { 'c': inlets.a * (inlets.b || 1), 'd': (inlets.b || 0) * 3 }; }); Rpd.nodetype('spec/foo', { inlets: { 'a': { type: 'spec/any' }, 'b': { type: 'spec/any' } }, outlets: { 'c': { type: 'spec/any' }, 'd': { type: 'spec/any' } }, process: processSpy }); withNewPatch(function(patch, updateSpy) { var node = patch.addNode('spec/foo'); node.inlets['a'].receive(7); node.inlets['b'].receive(2); node.inlets['b'].receive(6); var outletC = node.outlets['c']; var outletD = node.outlets['d']; expect(processSpy).toHaveBeenCalled(); expect(updateSpy).toHaveBeenCalledWith( jasmine.objectContaining({ type: 'outlet/update', outlet: outletC, value: 7 * 1 }) ); expect(updateSpy).toHaveBeenCalledWith( jasmine.objectContaining({ type: 'outlet/update', outlet: outletC, value: 7 * 2 }) ); expect(updateSpy).toHaveBeenCalledWith( jasmine.objectContaining({ type: 'outlet/update', outlet: outletC, value: 7 * 6 }) ); expect(updateSpy).toHaveBeenCalledWith( jasmine.objectContaining({ type: 'outlet/update', outlet: outletD, value: 0 * 3 }) ); expect(updateSpy).toHaveBeenCalledWith( jasmine.objectContaining({ type: 'outlet/update', outlet: outletD, value: 2 * 3 }) ); expect(updateSpy).toHaveBeenCalledWith( jasmine.objectContaining({ type: 'outlet/update', outlet: outletD, value: 6 * 3 }) ); }); }); describe('streaming to outlets', function() { beforeEach(function() { jasmine.clock().install(); }); afterEach(function() { jasmine.clock().uninstall(); }); it('passes streamed values to corresponding outlets', function() { var values = [ 3, 14, 15, 92 ]; var period = 30; processSpy.and.callFake(function(inlets) { return { 'out': Kefir.sequentially(period, values) .map(function(value) { return (inlets.in * value); }) }; }); Rpd.nodetype('spec/foo', { inlets: { 'in': { type: 'spec/any' } }, outlets: { 'out': { type: 'spec/any' } }, process: processSpy }); withNewPatch(function(patch, updateSpy) { var node = patch.addNode('spec/foo'); node.inlets['in'].receive(7); var outlet = node.outlets['out']; expect(processSpy).toHaveBeenCalled(); jasmine.clock().tick(period * (values.length + 1)); for (var i = 0; i < values.length; i++) { expect(updateSpy).toHaveBeenCalledWith( jasmine.objectContaining({ type: 'outlet/update', outlet: outlet, value: values[i] * 7 })); } }); }); it('new calls to the process function properly create new streams', function() { processSpy.and.callFake(function(inlets) { return { 'out': Kefir.interval(inlets.config.period, inlets.config.value) .take(inlets.config.count) }; }); Rpd.nodetype('spec/foo', { inlets: { 'config': { type: 'spec/any' } }, outlets: { 'out': { type: 'spec/any' } }, process: processSpy }); withNewPatch(function(patch, updateSpy) { var node = patch.addNode('spec/foo'); node.inlets['config'].receive({ period: 20, count: 10, value: 3 }); updateSpy.calls.reset(); var outlet = node.outlets['out']; jasmine.clock().tick(20 * 10 + 1); expect(updateSpy).toHaveBeenCalledWith( jasmine.objectContaining({ type: 'outlet/update', outlet: outlet, value: 3 })); expect(updateSpy.calls.count()).toBe(10); node.inlets['config'].receive({ period: 10, count: 5, value: 7 }); updateSpy.calls.reset(); jasmine.clock().tick(10 * 5 + 1); expect(updateSpy).not.toHaveBeenCalledWith( jasmine.objectContaining({ type: 'outlet/update', outlet: outlet, value: 3 })); expect(updateSpy).toHaveBeenCalledWith( jasmine.objectContaining({ type: 'outlet/update', outlet: outlet, value: 7 })); expect(updateSpy.calls.count()).toBe(5); }); }); it('users are able to turn off streams they send from outlets', function() { var lastStream; var firstTime = true; var pool = Kefir.pool(); processSpy.and.callFake(function(inlets) { if (lastStream) { firstTime = false; pool.unplug(lastStream); } lastStream = Kefir.interval(inlets.config.period, inlets.config.value); pool.plug(lastStream); //return { 'out': firstTime ? pool : Kefir.never() }; return firstTime ? { 'out': pool } : null; }); Rpd.nodetype('spec/foo', { inlets: { 'config': { type: 'spec/any' } }, outlets: { 'out': { type: 'spec/any' } }, process: processSpy }); withNewPatch(function(patch, updateSpy) { var node = patch.addNode('spec/foo'); var outlet = node.outlets['out']; var updateEventStream = outlet.event['outlet/update']; var outletUpdateSpy = jasmine.createSpy(); var value3spy = jasmine.createSpy('value-3'); var value7spy = jasmine.createSpy('value-7'); updateEventStream.onValue(outletUpdateSpy); updateEventStream.filter(function(value) { return (value === 3); }).onValue(value3spy); updateEventStream.filter(function(value) { return (value === 7); }).onValue(value7spy); node.inlets['config'].receive({ period: 20, value: 3 }); jasmine.clock().tick(20 * 10 + 1); expect(value3spy.calls.count()).toBe(10); expect(value7spy).not.toHaveBeenCalled(); value3spy.calls.reset(); node.inlets['config'].receive({ period: 10, value: 7 }); jasmine.clock().tick(10 * 5 + 1); pool.unplug(lastStream); expect(value3spy).not.toHaveBeenCalled(); expect(value7spy.calls.count()).toBe(5); expect(outletUpdateSpy.calls.count()).toBe(10 + 5); }); }); }); it('passes outlet values to process event', function() { processSpy.and.callFake(function(inlets) { return { 'c': inlets.a * inlets.b, 'd': inlets.b - inlets.a }; }); Rpd.nodetype('spec/foo', { inlets: { 'a': { type: 'spec/any' }, 'b': { type: 'spec/any' } }, outlets: { 'c': { type: 'spec/any' }, 'd': { type: 'spec/any' } }, process: processSpy }); withNewPatch(function(patch, updateSpy) { var node = patch.addNode('spec/foo'); node.inlets['a'].receive(7); node.inlets['b'].receive(2); var outletC = node.outlets['c']; var outletD = node.outlets['d']; expect(processSpy).toHaveBeenCalled(); expect(updateSpy).toHaveBeenCalledWith( jasmine.objectContaining({ type: 'node/process', inlets: { a: 7, b : 2 }, outlets: { c: 7 * 2, d: 2 - 7 } }) ); }); }); it('if no outlet was updated, does not fire the update for this outlet', function() { processSpy.and.callFake(function(inlets) { return {}; }); Rpd.nodetype('spec/foo', { inlets: { 'a': { type: 'spec/any', 'default': 4 }, 'b': { type: 'spec/any' } }, outlets: { 'c': { type: 'spec/any' }, 'd': { type: 'spec/any' } }, process: processSpy }); withNewPatch(function(patch, updateSpy) { var node = patch.addNode('spec/foo'); node.inlets['b'].receive(7); expect(updateSpy).not.toHaveBeenCalledWith( jasmine.anything(), jasmine.objectContaining({ type: 'outlet/update' }) ); }); }); it('treats no return value as no update to any outlet', function() { processSpy.and.callFake(function(inlets) { }); Rpd.nodetype('spec/foo', { inlets: { 'a': { type: 'spec/any', 'default': 2 }, 'b': { type: 'spec/any' } }, outlets: { 'c': { type: 'spec/any' }, 'd': { type: 'spec/any' } }, process: processSpy }); withNewPatch(function(patch, updateSpy) { var node = patch.addNode('spec/foo'); node.inlets['b'].receive(12); expect(updateSpy).not.toHaveBeenCalledWith( jasmine.objectContaining({ type: 'outlet/update' }) ); expect(processSpy).toHaveBeenCalledWith( jasmine.objectContaining({ a: 2 }), jasmine.anything() ); expect(processSpy).toHaveBeenCalledWith( jasmine.objectContaining({ b: 12 }), jasmine.anything() ); }); }); it('processing function receives node instance as `this`', function() { var nodeInSpy; var processSpy = jasmine.createSpy('process').and.callFake(function() { nodeInSpy = this; return {}; }); Rpd.nodetype('spec/foo', { inlets: { 'a': { type: 'spec/any' } }, process: processSpy }); withNewPatch(function(patch, updateSpy) { var node = patch.addNode('spec/foo'); node.inlets['a'].receive({}); expect(processSpy).toHaveBeenCalled(); expect(nodeInSpy).toBe(node); }); }); xdescribe('overriding channel type definition', function() { xit('overriding inlet adapt function', function() {}); xit('overriding inlet allow function', function() {}); xit('overriding inlet accept function', function() {}); xit('overriding inlet show function', function() {}); xit('overriding inlet tune function', function() {}); xit('overriding outlet tune function', function() {}); xit('subscribing to inlet events', function() {}); xit('subscribing to outlet events', function() {}); }); it('one could add inlets or outlets from the inside', function() { Rpd.nodetype('spec/foo', { inlets: { 'a': { type: 'spec/any' } }, // force call to process function process: processSpy.and.callFake(function(inlets) { if (!inlets.bar) this.addInlet('spec/any', 'bar'); }) }); withNewPatch(function(patch, updateSpy) { var node = patch.addNode('spec/foo'); node.inlets['a'].receive(2); // force call to process function expect(updateSpy).toHaveBeenCalledWith( jasmine.objectContaining({ type: 'node/add-inlet', inlet: jasmine.objectContaining({ alias: 'bar' }) }) ); expect(processSpy).toHaveBeenCalledOnce(); }); }); it('incoming values stream could be tuned', function() { Rpd.nodetype('spec/foo', { inlets: { 'a': { type: 'spec/any', 'default': -1 } }, process: processSpy, tune: function(incoming) { return incoming .filter(function(update) { return update.value !== 2; }) .map(function(update) { return { inlet: update.inlet, value: update.value * 10 }; }); } }); withNewPatch(function(patch, updateSpy) { var node = patch.addNode('spec/foo'); node.inlets['a'].receive(0); expect(processSpy).toHaveBeenCalledWith( jasmine.objectContaining({ a: 0 }), jasmine.anything() ); node.inlets['a'].receive(1); expect(processSpy).toHaveBeenCalledWith( jasmine.objectContaining({ a: 10 }), jasmine.anything() ); node.inlets['a'].receive(2); expect(processSpy).not.toHaveBeenCalledWith( jasmine.objectContaining({ a: 2 }), jasmine.anything() ); expect(processSpy).not.toHaveBeenCalledWith( jasmine.objectContaining({ a: 20 }), jasmine.anything() ); node.inlets['a'].receive(3); expect(processSpy).toHaveBeenCalledWith( jasmine.objectContaining({ a: 30 }), jasmine.anything() ); }); }); it('tuning function receives node instance as `this`', function() { var nodeInSpy; var tuneSpy = jasmine.createSpy('tune').and.callFake(function(stream) { nodeInSpy = this; return stream; }); Rpd.nodetype('spec/foo', { inlets: { 'a': { type: 'spec/any' } }, process: function() {}, tune: tuneSpy }); withNewPatch(function(patch, updateSpy) { var node = patch.addNode('spec/foo'); node.inlets['a'].receive({}); expect(tuneSpy).toHaveBeenCalled(); expect(nodeInSpy).toBe(node); }); }); it('tuning function may delay updates', function() { jasmine.clock().install(); Rpd.nodetype('spec/delay', { inlets: { 'in': { type: 'spec/any' } }, outlets: { 'out': { type: 'spec/any' } }, tune: function(updates) { return updates.delay(1000); // delays updates for one second }, process: function(inlets) { return { out: inlets.in }; } }); withNewPatch(function(patch, updateSpy) { var delayingNode = patch.addNode('spec/delay'); delayingNode.inlets['in'].receive(42); expect(updateSpy).not.toHaveBeenCalledWith( jasmine.objectContaining({ type: 'outlet/update' }) ); jasmine.clock().tick(500); expect(updateSpy).not.toHaveBeenCalledWith( jasmine.objectContaining({ type: 'outlet/update' }) ); jasmine.clock().tick(501); expect(updateSpy).toHaveBeenCalledWith( jasmine.objectContaining({ type: 'outlet/update', outlet: delayingNode.outlets['out'], value: 42 }) ); }); jasmine.clock().uninstall(); }); }); it('updates could be handled with custom handle function', function() { var inletUpdateSpy = jasmine.createSpy('inlet-update'); var outletConnectSpy = jasmine.createSpy('outlet-connect'); Rpd.nodetype('spec/foo', { inlets: { 'in': { type: 'spec/any' } }, outlets: { 'out': { type: 'spec/any' } }, handle: { 'inlet/update': inletUpdateSpy, 'outlet/connect': outletConnectSpy } }); withNewPatch(function(patch, updateSpy) { var firstNode = patch.addNode('spec/foo'); var secondNode = patch.addNode('spec/foo'); var fromOutlet = firstNode.outlets['out']; var toInlet = secondNode.inlets['in']; toInlet.receive(12); expect(inletUpdateSpy).toHaveBeenCalledWith( jasmine.objectContaining({ type: 'inlet/update', inlet: toInlet, value: 12 })); var link = fromOutlet.connect(toInlet); expect(outletConnectSpy).toHaveBeenCalledWith( jasmine.objectContaining({ type: 'outlet/connect', link: link })); }); }); it('default inlets and outlets could be prepared with custom prepare function', function() { var prepareSpy = jasmine.createSpy('prepare'); Rpd.nodetype('spec/foo', { inlets: { 'a': { type: 'spec/any' }, 'b': { type: 'spec/any' } }, outlets: { 'c': { type: 'spec/any' } }, prepare: prepareSpy }); withNewPatch(function(patch, updateSpy) { updateSpy.and.callFake(function(update) { if (update.type === 'node/is-ready') { expect(prepareSpy).toHaveBeenCalled(); } }); var node = patch.addNode('spec/foo'); expect(prepareSpy).toHaveBeenCalledWith( jasmine.objectContaining({ a: node.inlets['a'], b: node.inlets['b'] }), jasmine.objectContaining({ c: node.outlets['c'] }) ); expect(prepareSpy).toHaveBeenCalledOnce(); }); }); it('preparing function receives node instance as `this`', function() { var nodeInSpy; var prepareSpy = jasmine.createSpy('tune').and.callFake(function() { nodeInSpy = this; }); Rpd.nodetype('spec/foo', { inlets: { 'a': { type: 'spec/any' } }, prepare: prepareSpy }); withNewPatch(function(patch, updateSpy) { var node = patch.addNode('spec/foo'); expect(prepareSpy).toHaveBeenCalled(); expect(nodeInSpy).toBe(node); }); }); it('could be specified as a single function which returns the defition and gets node instance', function() { var definitionGenSpy = jasmine.createSpy('definition-generator') .and.callFake(function(node) { return { title: 'foo-' + node.id }; }); Rpd.nodetype('spec/foo', definitionGenSpy); withNewPatch(function(patch, updateSpy) { var firstNode = patch.addNode('spec/foo'), secondNode = patch.addNode('spec/foo'); expect(definitionGenSpy).toHaveBeenCalled(); expect(definitionGenSpy).toHaveBeenCalledWith(firstNode); expect(definitionGenSpy).toHaveBeenCalledWith(secondNode); expect(firstNode.def.title).toBe('foo-' + firstNode.id); expect(secondNode.def.title).toBe('foo-' + secondNode.id); }); }); xit('it is possible to handle events from node definition'); xit('`node/process` event is not fired to node definition handler unless `process` callback is defined'); });