Initial commit

Co-Authored-By: Eric Tuvesson <eric.tuvesson@gmail.com>
Co-Authored-By: mikaeltellhed <2311083+mikaeltellhed@users.noreply.github.com>
Co-Authored-By: kotte <14197736+mrtamagotchi@users.noreply.github.com>
Co-Authored-By: Anders Larsson <64838990+anders-topp@users.noreply.github.com>
Co-Authored-By: Johan  <4934465+joolsus@users.noreply.github.com>
Co-Authored-By: Tore Knudsen <18231882+torekndsn@users.noreply.github.com>
Co-Authored-By: victoratndl <99176179+victoratndl@users.noreply.github.com>
This commit is contained in:
Michael Cartner
2024-01-26 11:52:55 +01:00
commit b9c60b07dc
2789 changed files with 868795 additions and 0 deletions

View File

@@ -0,0 +1,372 @@
const NodeGraphModel = require('@noodl-models/nodegraphmodel').NodeGraphModel;
const NodeGraphNode = require('@noodl-models/nodegraphmodel').NodeGraphNode;
const { ComponentModel } = require('@noodl-models/componentmodel');
const NodeLibrary = require('@noodl-models/nodelibrary').NodeLibrary;
const { ProjectModel } = require('@noodl-models/projectmodel');
describe('Connecting component inputs and outputs', function () {
var g1, c1;
var ci, co, n1, n2, co2;
var p;
var con1, con2;
beforeEach(() => {
window.NodeLibraryData = require('../nodegraph/nodelibrary');
NodeLibrary.instance.loadLibrary();
g1 = new NodeGraphModel();
c1 = new ComponentModel({
name: 'c1',
graph: g1
});
ci = NodeGraphNode.fromJSON({
type: 'Component Inputs',
id: 'A'
});
g1.addRoot(ci);
co = NodeGraphNode.fromJSON({
type: 'Component Outputs',
id: 'B'
});
g1.addRoot(co);
co2 = NodeGraphNode.fromJSON({
type: 'Component Outputs',
id: 'B2'
});
g1.addRoot(co2);
n1 = NodeGraphNode.fromJSON({
type: 'group',
id: 'C'
});
g1.addRoot(n1);
n2 = NodeGraphNode.fromJSON({
type: 'group',
id: 'D'
});
g1.addRoot(n2);
p = new ProjectModel({
name: 'test'
});
p.addComponent(c1);
NodeLibrary.instance.registerModule(p);
});
afterEach(() => {
NodeLibrary.instance.unregisterModule(p);
});
function addPorts() {
c1.graph.findNodeWithId('B').addPort({
name: 'p1',
plug: 'input',
type: '*',
index: 0
});
c1.graph.findNodeWithId('B').addPort({
name: 'p2',
plug: 'input',
type: '*',
index: 1
});
c1.graph.findNodeWithId('A').addPort({
name: 'p3',
plug: 'output',
type: '*',
index: 2
});
c1.graph.findNodeWithId('B2').addPort({
name: 'p1',
plug: 'input',
type: '*',
index: 0
});
}
function addConnections() {
con1 = {
fromId: n1.id,
fromProperty: 'width',
toId: co.id,
toProperty: 'p1'
};
g1.addConnection(con1);
con2 = {
fromId: ci.id,
fromProperty: 'p3',
toId: n1.id,
toProperty: 'x'
};
g1.addConnection(con2);
}
// ------------------------------------------------------------------------------------------------------
it('can add input ports to the component', function () {
addPorts();
var ports = c1.getPorts();
expect(ports.length).toBe(3);
expect(ports[0].name).toBe('p1');
expect(ports[0].plug).toBe('output');
expect(ports[0].type).toBe('*');
expect(ports[1].name).toBe('p2');
expect(ports[1].plug).toBe('output');
expect(ports[1].type).toBe('*');
expect(ports[2].name).toBe('p3');
expect(ports[2].plug).toBe('input');
expect(ports[2].type).toBe('*');
});
// ------------------------------------------------------------------------------------------------------
it('can list ports correctly for component input/outputs', function () {
addPorts();
var ports = c1.graph.findNodeWithId('B').getPorts('input');
expect(ports[0].name).toBe('p1');
expect(ports[1].name).toBe('p2');
expect(ports[0].plug).toBe('input');
expect(ports[1].plug).toBe('input');
expect(ports[0].type).toBe('*');
expect(ports[1].type).toBe('*');
expect(ports.length).toBe(2);
var ports = c1.graph.findNodeWithId('A').getPorts('output');
expect(ports[0].name).toBe('p3');
expect(ports[0].plug).toBe('output');
expect(ports[0].type).toBe('*');
expect(ports.length).toBe(1);
});
// ------------------------------------------------------------------------------------------------------
it('reports connection status properly', function () {
addPorts();
// component input
var status = g1.getConnectionStatus({
sourceNode: ci,
sourcePort: 'p1',
targetNode: n1,
targetPort: 'width'
});
expect(status.connectable).toBe(true);
// number to number
var status = g1.getConnectionStatus({
sourceNode: n1,
sourcePort: 'screenX',
targetNode: n2,
targetPort: 'width'
});
expect(status.connectable).toBe(true);
// component outputs
var status = g1.getConnectionStatus({
sourceNode: n1,
sourcePort: 'width',
targetNode: co,
targetPort: 'p1'
});
expect(status.connectable).toBe(true);
});
// ------------------------------------------------------------------------------------------------------
it('can rename ports', function () {
addPorts();
addConnections();
expect(c1.graph.findNodeWithId('B').renamePortWithName('p1', 'p2')).toBe(false);
expect(c1.graph.findNodeWithId('B').renamePortWithName('p1', 'p1b')).toBe(true);
expect(c1.graph.findNodeWithId('A').renamePortWithName('p3', 'p3b')).toBe(true);
expect(con2.fromProperty).toBe('p3b');
expect(con1.toProperty).toBe('p1b');
expect(c1.graph.findNodeWithId('B').renamePortWithName('p1b', 'p1')).toBe(true);
expect(c1.graph.findNodeWithId('A').renamePortWithName('p3b', 'p3')).toBe(true);
});
// ------------------------------------------------------------------------------------------------------
it('cannot remove ports with connections', function () {
addPorts();
addConnections();
expect(c1.graph.findNodeWithId('B').removePortWithName('p1')).toBe(false);
expect(c1.graph.findNodeWithId('B').removePortWithName('p2')).toBe(true);
expect(c1.graph.findNodeWithId('A').removePortWithName('p3')).toBe(false);
expect(c1.findPortWithName('p2')).toBe(undefined);
expect(c1.findPortWithName('p1').name).toBe('p1');
});
// ------------------------------------------------------------------------------------------------------
it('can add more connections are report type correctly', function () {
addPorts();
addConnections();
g1.removeConnection(con1);
g1.removeConnection(con2);
// Single type, should just be number
g1.addConnection({
fromId: n1.id,
fromProperty: 'screenX',
toId: co2.id,
toProperty: 'p1'
});
expect(NodeLibrary.nameForPortType(c1.findPortWithName('p1').type)).toBe('number');
// Connect a number and boolean to the components outputs, but on different
// nodes
g1.addConnection({
fromId: n1.id,
fromProperty: 'clipOut',
toId: co.id,
toProperty: 'p1'
});
// Resulting type should be boolean (the only type that can be converted to from both boolean and number)
expect(NodeLibrary.nameForPortType(c1.findPortWithName('p1').type)).toBe('boolean');
// Adding a connection to a star type should not affect the resulting type
g1.addConnection({
fromId: ci.id,
fromProperty: 'p3',
toId: co2.id,
toProperty: 'p1'
});
expect(NodeLibrary.nameForPortType(c1.findPortWithName('p1').type)).toBe('boolean');
// Adding a reference type connection will cause it to break down, no type can be found
g1.addConnection({
fromId: n1.id,
fromProperty: 'this',
toId: co2.id,
toProperty: 'p1'
});
expect(c1.findPortWithName('p1').type).toBe('*');
});
it('can report plug correctly', function () {
addPorts();
expect(c1.findPortWithName('p1').plug).toBe('output');
expect(c1.findPortWithName('p3').plug).toBe('input');
// Adding an extra port of mixed type for p1 should change the plug
c1.graph.findNodeWithId('A').addPort({
name: 'p1',
plug: 'output',
type: '*'
});
expect(c1.getPorts().length).toBe(4);
expect(c1.getPorts()).toContain({
name: 'p1',
type: '*',
default: undefined,
group: undefined,
plug: 'output',
index: 0
});
expect(c1.getPorts()).toContain({
name: 'p1',
type: '*',
default: undefined,
group: undefined,
plug: 'input',
index: 1
});
});
it('can ignore * types', function () {
addPorts();
addConnections();
// Remove all connections
while (g1.connections.length > 0) {
g1.removeConnection(g1.connections[0]);
}
// Number type
g1.addConnection({
fromId: n1.id,
fromProperty: 'screenX',
toId: co.id,
toProperty: 'p1'
});
expect(c1.findPortWithName('p1').type).toBe('number');
// Add a connection to a * type
g1.addConnection({
fromId: ci.id,
fromProperty: 'p3',
toId: co.id,
toProperty: 'p1'
});
// Should just ignore * type and still be number
expect(c1.findPortWithName('p1').type).toBe('number');
});
it('can support units with default in types', function () {
addPorts();
addConnections();
// Remove all connections
while (g1.connections.length > 0) {
g1.removeConnection(g1.connections[0]);
}
// Add a connection to a type with units
g1.addConnection({
fromId: ci.id,
fromProperty: 'p3',
toId: n1.id,
toProperty: 'x'
});
// Should just ignore * type and still be number
expect(c1.findPortWithName('p3').type.name).toBe('number');
expect(c1.findPortWithName('p3').type.units).toEqual(['px', '%']);
expect(c1.findPortWithName('p3').default).toEqual({
value: 10,
unit: '%'
});
expect(c1.findPortWithName('p3').group).toBe('test'); // Should inherit from connected port
//Change parameter value should propagate to default
n1.setParameter('x', {
value: 50,
unit: 'px'
});
expect(c1.findPortWithName('p3').default).toEqual({
value: 50,
unit: 'px'
});
});
it('can rearrange ports', function () {
addPorts();
addConnections();
ci.arrangePort('p3', undefined, 'G1');
expect(c1.findPortWithName('p3').group).toBe('G1');
});
});

View File

@@ -0,0 +1,322 @@
const { ProjectModel } = require('@noodl-models/projectmodel');
const NodeLibrary = require('@noodl-models/nodelibrary').NodeLibrary;
const NodeGraphModel = require('@noodl-models/nodegraphmodel').NodeGraphModel;
const NodeGraphNode = require('@noodl-models/nodegraphmodel').NodeGraphNode;
describe('Component instances', function () {
var c1, c2;
var p, p2;
var w;
beforeEach(() => {
window.NodeLibraryData = require('../nodegraph/nodelibrary');
NodeLibrary.instance.loadLibrary();
p = ProjectModel.fromJSON(project);
NodeLibrary.instance.registerModule(p);
c1 = p.getComponentWithName('test');
c2 = p.getComponentWithName('Root');
});
afterEach(() => {
NodeLibrary.instance.unregisterModule(p);
});
it('can load project', function () {
expect(p).not.toBe(undefined);
});
it('can rename component inputs and outputs', function () {
expect(c1.graph.findNodeWithId('46a72429-263c-2ad1-3ada-ce8a98b27308').renamePortWithName('p1', 'p1b')).toBe(true);
expect(c1.graph.findNodeWithId('c01d18bc-b8cd-e792-6fd8-89694ef8da48').renamePortWithName('p3', 'p3b')).toBe(true);
expect(c2.graph.connections[0].toProperty).toBe('p1b');
expect(c2.graph.connections[1].fromProperty).toBe('p3b');
expect(c2.graph.findNodeWithId('56b67ac1-224e-c3c8-b058-6fee9c590bee').parameters['p1b']).toBe(50);
expect(c2.graph.findNodeWithId('56b67ac1-224e-c3c8-b058-6fee9c590bee').parameters['p1']).toBe(undefined);
});
xit('can detect unhealthy connections', function () {
c2.graph.evaluateHealth();
expect(
c2.graph.getConnectionHealth({
sourceId: '56b67ac1-224e-c3c8-b058-6fee9c590bee',
sourcePort: 'p3',
targetId: 'c0210ab9-94ab-c4c8-313b-3b394d5361f6',
targetPort: 'opacity'
}).healthy
).toBe(true);
// Remove p3 connection
c1.graph.removeConnection(c1.graph.connections[2]);
c2.graph.evaluateHealth();
expect(
c2.graph.getConnectionHealth({
sourceId: '56b67ac1-224e-c3c8-b058-6fee9c590bee',
sourcePort: 'p3b',
targetId: 'c0210ab9-94ab-c4c8-313b-3b394d5361f6',
targetPort: 'opacity'
}).healthy
).toBe(false);
// Connect p3 to a port of different type
c1.graph.addConnection({
fromId: 'f89b4fcd-5cfe-c47e-7fab-03948aad878c',
fromProperty: 'this',
toId: 'c01d18bc-b8cd-e792-6fd8-89694ef8da48',
toProperty: 'p3b'
});
// Health should still be bad (wrong type)
expect(
c2.graph.getConnectionHealth({
sourceId: '56b67ac1-224e-c3c8-b058-6fee9c590bee',
sourcePort: 'p3b',
targetId: 'c0210ab9-94ab-c4c8-313b-3b394d5361f6',
targetPort: 'opacity'
}).healthy
).toBe(false);
// Remove and restore
c1.graph.removeConnection(c1.graph.connections[2]);
c1.graph.addConnection({
fromId: 'f89b4fcd-5cfe-c47e-7fab-03948aad878c',
fromProperty: 'screenY',
toId: 'c01d18bc-b8cd-e792-6fd8-89694ef8da48',
toProperty: 'p3b'
});
c2.graph.evaluateHealth();
// Health should be back up
expect(
c2.graph.getConnectionHealth({
sourceId: '56b67ac1-224e-c3c8-b058-6fee9c590bee',
sourcePort: 'p3b',
targetId: 'c0210ab9-94ab-c4c8-313b-3b394d5361f6',
targetPort: 'opacity'
}).healthy
).toBe(true);
});
it('can detect unhealthy nodes', function () {
c2.graph.evaluateHealth();
// Multiple roots with same category is OK
var n = c2.graph.findNodeWithId('56b67ac1-224e-c3c8-b058-6fee9c590bee');
expect(n.getHealth().healthy).toBe(true);
c2.graph.removeNode(c2.graph.findNodeWithId('97720c17-1ff1-f4b0-8f97-8d70cf795348'));
c2.graph.removeNode(c2.graph.findNodeWithId('c0210ab9-94ab-c4c8-313b-3b394d5361f6'));
c2.graph.evaluateHealth();
expect(n.getHealth().healthy).toBe(true);
// The component cannot have visual children
var nn = NodeGraphNode.fromJSON({
type: 'group',
id: 'A'
});
n.addChild(nn);
c2.graph.evaluateHealth();
expect(n.getHealth().healthy).toBe(false);
// Adding a component children node will do it
c1.graph.findNodeWithId('f89b4fcd-5cfe-c47e-7fab-03948aad878c').addChild(
NodeGraphNode.fromJSON({
type: 'Component Children',
id: 'CC'
})
);
c2.graph.evaluateHealth();
expect(n.getHealth().healthy).toBe(true);
// Remove the visual root of the test component, health will become false again
c1.graph.removeNode(c1.graph.findNodeWithId('f89b4fcd-5cfe-c47e-7fab-03948aad878c'));
c2.graph.evaluateHealth();
expect(n.getHealth().healthy).toBe(false);
});
xit('component renamed are propageted to component references', function () {
var c3 = p.getComponentWithName('/has_comp_ref');
var c4 = p.getComponentWithName('/to_be_renamed');
var n = c3.graph.findNodeWithId('has_comp_ref_group');
expect(n.parameters['compref']).toBe('/to_be_renamed');
c4.rename('/is_renamed');
expect(n.parameters['compref']).toBe('/is_renamed');
});
var project = {
name: 'proj1',
components: [
{
name: 'outside',
graph: {
roots: [
{
id: 'AABB',
type: 'Component Inputs',
parameters: {},
children: [],
ports: [
{
name: 'p1',
plug: 'output',
type: '='
}
]
}
]
}
},
{
name: 'Root',
ports: [],
graph: {
connections: [
{
fromId: '97720c17-1ff1-f4b0-8f97-8d70cf795348',
fromProperty: 'screenX',
toId: '56b67ac1-224e-c3c8-b058-6fee9c590bee',
toProperty: 'p1'
},
{
fromId: '56b67ac1-224e-c3c8-b058-6fee9c590bee',
fromProperty: 'p3',
toId: 'c0210ab9-94ab-c4c8-313b-3b394d5361f6',
toProperty: 'opacity'
}
],
roots: [
{
id: '97720c17-1ff1-f4b0-8f97-8d70cf795348',
type: 'group',
x: 359,
y: 74,
parameters: {},
children: []
},
{
id: 'c0210ab9-94ab-c4c8-313b-3b394d5361f6',
type: 'group',
x: 457,
y: 276,
parameters: {},
children: []
},
{
id: '56b67ac1-224e-c3c8-b058-6fee9c590bee',
type: 'test',
x: 430,
y: 181,
parameters: {
p1: 50
},
children: []
}
]
}
},
{
name: 'test',
graph: {
connections: [
{
fromId: '46a72429-263c-2ad1-3ada-ce8a98b27308',
fromProperty: 'p2',
toId: 'f89b4fcd-5cfe-c47e-7fab-03948aad878c',
toProperty: 'clip'
},
{
fromId: '46a72429-263c-2ad1-3ada-ce8a98b27308',
fromProperty: 'p1',
toId: 'f89b4fcd-5cfe-c47e-7fab-03948aad878c',
toProperty: 'scaleX'
},
{
fromId: 'f89b4fcd-5cfe-c47e-7fab-03948aad878c',
fromProperty: 'screenX',
toId: 'c01d18bc-b8cd-e792-6fd8-89694ef8da48',
toProperty: 'p3'
}
],
roots: [
{
id: '46a72429-263c-2ad1-3ada-ce8a98b27308',
type: 'Component Inputs',
x: 336,
y: 115,
parameters: {},
children: [],
ports: [
{
name: 'p1',
plug: 'output',
type: '='
},
{
name: 'p2',
plug: 'output',
type: '='
}
]
},
{
id: 'f89b4fcd-5cfe-c47e-7fab-03948aad878c',
type: 'group',
x: 497,
y: 211,
parameters: {},
children: []
},
{
id: 'c01d18bc-b8cd-e792-6fd8-89694ef8da48',
type: 'Component Outputs',
x: 646,
y: 283,
parameters: {},
children: [],
ports: [
{
name: 'p3',
plug: 'input',
type: '='
}
]
}
]
}
},
// Test component references
{
name: '/has_comp_ref',
ports: [],
graph: {
roots: [
{
id: 'has_comp_ref_group',
type: 'group',
parameters: {
compref: '/to_be_renamed'
},
children: []
}
]
}
},
{
name: '/to_be_renamed',
graph: {}
}
]
};
});

View File

@@ -0,0 +1,147 @@
const { ComponentPorts } = require('@noodl-views/panels/componentports');
const { ProjectModel } = require('@noodl-models/projectmodel');
const NodeLibrary = require('@noodl-models/nodelibrary').NodeLibrary;
const { UndoQueue } = require('@noodl-models/undo-queue-model');
describe('Components ports panel unit tests', function () {
var cp, c;
beforeEach(() => {
var project = {
components: [
{
name: 'Root',
graph: {
roots: [
{
type: 'Component Inputs',
id: 'A'
}
]
}
}
]
};
window.NodeLibraryData = require('../nodegraph/nodelibrary');
NodeLibrary.instance.loadLibrary();
ProjectModel.instance = ProjectModel.fromJSON(project);
// NodeLibrary.instance.registerModule(project);
c = ProjectModel.instance.getComponentWithName('Root');
cp = new ComponentPorts({
model: c.graph.findNodeWithId('A'),
plug: 'input'
});
cp.render();
cp.canArrangeInGroups = true; // Enable arrange in groups
});
afterEach(() => {
ProjectModel.instance = undefined;
});
it('can add ports', function () {
expect(cp.performAdd('test').success).toBe(true);
expect(c.findPortWithName('test')).not.toBe(undefined);
// Cannot create with same name
expect(cp.performAdd('test').success).toBe(false);
// Can add with different name
expect(cp.performAdd('test2').success).toBe(true);
expect(c.findPortWithName('test2')).not.toBe(undefined);
});
it('can undo add ports', function () {
expect(cp.performAdd('undome').success).toBe(true);
expect(c.findPortWithName('undome')).not.toBe(undefined);
UndoQueue.instance.undo();
expect(c.findPortWithName('undome')).toBe(undefined);
});
it('can rename+undo ports', function () {
cp.performAdd('test');
cp.performAdd('test2');
// cannot rename to existing name
expect(
cp.performRename({
oldName: 'test',
newName: 'test2'
}).success
).toBe(false);
expect(
cp.performRename({
oldName: 'test',
newName: 'test3'
}).success
).toBe(true);
expect(c.findPortWithName('test3')).not.toBe(undefined);
expect(c.findPortWithName('test')).toBe(undefined);
// Undo rename
UndoQueue.instance.undo();
expect(c.findPortWithName('test3')).toBe(undefined);
expect(c.findPortWithName('test')).not.toBe(undefined);
});
it('can delete+undo ports', function () {
cp.performAdd('test');
cp.renderPorts(true);
expect(cp.performDelete('test').success).toBe(true);
expect(c.findPortWithName('test')).toBe(undefined);
UndoQueue.instance.undo();
expect(c.findPortWithName('test')).not.toBe(undefined);
});
it('can add group and port', function () {
expect(cp.performAddGroup('G1').success).toBe(true);
expect(cp.performAdd('P1').success).toBe(true);
cp.renderPorts(true);
expect(c.findPortWithName('P1').group).toBe('G1');
});
it('can rename group', function () {
cp.performAddGroup('G1');
cp.performAdd('P1');
cp.renderPorts(true);
expect(
cp.performRenameGroup({
newName: 'G2',
item: cp.findGroupWithName('G1')
}).success
).toBe(true);
expect(c.findPortWithName('P1').group).toBe('G2');
});
it('cannot add group with unqualified name', function () {
cp.performAddGroup('G2');
cp.performAdd('P1');
cp.renderPorts(true);
expect(cp.performAddGroup('G2').success).toBe(false); // Existing group
expect(cp.performAddGroup('').success).toBe(false); // Empty name
});
it('can delete group', function () {
cp.performAddGroup('G2');
cp.performAdd('P1');
cp.renderPorts(true);
expect(cp.performDeleteGroup(cp.findGroupWithName('G2')).success).toBe(true);
expect(c.findPortWithName('P1').group).toBe(undefined);
});
});

View File

@@ -0,0 +1,536 @@
const { ComponentsPanelView } = require('@noodl-views/panels/componentspanel/ComponentsPanel');
const { ProjectModel } = require('@noodl-models/projectmodel');
const { UndoQueue } = require('@noodl-models/undo-queue-model');
const NodeGraphEditor = require('@noodl-views/nodegrapheditor').NodeGraphEditor;
const ViewerConnection = require('../../src/editor/src/ViewerConnection');
describe('Components panel unit tests', function () {
var cp;
var p1;
var project = {
components: [
{
name: 'Root',
graph: {}
},
{
name: '/test/f1/a',
graph: {}
},
{
name: '/test/f2/a',
graph: {}
},
{
name: '/b',
graph: {}
},
{
name: '/test/ff/a',
graph: {}
},
{
name: '/q',
graph: {}
},
{
name: '/a',
graph: {}
},
{
name: '/dup/f1/a',
graph: {}
},
// Undo tests
{
name: '/delete_folder/delete_comp',
graph: {}
},
{
name: '/rename_folder/rename_comp',
graph: {}
},
{
name: '/drop/a',
graph: {}
},
{
name: '/drop2/a',
graph: {}
},
{
name: '/dropundo',
graph: {}
},
{
name: '/nested-target/a',
graph: {}
},
{
name: '/nested-dropme/test/b',
graph: {}
},
{
name: '/delete-me/with-content/a',
graph: {}
},
{
name: '/delete-me/b',
graph: {}
}
]
};
beforeAll(() => {
// Mock node graph editor
NodeGraphEditor.instance = {
getActiveComponent() {
return p1.getComponentWithName('Root');
},
on() {},
off() {},
switchToComponent() {}
};
// Viewerconnection mock
ViewerConnection.instance = {
on() {},
off() {}
};
});
afterAll(() => {
NodeGraphEditor.instance = undefined;
ViewerConnection.instance = undefined;
});
beforeEach(() => {
p1 = ProjectModel.instance = ProjectModel.fromJSON(project);
cp = new ComponentsPanelView({});
cp.setNodeGraphEditor(NodeGraphEditor.instance);
cp.render();
});
afterEach(() => {
cp.dispose();
ProjectModel.instance = undefined;
});
it('can setup view', function () {
expect(cp).not.toBe(undefined);
});
it('can add new folders', function () {
// Existing folder
expect(
cp.performAdd({
type: 'folder',
name: 'test'
}).success
).toBe(false);
// Empty name
expect(
cp.performAdd({
type: 'folder',
name: ''
}).success
).toBe(false);
// Add
expect(
cp.performAdd({
type: 'folder',
name: 'f3'
}).success
).toBe(true);
expect(cp.getFolderWithPath('/f3/')).not.toBe(undefined);
});
it('can add components', function () {
// Existing name
expect(
cp.performAdd({
type: 'component',
name: 'b',
parentPath: '/'
}).success
).toBe(false);
// Empty name
expect(
cp.performAdd({
type: 'component',
name: ''
}).success
).toBe(false);
// Add
expect(
cp.performAdd({
type: 'component',
name: 'c',
parentPath: '/'
}).success
).toBe(true);
expect(p1.getComponentWithName('/c')).not.toBe(undefined);
expect(cp.getFolderWithPath('/').hasComponentWithName('c')).toBe(true);
// Add to sub directory
expect(
cp.performAdd({
type: 'component',
name: 'subsub',
parentPath: '/test/ff/'
}).success
).toBe(true);
expect(p1.getComponentWithName('/test/ff/subsub')).not.toBe(undefined);
expect(cp.getFolderWithPath('/test/ff/').hasComponentWithName('subsub')).toBe(true);
});
it('can rename folders', function () {
// Existing name
expect(
cp.performRename({
type: 'folder',
name: 'f2',
folder: cp.getFolderWithPath('/test/ff/')
}).success
).toBe(false);
// Empty name
expect(
cp.performRename({
type: 'folder',
name: '',
folder: cp.getFolderWithPath('/test/ff/')
}).success
).toBe(false);
// Empty name
expect(
cp.performRename({
type: 'folder',
name: 'f4',
folder: cp.getFolderWithPath('/test/ff/')
}).success
).toBe(true);
expect(p1.getComponentWithName('/test/ff/a')).toBe(undefined);
expect(p1.getComponentWithName('/test/f4/a')).not.toBe(undefined);
expect(cp.getFolderWithPath('/test/ff/')).toBe(undefined);
expect(cp.getFolderWithPath('/test/f4/')).not.toBe(undefined);
});
it('can rename components', function () {
// Existing name
expect(
cp.performRename({
type: 'component',
name: 'b',
folder: cp.getFolderWithPath('/'),
component: p1.getComponentWithName('/q')
}).success
).toBe(false);
// Empty name
expect(
cp.performRename({
type: 'component',
name: '',
folder: cp.getFolderWithPath('/'),
component: p1.getComponentWithName('/q')
}).success
).toBe(false);
// Empty name
expect(
cp.performRename({
type: 'component',
name: 'q2',
folder: cp.getFolderWithPath('/'),
component: p1.getComponentWithName('/q')
}).success
).toBe(true);
expect(p1.getComponentWithName('/q')).toBe(undefined);
expect(p1.getComponentWithName('/q2')).not.toBe(undefined);
});
it('can detect duplicates', function () {
// Cannot move to folder containing a comp with same name
expect(
cp.getAcceptableDropType({
type: 'component',
component: p1.getComponentWithName('/a'),
targetFolder: cp.getFolderWithPath('/test/f1/')
})
).toBe(false);
// Cannot move folder to folder containing a folder with same name
expect(
cp.getAcceptableDropType({
type: 'folder',
folder: cp.getFolderWithPath('/dup/f1/'),
targetFolder: cp.getFolderWithPath('/test/')
})
).toBe(false);
});
it('can make correct drops of folders', function () {
// Can move a folder into a folder
expect(
cp.getAcceptableDropType({
type: 'folder',
folder: cp.getFolderWithPath('/test/f1/'),
targetFolder: cp.getFolderWithPath('/test/f2/')
})
).toBe('folder');
// Make the move
cp.dropOn({
type: 'folder',
folder: cp.getFolderWithPath('/test/f1/'),
targetFolder: cp.getFolderWithPath('/test/f2/')
});
expect(p1.getComponentWithName('/test/f2/f1/a')).not.toBe(undefined);
expect(cp.getFolderWithPath('/test/f2/f1/').name).toBe('f1');
// expect(cp.getFolderWithPath('/test/f1/')).toBe(undefined);
// Moving to an ancestor or same folder should not be acceptable
expect(
cp.getAcceptableDropType({
type: 'folder',
folder: cp.getFolderWithPath('/test/f2/'),
targetFolder: cp.getFolderWithPath('/test/f2/f1/')
})
).toBe(false);
expect(
cp.getAcceptableDropType({
type: 'folder',
folder: cp.getFolderWithPath('/test/f2/'),
targetFolder: cp.getFolderWithPath('/test/f2/')
})
).toBe(false);
});
it('can make correct drops of components', function () {
// Can move into a new folder
expect(
cp.getAcceptableDropType({
type: 'component',
folder: cp.getFolderWithPath('/'),
component: p1.getComponentWithName('/b'),
targetFolder: cp.getFolderWithPath('/test/f2/')
})
).toBe('component');
// Cannot drop to same folder
expect(
cp.getAcceptableDropType({
type: 'component',
folder: cp.getFolderWithPath('/'),
component: p1.getComponentWithName('/b'),
targetFolder: cp.getFolderWithPath('/')
})
).toBe(false);
// Make the drop
cp.dropOn({
type: 'component',
folder: cp.getFolderWithPath('/'),
component: p1.getComponentWithName('/b'),
targetFolder: cp.getFolderWithPath('/test/f2/')
});
expect(p1.getComponentWithName('/test/f2/b')).not.toBe(undefined);
expect(cp.getFolderWithPath('/').hasComponentWithName('b')).toBe(false);
expect(cp.getFolderWithPath('/test/f2/').hasComponentWithName('b')).toBe(true);
expect(p1.getComponentWithName('/b')).toBe(undefined);
});
//TODO: empty folders are removed when moved, but the undo function does not restore them. This is a bug.
xit('can drop empty folders', function () {
cp.performAdd({
type: 'folder',
name: 'empty_folder',
parentFolder: cp.getFolderWithPath('/')
});
expect(cp.getFolderWithPath('/empty_folder/')).not.toBe(undefined);
// Drop empty folder
cp.dropOn({
type: 'folder',
folder: cp.getFolderWithPath('/empty_folder/'),
targetFolder: cp.getFolderWithPath('/test/')
});
expect(cp.getFolderWithPath('/empty_folder/')).toBe(undefined);
//empty folders are removed when moved
expect(cp.getFolderWithPath('/test/empty_folder/')).toBe(undefined);
UndoQueue.instance.undo();
expect(cp.getFolderWithPath('/empty_folder/')).not.toBe(undefined);
// expect(cp.getFolderWithPath('/test/empty_folder/')).toBe(undefined);
});
it('can undo add/delete/rename component and folder', function () {
// Add component
expect(
cp.performAdd({
type: 'component',
name: 'undome',
parentPath: '/'
}).success
).toBe(true);
expect(p1.getComponentWithName('/undome')).not.toBe(undefined);
expect(UndoQueue.instance.undo().label).toBe('add component');
expect(p1.getComponentWithName('/undome')).toBe(undefined);
// Add folder
expect(
cp.performAdd({
type: 'folder',
name: 'undome',
parentPath: '/'
}).success
).toBe(true);
expect(cp.getFolderWithPath('/undome/')).not.toBe(undefined);
expect(UndoQueue.instance.undo().label).toBe('add folder');
expect(cp.getFolderWithPath('/undome/')).toBe(undefined);
// Delete component
expect(
cp.performDelete({
type: 'component',
folder: cp.getFolderWithPath('/delete_folder/'),
component: p1.getComponentWithName('/delete_folder/delete_comp')
}).success
).toBe(true);
expect(p1.getComponentWithName('/delete_folder/delete_comp')).toBe(undefined);
expect(UndoQueue.instance.undo().label).toBe('delete component');
expect(p1.getComponentWithName('/delete_folder/delete_comp')).not.toBe(undefined);
expect(UndoQueue.instance.redo().label).toBe('delete component'); // Folder must be empty for next test to run
// Delete folder
expect(
cp.performDelete({
type: 'folder',
folder: cp.getFolderWithPath('/delete_folder/')
}).success
).toBe(true);
expect(cp.getFolderWithPath('/delete_folder/')).toBe(undefined);
expect(UndoQueue.instance.undo().label).toBe('delete folder');
expect(cp.getFolderWithPath('/delete_folder/')).not.toBe(undefined);
// Rename component
expect(
cp.performRename({
type: 'component',
name: 'newname',
folder: cp.getFolderWithPath('/rename_folder/'),
component: p1.getComponentWithName('/rename_folder/rename_comp')
}).success
).toBe(true);
expect(p1.getComponentWithName('/rename_folder/newname')).not.toBe(undefined);
expect(p1.getComponentWithName('/rename_folder/rename_comp')).toBe(undefined);
expect(UndoQueue.instance.undo().label).toBe('rename component');
expect(p1.getComponentWithName('/rename_folder/newname')).toBe(undefined);
expect(p1.getComponentWithName('/rename_folder/rename_comp')).not.toBe(undefined);
// Rename folder
expect(
cp.performRename({
type: 'folder',
name: 'newname',
folder: cp.getFolderWithPath('/rename_folder/')
}).success
).toBe(true);
expect(p1.getComponentWithName('/newname/rename_comp')).not.toBe(undefined);
expect(p1.getComponentWithName('/rename_folder/rename_comp')).toBe(undefined);
expect(cp.getFolderWithPath('/rename_folder/')).toBe(undefined);
expect(cp.getFolderWithPath('/newname/')).not.toBe(undefined);
expect(UndoQueue.instance.undo().label).toBe('rename folder');
expect(p1.getComponentWithName('/newname/rename_comp')).toBe(undefined);
expect(p1.getComponentWithName('/rename_folder/rename_comp')).not.toBe(undefined);
expect(cp.getFolderWithPath('/rename_folder/')).not.toBe(undefined);
expect(cp.getFolderWithPath('/newname/')).toBe(undefined);
});
it('can undo drop on folder', function () {
// Component on folder
cp.dropOn({
type: 'component',
folder: cp.getFolderWithPath('/'),
component: p1.getComponentWithName('/dropundo'),
targetFolder: cp.getFolderWithPath('/drop/')
});
expect(p1.getComponentWithName('/drop/dropundo')).not.toBe(undefined);
expect(UndoQueue.instance.undo().label).toBe('move component to folder');
// expect(p1.getComponentWithName('/drop/dropundo')).toBe(undefined);
expect(p1.getComponentWithName('/dropundo')).not.toBe(undefined);
expect(cp.getFolderWithPath('/drop/').hasComponentWithName('dropundo')).toBe(false);
// Folder on folder
cp.dropOn({
type: 'folder',
folder: cp.getFolderWithPath('/drop/'),
targetFolder: cp.getFolderWithPath('/drop2/')
});
expect(cp.getFolderWithPath('/drop2/drop/')).not.toBe(undefined);
expect(UndoQueue.instance.undo().label).toBe('move folder to folder');
// expect(cp.getFolderWithPath('/drop2/drop/')).toBe(undefined);
});
it('can make correct drops of nested folders and undo', function () {
cp.dropOn({
type: 'folder',
folder: cp.getFolderWithPath('/nested-dropme/'),
targetFolder: cp.getFolderWithPath('/nested-target/')
});
expect(cp.getFolderWithPath('/nested-target/nested-dropme/')).not.toBe(undefined);
expect(p1.getComponentWithName('/nested-target/nested-dropme/test/b')).not.toBe(undefined);
expect(p1.getComponentWithName('/nested-dropme/test/b')).toBe(undefined);
// expect(cp.getFolderWithPath('/nested-dropme/')).toBe(undefined);
UndoQueue.instance.undo();
// expect(cp.getFolderWithPath('/nested-target/nested-dropme/')).toBe(undefined);
expect(p1.getComponentWithName('/nested-target/nested-dropme/test/b')).toBe(undefined);
expect(p1.getComponentWithName('/nested-dropme/test/b')).not.toBe(undefined);
expect(cp.getFolderWithPath('/nested-dropme/')).not.toBe(undefined);
});
it('can delete folder with content', function () {
// Delete folder
expect(
cp.performDelete({
type: 'folder',
folder: cp.getFolderWithPath('/delete-me/')
}).success
).toBe(true);
expect(cp.getFolderWithPath('/delete-me/')).toBe(undefined);
expect(cp.getFolderWithPath('/delete-me/with-content/')).toBe(undefined);
expect(p1.getComponentWithName('/delete-me/with-content/a')).toBe(undefined);
expect(p1.getComponentWithName('/delete-me/b')).toBe(undefined);
UndoQueue.instance.undo();
expect(cp.getFolderWithPath('/delete-me/')).not.toBe(undefined);
expect(cp.getFolderWithPath('/delete-me/with-content/')).not.toBe(undefined);
expect(p1.getComponentWithName('/delete-me/with-content/a')).not.toBe(undefined);
expect(p1.getComponentWithName('/delete-me/b')).not.toBe(undefined);
});
});

View File

@@ -0,0 +1,48 @@
const { ProjectModel } = require('@noodl-models/projectmodel');
describe('Conditional ports tests', function () {
beforeEach(() => {
ProjectModel.instance = ProjectModel.fromJSON({
components: [
{
name: 'comp1',
graph: {
roots: [
{
type: 'Anim',
id: 'A-1',
parameters: {
type: 'typeA'
}
}
]
}
}
]
});
});
xit('can get correct ports after load', function () {
var ports = ProjectModel.instance.findNodeWithId('A-1').getPorts();
expect(ports.length).toBe(2);
expect(ports[1]).toEqual({
name: 'from',
plug: 'input',
type: 'number',
index: 1
});
});
xit('can react to param changes', function () {
ProjectModel.instance.findNodeWithId('A-1').setParameter('type', 'typeB');
var ports = ProjectModel.instance.findNodeWithId('A-1').getPorts();
expect(ports.length).toBe(2);
expect(ports[1]).toEqual({
name: 'to',
plug: 'input',
type: 'number',
index: 1
});
});
});

View File

@@ -0,0 +1,89 @@
const { ProjectModel } = require('@noodl-models/projectmodel');
describe('Dynamic ports from viewer tests', function () {
// Setup
var projectJSON = {
components: [
{
name: 'Root',
graph: {
connections: [
{
fromId: 'A',
fromProperty: 'dynaA',
toId: 'B',
toProperty: 'x'
},
{
fromId: 'B',
fromProperty: 'y',
toId: 'A',
toProperty: 'dynaB'
}
],
roots: [
{
type: 'rectangle',
id: 'A',
parameters: {
dynaA: 10,
dynaB: 'hej'
}
},
{
type: 'rectangle',
id: 'B'
}
]
}
}
]
};
var project = ProjectModel.fromJSON(projectJSON);
it('can rename inputs', function () {
var n = project.findNodeWithId('A');
n.setDynamicPorts([
{
name: 'dynaA',
type: 'number'
},
{
name: 'dynaB',
type: 'string'
}
]);
// Now renamed
n.setDynamicPorts(
[
{
name: 'bosseA',
type: 'number'
},
{
name: 'bosseB',
type: 'string'
}
],
{
renamed: {
plug: 'input',
patterns: ['{{*}}A', '{{*}}B'],
before: 'dyna',
after: 'bosse'
}
}
);
expect(n.parameters['bosseA']).toBe(10);
expect(n.parameters['bosseB']).toBe('hej');
expect(n.parameters['dynaA']).toBe(undefined);
expect(n.parameters['dynaB']).toBe(undefined);
expect(project.getComponentWithName('Root').graph.connections[0].fromProperty).toBe('bosseA');
expect(project.getComponentWithName('Root').graph.connections[1].toProperty).toBe('bosseB');
});
});

View File

@@ -0,0 +1,82 @@
const { ProjectModel } = require('@noodl-models/projectmodel');
describe('Expanded ports tests', function () {
beforeEach(() => {
ProjectModel.instance = ProjectModel.fromJSON(getProject());
});
xit('can get correct ports after load', function () {
var ports = ProjectModel.instance.findNodeWithId('EP-1').getPorts();
expect(ports.length).toBe(3);
expect(ports[1]).toEqual({
name: 'Affe.A',
index: 101,
plug: 'input',
type: 'number'
});
expect(ports[2]).toEqual({
name: 'Affe.B',
index: 102,
plug: 'input',
type: 'number'
});
});
xit('can react to param changes', function () {
ProjectModel.instance.findNodeWithId('EP-1').setParameter('Affe.A', 'something');
var ports = ProjectModel.instance.findNodeWithId('EP-1').getPorts();
expect(ports.length).toBe(2);
expect(ports[1]).toEqual({
name: 'Affe.A',
index: 101,
plug: 'input',
type: 'number'
});
});
xit('can rename ports and parameters are copied', function () {
ProjectModel.instance.findNodeWithId('EP-2').setParameter('Bosse.A', 'grr');
ProjectModel.instance.findNodeWithId('EP-2').renamePortWithName('Bosse', 'Oscar');
expect(ProjectModel.instance.findNodeWithId('EP-2').parameters['Oscar.A']).toBe('grr');
expect(ProjectModel.instance.findNodeWithId('EP-2').parameters['Bosse.A']).toBe(undefined);
});
function getProject() {
return {
components: [
{
name: 'comp1',
graph: {
roots: [
{
type: 'ExpandPorts',
id: 'EP-1',
ports: [
{
type: 'number',
name: 'Affe',
plug: 'input'
}
]
},
{
type: 'ExpandPorts',
id: 'EP-2',
ports: [
{
type: 'number',
name: 'Bosse',
plug: 'input'
}
]
}
]
}
}
]
};
}
});

View File

@@ -0,0 +1,9 @@
export * from './componentconnections';
export * from './componentinstances';
export * from './componentports';
export * from './componentspanel';
export * from './conditionalports';
export * from './dynamicports';
export * from './expandedports';
export * from './numberedports';
export * from './portchannels';

View File

@@ -0,0 +1,100 @@
const NodeGraphModel = require('@noodl-models/nodegraphmodel').NodeGraphModel;
const NodeGraphNode = require('@noodl-models/nodegraphmodel').NodeGraphNode;
describe('Numbered ports tests', function () {
var g1, e1, e2, e3;
beforeEach(() => {
g1 = new NodeGraphModel();
e1 = NodeGraphNode.fromJSON({
id: 'A',
type: 'nodeWithNumberedPorts'
});
g1.addRoot(e1);
});
xit('can add new ports dynamically', function () {
var ports = e1.getPorts();
expect(ports.length).toBe(1);
expect(ports[0].name).toBe('my number 0');
expect(ports[0].displayName).toBe('My number 0');
expect(ports[0].type).toBe('number');
expect(ports[0].group).toBe('My group');
e1.setParameter('my number 0', 10);
var ports = e1.getPorts();
expect(ports.length).toBe(2);
expect(ports[1].name).toBe('my number 1');
expect(ports[1].displayName).toBe('My number 1');
});
xit('can find highest parameter', function () {
// The number of parameters should be defined by the highest parameter that is
// not undefined
e1.setParameter('my number 1', 20);
var ports = e1.getPorts();
expect(ports.length).toBe(3);
// Setting to undefined should still be there because 'my number 1' is 20
e1.setParameter('my number 0', undefined);
var ports = e1.getPorts();
expect(ports.length).toBe(3);
});
xit('can detect connections', function () {
e2 = NodeGraphNode.fromJSON({
id: 'B',
type: 'nodeWithNumberedPorts'
});
g1.addRoot(e2);
g1.addConnection({
fromId: 'A',
fromProperty: 'my number 2',
toId: 'B',
toProperty: 'my number 1'
});
// A should now have 4 ports (connected to my number 2)
var ports = e1.getPorts();
expect(ports.length).toBe(4);
// B should have 3 ports (connected to my number 1)
var ports = e2.getPorts();
expect(ports.length).toBe(3);
});
xit('can generate selectors', function () {
e3 = NodeGraphNode.fromJSON({
id: 'C',
type: 'nodeWithNumberedPortsAndSelectors'
});
g1.addRoot(e3);
var ports = e3.getPorts();
expect(ports.length).toBe(2);
expect(ports[1].name).toBe('startAt');
expect(ports[1].group).toBe('My selectors');
expect(ports[1].type.enums.length).toBe(0);
expect(ports[1].type.name).toBe('enum');
// Add a dynamic port, should be listed in the selector
e3.setParameter('my number 0', 10);
var ports = e3.getPorts();
expect(ports.length).toBe(3);
expect(ports[2].type.enums.length).toBe(1);
expect(ports[2].type.enums[0].value).toBe('my number 0');
expect(ports[2].type.enums[0].label).toBe('My number 0');
// More
e3.setParameter('my number 1', 20);
var ports = e3.getPorts();
expect(ports.length).toBe(4);
expect(ports[3].type.enums.length).toBe(2);
expect(ports[3].type.enums[0].value).toBe('my number 0');
expect(ports[3].type.enums[0].label).toBe('My number 0');
expect(ports[3].type.enums[1].value).toBe('my number 1');
expect(ports[3].type.enums[1].label).toBe('My number 1');
});
});

View File

@@ -0,0 +1,191 @@
const { ProjectModel } = require('@noodl-models/projectmodel');
describe('Port channels tests', function () {
beforeEach(() => {
ProjectModel.instance = ProjectModel.fromJSON(getProject());
});
afterAll(() => {
//unregister the project model so subsequent tests arent affected
//(the channels can affect other tests)
ProjectModel.instance = undefined;
});
xit('can collect channel names', function () {
var ports = ProjectModel.instance.findNodeWithId('ER-1').getPorts();
expect(ports.length).toBe(1);
expect(ports[0].name).toBe('channel');
expect(ports[0].type).toEqual({
name: 'enum',
enums: ['channelA', 'channelB'],
allowEditOnly: true
});
});
xit('can collect ports when channel name is set', function () {
ProjectModel.instance.findNodeWithId('ER-1').setParameter('channel', 'channelA');
var ports = ProjectModel.instance.findNodeWithId('ER-1').getPorts();
expect(ports.length).toBe(4);
expect(ports[1]).toEqual({
name: 'p1',
type: '*',
plug: 'output',
index: 1
});
expect(ports[2]).toEqual({
name: 'p2',
type: '*',
plug: 'output',
index: 2
});
expect(ports[3]).toEqual({
name: 'p3',
type: '*',
plug: 'output',
index: 3
});
});
xit('can react to port name change', function () {
ProjectModel.instance.findNodeWithId('ER-1').setParameter('channel', 'channelA');
ProjectModel.instance.findNodeWithId('ES-1').renamePortWithName('p2', 'p2b');
var ports = ProjectModel.instance.findNodeWithId('ER-1').getPorts();
expect(ports.length).toBe(5);
expect(ports[1]).toEqual({
name: 'p1',
type: '*',
plug: 'output',
index: 1
});
expect(ports[2]).toEqual({
name: 'p2',
type: '*',
plug: 'output',
index: 2
});
expect(ports[3]).toEqual({
name: 'p2b',
type: '*',
plug: 'output',
index: 3
});
expect(ports[4]).toEqual({
name: 'p3',
type: '*',
plug: 'output',
index: 4
});
});
xit('can react to channel changes', function () {
ProjectModel.instance.findNodeWithId('ER-1').setParameter('channel', 'channelA');
ProjectModel.instance.findNodeWithId('ES-1').setParameter('channel', 'channelC');
var ports = ProjectModel.instance.findNodeWithId('ER-1').getPorts();
expect(ports.length).toBe(3);
expect(ports[0].name).toBe('channel');
expect(ports[0].type).toEqual({
name: 'enum',
enums: ['channelA', 'channelB', 'channelC'],
allowEditOnly: true
});
expect(ports[1]).toEqual({
name: 'p2',
type: '*',
plug: 'output',
index: 1
});
expect(ports[2]).toEqual({
name: 'p3',
type: '*',
plug: 'output',
index: 2
});
});
function getProject() {
return {
components: [
{
name: 'comp1',
graph: {
roots: [
{
type: 'Event Sender',
id: 'ES-1',
parameters: {
channel: 'channelA'
},
ports: [
{
name: 'p1',
type: '*',
plug: 'input'
},
{
name: 'p2',
type: '*',
plug: 'input'
}
]
},
{
type: 'Event Receiver',
id: 'ER-1'
}
]
}
},
{
name: 'comp2',
graph: {
roots: [
{
type: 'Event Sender',
id: 'ES-2',
parameters: {
channel: 'channelA'
},
ports: [
{
name: 'p2',
type: '*',
plug: 'input'
},
{
name: 'p3',
type: '*',
plug: 'input'
}
]
},
{
type: 'Event Sender',
id: 'ES-3',
parameters: {
channel: 'channelB'
},
ports: [
{
name: 'p3',
type: '*',
plug: 'input'
},
{
name: 'p4',
type: '*',
plug: 'input'
}
]
}
]
}
}
]
};
}
});