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,55 @@
const ActiveWarnings = require('./editorconnection.activewarnings');
describe('Tracks active warnings that are sent to the editor', ()=>{
let activeWarnings;
beforeEach(async ()=>{
activeWarnings = new ActiveWarnings();
});
test('Set and clear a warning', () => {
expect(activeWarnings.setWarning('testId', 'testKey', 'testWarning')).toBe(true);
//these warnings shouldnt exist
expect(activeWarnings.clearWarning('testId', 'otherKey')).toEqual(false);
expect(activeWarnings.clearWarning('otherId', 'testKey')).toEqual(false);
//this warning is the one we set
expect(activeWarnings.clearWarning('testId', 'testKey')).toEqual(true);
//and now the warning should be gone
expect(activeWarnings.clearWarning('testId', 'testKey')).toEqual(false);
});
test('Set and clear multiple warning on one node', () => {
expect(activeWarnings.setWarning('testId', 'testKey1', 'testWarning')).toBe(true);
expect(activeWarnings.setWarning('testId', 'testKey2', 'testWarning')).toBe(true);
expect(activeWarnings.clearWarning('testId', 'testKey1')).toEqual(true);
expect(activeWarnings.clearWarning('testId', 'testKey1')).toEqual(false);
expect(activeWarnings.clearWarning('testId', 'testKey2')).toEqual(true);
expect(activeWarnings.clearWarning('testId', 'testKey2')).toEqual(false);
});
test('Clear multiple warnings at once', () => {
expect(activeWarnings.setWarning('testId1', 'testKey1', 'testWarning')).toBe(true);
expect(activeWarnings.setWarning('testId1', 'testKey2', 'testWarning')).toBe(true);
expect(activeWarnings.setWarning('testId2', 'testKey1', 'testWarning')).toBe(true);
expect(activeWarnings.clearWarnings('testId3')).toEqual(false);
expect(activeWarnings.clearWarnings('testId1')).toEqual(true);
expect(activeWarnings.clearWarnings('testId1')).toEqual(false);
expect(activeWarnings.clearWarnings('testId2')).toEqual(true);
expect(activeWarnings.clearWarnings('testId2')).toEqual(false);
});
test('Set same warning multiple times', () => {
expect(activeWarnings.setWarning('testId', 'testKey', 'testWarning')).toBe(true);
expect(activeWarnings.setWarning('testId', 'testKey', 'testWarning')).toBe(false);
expect(activeWarnings.setWarning('testId', 'testKey', 'testWarning2')).toBe(true);
});
});

View File

@@ -0,0 +1,98 @@
const handleEvent = require('./editormodeleventshandler').handleEvent;
const GraphModel = require('./models/graphmodel');
const NodeContext = require('./nodecontext');
describe('Component ports update when on the componentPortsUpdated event', ()=>{
let graphModel;
let nodeContext;
beforeEach(async ()=>{
nodeContext = new NodeContext();
graphModel = new GraphModel();
await graphModel.importComponentFromEditorData({
id: 'component',
name: 'testComponent',
ports: [
{ name: "testInput1", plug: "input", type: 'string'},
{ name: "testInput2", plug: "input", type: 'string'},
{ name: "testOutput1", plug: "output", type: 'string'},
{ name: "testOutput2", plug: "output", type: 'string'}
]
});
});
function updateAndMatchPorts(newInputPorts, newOutputPorts) {
handleEvent(nodeContext, graphModel, {
type: 'componentPortsUpdated',
componentName: 'testComponent',
ports: newInputPorts.concat(newOutputPorts)
});
const componentModel = graphModel.getComponentWithName('testComponent');
const inputPorts = componentModel.getInputPorts();
for(const port of newInputPorts) {
expect(inputPorts.hasOwnProperty(port.name)).toBeTruthy();
expect(inputPorts[port.name].type).toEqual(port.type);
}
expect(Object.keys(inputPorts).length).toBe(newInputPorts.length);
const outputPorts = componentModel.getOutputPorts();
for(const port of newOutputPorts) {
expect(outputPorts.hasOwnProperty(port.name)).toBeTruthy();
expect(outputPorts[port.name].type).toEqual(port.type);
}
expect(Object.keys(outputPorts).length).toBe(newOutputPorts.length);
}
test('Input ports are added', () => {
updateAndMatchPorts([
{ name: "testInput1", plug: "input", type: 'string'},
{ name: "testInput2", plug: "input", type: 'string'},
],
[
{name: "testOutput1", plug: "output", type: 'string'}
]);
});
test('Input ports are removed', () => {
updateAndMatchPorts([],
[
{name: "testOutput1", plug: "output", type: 'string'}
]);
});
test('Input port types are updated', () => {
updateAndMatchPorts([
{ name: "testInput1", plug: "input", type: 'boolean'},
{ name: "testInput2", plug: "input", type: {name:'number'}}
],
[]);
});
test('Output ports are added', () => {
updateAndMatchPorts([
{ name: "testInput1", plug: "input", type: 'string'}
],
[
{name: "testOutput1", plug: "output", type: 'string'},
{name: "testOutput2", plug: "output", type: 'string'}
]);
});
test('Output ports are removed', () => {
updateAndMatchPorts([
{ name: "testInput1", plug: "input", type: 'string'}
],
[
]);
});
test('Output port types are updated', () => {
updateAndMatchPorts([],
[
{ name: "testOutput1", plug: "output", type: 'number'},
{ name: "testOutput2", plug: "output", type: {name:'boolean'}}
]);
});
});

View File

@@ -0,0 +1,61 @@
const EventSender = require('./eventsender');
describe('EventSender', ()=> {
let eventSender;
beforeEach(()=>{
eventSender = new EventSender();
});
it('can send events', ()=> {
const mockCallback = jest.fn(() => {});
eventSender.on('testEvent', mockCallback);
const someArgument = {lol:'troll'};
eventSender.emit('testEvent', someArgument);
expect(mockCallback.mock.calls.length).toBe(1);
expect(mockCallback.mock.calls[0][0]).toBe(someArgument);
});
it('can remove a listener', ()=> {
const mockCallback = jest.fn(() => {});
const mockCallback2 = jest.fn(() => {});
const ref = {};
const ref2 = {};
eventSender.on('testEvent', mockCallback, ref);
eventSender.on('testEvent', mockCallback2, ref2);
eventSender.removeListenersWithRef(ref);
eventSender.emit('testEvent');
expect(mockCallback.mock.calls.length).toBe(0);
expect(mockCallback2.mock.calls.length).toBe(1);
});
it('can remove all listeners', ()=> {
const mockCallback = jest.fn(() => {});
const mockCallback2 = jest.fn(() => {});
const ref = {};
const ref2 = {};
eventSender.on('testEvent', mockCallback, ref);
eventSender.on('testEvent2', mockCallback2, ref2);
eventSender.removeAllListeners('testEvent');
eventSender.emit('testEvent');
eventSender.emit('testEvent2');
expect(mockCallback.mock.calls.length).toBe(0);
expect(mockCallback2.mock.calls.length).toBe(1);
});
});

View File

@@ -0,0 +1,67 @@
const { ComponentModel } = require('./componentmodel');
const NodeModel = require('./nodemodel');
test('Returns all nodes in the component', ()=>{
const component = new ComponentModel('testComponent');
component.addNode(new NodeModel('id1', 'testNode'));
let nodes = component.getAllNodes();
expect(nodes.length).toBe(1);
expect(nodes[0].id).toBe('id1');
component.addNode(new NodeModel('id2', 'testNode'));
nodes = component.getAllNodes();
expect(nodes.length).toBe(2);
//the order is not guaranteed, so let's just use find
expect(nodes.find(n=>n.id==='id1')).not.toBe(undefined);
expect(nodes.find(n=>n.id==='id2')).not.toBe(undefined);
});
test('Connections can be added', ()=>{
const component = new ComponentModel('testComponent');
component.addNode(new NodeModel('id1', 'testNode'));
component.addNode(new NodeModel('id2', 'testNode'));
component.addConnection({
sourceId: 'id1',
sourcePort: 'test',
targetId: 'id2',
targetPort: 'test'
});
const connections = component.getConnectionsFrom('id1');
expect(connections.length).toBe(1);
});
test('Connections can be removed', ()=>{
const connection = {
sourceId: 'id1',
sourcePort: 'test',
targetId: 'id2',
targetPort: 'test'
};
const component = new ComponentModel('testComponent');
component.addNode(new NodeModel('id1', 'testNode'));
component.addNode(new NodeModel('id2', 'testNode'));
component.addConnection(connection);
//test with a copy of the object, and not the same object, to verify it works then as well
component.removeConnection(JSON.parse(JSON.stringify(connection)));
expect(component.getAllConnections().length).toBe(0);
});
test('Removing non-existing connection should not throw', ()=>{
const connection = {
sourceId: 'id1',
sourcePort: 'test',
targetId: 'id2',
targetPort: 'test'
};
const component = new ComponentModel('testComponent');
component.addNode(new NodeModel('id1', 'testNode'));
component.addNode(new NodeModel('id2', 'testNode'));
component.addConnection(connection);
expect(()=>component.removeConnection({})).not.toThrow();
expect(component.getAllConnections().length).toBe(1);
});

View File

@@ -0,0 +1,50 @@
const NodeContext = require('./nodecontext');
const NodeDefinition = require('./nodedefinition');
const ComponentInstance = require('./nodes/componentinstance');
const { ComponentModel } = require('./models/componentmodel');
describe('NodeContext', ()=>{
test("can detect cyclic updates", async () => {
const testNodeDefinition = NodeDefinition.defineNode({
name: 'Test Node',
category: 'test',
initialize() {this._internal.count = 0;},
inputs: {
input: {
set() {
this._internal.count++;
this.flagOutputDirty('output');
}
}
},
outputs: {
output: {type: 'number', get() {return Math.random()}}
},
})
const context = new NodeContext();
context.nodeRegister.register(testNodeDefinition);
const componentModel = await ComponentModel.createFromExportData({
name: 'testComponent',
id: '1',
nodes: [
{ id: '2', type: 'Test Node', parameters: {input: true}},
{ id: '3', type: 'Test Node'}
],
connections: [
{sourceId: '2', sourcePort: 'output', targetId: '3', targetPort: 'input'},
{sourceId: '3', sourcePort: 'output', targetId: '2', targetPort: 'input'}
]
});
const componentInstance = new ComponentInstance(context);
await componentInstance.setComponentModel(componentModel);
context.update();
expect(componentInstance.nodeScope.getNodeWithId('2')._internal.count).toBeGreaterThan(50);
expect(componentInstance.nodeScope.getNodeWithId('3')._internal.count).toBeGreaterThan(50);
});
});

View File

@@ -0,0 +1,241 @@
const ComponentInstance = require('./componentinstance');
const { ComponentModel } = require('../models/componentmodel');
const GraphModel = require('../models/graphmodel');
const NodeContext = require('../nodecontext');
const ComponentInputs = require('./componentinputs');
const ComponentOutputs = require('./componentoutputs');
const NodeDefinition = require('../nodedefinition');
async function setupComponent() {
const context = new NodeContext();
context.nodeRegister.register(NodeDefinition.defineNode(ComponentInputs.node));
context.nodeRegister.register(NodeDefinition.defineNode(ComponentOutputs.node));
const componentModel = await ComponentModel.createFromExportData({
name: 'testComponent',
id: 'loltroll2',
nodes: [
{
id: 'loltroll',
type: 'Component Inputs',
ports: [{ name: 'textInput', plug: 'output', type: 'string' }]
},
{
id: 'componentOutputs',
type: 'Component Outputs',
ports: [{ name: 'textOutput', plug: 'input', type: 'string' }]
}
],
ports: [
{ name: 'textInput', plug: 'input', type: 'string' },
{ name: 'textOutput', plug: 'output', type: 'string' }
],
connections: []
});
const componentInstance = new ComponentInstance(context);
await componentInstance.setComponentModel(componentModel);
return { componentInstance, componentModel };
}
function createTestNodeDefinition() {
return NodeDefinition.defineNode({
name: 'Test Node',
category: 'test',
initialize() {
this.inputHistory = [];
},
inputs: {
input: {
type: 'string',
set(value) {
this.testValue = value;
this.flagOutputDirty('output');
this.inputHistory.push(value);
}
}
},
outputs: {
output: {
type: 'string',
get() {
return this.testValue;
}
}
}
});
}
test('Component inputs are registered', async () => {
const { componentInstance } = await setupComponent();
expect(componentInstance.hasInput('textInput')).toBeTruthy();
});
test('Internal component inputs are removed when a component input node is removed', async () => {
const { componentInstance, componentModel } = await setupComponent();
expect(componentInstance._internal.componentInputs.length).toBe(1);
await componentModel.removeNodeWithId('loltroll');
expect(componentInstance._internal.componentInputs.length).toBe(0);
});
test('Component outputs are registered', async () => {
const { componentInstance } = await setupComponent();
expect(componentInstance.hasOutput('textOutput')).toBeTruthy();
});
test('Internal component outputs are removed when a component output node is removed', async () => {
const { componentInstance, componentModel } = await setupComponent();
expect(componentInstance._internal.componentOutputs.length).toBe(1);
await componentModel.removeNodeWithId('componentOutputs');
expect(componentInstance._internal.componentOutputs.length).toBe(0);
});
test('Parameters should be overwritten by connections', async () => {
const context = new NodeContext();
context.nodeRegister.register(createTestNodeDefinition());
const componentModel = await ComponentModel.createFromExportData({
name: 'rootComponent',
id: 'testid',
nodes: [
{
id: 'testNodeEnd',
type: 'Test Node',
parameters: { input: 'param-value' }
},
{
id: 'testNodeStart',
type: 'Test Node',
parameters: { input: 'connection-value' }
}
],
connections: [{ sourceId: 'testNodeStart', sourcePort: 'output', targetId: 'testNodeEnd', targetPort: 'input' }]
});
const componentInstance = new ComponentInstance(context);
await componentInstance.setComponentModel(componentModel);
const testnodeEnd = componentInstance.nodeScope.getNodeWithId('testNodeEnd');
testnodeEnd.update();
expect(testnodeEnd.inputHistory.length).toBe(1);
expect(testnodeEnd.inputHistory[0]).toBe('connection-value');
});
test('Component inputs should not interfere with internal nodes', async () => {
const context = new NodeContext();
context.nodeRegister.register(NodeDefinition.defineNode(ComponentInputs.node));
context.nodeRegister.register(createTestNodeDefinition());
const componentModel = await ComponentModel.createFromExportData({
name: 'rootComponent',
id: 'testid',
nodes: [
{
id: 'testnode',
type: 'Test Node',
parameters: { input: 'param-value' }
},
{
id: 'compinput',
type: 'Component Inputs',
ports: [{ name: 'output', plug: 'output', type: 'string' }]
}
],
connections: [{ sourceId: 'compinput', sourcePort: 'output', targetId: 'testnode', targetPort: 'input' }]
});
const componentInstance = new ComponentInstance(context);
await componentInstance.setComponentModel(componentModel);
const testnode = componentInstance.nodeScope.getNodeWithId('testnode');
testnode.update();
expect(testnode.inputHistory.length).toBe(1);
expect(testnode.inputHistory[0]).toBe('param-value');
});
test('No delays in component inputs and outputs', async () => {
const context = new NodeContext();
context.nodeRegister.register(NodeDefinition.defineNode(ComponentInputs.node));
context.nodeRegister.register(NodeDefinition.defineNode(ComponentOutputs.node));
context.nodeRegister.register(createTestNodeDefinition());
const graph = new GraphModel();
graph.on('componentAdded', (component) => context.registerComponentModel(component));
await graph.importEditorData({
components: [
{
name: 'testComponent',
nodes: [
{
id: 'compinput',
type: 'Component Inputs',
ports: [{ name: 'textInput', plug: 'output', type: 'string' }]
},
{
id: 'testnode-inner',
type: 'Test Node',
parameters: { input: 'inner-value' }
},
{
id: 'compoutput',
type: 'Component Outputs',
ports: [{ name: 'textOutput', plug: 'input', type: 'string' }]
}
],
connections: [
{ sourceId: 'compinput', sourcePort: 'textInput', targetId: 'testnode-inner', targetPort: 'input' },
{ sourceId: 'testnode-inner', sourcePort: 'output', targetId: 'compoutput', targetPort: 'textOutput' }
],
ports: [
{ name: 'textInput', plug: 'input', type: 'string' },
{ name: 'textOutput', plug: 'output', type: 'string' }
]
},
{
name: 'rootComponent',
nodes: [
{
id: 'testComponent',
type: 'testComponent'
},
{
id: 'testnodeEnd',
type: 'Test Node'
},
{
id: 'testNodeStart',
type: 'Test Node',
parameters: { input: 'outer-value' }
}
],
connections: [
{ sourceId: 'testComponent', sourcePort: 'textOutput', targetId: 'testnodeEnd', targetPort: 'input' },
{ sourceId: 'testNodeStart', sourcePort: 'output', targetId: 'testComponent', targetPort: 'textInput' }
]
}
]
});
const rootComponent = await context.createComponentInstanceNode('rootComponent');
context.setRootComponent(rootComponent);
const testnodeEnd = rootComponent.nodeScope.getNodeWithId('testnodeEnd');
const testnodeInner = rootComponent.nodeScope.getNodesWithIdRecursive('testnode-inner')[0];
context.update();
expect(testnodeEnd.inputHistory.length).toBe(1);
expect(testnodeEnd.inputHistory[0]).toBe('outer-value');
expect(testnodeInner.inputHistory.length).toBe(1);
expect(testnodeInner.inputHistory[0]).toBe('outer-value');
});

View File

@@ -0,0 +1,157 @@
const NodeScope = require('./nodescope');
const NodeContext = require('./nodecontext');
const NodeDefinition = require('./nodedefinition');
const { ComponentModel } = require('./models/componentmodel');
const GraphModel = require('./models/graphmodel');
function createTestNodeDefinition({onInit, onDelete}) {
return NodeDefinition.defineNode({
name: 'Test Node',
category: 'test',
initialize() {
this.children = [];
onInit && onInit.call(this);
onDelete && this.addDeleteListener(onDelete);
},
methods: {
addChild(child) {
this.children.push(child);
},
removeChild(child) {
this.children.splice(this.children.indexOf(child), 1);
},
getChildren() {
return this.children;
}
}
});
}
//Create a graph that includes a two levels of component children
async function createTestRootComponent(args) {
const context = new NodeContext();
context.nodeRegister.register(createTestNodeDefinition(args || {}));
const graph = new GraphModel();
graph.on("componentAdded", component => context.registerComponentModel(component) );
await graph.importEditorData( {
components: [
{
name: 'rootComponent',
nodes: [ {
id: 'test-component',
type: 'testComponent',
children: [
{ id: 'test-node-from-root', type: 'Test Node' },
{
id: 'test-component-child',
type: 'testComponent',
children: [
{ id: 'test-node-in-component-child', type: 'Test Node' },
]
}
]
} ],
},
{
name: 'testComponent',
nodes: [ {
id: 'test-node',
type: 'Test Node',
children: [
{
id: 'test-node-child',
type: 'Test Node',
children: [{id: 'component-children', type: 'Component Children'}]
}
]
} ],
}
]
});
const rootComponent = await context.createComponentInstanceNode("rootComponent");
context.setRootComponent(rootComponent);
return rootComponent;
}
test('find nodes by id', async () => {
const nodeScope = (await createTestRootComponent()).nodeScope;
expect(nodeScope.hasNodeWithId('test-component')).toBe(true);
const testComponent = nodeScope.getNodeWithId('test-component');
expect(testComponent.nodeScope.hasNodeWithId('test-node')).toBe(true);
});
test('delete component', async () => {
const nodeScope = (await createTestRootComponent()).nodeScope;
nodeScope.deleteNode(nodeScope.getNodeWithId('test-component'));
expect(nodeScope.hasNodeWithId('test-node')).toBe(false);
});
test('delete hierarchy', async () => {
let testNodeCount = 0;
const testComponent = await createTestRootComponent({
onInit: () => testNodeCount++,
onDelete: () => testNodeCount--
});
const nodeScope = testComponent.nodeScope.getNodeWithId('test-component').nodeScope;
expect(testNodeCount).toBe(6);
nodeScope.deleteNode(nodeScope.getNodeWithId('test-node'));
expect(nodeScope.hasNodeWithId('test-node')).toBe(false);
expect(nodeScope.hasNodeWithId('test-node-child')).toBe(false);
expect(testNodeCount).toBe(3);
});
test('delete child in a component children hierarchy', async () => {
let testNodeCount = 0;
const testComponent = await createTestRootComponent({
onInit: () => testNodeCount++,
onDelete: () => testNodeCount--
});
const nodeScope = testComponent.nodeScope;
expect(testNodeCount).toBe(6);
expect(nodeScope.hasNodeWithId('test-node-from-root')).toBe(true);
nodeScope.deleteNode(nodeScope.getNodeWithId('test-node-from-root'));
expect(nodeScope.hasNodeWithId('test-node-from-root')).toBe(false);
expect(testNodeCount).toBe(5);
});
test('delete component with component children', async () => {
let testNodeCount = 0;
const testComponent = await createTestRootComponent({
onInit: () => testNodeCount++,
onDelete: () => testNodeCount--
});
const nodeScope = testComponent.nodeScope;
expect(testNodeCount).toBe(6);
const node = nodeScope.getNodeWithId('test-component-child');
nodeScope.deleteNode(node);
expect(testNodeCount).toBe(3);
});
test('delete entire scope and nested scopes', async () => {
let testNodeCount = 0;
const testComponent = await createTestRootComponent({
onInit: () => testNodeCount++,
onDelete: () => testNodeCount--
});
const nodeScope = testComponent.nodeScope;
expect(testNodeCount).toBe(6);
nodeScope.reset();
expect(testNodeCount).toBe(0);
});

View File

@@ -0,0 +1,7 @@
const OutputProperty = require('./outputproperty');
test('Throws an exception if no owner is specified', () => {
expect(()=>{
new OutputProperty();
}).toThrow(Error);
});