fbp-graph
Version:
JavaScript FBP graph library
801 lines • 33.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const lib = require("../lib/index");
const chai = require("chai");
const Platform_1 = require("../lib/Platform");
describe('FBP Graph', () => {
describe('with case sensitivity', () => {
describe('Unnamed graph instance', () => it('should have an empty name', () => {
const g = new lib.graph.Graph();
chai.expect(g.name).to.equal('');
}));
describe('with new instance', () => {
let g = new lib.graph.Graph('Foo bar', { caseSensitive: true });
it('should get a name from constructor', () => {
chai.expect(g.name).to.equal('Foo bar');
});
it('should have no nodes initially', () => {
chai.expect(g.nodes).to.eql([]);
});
it('should have no edges initially', () => {
chai.expect(g.edges).to.eql([]);
});
it('should have no initializers initially', () => {
chai.expect(g.initializers).to.eql([]);
});
it('should have no inports initially', () => {
chai.expect(g.inports).to.eql({});
});
it('should have no outports initially', () => {
chai.expect(g.outports).to.eql({});
});
describe('New node', () => {
let n;
it('should emit an event', (done) => {
g.once('addNode', (node) => {
chai.expect(node.id).to.equal('Foo');
chai.expect(node.component).to.equal('Bar');
n = node;
done();
});
g.addNode('Foo', 'Bar');
});
it('should be in graph\'s list of nodes', () => {
chai.expect(g.nodes.length).to.equal(1);
chai.expect(g.nodes.indexOf(n)).to.equal(0);
});
it('should be accessible via the getter', () => {
const node = g.getNode('Foo');
// @ts-ignore
chai.expect(node.id).to.equal('Foo');
chai.expect(node).to.equal(n);
});
it('should have empty metadata', () => {
const node = g.getNode('Foo');
// @ts-ignore
chai.expect(JSON.stringify(node.metadata)).to.equal('{}');
// @ts-ignore
chai.expect(node.display).to.equal(undefined);
});
it('should be available in the JSON export', () => {
const json = g.toJSON();
// @ts-ignore
chai.expect(typeof json.processes.Foo).to.equal('object');
// @ts-ignore
chai.expect(json.processes.Foo.component).to.equal('Bar');
// @ts-ignore
chai.expect(json.processes.Foo.display).to.be.a('undefined');
});
it('removing should emit an event', (done) => {
g.once('removeNode', (node) => {
chai.expect(node.id).to.equal('Foo');
chai.expect(node).to.equal(n);
done();
});
g.removeNode('Foo');
});
it('should not be available after removal', () => {
const node = g.getNode('Foo');
chai.expect(node).to.be.a('null');
chai.expect(g.nodes.length).to.equal(0);
chai.expect(g.nodes.indexOf(n)).to.equal(-1);
});
});
describe('New edge', () => {
it('should emit an event', (done) => {
g.addNode('Foo', 'foo');
g.addNode('Bar', 'bar');
g.once('addEdge', (edge) => {
chai.expect(edge.from.node).to.equal('Foo');
chai.expect(edge.to.port).to.equal('In');
done();
});
g.addEdge('Foo', 'Out', 'Bar', 'In');
});
it('should add an edge', () => {
g.addEdge('Foo', 'out', 'Bar', 'in2');
chai.expect(g.edges.length).equal(2);
});
it('should refuse to add a duplicate edge', () => {
const edge = g.edges[0];
g.addEdge(edge.from.node, edge.from.port, edge.to.node, edge.to.port);
chai.expect(g.edges.length).equal(2);
});
});
describe('New edge with index', () => {
it('should emit an event', (done) => {
g.once('addEdge', (edge) => {
chai.expect(edge.from.node).to.equal('Foo');
chai.expect(edge.to.port).to.equal('in');
chai.expect(edge.to.index).to.equal(1);
chai.expect(edge.from.index).to.be.an('undefined');
chai.expect(g.edges.length).equal(3);
done();
});
g.addEdgeIndex('Foo', 'out', undefined, 'Bar', 'in', 1);
});
it('should add an edge', () => {
// @ts-ignore
g.addEdgeIndex('Foo', 'out', 2, 'Bar', 'in2');
chai.expect(g.edges.length).equal(4);
});
});
});
describe('loaded from JSON (with case sensitive port names)', () => {
const jsonString = `\
{
"caseSensitive": true,
"properties": {
"name": "Example",
"foo": "Baz",
"bar": "Foo"
},
"inports": {
"inPut": {
"process": "Foo",
"port": "inPut",
"metadata": {
"x": 5,
"y": 100
}
}
},
"outports": {
"outPut": {
"process": "Bar",
"port": "outPut",
"metadata": {
"x": 500,
"y": 505
}
}
},
"groups": [
{
"name": "first",
"nodes": [
"Foo"
],
"metadata": {
"label": "Main"
}
},
{
"name": "second",
"nodes": [
"Foo2",
"Bar2"
]
}
],
"processes": {
"Foo": {
"component": "Bar",
"metadata": {
"display": {
"x": 100,
"y": 200
},
"routes": [
"one",
"two"
],
"hello": "World"
}
},
"Bar": {
"component": "Baz",
"metadata": {}
},
"Foo2": {
"component": "foo",
"metadata": {}
},
"Bar2": {
"component": "bar",
"metadata": {}
}
},
"connections": [
{
"src": {
"process": "Foo",
"port": "outPut"
},
"tgt": {
"process": "Bar",
"port": "inPut"
},
"metadata": {
"route": "foo",
"hello": "World"
}
},
{
"src": {
"process": "Foo",
"port": "out2"
},
"tgt": {
"process": "Bar",
"port": "in2",
"index": 2
},
"metadata": {
"route": "foo",
"hello": "World"
}
},
{
"data": "Hello, world!",
"tgt": {
"process": "Foo",
"port": "inPut"
}
},
{
"data": "Hello, world, 2!",
"tgt": {
"process": "Foo",
"port": "in2"
}
},
{
"data": "Cheers, world!",
"tgt": {
"process": "Foo",
"port": "arr",
"index": 0
}
},
{
"data": "Cheers, world, 2!",
"tgt": {
"process": "Foo",
"port": "arr",
"index": 1
}
}
]
}\
`;
const json = JSON.parse(jsonString);
let g;
it('should produce a Graph when input is string', () => lib.graph.loadJSON(jsonString)
.then((instance) => {
g = instance;
chai.expect(g).to.be.an('object');
}));
it('should produce a Graph when input is JSON', () => lib.graph.loadJSON(json)
.then((instance) => {
g = instance;
chai.expect(g).to.be.an('object');
}));
it('should not mutate the inputted json object', (done) => {
chai.expect(Object.keys(json.processes).length).to.equal(4);
lib.graph.loadJSON(json, (err, instance) => {
if (err) {
done(err);
return;
}
if (!instance) {
done(new Error('No graph loaded'));
return;
}
instance.addNode('Split1', 'Split');
instance.addNode('Split1', 'Split');
instance.addNode('Split1', 'Split');
instance.addNode('Split1', 'Split');
instance.addNode('Split1', 'Split');
instance.addNode('Split1', 'Split');
instance.addNode('Split1', 'Split');
instance.addNode('Split1', 'Split');
instance.addNode('Split1', 'Split');
instance.addNode('Split1', 'Split');
instance.addNode('Split1', 'Split');
chai.expect(Object.keys(json.processes).length).to.equal(4);
done();
});
});
it('should have a name', () => chai.expect(g.name).to.equal('Example'));
it('should have graph metadata intact', () => chai.expect(g.properties).to.eql({
foo: 'Baz',
bar: 'Foo',
}));
it('should produce same JSON when serialized', () => chai.expect(JSON.stringify(g.toJSON())).to.equal(JSON.stringify(json)));
it('should allow modifying graph metadata', (done) => {
g.once('changeProperties', (properties) => {
chai.expect(properties).to.equal(g.properties);
chai.expect(g.properties).to.eql({
foo: 'Baz',
bar: 'Bar',
hello: 'World',
});
done();
});
g.setProperties({
hello: 'World',
bar: 'Bar',
});
});
it('should contain four nodes', () => chai.expect(g.nodes.length).to.equal(4));
it('the first Node should have its metadata intact', () => {
const node = g.getNode('Foo');
chai.expect(node.metadata).to.be.an('object');
chai.expect(node.metadata.display).to.be.an('object');
chai.expect(node.metadata.display.x).to.equal(100);
chai.expect(node.metadata.display.y).to.equal(200);
chai.expect(node.metadata.routes).to.be.an('array');
chai.expect(node.metadata.routes).to.contain('one');
chai.expect(node.metadata.routes).to.contain('two');
});
it('should allow modifying node metadata', (done) => {
g.once('changeNode', (node) => {
chai.expect(node.id).to.equal('Foo');
chai.expect(node.metadata.routes).to.be.an('array');
chai.expect(node.metadata.routes).to.contain('one');
chai.expect(node.metadata.routes).to.contain('two');
chai.expect(node.metadata.hello).to.equal('World');
done();
});
g.setNodeMetadata('Foo', { hello: 'World' });
});
it('should contain two connections', () => chai.expect(g.edges.length).to.equal(2));
it('the first Edge should have its metadata intact', () => {
const edge = g.edges[0];
chai.expect(edge.metadata).to.be.an('object');
chai.expect(edge.metadata.route).equal('foo');
});
it('should allow modifying edge metadata', (done) => {
const e = g.edges[0];
g.once('changeEdge', (edge) => {
chai.expect(edge).to.equal(e);
chai.expect(edge.metadata.route).to.equal('foo');
chai.expect(edge.metadata.hello).to.equal('World');
done();
});
g.setEdgeMetadata(e.from.node, e.from.port, e.to.node, e.to.port, { hello: 'World' });
});
it('should contain four IIPs', () => {
chai.expect(g.initializers.length).to.equal(4);
});
it('should contain one published inport', () => {
chai.expect(Object.keys(g.inports).length).to.equal(1);
});
it('should contain one published outport', () => {
chai.expect(Object.keys(g.outports).length).to.equal(1);
});
it('should keep the output export metadata intact', () => {
const exp = g.outports.outPut;
chai.expect(exp.metadata.x).to.equal(500);
chai.expect(exp.metadata.y).to.equal(505);
});
it('should contain two groups', () => chai.expect(g.groups.length).to.equal(2));
it('should allow modifying group metadata', (done) => {
const group = g.groups[0];
g.once('changeGroup', (grp) => {
chai.expect(grp).to.equal(group);
chai.expect(grp.metadata.label).to.equal('Main');
chai.expect(grp.metadata.foo).to.equal('Bar');
chai.expect(g.groups[1].metadata).to.eql({});
done();
});
g.setGroupMetadata('first', { foo: 'Bar' });
});
it('should allow renaming groups', (done) => {
const group = g.groups[0];
g.once('renameGroup', (oldName, newName) => {
chai.expect(oldName).to.equal('first');
chai.expect(newName).to.equal('renamed');
chai.expect(group.name).to.equal(newName);
done();
});
g.renameGroup('first', 'renamed');
});
describe('renaming a node', () => {
it('should emit an event', (done) => {
g.once('renameNode', (oldId, newId) => {
chai.expect(oldId).to.equal('Foo');
chai.expect(newId).to.equal('Baz');
done();
});
g.renameNode('Foo', 'Baz');
});
it('should be available with the new name', () => {
chai.expect(g.getNode('Baz')).to.be.an('object');
});
it('shouldn\'t be available with the old name', () => {
chai.expect(g.getNode('Foo')).to.be.a('null');
});
it('should have the edge still going from it', () => {
let connection = null;
g.edges.forEach((edge) => {
if (edge.from.node === 'Baz') {
connection = edge;
}
});
chai.expect(connection).to.be.an('object');
});
it('should still be exported', () => chai.expect(g.inports.inPut.process).to.equal('Baz'));
it('should still be grouped', () => {
let groups = 0;
g.groups.forEach((group) => {
if (group.nodes.indexOf('Baz') !== -1) {
groups += 1;
}
});
chai.expect(groups).to.equal(1);
});
it('shouldn\'t be have edges with the old name', () => {
let connection = null;
g.edges.forEach((edge) => {
if (edge.from.node === 'Foo') {
connection = edge;
}
if (edge.to.node === 'Foo') {
connection = edge;
}
});
chai.expect(connection).to.be.a('null');
});
it('should have the IIP still going to it', () => {
let iip = null;
g.initializers.forEach((edge) => {
if (edge.to.node === 'Baz') {
iip = edge;
}
});
chai.expect(iip).to.be.an('object');
});
it('shouldn\'t have IIPs going to the old name', () => {
let iip = null;
g.initializers.forEach((edge) => {
if (edge.to.node === 'Foo') {
iip = edge;
}
});
chai.expect(iip).to.be.a('null');
});
it('shouldn\'t be grouped with the old name', () => {
let groups = 0;
g.groups.forEach((group) => {
if (group.nodes.indexOf('Foo') !== -1) {
groups += 1;
}
});
chai.expect(groups).to.equal(0);
});
});
describe('renaming an inport', () => {
it('should emit an event', (done) => {
g.once('renameInport', (oldName, newName) => {
chai.expect(oldName).to.equal('inPut');
chai.expect(newName).to.equal('opt');
chai.expect(g.inports.inPut).to.be.an('undefined');
chai.expect(g.inports.opt).to.be.an('object');
chai.expect(g.inports.opt.process).to.equal('Baz');
chai.expect(g.inports.opt.port).to.equal('inPut');
done();
});
g.renameInport('inPut', 'opt');
});
});
describe('renaming an outport', () => {
it('should emit an event', (done) => {
g.once('renameOutport', (oldName, newName) => {
chai.expect(oldName).to.equal('outPut');
chai.expect(newName).to.equal('foo');
chai.expect(g.outports.outPut).to.be.an('undefined');
chai.expect(g.outports.foo).to.be.an('object');
chai.expect(g.outports.foo.process).to.equal('Bar');
chai.expect(g.outports.foo.port).to.equal('outPut');
done();
});
g.renameOutport('outPut', 'foo');
});
});
describe('removing a node', () => {
it('should emit an event', (done) => {
g.once('removeNode', (node) => {
chai.expect(node.id).to.equal('Baz');
done();
});
g.removeNode('Baz');
});
it('shouldn\'t have edges left behind', () => {
let connections = 0;
g.edges.forEach((edge) => {
if (edge.from.node === 'Baz') {
connections += 1;
}
if (edge.to.node === 'Baz') {
connections += 1;
}
});
chai.expect(connections).to.equal(0);
});
it('shouldn\'t have IIPs left behind', () => {
let connections = 0;
g.initializers.forEach((edge) => {
if (edge.to.node === 'Baz') {
connections += 1;
}
});
chai.expect(connections).to.equal(0);
});
it('shouldn\'t be grouped', () => {
let groups = 0;
g.groups.forEach((group) => {
if (group.nodes.indexOf('Baz') !== -1) {
groups += 1;
}
});
chai.expect(groups).to.equal(0);
});
it('shouldn\'t affect other groups', () => {
const otherGroup = g.groups[0];
chai.expect(otherGroup.nodes.length).to.equal(2);
});
});
});
describe('with multiple connected ArrayPorts', () => {
const g = new lib.graph.Graph();
g.addNode('Split1', 'Split');
g.addNode('Split2', 'Split');
g.addNode('Merge1', 'Merge');
g.addNode('Merge2', 'Merge');
g.addEdge('Split1', 'out', 'Merge1', 'in');
g.addEdge('Split1', 'out', 'Merge2', 'in');
g.addEdge('Split2', 'out', 'Merge1', 'in');
g.addEdge('Split2', 'out', 'Merge2', 'in');
it('should contain four nodes', () => chai.expect(g.nodes.length).to.equal(4));
it('should contain four edges', () => chai.expect(g.edges.length).to.equal(4));
it('should allow a specific edge to be removed', () => {
g.removeEdge('Split1', 'out', 'Merge2', 'in');
chai.expect(g.edges.length).to.equal(3);
});
it('shouldn\'t contain the removed connection from Split1', () => {
let connection;
g.edges.forEach((edge) => {
if ((edge.from.node === 'Split1') && (edge.to.node === 'Merge2')) {
connection = edge;
}
});
chai.expect(connection).to.be.a('undefined');
});
it('should still contain the other connection from Split1', () => {
let connection;
g.edges.forEach((edge) => {
if ((edge.from.node === 'Split1') && (edge.to.node === 'Merge1')) {
connection = edge;
}
});
chai.expect(connection).to.be.an('object');
});
});
describe('with an Initial Information Packet', () => {
const g = new lib.graph.Graph();
g.addNode('Split', 'Split');
g.addInitial('Foo', 'Split', 'in');
it('should contain one node', () => chai.expect(g.nodes.length).to.equal(1));
it('should contain no edges', () => chai.expect(g.edges.length).to.equal(0));
it('should contain one IIP', () => chai.expect(g.initializers.length).to.equal(1));
describe('on removing that IIP', () => {
it('should emit a removeInitial event', (done) => {
g.once('removeInitial', (iip) => {
chai.expect(iip.from.data).to.equal('Foo');
chai.expect(iip.to.node).to.equal('Split');
chai.expect(iip.to.port).to.equal('in');
done();
});
g.removeInitial('Split', 'in');
});
it('should contain no IIPs', () => chai.expect(g.initializers.length).to.equal(0));
});
});
describe('with an Inport Initial Information Packet', () => {
const g = new lib.graph.Graph();
g.addNode('Split', 'Split');
g.addInport('testinport', 'Split', 'in');
g.addGraphInitial('Foo', 'testinport');
it('should contain one node', () => chai.expect(g.nodes.length).to.equal(1));
it('should contain no edges', () => chai.expect(g.edges.length).to.equal(0));
it('should contain one IIP for the correct node', () => {
chai.expect(g.initializers.length).to.equal(1);
chai.expect(g.initializers[0].from.data).to.equal('Foo');
chai.expect(g.initializers[0].to.node).to.equal('Split');
chai.expect(g.initializers[0].to.port).to.equal('in');
});
describe('on removing that IIP', () => {
it('should emit a removeInitial event', (done) => {
g.once('removeInitial', (iip) => {
chai.expect(iip.from.data).to.equal('Foo');
chai.expect(iip.to.node).to.equal('Split');
chai.expect(iip.to.port).to.equal('in');
done();
});
g.removeGraphInitial('testinport');
});
it('should contain no IIPs', () => chai.expect(g.initializers.length).to.equal(0));
});
describe('on adding IIP for a non-existent inport', () => {
g.addGraphInitial('Bar', 'nonexistent');
it('should not add any IIP', () => chai.expect(g.initializers.length).to.equal(0));
});
});
describe('with an indexed Inport Initial Information Packet', () => {
const g = new lib.graph.Graph();
g.addNode('Split', 'Split');
g.addInport('testinport', 'Split', 'in');
g.addGraphInitialIndex('Foo', 'testinport', 1);
it('should contain one node', () => {
chai.expect(g.nodes.length).to.equal(1);
});
it('should contain no edges', () => {
chai.expect(g.edges).to.eql([]);
});
it('should contain one IIP for the correct node', () => {
chai.expect(g.initializers.length).to.equal(1);
chai.expect(g.initializers[0].from.data).to.equal('Foo');
chai.expect(g.initializers[0].to.node).to.equal('Split');
chai.expect(g.initializers[0].to.port).to.equal('in');
chai.expect(g.initializers[0].to.index).to.equal(1);
});
describe('on removing that IIP', () => {
it('should emit a removeInitial event', (done) => {
g.once('removeInitial', (iip) => {
chai.expect(iip.from.data).to.equal('Foo');
chai.expect(iip.to.node).to.equal('Split');
chai.expect(iip.to.port).to.equal('in');
done();
});
g.removeGraphInitial('testinport');
});
it('should contain no IIPs', () => chai.expect(g.initializers.length).to.equal(0));
});
describe('on adding IIP for a non-existent inport', () => {
g.addGraphInitialIndex('Bar', 'nonexistent', 1);
it('should not add any IIP', () => chai.expect(g.initializers.length).to.equal(0));
});
});
describe('with no nodes', () => {
const g = new lib.graph.Graph();
it('should not allow adding edges', () => {
g.addEdge('Foo', 'out', 'Bar', 'in');
chai.expect(g.edges).to.eql([]);
});
it('should not allow adding IIPs', () => {
g.addInitial('Hello', 'Bar', 'in');
chai.expect(g.initializers).to.eql([]);
});
});
describe('saving and loading files', () => {
describe('with .json suffix', () => {
let originalGraph;
let graphPath;
before(function () {
if (Platform_1.isBrowser()) {
this.skip();
return;
}
// eslint-disable-next-line global-require
const path = require('path');
graphPath = path.resolve(__dirname, 'foo.json');
});
after((done) => {
if (Platform_1.isBrowser()) {
done();
return;
}
// eslint-disable-next-line global-require
const fs = require('fs');
fs.unlink(graphPath, done);
});
it('should be possible to save a graph to a file', () => {
const g = new lib.graph.Graph();
g.addNode('Foo', 'Bar');
originalGraph = g.toJSON();
return g.save(graphPath);
});
it('should be possible to save a graph to a file with a callback', (done) => {
const g = new lib.graph.Graph();
g.addNode('Foo', 'Bar');
originalGraph = g.toJSON();
g.save(graphPath, done);
});
it('should be possible to load a graph from a file', () => lib.graph.loadFile(graphPath)
.then((g) => {
chai.expect(g).to.be.an('object');
chai.expect(g.toJSON()).to.eql(originalGraph);
}));
it('should be possible to load a graph from a file with a callback', (done) => {
lib.graph.loadFile(graphPath, (err, g) => {
if (err) {
done(err);
return;
}
if (!g) {
done(new Error('No graph'));
return;
}
chai.expect(g.toJSON()).to.eql(originalGraph);
done();
});
});
});
describe('without .json suffix', () => {
let graphPathLegacy;
let graphPathLegacySuffix;
let originalGraph;
before(function () {
if (Platform_1.isBrowser()) {
this.skip();
return;
}
// eslint-disable-next-line global-require
const path = require('path');
graphPathLegacy = path.resolve(__dirname, 'bar');
graphPathLegacySuffix = path.resolve(__dirname, 'bar.json');
});
after((done) => {
if (Platform_1.isBrowser()) {
done();
return;
}
// eslint-disable-next-line global-require
const fs = require('fs');
fs.unlink(graphPathLegacySuffix, done);
});
it('should be possible to save a graph to a file', (done) => {
const g = new lib.graph.Graph();
g.addNode('Foo', 'Bar');
originalGraph = g.toJSON();
g.save(graphPathLegacy, done);
});
it('should be possible to load a graph from a file', (done) => {
lib.graph.loadFile(graphPathLegacySuffix, (err, g) => {
if (err) {
done(err);
return;
}
if (!g) {
done(new Error('No graph'));
return;
}
chai.expect(g.toJSON()).to.eql(originalGraph);
done();
});
});
});
});
});
describe('without case sensitivity', () => {
describe('Graph operations should convert port names to lowercase', () => {
let g;
beforeEach(() => {
g = new lib.graph.Graph('Hola');
});
it('should have case sensitive property set to false', () => {
chai.expect(g.caseSensitive).to.equal(false);
});
it('should have case insensitive ports on edges', (done) => {
g.addNode('Foo', 'foo');
g.addNode('Bar', 'bar');
g.once('addEdge', (edge) => {
chai.expect(edge.from.node).to.equal('Foo');
chai.expect(edge.to.port).to.equal('input');
chai.expect(edge.from.port).to.equal('output');
g.once('removeEdge', () => {
setTimeout(() => {
chai.expect(g.edges).to.eql([]);
done();
}, 0);
});
g.removeEdge('Foo', 'outPut', 'Bar', 'inPut');
});
g.addEdge('Foo', 'outPut', 'Bar', 'inPut');
});
});
});
});
//# sourceMappingURL=Graph.js.map