noflo
Version:
Flow-Based Programming environment for JavaScript
1,743 lines (1,641 loc) • 87.6 kB
JavaScript
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; }