UNPKG

noflo

Version:

Flow-Based Programming environment for JavaScript

1,743 lines (1,641 loc) 87.6 kB
describe('Component', () => { describe('with required ports', () => { it('should throw an error upon sending packet to an unattached required port', () => { const s2 = new noflo.internalSocket.InternalSocket(); const c = new noflo.Component({ outPorts: { required_port: { required: true, }, optional_port: {}, }, }); c.outPorts.optional_port.attach(s2); chai.expect(() => c.outPorts.required_port.send('foo')).to.throw(); }); it('should be cool with an attached port', () => { const s1 = new noflo.internalSocket.InternalSocket(); const s2 = new noflo.internalSocket.InternalSocket(); const c = new noflo.Component({ inPorts: { required_port: { required: true, }, optional_port: {}, }, }); c.inPorts.required_port.attach(s1); c.inPorts.optional_port.attach(s2); const f = function () { s1.send('some-more-data'); s2.send('some-data'); }; chai.expect(f).to.not.throw(); }); }); describe('with component creation shorthand', () => { it('should make component creation easy', (done) => { const c = new noflo.Component({ inPorts: { in: { datatype: 'string', required: true, }, just_processor: {}, }, process(input, output) { let packet; if (input.hasData('in')) { packet = input.getData('in'); chai.expect(packet).to.equal('some-data'); output.done(); return; } if (input.hasData('just_processor')) { packet = input.getData('just_processor'); chai.expect(packet).to.equal('some-data'); output.done(); done(); } }, }); const s1 = new noflo.internalSocket.InternalSocket(); c.inPorts.in.attach(s1); c.inPorts.in.nodeInstance = c; const s2 = new noflo.internalSocket.InternalSocket(); c.inPorts.just_processor.attach(s1); c.inPorts.just_processor.nodeInstance = c; s1.send('some-data'); s2.send('some-data'); }); it('should throw errors if there is no error port', (done) => { const c = new noflo.Component({ inPorts: { in: { datatype: 'string', required: true, }, }, process(input, output) { const packet = input.getData('in'); chai.expect(packet).to.equal('some-data'); chai.expect(() => output.error(new Error())).to.throw(Error); done(); }, }); const s1 = new noflo.internalSocket.InternalSocket(); c.inPorts.in.attach(s1); c.inPorts.in.nodeInstance = c; s1.send('some-data'); }); it('should throw errors if there is a non-attached error port', (done) => { const c = new noflo.Component({ inPorts: { in: { datatype: 'string', required: true, }, }, outPorts: { error: { datatype: 'object', required: true, }, }, process(input, output) { const packet = input.getData('in'); chai.expect(packet).to.equal('some-data'); chai.expect(() => output.error(new Error())).to.throw(Error); done(); }, }); const s1 = new noflo.internalSocket.InternalSocket(); c.inPorts.in.attach(s1); c.inPorts.in.nodeInstance = c; s1.send('some-data'); }); it('should not throw errors if there is a non-required error port', (done) => { const c = new noflo.Component({ inPorts: { in: { datatype: 'string', required: true, }, }, outPorts: { error: { required: false, }, }, process(input) { const packet = input.getData('in'); chai.expect(packet).to.equal('some-data'); c.error(new Error()); done(); }, }); const s1 = new noflo.internalSocket.InternalSocket(); c.inPorts.in.attach(s1); c.inPorts.in.nodeInstance = c; s1.send('some-data'); }); it('should send errors if there is a connected error port', (done) => { const c = new noflo.Component({ inPorts: { in: { datatype: 'string', required: true, }, }, outPorts: { error: { datatype: 'object', }, }, process(input, output) { if (!input.hasData('in')) { return; } const packet = input.getData('in'); chai.expect(packet).to.equal('some-data'); output.done(new Error()); }, }); const s1 = new noflo.internalSocket.InternalSocket(); const s2 = new noflo.internalSocket.InternalSocket(); const groups = [ 'foo', 'bar', ]; s2.on('begingroup', (grp) => { chai.expect(grp).to.equal(groups.shift()); }); s2.on('data', (err) => { chai.expect(err).to.be.an.instanceOf(Error); chai.expect(groups.length).to.equal(0); done(); }); c.inPorts.in.attach(s1); c.outPorts.error.attach(s2); c.inPorts.in.nodeInstance = c; s1.beginGroup('foo'); s1.beginGroup('bar'); s1.send('some-data'); }); }); describe('defining ports with invalid names', () => { it('should throw an error with uppercase letters in inport', () => { const shorthand = () => new noflo.Component({ inPorts: { fooPort: {}, }, }); chai.expect(shorthand).to.throw(); }); it('should throw an error with uppercase letters in outport', () => { const shorthand = () => new noflo.Component({ outPorts: { BarPort: {}, }, }); chai.expect(shorthand).to.throw(); }); it('should throw an error with special characters in inport', () => { const shorthand = () => new noflo.Component({ inPorts: { '$%^&*a': {}, }, }); chai.expect(shorthand).to.throw(); }); }); describe('with non-existing ports', () => { const getComponent = function () { return new noflo.Component({ inPorts: { in: {}, }, outPorts: { out: {}, }, }); }; it('should throw an error when checking attached for non-existing port', (done) => { const c = getComponent(); c.process((input) => { try { input.attached('foo'); } catch (e) { chai.expect(e).to.be.an('Error'); chai.expect(e.message).to.contain('foo'); done(); return; } done(new Error('Expected a throw')); }); const sin1 = noflo.internalSocket.createSocket(); c.inPorts.in.attach(sin1); sin1.send('hello'); }); it('should throw an error when checking IP for non-existing port', (done) => { const c = getComponent(); c.process((input) => { try { input.has('foo'); } catch (e) { chai.expect(e).to.be.an('Error'); chai.expect(e.message).to.contain('foo'); done(); return; } done(new Error('Expected a throw')); }); const sin1 = noflo.internalSocket.createSocket(); c.inPorts.in.attach(sin1); sin1.send('hello'); }); it('should throw an error when checking IP for non-existing addressable port', (done) => { const c = getComponent(); c.process((input) => { try { input.has(['foo', 0]); } catch (e) { chai.expect(e).to.be.an('Error'); chai.expect(e.message).to.contain('foo'); done(); return; } done(new Error('Expected a throw')); }); const sin1 = noflo.internalSocket.createSocket(); c.inPorts.in.attach(sin1); sin1.send('hello'); }); it('should throw an error when checking data for non-existing port', (done) => { const c = getComponent(); c.process((input) => { try { input.hasData('foo'); } catch (e) { chai.expect(e).to.be.an('Error'); chai.expect(e.message).to.contain('foo'); done(); return; } done(new Error('Expected a throw')); }); const sin1 = noflo.internalSocket.createSocket(); c.inPorts.in.attach(sin1); sin1.send('hello'); }); it('should throw an error when checking stream for non-existing port', (done) => { const c = getComponent(); c.process((input) => { try { input.hasStream('foo'); } catch (e) { chai.expect(e).to.be.an('Error'); chai.expect(e.message).to.contain('foo'); done(); return; } done(new Error('Expected a throw')); }); const sin1 = noflo.internalSocket.createSocket(); c.inPorts.in.attach(sin1); sin1.send('hello'); }); }); describe('starting a component', () => { it('should flag the component as started', (done) => { const c = new noflo.Component({ inPorts: { in: { datatype: 'string', required: true, }, }, }); const i = new noflo.internalSocket.InternalSocket(); c.inPorts.in.attach(i); c.start((err) => { if (err) { done(err); return; } chai.expect(c.started).to.equal(true); chai.expect(c.isStarted()).to.equal(true); done(); }); }); }); describe('shutting down a component', () => { it('should flag the component as not started', (done) => { const c = new noflo.Component({ inPorts: { in: { datatype: 'string', required: true, }, }, }); const i = new noflo.internalSocket.InternalSocket(); c.inPorts.in.attach(i); c.start((err) => { if (err) { done(err); return; } chai.expect(c.isStarted()).to.equal(true); c.shutdown((err) => { if (err) { done(err); return; } chai.expect(c.started).to.equal(false); chai.expect(c.isStarted()).to.equal(false); done(); }); }); }); }); describe('with object-based IPs', () => { it('should speak IP objects', (done) => { const c = new noflo.Component({ inPorts: { in: { datatype: 'string', }, }, outPorts: { out: { datatype: 'string', }, }, process(input, output) { output.sendDone(input.get('in')); }, }); const s1 = new noflo.internalSocket.InternalSocket(); const s2 = new noflo.internalSocket.InternalSocket(); s2.on('ip', (ip) => { chai.expect(ip).to.be.an('object'); chai.expect(ip.type).to.equal('data'); chai.expect(ip.groups).to.be.an('array'); chai.expect(ip.groups).to.eql(['foo']); chai.expect(ip.data).to.be.a('string'); chai.expect(ip.data).to.equal('some-data'); done(); }); c.inPorts.in.attach(s1); c.outPorts.out.attach(s2); s1.post(new noflo.IP('data', 'some-data', { groups: ['foo'] })); }); it('should support substreams', (done) => { const c = new noflo.Component({ forwardBrackets: {}, inPorts: { tags: { datatype: 'string', }, }, outPorts: { html: { datatype: 'string', }, }, process(input, output) { const ip = input.get('tags'); switch (ip.type) { case 'openBracket': c.str += `<${ip.data}>`; c.level++; break; case 'data': c.str += ip.data; break; case 'closeBracket': c.str += `</${ip.data}>`; c.level--; if (c.level === 0) { output.send({ html: c.str }); c.str = ''; } break; } output.done(); }, }); c.str = ''; c.level = 0; const d = new noflo.Component({ inPorts: { bang: { datatype: 'bang', }, }, outPorts: { tags: { datatype: 'string', }, }, process(input, output) { input.getData('bang'); output.send({ tags: new noflo.IP('openBracket', 'p') }); output.send({ tags: new noflo.IP('openBracket', 'em') }); output.send({ tags: new noflo.IP('data', 'Hello') }); output.send({ tags: new noflo.IP('closeBracket', 'em') }); output.send({ tags: new noflo.IP('data', ', ') }); output.send({ tags: new noflo.IP('openBracket', 'strong') }); output.send({ tags: new noflo.IP('data', 'World!') }); output.send({ tags: new noflo.IP('closeBracket', 'strong') }); output.send({ tags: new noflo.IP('closeBracket', 'p') }); outout.done(); }, }); const s1 = new noflo.internalSocket.InternalSocket(); const s2 = new noflo.internalSocket.InternalSocket(); const s3 = new noflo.internalSocket.InternalSocket(); s3.on('ip', (ip) => { chai.expect(ip).to.be.an('object'); chai.expect(ip.type).to.equal('data'); chai.expect(ip.data).to.equal('<p><em>Hello</em>, <strong>World!</strong></p>'); done(); }); d.inPorts.bang.attach(s1); d.outPorts.tags.attach(s2); c.inPorts.tags.attach(s2); c.outPorts.html.attach(s3); s1.post(new noflo.IP('data', 'start')); }); }); describe('with process function', () => { let c = null; let sin1 = null; let sin2 = null; let sin3 = null; let sout1 = null; let sout2 = null; beforeEach((done) => { sin1 = new noflo.internalSocket.InternalSocket(); sin2 = new noflo.internalSocket.InternalSocket(); sin3 = new noflo.internalSocket.InternalSocket(); sout1 = new noflo.internalSocket.InternalSocket(); sout2 = new noflo.internalSocket.InternalSocket(); done(); }); it('should trigger on IPs', (done) => { let hadIPs = []; c = new noflo.Component({ inPorts: { foo: { datatype: 'string' }, bar: { datatype: 'string' }, }, outPorts: { baz: { datatype: 'boolean' }, }, process(input, output) { hadIPs = []; if (input.has('foo')) { hadIPs.push('foo'); } if (input.has('bar')) { hadIPs.push('bar'); } output.sendDone({ baz: true }); }, }); c.inPorts.foo.attach(sin1); c.inPorts.bar.attach(sin2); c.outPorts.baz.attach(sout1); let count = 0; sout1.on('ip', () => { count++; if (count === 1) { chai.expect(hadIPs).to.eql(['foo']); } if (count === 2) { chai.expect(hadIPs).to.eql(['foo', 'bar']); done(); } }); sin1.post(new noflo.IP('data', 'first')); sin2.post(new noflo.IP('data', 'second')); }); it('should trigger on IPs to addressable ports', (done) => { const receivedIndexes = []; c = new noflo.Component({ inPorts: { foo: { datatype: 'string', addressable: true, }, }, outPorts: { baz: { datatype: 'boolean', }, }, process(input, output) { // See what inbound connection indexes have data const indexesWithData = input.attached('foo').filter((idx) => input.hasData(['foo', idx])); if (!indexesWithData.length) { return; } // Read from the first of them const indexToUse = indexesWithData[0]; const packet = input.get(['foo', indexToUse]); receivedIndexes.push({ idx: indexToUse, payload: packet.data, }); output.sendDone({ baz: true }); }, }); c.inPorts.foo.attach(sin1, 1); c.inPorts.foo.attach(sin2, 0); c.outPorts.baz.attach(sout1); let count = 0; sout1.on('ip', () => { count++; if (count === 1) { chai.expect(receivedIndexes).to.eql([{ idx: 1, payload: 'first', }, ]); } if (count === 2) { chai.expect(receivedIndexes).to.eql([{ idx: 1, payload: 'first', }, { idx: 0, payload: 'second', }, ]); done(); } }); sin1.post(new noflo.IP('data', 'first')); sin2.post(new noflo.IP('data', 'second')); }); it('should be able to send IPs to addressable connections', (done) => { const expected = [{ data: 'first', index: 1, }, { data: 'second', index: 0, }, ]; c = new noflo.Component({ inPorts: { foo: { datatype: 'string', }, }, outPorts: { baz: { datatype: 'boolean', addressable: true, }, }, process(input, output) { if (!input.has('foo')) { return; } const packet = input.get('foo'); output.sendDone(new noflo.IP('data', packet.data, { index: expected.length - 1 })); }, }); c.inPorts.foo.attach(sin1); c.outPorts.baz.attach(sout1, 1); c.outPorts.baz.attach(sout2, 0); sout1.on('ip', (ip) => { const exp = expected.shift(); const received = { data: ip.data, index: 1, }; chai.expect(received).to.eql(exp); if (!expected.length) { done(); } }); sout2.on('ip', (ip) => { const exp = expected.shift(); const received = { data: ip.data, index: 0, }; chai.expect(received).to.eql(exp); if (!expected.length) { done(); } }); sin1.post(new noflo.IP('data', 'first')); sin1.post(new noflo.IP('data', 'second')); }); it('trying to send to addressable port without providing index should fail', (done) => { c = new noflo.Component({ inPorts: { foo: { datatype: 'string', }, }, outPorts: { baz: { datatype: 'boolean', addressable: true, }, }, process(input, output) { if (!input.hasData('foo')) { return; } const packet = input.get('foo'); const noIndex = new noflo.IP('data', packet.data); chai.expect(() => output.sendDone(noIndex)).to.throw(Error); done(); }, }); c.inPorts.foo.attach(sin1); c.outPorts.baz.attach(sout1, 1); c.outPorts.baz.attach(sout2, 0); sout1.on('ip', () => {}); sout2.on('ip', () => {}); sin1.post(new noflo.IP('data', 'first')); }); it('should be able to send falsy IPs', (done) => { const expected = [{ port: 'out1', data: 1, }, { port: 'out2', data: 0, }, ]; c = new noflo.Component({ inPorts: { foo: { datatype: 'string', }, }, outPorts: { out1: { datatype: 'int', }, out2: { datatype: 'int', }, }, process(input, output) { if (!input.has('foo')) { return; } input.get('foo'); output.sendDone({ out1: 1, out2: 0, }); }, }); c.inPorts.foo.attach(sin1); c.outPorts.out1.attach(sout1, 1); c.outPorts.out2.attach(sout2, 0); sout1.on('ip', (ip) => { const exp = expected.shift(); const received = { port: 'out1', data: ip.data, }; chai.expect(received).to.eql(exp); if (!expected.length) { done(); } }); sout2.on('ip', (ip) => { const exp = expected.shift(); const received = { port: 'out2', data: ip.data, }; chai.expect(received).to.eql(exp); if (!expected.length) { done(); } }); sin1.post(new noflo.IP('data', 'first')); }); it('should not be triggered by non-triggering ports', (done) => { const triggered = []; c = new noflo.Component({ inPorts: { foo: { datatype: 'string', triggering: false, }, bar: { datatype: 'string' }, }, outPorts: { baz: { datatype: 'boolean' }, }, process(input, output) { triggered.push(input.port.name); output.sendDone({ baz: true }); }, }); c.inPorts.foo.attach(sin1); c.inPorts.bar.attach(sin2); c.outPorts.baz.attach(sout1); let count = 0; sout1.on('ip', () => { count++; if (count === 1) { chai.expect(triggered).to.eql(['bar']); } if (count === 2) { chai.expect(triggered).to.eql(['bar', 'bar']); done(); } }); sin1.post(new noflo.IP('data', 'first')); sin2.post(new noflo.IP('data', 'second')); sin1.post(new noflo.IP('data', 'first')); sin2.post(new noflo.IP('data', 'second')); }); it('should fetch undefined for premature data', (done) => { c = new noflo.Component({ inPorts: { foo: { datatype: 'string', }, bar: { datatype: 'boolean', triggering: false, control: true, }, baz: { datatype: 'string', triggering: false, control: true, }, }, process(input) { if (!input.has('foo')) { return; } const [foo, bar, baz] = input.getData('foo', 'bar', 'baz'); chai.expect(foo).to.be.a('string'); chai.expect(bar).to.be.undefined; chai.expect(baz).to.be.undefined; done(); }, }); c.inPorts.foo.attach(sin1); c.inPorts.bar.attach(sin2); c.inPorts.baz.attach(sin3); sin1.post(new noflo.IP('data', 'AZ')); sin2.post(new noflo.IP('data', true)); sin3.post(new noflo.IP('data', 'first')); }); it('should receive and send complete noflo.IP objects', (done) => { c = new noflo.Component({ inPorts: { foo: { datatype: 'string' }, bar: { datatype: 'string' }, }, outPorts: { baz: { datatype: 'object' }, }, process(input, output) { if (!input.has('foo', 'bar')) { return; } const [foo, bar] = input.get('foo', 'bar'); const baz = { foo: foo.data, bar: bar.data, groups: foo.groups, type: bar.type, }; output.sendDone({ baz: new noflo.IP('data', baz, { groups: ['baz'] }), }); }, }); c.inPorts.foo.attach(sin1); c.inPorts.bar.attach(sin2); c.outPorts.baz.attach(sout1); sout1.once('ip', (ip) => { chai.expect(ip).to.be.an('object'); chai.expect(ip.type).to.equal('data'); chai.expect(ip.data.foo).to.equal('foo'); chai.expect(ip.data.bar).to.equal('bar'); chai.expect(ip.data.groups).to.eql(['foo']); chai.expect(ip.data.type).to.equal('data'); chai.expect(ip.groups).to.eql(['baz']); done(); }); sin1.post(new noflo.IP('data', 'foo', { groups: ['foo'] })); sin2.post(new noflo.IP('data', 'bar', { groups: ['bar'] })); }); it('should stamp IP objects with the datatype of the outport when sending', (done) => { c = new noflo.Component({ inPorts: { foo: { datatype: 'all' }, }, outPorts: { baz: { datatype: 'string' }, }, process(input, output) { if (!input.has('foo')) { return; } const foo = input.get('foo'); output.sendDone({ baz: foo }); }, }); c.inPorts.foo.attach(sin1); c.outPorts.baz.attach(sout1); sout1.once('ip', (ip) => { chai.expect(ip).to.be.an('object'); chai.expect(ip.type).to.equal('data'); chai.expect(ip.data).to.equal('foo'); chai.expect(ip.datatype).to.equal('string'); done(); }); sin1.post(new noflo.IP('data', 'foo')); }); it('should stamp IP objects with the datatype of the inport when receiving', (done) => { c = new noflo.Component({ inPorts: { foo: { datatype: 'string' }, }, outPorts: { baz: { datatype: 'all' }, }, process(input, output) { if (!input.has('foo')) { return; } const foo = input.get('foo'); output.sendDone({ baz: foo }); }, }); c.inPorts.foo.attach(sin1); c.outPorts.baz.attach(sout1); sout1.once('ip', (ip) => { chai.expect(ip).to.be.an('object'); chai.expect(ip.type).to.equal('data'); chai.expect(ip.data).to.equal('foo'); chai.expect(ip.datatype).to.equal('string'); done(); }); sin1.post(new noflo.IP('data', 'foo')); }); it('should stamp IP objects with the schema of the outport when sending', (done) => { c = new noflo.Component({ inPorts: { foo: { datatype: 'all' }, }, outPorts: { baz: { datatype: 'string', schema: 'text/markdown', }, }, process(input, output) { if (!input.has('foo')) { return; } const foo = input.get('foo'); output.sendDone({ baz: foo }); }, }); c.inPorts.foo.attach(sin1); c.outPorts.baz.attach(sout1); sout1.once('ip', (ip) => { chai.expect(ip).to.be.an('object'); chai.expect(ip.type).to.equal('data'); chai.expect(ip.data).to.equal('foo'); chai.expect(ip.datatype).to.equal('string'); chai.expect(ip.schema).to.equal('text/markdown'); done(); }); sin1.post(new noflo.IP('data', 'foo')); }); it('should stamp IP objects with the schema of the inport when receiving', (done) => { c = new noflo.Component({ inPorts: { foo: { datatype: 'string', schema: 'text/markdown', }, }, outPorts: { baz: { datatype: 'all' }, }, process(input, output) { if (!input.has('foo')) { return; } const foo = input.get('foo'); output.sendDone({ baz: foo }); }, }); c.inPorts.foo.attach(sin1); c.outPorts.baz.attach(sout1); sout1.once('ip', (ip) => { chai.expect(ip).to.be.an('object'); chai.expect(ip.type).to.equal('data'); chai.expect(ip.data).to.equal('foo'); chai.expect(ip.datatype).to.equal('string'); chai.expect(ip.schema).to.equal('text/markdown'); done(); }); sin1.post(new noflo.IP('data', 'foo')); }); it('should receive and send just IP data if wanted', (done) => { c = new noflo.Component({ inPorts: { foo: { datatype: 'string' }, bar: { datatype: 'string' }, }, outPorts: { baz: { datatype: 'object' }, }, process(input, output) { if (!input.has('foo', 'bar')) { return; } const [foo, bar] = input.getData('foo', 'bar'); const baz = { foo, bar, }; output.sendDone({ baz }); }, }); c.inPorts.foo.attach(sin1); c.inPorts.bar.attach(sin2); c.outPorts.baz.attach(sout1); sout1.once('ip', (ip) => { chai.expect(ip).to.be.an('object'); chai.expect(ip.type).to.equal('data'); chai.expect(ip.data.foo).to.equal('foo'); chai.expect(ip.data.bar).to.equal('bar'); done(); }); sin1.post(new noflo.IP('data', 'foo', { groups: ['foo'] })); sin2.post(new noflo.IP('data', 'bar', { groups: ['bar'] })); }); it('should receive IPs and be able to selectively find them', (done) => { let called = 0; c = new noflo.Component({ inPorts: { foo: { datatype: 'string' }, bar: { datatype: 'string' }, }, outPorts: { baz: { datatype: 'object' }, }, process(input, output) { const validate = function (ip) { called++; return (ip.type === 'data') && (ip.data === 'hello'); }; if (!input.has('foo', 'bar', validate)) { return; } let foo = input.get('foo'); while ((foo != null ? foo.type : undefined) !== 'data') { foo = input.get('foo'); } const bar = input.getData('bar'); output.sendDone({ baz: `${foo.data}:${bar}` }); }, }); c.inPorts.foo.attach(sin1); c.inPorts.bar.attach(sin2); c.outPorts.baz.attach(sout1); let shouldHaveSent = false; sout1.on('ip', (ip) => { chai.expect(shouldHaveSent, 'Should not sent before its time').to.equal(true); chai.expect(ip).to.be.an('object'); chai.expect(ip.type).to.equal('data'); chai.expect(ip.data).to.equal('hello:hello'); chai.expect(called).to.equal(10); done(); }); sin1.post(new noflo.IP('openBracket', 'a')); sin1.post(new noflo.IP('data', 'hello', sin1.post(new noflo.IP('closeBracket', 'a')))); shouldHaveSent = true; sin2.post(new noflo.IP('data', 'hello')); }); it('should keep last value for controls', (done) => { c = new noflo.Component({ inPorts: { foo: { datatype: 'string' }, bar: { datatype: 'string', control: true, }, }, outPorts: { baz: { datatype: 'object' }, }, process(input, output) { if (!input.has('foo', 'bar')) { return; } const [foo, bar] = input.getData('foo', 'bar'); const baz = { foo, bar, }; output.sendDone({ baz }); }, }); c.inPorts.foo.attach(sin1); c.inPorts.bar.attach(sin2); c.outPorts.baz.attach(sout1); sout1.once('ip', (ip) => { chai.expect(ip).to.be.an('object'); chai.expect(ip.type).to.equal('data'); chai.expect(ip.data.foo).to.equal('foo'); chai.expect(ip.data.bar).to.equal('bar'); sout1.once('ip', (ip) => { chai.expect(ip).to.be.an('object'); chai.expect(ip.type).to.equal('data'); chai.expect(ip.data.foo).to.equal('boo'); chai.expect(ip.data.bar).to.equal('bar'); done(); }); }); sin1.post(new noflo.IP('data', 'foo')); sin2.post(new noflo.IP('data', 'bar')); sin1.post(new noflo.IP('data', 'boo')); }); it('should keep last data-typed IP packet for controls', (done) => { c = new noflo.Component({ inPorts: { foo: { datatype: 'string' }, bar: { datatype: 'string', control: true, }, }, outPorts: { baz: { datatype: 'object' }, }, process(input, output) { if (!input.has('foo', 'bar')) { return; } const [foo, bar] = input.getData('foo', 'bar'); const baz = { foo, bar, }; output.sendDone({ baz }); }, }); c.inPorts.foo.attach(sin1); c.inPorts.bar.attach(sin2); c.outPorts.baz.attach(sout1); sout1.once('ip', (ip) => { chai.expect(ip).to.be.an('object'); chai.expect(ip.type).to.equal('data'); chai.expect(ip.data.foo).to.equal('foo'); chai.expect(ip.data.bar).to.equal('bar'); sout1.once('ip', (ip) => { chai.expect(ip).to.be.an('object'); chai.expect(ip.type).to.equal('data'); chai.expect(ip.data.foo).to.equal('boo'); chai.expect(ip.data.bar).to.equal('bar'); done(); }); }); sin1.post(new noflo.IP('data', 'foo')); sin2.post(new noflo.IP('openBracket')); sin2.post(new noflo.IP('data', 'bar')); sin2.post(new noflo.IP('closeBracket')); sin1.post(new noflo.IP('data', 'boo')); }); it('should isolate packets with different scopes', (done) => { c = new noflo.Component({ inPorts: { foo: { datatype: 'string' }, bar: { datatype: 'string' }, }, outPorts: { baz: { datatype: 'string' }, }, process(input, output) { if (!input.has('foo', 'bar')) { return; } const [foo, bar] = input.getData('foo', 'bar'); output.sendDone({ baz: `${foo} and ${bar}` }); }, }); c.inPorts.foo.attach(sin1); c.inPorts.bar.attach(sin2); c.outPorts.baz.attach(sout1); sout1.once('ip', (ip) => { chai.expect(ip).to.be.an('object'); chai.expect(ip.type).to.equal('data'); chai.expect(ip.scope).to.equal('1'); chai.expect(ip.data).to.equal('Josh and Laura'); sout1.once('ip', (ip) => { chai.expect(ip).to.be.an('object'); chai.expect(ip.type).to.equal('data'); chai.expect(ip.scope).to.equal('2'); chai.expect(ip.data).to.equal('Jane and Luke'); done(); }); }); sin1.post(new noflo.IP('data', 'Josh', { scope: '1' })); sin2.post(new noflo.IP('data', 'Luke', { scope: '2' })); sin2.post(new noflo.IP('data', 'Laura', { scope: '1' })); sin1.post(new noflo.IP('data', 'Jane', { scope: '2' })); }); it('should be able to change scope', (done) => { c = new noflo.Component({ inPorts: { foo: { datatype: 'string' }, }, outPorts: { baz: { datatype: 'string' }, }, process(input, output) { const foo = input.getData('foo'); output.sendDone({ baz: new noflo.IP('data', foo, { scope: 'baz' }) }); }, }); c.inPorts.foo.attach(sin1); c.outPorts.baz.attach(sout1); sout1.once('ip', (ip) => { chai.expect(ip).to.be.an('object'); chai.expect(ip.type).to.equal('data'); chai.expect(ip.scope).to.equal('baz'); chai.expect(ip.data).to.equal('foo'); done(); }); sin1.post(new noflo.IP('data', 'foo', { scope: 'foo' })); }); it('should support integer scopes', (done) => { c = new noflo.Component({ inPorts: { foo: { datatype: 'string' }, bar: { datatype: 'string' }, }, outPorts: { baz: { datatype: 'string' }, }, process(input, output) { if (!input.has('foo', 'bar')) { return; } const [foo, bar] = input.getData('foo', 'bar'); output.sendDone({ baz: `${foo} and ${bar}` }); }, }); c.inPorts.foo.attach(sin1); c.inPorts.bar.attach(sin2); c.outPorts.baz.attach(sout1); sout1.once('ip', (ip) => { chai.expect(ip).to.be.an('object'); chai.expect(ip.type).to.equal('data'); chai.expect(ip.scope).to.equal(1); chai.expect(ip.data).to.equal('Josh and Laura'); sout1.once('ip', (ip) => { chai.expect(ip).to.be.an('object'); chai.expect(ip.type).to.equal('data'); chai.expect(ip.scope).to.equal(0); chai.expect(ip.data).to.equal('Jane and Luke'); sout1.once('ip', (ip) => { chai.expect(ip).to.be.an('object'); chai.expect(ip.type).to.equal('data'); chai.expect(ip.scope).to.be.null; chai.expect(ip.data).to.equal('Tom and Anna'); done(); }); }); }); sin1.post(new noflo.IP('data', 'Tom')); sin1.post(new noflo.IP('data', 'Josh', { scope: 1 })); sin2.post(new noflo.IP('data', 'Luke', { scope: 0 })); sin2.post(new noflo.IP('data', 'Laura', { scope: 1 })); sin1.post(new noflo.IP('data', 'Jane', { scope: 0 })); sin2.post(new noflo.IP('data', 'Anna')); }); it('should preserve order between input and output', (done) => { c = new noflo.Component({ inPorts: { msg: { datatype: 'string' }, delay: { datatype: 'int' }, }, outPorts: { out: { datatype: 'object' }, }, ordered: true, process(input, output) { if (!input.has('msg', 'delay')) { return; } const [msg, delay] = input.getData('msg', 'delay'); setTimeout(() => output.sendDone({ out: { msg, delay } }), delay); }, }); c.inPorts.msg.attach(sin1); c.inPorts.delay.attach(sin2); c.outPorts.out.attach(sout1); const sample = [ { delay: 30, msg: 'one' }, { delay: 0, msg: 'two' }, { delay: 20, msg: 'three' }, { delay: 10, msg: 'four' }, ]; sout1.on('ip', (ip) => { chai.expect(ip.data).to.eql(sample.shift()); if (sample.length === 0) { done(); } }); for (const ip of sample) { sin1.post(new noflo.IP('data', ip.msg)); sin2.post(new noflo.IP('data', ip.delay)); } }); it('should ignore order between input and output', (done) => { c = new noflo.Component({ inPorts: { msg: { datatype: 'string' }, delay: { datatype: 'int' }, }, outPorts: { out: { datatype: 'object' }, }, ordered: false, process(input, output) { if (!input.has('msg', 'delay')) { return; } const [msg, delay] = input.getData('msg', 'delay'); setTimeout(() => output.sendDone({ out: { msg, delay } }), delay); }, }); c.inPorts.msg.attach(sin1); c.inPorts.delay.attach(sin2); c.outPorts.out.attach(sout1); const sample = [ { delay: 30, msg: 'one' }, { delay: 0, msg: 'two' }, { delay: 20, msg: 'three' }, { delay: 10, msg: 'four' }, ]; let count = 0; sout1.on('ip', (ip) => { let src; count++; switch (count) { case 1: src = sample[1]; break; case 2: src = sample[3]; break; case 3: src = sample[2]; break; case 4: src = sample[0]; break; } chai.expect(ip.data).to.eql(src); if (count === 4) { done(); } }); for (const ip of sample) { sin1.post(new noflo.IP('data', ip.msg)); sin2.post(new noflo.IP('data', ip.delay)); } }); it('should throw errors if there is no error port', (done) => { c = new noflo.Component({ inPorts: { in: { datatype: 'string', required: true, }, }, process(input, output) { const packet = input.get('in'); chai.expect(packet.data).to.equal('some-data'); chai.expect(() => output.done(new Error('Should fail'))).to.throw(Error); done(); }, }); c.inPorts.in.attach(sin1); sin1.post(new noflo.IP('data', 'some-data')); }); it('should throw errors if there is a non-attached error port', (done) => { c = new noflo.Component({ inPorts: { in: { datatype: 'string', required: true, }, }, outPorts: { error: { datatype: 'object', required: true, }, }, process(input, output) { const packet = input.get('in'); chai.expect(packet.data).to.equal('some-data'); chai.expect(() => output.sendDone(new Error('Should fail'))).to.throw(Error); done(); }, }); c.inPorts.in.attach(sin1); sin1.post(new noflo.IP('data', 'some-data')); }); it('should not throw errors if there is a non-required error port', (done) => { c = new noflo.Component({ inPorts: { in: { datatype: 'string', required: true, }, }, outPorts: { error: { required: false, }, }, process(input, output) { const packet = input.get('in'); chai.expect(packet.data).to.equal('some-data'); output.sendDone(new Error('Should not fail')); done(); }, }); c.inPorts.in.attach(sin1); sin1.post(new noflo.IP('data', 'some-data')); }); it('should send out string other port if there is only one port aside from error', (done) => { c = new noflo.Component({ inPorts: { in: { datatype: 'all', required: true, }, }, outPorts: { out: { required: true, }, error: { required: false, }, }, process(input, output) { input.get('in'); output.sendDone('some data'); }, }); sout1.on('ip', (ip) => { chai.expect(ip).to.be.an('object'); chai.expect(ip.data).to.equal('some data'); done(); }); c.inPorts.in.attach(sin1); c.outPorts.out.attach(sout1); sin1.post(new noflo.IP('data', 'first')); }); it('should send object out other port if there is only one port aside from error', (done) => { c = new noflo.Component({ inPorts: { in: { datatype: 'all', required: true, }, }, outPorts: { out: { required: true, }, error: { required: false, }, }, process(input, output) { input.get('in'); output.sendDone({ some: 'data' }); }, }); sout1.on('ip', (ip) => { chai.expect(ip).to.be.an('object'); chai.expect(ip.data).to.eql({ some: 'data' }); done(); }); c.inPorts.in.attach(sin1); c.outPorts.out.attach(sout1); sin1.post(new noflo.IP('data', 'first')); }); it('should throw an error if sending without specifying a port and there are multiple ports', (done) => { const f = function () { c = new noflo.Component({ inPorts: { in: { datatype: 'string', required: true, }, }, outPorts: { out: { datatype: 'all', }, eh: { required: false, }, }, process(input, output) { output.sendDone('test'); }, }); c.inPorts.in.attach(sin1); sin1.post(new noflo.IP('data', 'some-data')); }; chai.expect(f).to.throw(Error); done(); }); it('should send errors if there is a connected error port', (done) => { c = new noflo.Component({ inPorts: { in: { datatype: 'string', required: true, }, }, outPorts: { error: { datatype: 'object', }, }, process(input, output) { const packet = input.get('in'); chai.expect(packet.data).to.equal('some-data'); chai.expect(packet.scope).to.equal('some-scope'); output.sendDone(new Error('Should fail')); }, }); sout1.on('ip', (ip) => { chai.expect(ip).to.be.an('object'); chai.expect(ip.data).to.be.an.instanceOf(Error); chai.expect(ip.scope).to.equal('some-scope'); done(); }); c.inPorts.in.attach(sin1); c.outPorts.error.attach(sout1); sin1.post(new noflo.IP('data', 'some-data', { scope: 'some-scope' })); }); it('should send substreams with multiple errors per activation', (done) => { c = new noflo.Component({ inPorts: { in: { datatype: 'string', required: true, }, }, outPorts: { error: { datatype: 'object', }, }, process(input, output) { const packet = input.get('in'); chai.expect(packet.data).to.equal('some-data'); chai.expect(packet.scope).to.equal('some-scope'); const errors = []; errors.push(new Error('One thing is invalid')); errors.push(new Error('Another thing is invalid')); output.sendDone(errors); }, }); const expected = [ '<', 'One thing is invalid', 'Another thing is invalid', '>', ]; const actual = []; let count = 0; sout1.on('ip', (ip) => { count++; chai.expect(ip).to.be.an('object'); chai.expect(ip.scope).to.equal('some-scope'); if (ip.type === 'openBracket') { actual.push('<'); } if (ip.type === 'closeBracket') { actual.push('>'); } if (ip.type === 'data') { chai.expect(ip.data).to.be.an.instanceOf(Error); actual.push(ip.data.message); } if (count === 4) { chai.expect(actual).to.eql(expected); done(); } }); c.inPorts.in.attach(sin1); c.outPorts.error.attach(sout1); sin1.post(new noflo.IP('data', 'some-data', { scope: 'some-scope' })); }); it('should forward brackets for map-style components', (done) => { c = new noflo.Component({ inPorts: { in: { datatype: 'string', }, }, outPorts: { out: { datatype: 'string', }, error: { datatype: 'object', }, }, process(input, output) { const str = input.getData(); if (typeof str !== 'string') { output.sendDone(new Error('Input is not string')); return; } output.pass(str.toUpperCase()); }, }); c.inPorts.in.attach(sin1); c.outPorts.out.attach(sout1); c.outPorts.error.attach(sout2); const source = [ '<', 'foo', 'bar', '>', ]; let count = 0; sout1.on('ip', (ip) => { const data = (() => { switch (ip.type) { case 'openBracket': return '<'; case 'closeBracket': return '>'; default: return ip.data; } })(); chai.expect(data).to.equal(source[count].toUpperCase()); count++; if (count === 4) { done(); } }); sout2.on('ip', (ip) => { if (ip.type !== 'data') { return; } console.log('Unexpected error', ip); done(ip.data); }); for (const data of source) { switch (data) { case '<': sin1.post(new noflo.IP('openBracket')); break; case '>': sin1.post(new noflo.IP('closeBracket')); break; default: sin1.post(new noflo.IP('data', data)); } } }); it('should forward brackets for map-style components with addressable outport', (done) => { let sent = false; c = new noflo.Component({ inPorts: { in: { datatype: 'string', }, }, outPorts: { out: { datatype: 'string', addressable: true, }, }, process(input, output) { if (!input.hasData()) { return; } const string = input.getData(); const idx = sent ? 0 : 1; sent = true; output.sendDone(new noflo.IP('data', string, { index: idx })); }, }); c.inPorts.in.attach(sin1); c.outPorts.out.attach(sout1, 1); c.outPorts.out.attach(sout2, 0); const expected = [ '1 < a', '1 < foo', '1 DATA first', '1 > foo', '0 < a', '0 < bar', '0 DATA second', '0 > bar', '0 > a', '1 > a', ]; const received = []; sout1.on('ip', (ip) => { switch (ip.type) { case 'openBracket': received.push(`1 < ${ip.data}`); break; case 'data': received.push(`1 DATA ${ip.data}`); break; case 'closeBracket': received.push(`1 > ${ip.data}`); break; } if (received.length !== expected.length) { return; } chai.expect(received).to.eql(expected); done(); }); sout2.on('ip', (ip) => { switch (ip.type) { case 'openBracket': received.push(`0 < ${ip.data}`); break; case 'data': received.push(`0 DATA ${ip.data}`); break; case 'closeBracket': received.push(`0 > ${ip.data}`); break; } if (received.length !== expected.length) { return; }