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,178 @@
const { ProjectModel } = require('@noodl-models/projectmodel');
const NodeLibrary = require('@noodl-models/nodelibrary').NodeLibrary;
const WarningsModel = require('@noodl-models/warningsmodel').WarningsModel;
describe('Conflict warnings', function () {
var p;
beforeEach(() => {
window.NodeLibraryData = require('../nodegraph/nodelibrary');
NodeLibrary.instance.loadLibrary();
p = ProjectModel.fromJSON(getProject());
ProjectModel.instance = p;
});
it('can load project', function () {
expect(p).not.toBe(undefined);
});
it('can count total number of conflicts', function () {
p.getComponentWithName('comp1').graph.evaluateHealth();
p.getComponentWithName('comp2').graph.evaluateHealth();
p.getComponentWithName('comp3').graph.evaluateHealth();
expect(
WarningsModel.instance.getTotalNumberOfWarningsMatching(
(key, ref, warning) => warning.warning.type === 'conflict'
)
).toBe(4);
});
it('can count warnings for a components', function () {
const c = p.getComponentWithName('comp1');
c.graph.evaluateHealth();
let warnings = WarningsModel.instance.getNumberOfWarningsForComponent(c);
expect(warnings).toBe(1);
//conflict warnings are global, so test counting all non-global conflicts as well
warnings = WarningsModel.instance.getNumberOfWarningsForComponent(c, {
excludeGlobal: true
});
expect(warnings).toBe(0);
});
it('can detect param conflict warnings', function () {
var c = p.getComponentWithName('comp1');
c.graph.evaluateHealth();
var w = WarningsModel.instance.warnings['comp1'];
expect(w['node/A']['node-param-conflict-A']).not.toBe(undefined);
expect(WarningsModel.instance.getNumberOfWarningsForComponent(c)).toBe(1);
});
it('can detect type conflict warnings', function () {
var c = p.getComponentWithName('comp2');
c.graph.evaluateHealth();
var w = WarningsModel.instance.warnings['comp2'];
expect(w['node/B']['node-type-conflict']).not.toBe(undefined);
expect(WarningsModel.instance.getNumberOfWarningsForComponent(c)).toBe(1);
});
it('can clear warnings', function () {
// Clear the warning
var c = p.getComponentWithName('comp1');
WarningsModel.instance.setWarning(
{
component: c,
node: c.graph.roots[0],
key: 'node-param-conflict-A'
},
undefined
);
expect(WarningsModel.instance.hasComponentWarnings(c)).toBe(false);
// Clear warnings
var c = p.getComponentWithName('comp2');
WarningsModel.instance.clearWarningsForRef({
component: c,
node: c.graph.roots[0]
});
expect(WarningsModel.instance.hasComponentWarnings(c)).toBe(false);
});
it('can format warnings', function () {
var c = p.getComponentWithName('comp3');
c.graph.evaluateHealth();
var w = WarningsModel.instance.warnings['comp3'];
expect(w['node/C']['node-param-conflict-alignX']).not.toBe(undefined);
expect(w['node/C']['node-param-conflict-alignX'].warning.message).toEqual('Merge conflict at parameter AlignX');
expect(w['node/C']['node-param-conflict-x']).not.toBe(undefined);
expect(w['node/C']['node-param-conflict-x'].warning.message).toEqual('Merge conflict at parameter x');
});
it('clear warnings when project is unloaded', function () {
var c = p.getComponentWithName('comp1');
c.graph.evaluateHealth();
expect(WarningsModel.instance.hasComponentWarnings(c)).toBe(true);
ProjectModel.instance = undefined;
expect(WarningsModel.instance.warnings).toEqual({});
});
function getProject() {
return {
name: 'p',
components: [
{
name: 'comp1',
graph: {
roots: [
{
type: 'group',
id: 'A',
conflicts: [
{
type: 'parameter',
name: 'A',
ours: 0,
theirs: 1
}
]
}
],
connections: []
}
},
{
name: 'comp2',
graph: {
roots: [
{
type: 'group',
id: 'B',
conflicts: [
{
type: 'typename',
ours: '/a',
theirs: '/b'
}
]
}
]
}
},
{
name: 'comp3',
graph: {
roots: [
{
type: 'group',
id: 'C',
conflicts: [
{
type: 'parameter',
name: 'x',
ours: {
value: 10,
unit: '%'
},
theirs: 10
},
{
type: 'parameter',
name: 'alignX',
ours: 'left',
theirs: 'right'
}
]
}
]
}
}
]
};
}
});

View File

@@ -0,0 +1,51 @@
const CreateNewNodePanel = require('@noodl-views/createnewnodepanel');
const { ProjectModel } = require('@noodl-models/projectmodel');
const NodeLibrary = require('@noodl-models/nodelibrary').NodeLibrary;
describe('Create new node panel unit tests', function () {
var p, cp;
xit('can setup panel', function () {
ProjectModel.instance = new ProjectModel();
p = ProjectModel.fromJSON(project);
cp = new CreateNewNodePanel({
model: p.getComponentWithName('Root').graph,
pos: {
x: 100,
y: 50
},
runtimeType: 'browser'
});
cp.render();
expect(cp).not.toBe(undefined);
});
xit('can create nodes', function () {
var c = p.getComponentWithName('Root');
cp.performCreate(NodeLibrary.instance.getNodeTypeWithName('group'));
expect(c.graph.roots.length).toBe(2);
expect(c.graph.roots[1].type).toBe(NodeLibrary.instance.getNodeTypeWithName('group'));
expect(c.graph.roots[1].x).toBe(100);
expect(c.graph.roots[1].y).toBe(50);
expect(c.graph.roots[1].version).toBe(2);
});
var project = {
components: [
{
name: 'Root',
graph: {
roots: [
{
id: 'A',
type: 'group'
}
]
}
}
]
};
});

View File

@@ -0,0 +1,101 @@
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;
describe('Create status tests', function () {
var g1, c1, c2;
beforeEach(() => {
window.NodeLibraryData = require('../nodegraph/nodelibrary');
NodeLibrary.instance.loadLibrary();
});
it("can detect nodes that can't have/be children", function () {
g1 = new NodeGraphModel();
c1 = new ComponentModel({
graph: g1,
name: 'c1'
});
var n1 = NodeGraphNode.fromJSON(
{
type: 'group',
id: 'A'
},
g1
);
g1.addRoot(n1);
var n2 = NodeGraphNode.fromJSON(
{
type: 'animation',
id: 'B'
},
g1
);
g1.addRoot(n2);
var n3 = NodeGraphNode.fromJSON(
{
type: 'Component Children',
id: 'C'
},
g1
);
g1.addRoot(n3);
// Nodes that can have children
var status = c1.getCreateStatus({
parent: n1,
type: NodeLibrary.instance.getNodeTypeWithName('group')
});
expect(status.creatable).toBe(true);
// Animation nodes cannot be children
var status = c1.getCreateStatus({
parent: n1,
type: NodeLibrary.instance.getNodeTypeWithName('animation')
});
expect(status.creatable).toBe(false);
// Animation nodes cannot have children
var status = c1.getCreateStatus({
parent: n2,
type: NodeLibrary.instance.getNodeTypeWithName('group')
});
expect(status.creatable).toBe(false);
// Instance of itself
var status = c1.getCreateStatus({
type: c1
});
expect(status.creatable).toBe(false);
});
it('can detect circular references', function () {
c1 = ComponentModel.fromJSON({
name: 'comp1',
graph: {}
});
c2 = ComponentModel.fromJSON({
name: 'comp2',
graph: {}
});
c1.graph.addRoot(
NodeGraphNode.fromJSON(
{
type: c2,
id: 'C'
},
c1.graph
)
);
var status = c2.getCreateStatus({
type: c1
});
expect(status.creatable).toBe(false);
});
});

View File

@@ -0,0 +1,496 @@
const NodeLibrary = require('@noodl-models/nodelibrary').NodeLibrary;
const Exporter = require('@noodl-utils/exporter');
const { ProjectModel } = require('@noodl-models/projectmodel');
describe('export tests', function () {
xit('can export ports on components', function () {
ProjectModel.instance = ProjectModel.fromJSON(project1);
NodeLibrary.instance.registerModule(ProjectModel.instance);
ProjectModel.instance.setRootNode(ProjectModel.instance.findNodeWithId('Image-1'));
const json = Exporter.exportToJSON(ProjectModel.instance);
// Should not export types where the type has not been resolved
expect(json.components[0].ports.length).toBe(2);
expect(json.components[0].ports[0].name).toBe('out1');
expect(json.components[0].ports[0].plug).toBe('output');
expect(NodeLibrary.nameForPortType(json.components[0].ports[0].type)).toBe('string');
expect(json.components[0].ports[1].name).toBe('out2');
expect(json.components[0].ports[1].plug).toBe('output');
expect(NodeLibrary.nameForPortType(json.components[0].ports[1].type)).toBe('number');
NodeLibrary.instance.unregisterModule(ProjectModel.instance);
});
it('project settings are exported', function () {
ProjectModel.instance = ProjectModel.fromJSON(project2);
NodeLibrary.instance.registerModule(ProjectModel.instance);
ProjectModel.instance.setRootNode(ProjectModel.instance.findNodeWithId('Group-1'));
const json = Exporter.exportToJSON(ProjectModel.instance);
expect(json.settings.canvasWidth).toBe(303);
expect(json.settings.canvasHeight).toBe(404);
NodeLibrary.instance.unregisterModule(ProjectModel.instance);
});
function matchBundle(bundle, componentIndex) {
function arrayHasSameElements(a, b) {
//check if equal but ignore order
if (a.length !== b.length) return false;
return a.every((e) => b.includes(e));
}
return Object.values(componentIndex).some((b) => arrayHasSameElements(bundle, b));
}
it('can export an index that includes pages and for each nodes', function () {
ProjectModel.instance = ProjectModel.fromJSON({
components: [
{
name: '/root',
graph: {
roots: [
{
id: 'nav-stack',
type: 'Page Stack',
parameters: {
pages: [
{
id: 'p1',
label: 'Page1'
},
{
id: 'p2',
label: 'Page2'
}
],
'pageComp-p1': '/page1',
'pageComp-p2': '/page2'
}
},
{
id: 'n0',
type: '/shared-comp'
}
]
}
},
{
name: '/page1',
graph: {
roots: [
{
id: 'n0',
type: '/shared-comp'
}
]
}
},
{
name: '/page2',
graph: {}
},
{
name: '/shared-comp',
graph: {}
},
{
name: '/remaining-comp',
graph: {}
}
]
});
ProjectModel.instance.setRootNode(ProjectModel.instance.findNodeWithId('nav-stack'));
const json = Exporter.exportToJSON(ProjectModel.instance);
//make sure the root components are present directly in the export, not in a bundle
expect(json.components.find((c) => c.name === '/root'));
expect(json.components.find((c) => c.name === '/shared-comp'));
//this test assume the bundles are emitted in a specific order
//it makes the test tied to implementation specifics, so not great
const bundles = {
b2: {
components: ['/page1'],
dependencies: []
},
b3: {
components: ['/page2'],
dependencies: []
},
b4: {
components: ['/remaining-comp'],
dependencies: []
}
};
expect(json.componentIndex).toEqual(bundles);
});
it('can follow For Each nodes when collecting dependencies', function () {
ProjectModel.instance = ProjectModel.fromJSON({
components: [
{
name: '/root',
graph: {
roots: [
{
id: 'node1',
type: 'For Each',
parameters: {
template: '/for-each-comp'
}
}
]
}
},
{
name: '/for-each-comp',
graph: {}
}
]
});
const allComponents = ProjectModel.instance.getComponents();
const rootComponent = allComponents.find((c) => c.name === '/root');
const graph = Exporter._collectDependencyGraph(rootComponent, allComponents);
const deps = Exporter._flattenDependencyGraph(graph);
expect(deps.length).toBe(2);
expect(deps.find((c) => c.name === '/root'));
expect(deps.find((c) => c.name === '/for-each-comp'));
});
it("creates bundles that doesn't have duplicates", function () {
ProjectModel.instance = ProjectModel.fromJSON({
components: [
{
name: '/comp1',
graph: {
roots: [
{
id: 'nav-stack',
type: 'Page Stack',
parameters: {
pages: [
{
id: 'p1',
label: 'Page1'
},
{
id: 'p2',
label: 'Page2'
}
],
'pageComp-p1': '/page1',
'pageComp-p2': '/page2'
}
},
{
id: 'n0',
type: '/shared-comp'
}
]
}
},
{
name: '/page1',
id: 'page1',
graph: {
roots: [
{
id: 'n1',
type: '/shared-comp'
},
{
id: 'n2',
type: '/comp-used-on-both-pages'
}
]
}
},
{
name: '/page2',
id: 'page2',
graph: {
roots: [
{
id: 'n3',
type: '/comp-used-on-both-pages'
}
]
}
},
{
name: '/shared-comp',
graph: {}
},
{
name: '/comp-used-on-both-pages',
graph: {}
}
]
});
ProjectModel.instance.setRootNode(ProjectModel.instance.findNodeWithId('nav-stack'));
const json = Exporter.exportToJSON(ProjectModel.instance);
const componentCount = {};
for (const name in json.componentIndex) {
for (const comp of json.componentIndex[name].components) {
if (componentCount[comp]) componentCount[comp]++;
else componentCount[comp] = 1;
}
}
for (const name in componentCount) {
expect(componentCount[name]).toBe(1, 'Component ' + name + ' is in multiple bundles');
}
});
it('calculated dependencies for bundles', function () {
ProjectModel.instance = ProjectModel.fromJSON({
components: [
{
name: '/comp1',
graph: {
roots: [
{
id: 'nav-stack',
type: 'Page Stack',
parameters: {
pages: [
{
id: 'p1',
label: 'Page1'
},
{
id: 'p2',
label: 'Page2'
}
],
'pageComp-p1': '/page1',
'pageComp-p2': '/page2'
}
},
{
id: 'n0',
type: '/shared-comp'
}
]
}
},
{
name: '/page1',
id: 'page1',
graph: {
roots: [
{
id: 'n1',
type: '/shared-comp'
},
{
id: 'n2',
type: '/comp-used-on-both-pages'
}
]
}
},
{
name: '/page2',
id: 'page2',
graph: {
roots: [
{
id: 'n3',
type: '/comp-used-on-both-pages'
}
]
}
},
{
name: '/shared-comp',
graph: {}
},
{
name: '/comp-used-on-both-pages',
graph: {}
}
]
});
ProjectModel.instance.setRootNode(ProjectModel.instance.findNodeWithId('nav-stack'));
const allComponents = ProjectModel.instance.getComponents();
const rootComponent = allComponents.find((c) => c.name === '/comp1');
const componentIndex = Exporter.getComponentIndex(rootComponent, allComponents);
//this test assume the bundles are emitted in a specific order
//it makes the test tied to implementation specifics, so not great
const bundles = {
b0: {
components: ['/comp1'],
dependencies: ['b1']
},
b1: {
components: ['/shared-comp'],
dependencies: []
},
b2: {
components: ['/page1'],
dependencies: ['b1', 'b3']
},
b3: {
components: ['/comp-used-on-both-pages'],
dependencies: []
},
b4: {
components: ['/page2'],
dependencies: ['b3']
}
};
expect(componentIndex).toEqual(bundles);
});
xit('ignores project settings flagged to be excluded', function () {
ProjectModel.instance = ProjectModel.fromJSON({
components: [
{
name: '/comp2',
graph: {
roots: [
{
id: 'Group-1',
type: 'group'
}
]
}
}
],
settings: {
someSetting: 'test',
someSetting2: 'test2',
settingIgnoredInExport: 'test3'
}
});
NodeLibrary.instance.registerModule(ProjectModel.instance);
ProjectModel.instance.setRootNode(ProjectModel.instance.findNodeWithId('Group-1'));
const json = Exporter.exportToJSON(ProjectModel.instance);
expect(json.settings.someSetting).toBe('test');
expect(json.settings.someSetting2).toBe('test2');
expect(json.settings.settingIgnoredInExport).toBe(undefined);
NodeLibrary.instance.unregisterModule(ProjectModel.instance);
});
var project1 = {
components: [
{
name: '/comp1',
graph: {
roots: [
{
id: 'Image-1',
type: 'image',
parameters: {
image: 'pic1.png',
css: '%%%mycss {background:#ff00ff;}'
}
},
{
id: 'Comp-2',
type: '/comp2'
},
{
id: 'CO-1',
type: 'Component Outputs',
ports: [
{
name: 'out1',
type: '*',
plug: 'input'
},
{
name: 'out2',
type: '*',
plug: 'input'
}
]
}
],
connections: [
{
fromId: 'Image-1',
fromProperty: 'image',
toId: 'CO-1',
toProperty: 'out1'
},
{
fromId: 'Image-1',
fromProperty: 'screenX',
toId: 'CO-1',
toProperty: 'out2'
}
]
}
},
// Should be excluded
{
name: '/comp3',
graph: {
roots: [
{
id: 'Image-3',
type: 'image'
}
]
}
},
{
name: '/comp2',
graph: {
roots: [
{
id: 'Image-2',
type: 'image'
}
]
}
}
]
};
// Second project for cross project reference
var project2 = {
components: [
{
name: '/comp2',
graph: {
roots: [
{
id: 'Group-1',
type: 'group'
}
]
}
}
],
settings: {
canvasWidth: 303,
canvasHeight: 404
}
};
});

View File

@@ -0,0 +1,117 @@
const NodeLibrary = require('@noodl-models/nodelibrary').NodeLibrary;
const NodeGraphNode = require('@noodl-models/nodegraphmodel').NodeGraphNode;
const { ComponentModel } = require('@noodl-models/componentmodel');
describe('Component instances', function () {
var c;
beforeEach(() => {
window.NodeLibraryData = require('../nodegraph/nodelibrary');
NodeLibrary.instance.loadLibrary();
c = ComponentModel.fromJSON({
name: 'test',
graph: {
roots: [
{
type: 'group',
id: 'Group-1'
}
]
}
});
});
it('can load component', function () {
expect(c).not.toBe(undefined);
});
it('can detect children that can be attached', function () {
const n1 = NodeGraphNode.fromJSON({
type: 'image'
});
const n2 = NodeGraphNode.fromJSON({
type: 'animation'
});
const n3 = NodeGraphNode.fromJSON({
type: 'scaleModifier'
});
const n4 = NodeGraphNode.fromJSON({
type: 'image'
});
// Can accept an image
expect(c.graph.findNodeWithId('Group-1').canAcceptChildren([n1])).toBe(true);
// Not all nodes can be children
expect(c.graph.findNodeWithId('Group-1').canAcceptChildren([n1, n2])).toBe(false);
// Not all nodes have accepted categories
expect(c.graph.findNodeWithId('Group-1').canAcceptChildren([n1, n3])).toBe(false);
// Multiple nodes with same category should be OK
expect(c.graph.findNodeWithId('Group-1').canAcceptChildren([n1, n4])).toBe(true);
});
it('can report allow as child for components', function () {
var c2 = ComponentModel.fromJSON({
name: 'test',
graph: {}
});
// No root nodes that can be children
expect(c2.allowAsChild).toBe(false);
c2.graph.addRoot(
NodeGraphNode.fromJSON({
type: 'group'
})
);
// Group can be child
expect(c2.allowAsChild).toBe(true);
// Report correct category
expect(c2.category).toBe('visuals');
});
it('can report accepted children for components', function () {
expect(c.allowChildrenWithCategory).toBe(undefined);
c.graph.addRoot(
NodeGraphNode.fromJSON({
type: 'Component Children'
})
);
expect(c.allowChildrenWithCategory).toEqual(['visuals']);
c.graph.addRoot(
NodeGraphNode.fromJSON({
type: 'Component Modifier Children'
})
);
expect(c.allowChildrenWithCategory).toEqual(['modifiers', 'visuals']);
});
it('can detect allow export root', function () {
var c2 = ComponentModel.fromJSON({
name: 'test',
graph: {}
});
expect(c2.allowAsExportRoot).toBe(false);
c2.graph.addRoot(
NodeGraphNode.fromJSON({
type: 'group'
})
);
expect(c2.allowAsExportRoot).toBe(true);
});
});

View File

@@ -0,0 +1,11 @@
export * from './conflictwarnings';
export * from './createnewnode';
export * from './createstatus';
export * from './export';
export * from './hierarchy';
export * from './nodegrapheditor';
export * from './nodegraphmodel';
export * from './nodelibrary-spec';
export * from './propertyeditor';
export * from './typechangepropagation';
export * from './warnings-model-spec';

View File

@@ -0,0 +1,574 @@
const NodeGraphNode = require('@noodl-models/nodegraphmodel').NodeGraphNode;
const NodeGraphEditor = require('@noodl-views/nodegrapheditor').NodeGraphEditor;
const { ProjectModel } = require('@noodl-models/projectmodel');
const NodeLibrary = require('@noodl-models/nodelibrary').NodeLibrary;
const PopupLayer = require('@noodl-views/popuplayer');
const DebugInspector = require('@noodl-utils/debuginspector');
const ViewerConnection = require('../../src/editor/src/ViewerConnection');
describe('Node graph editor auto tests', function () {
var c1, g1;
// Records and store mouse calls to the node graph editor
var storeTimeout;
var recordedEvents = [];
function storeEvents() {
clearTimeout(storeTimeout);
storeTimeout = setTimeout(function () {
localStorage['recordedEvents'] = JSON.stringify(recordedEvents);
}, 1000);
}
function recordEvents() {
var types = ['mousedown', 'mousemove', 'mouseup'];
// mouse
var old = NodeGraphEditor.instance.mouse;
NodeGraphEditor.instance.mouse = function (type, pos, evt) {
var now = +new Date();
recordedEvents.push({
type: type,
time: now,
x: pos.x,
y: pos.y,
evt: evt
? {
button: evt.button,
shiftKey: evt.shiftKey ? true : undefined,
ctrlKey: evt.ctrlKey ? true : undefined,
spaceKey: evt.spaceKey ? true : undefined
}
: undefined
});
storeEvents();
return old.call(this, type, pos, evt);
};
// cut, copy, paste
var oldCut = NodeGraphEditor.instance.cut;
NodeGraphEditor.instance.cut = function () {
var now = +new Date();
recordedEvents.push({
type: 'cut',
time: now
});
return oldCut.call(this);
};
var oldCopy = NodeGraphEditor.instance.copy;
NodeGraphEditor.instance.copy = function () {
var now = +new Date();
recordedEvents.push({
type: 'copy',
time: now
});
return oldCopy.call(this);
};
var oldPaste = NodeGraphEditor.instance.paste;
NodeGraphEditor.instance.paste = function () {
var now = +new Date();
recordedEvents.push({
type: 'paste',
time: now
});
return oldPaste.call(this);
};
}
// Plays back previously stored mouse calls
function playEvents(events, done, realtime) {
var i = 0;
function play() {
var event = events[i];
var type = event.type;
if (type === 'cut') {
NodeGraphEditor.instance.cut();
} else if (type === 'copy') {
NodeGraphEditor.instance.copy();
} else if (type === 'paste') {
NodeGraphEditor.instance.paste();
} else {
NodeGraphEditor.instance.mouse(
event.type,
{
x: event.x,
y: event.y
},
event.evt
);
}
if (events[i + 1]) {
setTimeout(function () {
i++;
play();
}, events[i + 1].time - event.time);
} else {
done();
}
}
play();
}
// Sets up a fresh node graph editor
function setup() {
$('body').append(
'<div id="node-graph-editor" style="position:absolute; top:0px; right:0px; width:400px; height:800px; background-color:white;"></div>'
);
// Disable context menu
$('body').on('contextmenu', function () {
return false;
});
// NodeLibrary.instance = new NodeLibrary();
ProjectModel.instance = ProjectModel.fromJSON(project);
NodeLibrary.instance.registerModule(ProjectModel.instance);
c1 = ProjectModel.instance.getComponentWithName('Root');
g1 = c1.graph;
// Mocking panel instance
PopupLayer.instance = {
showTooltip: function () {},
hideTooltip: function () {},
showToast: function () {},
on: function () {},
hidePopup: function () {},
showPopup: function () {},
isDragging: function () {
return false;
},
showModal: function () {},
hideModal: function () {},
hideAllModalsAndPopups: function () {}
};
// Mocking viewer connection
ViewerConnection.instance = {
on: function () {},
sendNodeHighlighted: function () {}
};
NodeGraphEditor.instance = new NodeGraphEditor({
model: g1
});
NodeGraphEditor.instance.setPanAndScale({
scale: 1,
x: 0,
y: 0
});
NodeGraphEditor.instance.render();
$('#node-graph-editor').append(NodeGraphEditor.instance.el);
NodeGraphEditor.instance.resize({
x: 0,
y: 0,
width: 400,
height: 800
});
}
var project = {
components: [
{
name: 'Root',
ports: [],
visual: true,
visualRootId: '14e31556-f569-21bb-e948-65af515ae574',
canHaveVisualChildren: false,
graph: {
connections: [],
roots: []
}
}
]
};
function setupNodes() {
g1.addRoot(
NodeGraphNode.fromJSON({
id: '14e31556-f569-21bb-e948-65af515ae574',
type: 'group',
label: 'group1',
x: 49,
y: 75
})
);
g1.addRoot(
NodeGraphNode.fromJSON({
id: '33a2be5c-b341-27b4-292f-aee1b5bc30fe',
type: 'group',
label: 'group2',
x: 49,
y: 164
})
);
g1.addRoot(
NodeGraphNode.fromJSON({
id: '34ea0053-3334-d2cb-3a31-de577030102e',
type: 'group',
label: 'group3',
x: 49,
y: 267
})
);
}
function setupNodes2() {
g1.addRoot(
NodeGraphNode.fromJSON({
id: 'A',
type: 'group',
label: 'group4',
x: 49,
y: 367
})
);
}
function setupConnections() {
g1.addConnection({
fromId: '14e31556-f569-21bb-e948-65af515ae574',
fromProperty: 'screenX',
toId: '33a2be5c-b341-27b4-292f-aee1b5bc30fe',
toProperty: 'x'
});
}
function setupConnections2() {
g1.addConnection({
fromId: '34ea0053-3334-d2cb-3a31-de577030102e',
fromProperty: 'screenX',
toId: '33a2be5c-b341-27b4-292f-aee1b5bc30fe',
toProperty: 'x'
});
g1.addConnection({
fromId: '14e31556-f569-21bb-e948-65af515ae574',
fromProperty: 'screenX',
toId: '34ea0053-3334-d2cb-3a31-de577030102e',
toProperty: 'y'
});
}
function teardownNodes() {
g1.removeAllNodes();
}
// Closes and tears the node graph editor down
function teardown() {
$('#node-graph-editor').remove();
NodeGraphEditor.instance = undefined;
}
it('can setup view', function () {
setup();
expect(NodeGraphEditor.instance).not.toBe(undefined);
});
/* it('can record events',function(done) {
setupNodes();
// setupNodes2();
recordEvents();
});
return;*/
// Multi re arrange
xit('Multi rearrange nodes, attach, detach', function (done) {
setupNodes();
setupNodes2();
playEvents(require('../recordings/multirearrange.json'), function () {
expect(g1.roots.length).toBe(2);
var n1 = g1.findNodeWithId('14e31556-f569-21bb-e948-65af515ae574');
var n2 = g1.findNodeWithId('33a2be5c-b341-27b4-292f-aee1b5bc30fe');
var n3 = g1.findNodeWithId('34ea0053-3334-d2cb-3a31-de577030102e');
var n4 = g1.findNodeWithId('A');
expect(n1.children[0]).toBe(n3);
expect(n4.children[0]).toBe(n2);
expect(NodeGraphEditor.instance.verifyWithModel()).toBe(true);
teardownNodes();
done();
});
});
xit('cut n paste', function (done) {
setupNodes();
playEvents(require('../recordings/cutnpaste.json'), function () {
expect(g1.roots.length).toBe(3);
expect(g1.findNodeWithId('33a2be5c-b341-27b4-292f-aee1b5bc30fe')).not.toBe(undefined);
expect(g1.findNodeWithId('34ea0053-3334-d2cb-3a31-de577030102e')).not.toBe(undefined);
expect(g1.findNodeWithId('14e31556-f569-21bb-e948-65af515ae574')).not.toBe(undefined);
expect(NodeGraphEditor.instance.verifyWithModel()).toBe(true);
teardownNodes();
done();
});
});
xit('can undo attach, detach and move', function (done) {
setupNodes();
playEvents(require('../recordings/undoarrange.json'), function () {
// group3
expect(g1.findNodeWithId('34ea0053-3334-d2cb-3a31-de577030102e').parent).toBe(undefined);
// group2 child to group1
expect(g1.findNodeWithId('33a2be5c-b341-27b4-292f-aee1b5bc30fe').parent).toBe(
g1.findNodeWithId('14e31556-f569-21bb-e948-65af515ae574')
);
NodeGraphEditor.instance.undo(); // Undo detach
// group3 child to group1
expect(g1.findNodeWithId('34ea0053-3334-d2cb-3a31-de577030102e').parent).toBe(
g1.findNodeWithId('14e31556-f569-21bb-e948-65af515ae574')
);
NodeGraphEditor.instance.undo(); // Undo attach
expect(g1.findNodeWithId('34ea0053-3334-d2cb-3a31-de577030102e').parent).toBe(undefined);
expect(g1.findNodeWithId('33a2be5c-b341-27b4-292f-aee1b5bc30fe').parent).toBe(undefined);
NodeGraphEditor.instance.undo(); // Undo move
expect(g1.findNodeWithId('34ea0053-3334-d2cb-3a31-de577030102e').x).toBe(49);
expect(g1.findNodeWithId('34ea0053-3334-d2cb-3a31-de577030102e').y).toBe(267);
expect(NodeGraphEditor.instance.verifyWithModel()).toBe(true);
teardownNodes();
done();
});
});
// Single re arrange
xit('single rearrange nodes, attach, detach', function (done) {
setupNodes();
playEvents(require('../recordings/singlerearrange.json'), function () {
var n = g1.findNodeWithId('14e31556-f569-21bb-e948-65af515ae574');
expect(n.parent).toBe(undefined);
var n1 = g1.findNodeWithId('33a2be5c-b341-27b4-292f-aee1b5bc30fe');
var n2 = g1.findNodeWithId('34ea0053-3334-d2cb-3a31-de577030102e');
expect(n1.parent).toBe(n);
expect(n2.parent).toBe(n);
expect(n.children[0]).toBe(n2);
expect(n.children[1]).toBe(n1);
expect(g1.roots.length).toBe(1);
expect(NodeGraphEditor.instance.verifyWithModel()).toBe(true);
teardownNodes();
done();
});
});
// Multi select and mode nodes
xit('can multiselect and move nodes', function (done) {
setupNodes();
playEvents(require('../recordings/multimove.json'), function () {
var n1 = NodeGraphEditor.instance.model.findNodeWithId('14e31556-f569-21bb-e948-65af515ae574');
var n2 = NodeGraphEditor.instance.model.findNodeWithId('33a2be5c-b341-27b4-292f-aee1b5bc30fe');
expect(n1.x).toBe(50);
expect(n1.y).toBe(365);
expect(n2.x).toBe(50);
expect(n2.y).toBe(454);
expect(NodeGraphEditor.instance.verifyWithModel()).toBe(true);
teardownNodes();
done();
});
});
// Drag to move nodes
xit('can move nodes', function (done) {
setupNodes();
playEvents(require('../recordings/movenode.json'), function () {
var n = NodeGraphEditor.instance.model.findNodeWithId('14e31556-f569-21bb-e948-65af515ae574');
expect(n.x).toBe(47);
expect(n.y).toBe(408);
expect(NodeGraphEditor.instance.verifyWithModel()).toBe(true);
teardownNodes();
done();
});
});
xit('can undo cut and paste', function (done) {
setupNodes();
playEvents(require('../recordings/undocutpaste.json'), function () {
expect(g1.roots.length).toBe(3);
expect(g1.roots[1].label).toBe('group2');
expect(g1.roots[2].label).toBe('group3');
expect(g1.findNodeWithId('33a2be5c-b341-27b4-292f-aee1b5bc30fe')).toBe(undefined);
expect(g1.findNodeWithId('34ea0053-3334-d2cb-3a31-de577030102e')).toBe(undefined);
NodeGraphEditor.instance.undo(); // Undo paste
expect(g1.roots.length).toBe(1);
NodeGraphEditor.instance.undo(); // Undo cut
expect(g1.findNodeWithId('33a2be5c-b341-27b4-292f-aee1b5bc30fe')).not.toBe(undefined);
expect(g1.findNodeWithId('34ea0053-3334-d2cb-3a31-de577030102e')).not.toBe(undefined);
expect(NodeGraphEditor.instance.verifyWithModel()).toBe(true);
teardownNodes();
done();
});
});
// Undo cut children
xit('Can undo cut children', function (done) {
setupNodes();
setupNodes2();
playEvents(require('../recordings/undocutchildren.json'), function () {
expect(g1.findNodeWithId('14e31556-f569-21bb-e948-65af515ae574').children[0]).toBe(g1.findNodeWithId('A'));
NodeGraphEditor.instance.undo();
expect(g1.findNodeWithId('14e31556-f569-21bb-e948-65af515ae574').children[0]).toBe(
g1.findNodeWithId('33a2be5c-b341-27b4-292f-aee1b5bc30fe')
);
expect(g1.findNodeWithId('33a2be5c-b341-27b4-292f-aee1b5bc30fe').children[0]).toBe(
g1.findNodeWithId('34ea0053-3334-d2cb-3a31-de577030102e')
);
expect(NodeGraphEditor.instance.verifyWithModel()).toBe(true);
teardownNodes();
done();
});
});
// Undo cut connections
xit('Can undo cut connections', function (done) {
setupNodes();
setupConnections();
setupConnections2();
playEvents(require('../recordings/undocutconnections.json'), function () {
expect(g1.connections.length).toBe(0);
NodeGraphEditor.instance.undo();
expect(g1.connections.length).toBe(3);
expect(NodeGraphEditor.instance.verifyWithModel()).toBe(true);
teardownNodes();
done();
});
});
// Can delete and undo connection
xit('can delete and undo delete connection', function (done) {
setupNodes();
setupConnections();
playEvents(require('../recordings/deletecon.json'), function () {
expect(g1.connections.length).toBe(0);
NodeGraphEditor.instance.undo();
expect(g1.connections.length).toBe(1);
expect(g1.connections[0].fromId).toBe('14e31556-f569-21bb-e948-65af515ae574');
expect(NodeGraphEditor.instance.verifyWithModel()).toBe(true);
teardownNodes();
done();
});
});
// Delete
xit('can delete nodes and undo', function (done) {
setupNodes();
setupConnections();
playEvents(require('../recordings/deleteandundo.json'), function () {
NodeGraphEditor.instance.delete();
expect(g1.findNodeWithId('14e31556-f569-21bb-e948-65af515ae574')).toBe(undefined);
expect(g1.findNodeWithId('33a2be5c-b341-27b4-292f-aee1b5bc30fe')).toBe(undefined);
expect(g1.findNodeWithId('34ea0053-3334-d2cb-3a31-de577030102e')).not.toBe(undefined);
expect(g1.connections.length).toBe(0);
NodeGraphEditor.instance.undo();
expect(g1.findNodeWithId('14e31556-f569-21bb-e948-65af515ae574')).not.toBe(undefined);
expect(g1.findNodeWithId('33a2be5c-b341-27b4-292f-aee1b5bc30fe')).not.toBe(undefined);
expect(g1.connections.length).toBe(1);
teardownNodes();
done();
});
});
xit('Can create inspectors', function (done) {
setupNodes();
setupConnections();
setupConnections2();
playEvents(
require('../recordings/connectiondebugger.json'),
function () {
var model = DebugInspector.InspectorsModel.instanceForProject(ProjectModel.instance);
var inspectors = model.getInspectors();
expect(inspectors[1]).toEqual({
connectionKey: '14e31556-f569-21bb-e948-65af515ae574screenX33a2be5c-b341-27b4-292f-aee1b5bc30fex',
pinned: true,
position: 0.390625,
type: 'connection',
connection: {
fromId: '14e31556-f569-21bb-e948-65af515ae574',
fromProperty: 'screenX',
toId: '33a2be5c-b341-27b4-292f-aee1b5bc30fe',
toProperty: 'x'
}
});
expect(inspectors[0]).toEqual({
connectionKey: '34ea0053-3334-d2cb-3a31-de577030102escreenX33a2be5c-b341-27b4-292f-aee1b5bc30fex',
pinned: true,
position: 0.515625,
type: 'connection',
connection: {
fromId: '34ea0053-3334-d2cb-3a31-de577030102e',
fromProperty: 'screenX',
toId: '33a2be5c-b341-27b4-292f-aee1b5bc30fe',
toProperty: 'x'
}
});
expect(inspectors.length).toBe(2);
teardownNodes();
done();
},
true
); // Play in real time
});
// Multi select and mode nodes
it('can tear down', function () {
teardown();
expect(NodeGraphEditor.instance).toBe(undefined);
});
});

View File

@@ -0,0 +1,173 @@
const NodeGraphModel = require('@noodl-models/nodegraphmodel').NodeGraphModel;
const NodeGraphNode = require('@noodl-models/nodegraphmodel').NodeGraphNode;
describe('Node graph basic tests', function () {
it('fires nodeAdded, nodeRemoved and connectionAdded, connectionRemoved events', function () {
const g1 = new NodeGraphModel();
var handlers = jasmine.createSpyObj('handlers', [
'nodeAdded',
'nodeRemoved',
'connectionAdded',
'connectionRemoved'
]);
g1.on('nodeAdded', handlers.nodeAdded);
g1.on('nodeRemoved', handlers.nodeRemoved);
g1.on('connectionAdded', handlers.connectionAdded);
g1.on('connectionRemoved', handlers.connectionRemoved);
const n1 = NodeGraphNode.fromJSON({
type: 'layer',
id: 'A'
});
g1.addRoot(n1);
expect(handlers.nodeAdded.calls.argsFor(0)[0].model).toBe(n1);
const n2 = NodeGraphNode.fromJSON({
type: 'layer',
id: 'B'
});
g1.addRoot(n2);
expect(handlers.nodeAdded.calls.argsFor(1)[0].model).toBe(n2);
const con = {
fromId: 'A',
fromProperty: 'color',
toId: 'B',
toProperty: 'color'
};
g1.addConnection(con);
expect(handlers.connectionAdded.calls.argsFor(0)[0].model).toBe(con);
g1.removeConnection(con);
expect(handlers.connectionRemoved.calls.argsFor(0)[0].model).toBe(con);
g1.removeNode(n2);
expect(handlers.nodeRemoved.calls.argsFor(0)[0].model).toBe(n2);
});
it('can change the id on all nodes', () => {
const g1 = new NodeGraphModel();
const n1 = NodeGraphNode.fromJSON({
type: 'layer',
id: 'A'
});
g1.addRoot(n1);
const n2 = NodeGraphNode.fromJSON({
type: 'layer',
id: 'B'
});
g1.addRoot(n2);
const con = {
fromId: 'A',
fromProperty: 'color',
toId: 'B',
toProperty: 'color'
};
g1.addConnection(con);
g1.rekeyAllIds();
expect(n1.id).not.toEqual('A');
expect(n2.id).not.toEqual('B');
expect(con.fromId).toEqual(n1.id);
expect(con.toId).toEqual(n2.id);
});
describe('tracks what nodes are in the graph correctly', () => {
it('can add and remove one node', () => {
const graph = new NodeGraphModel();
//add a root
expect(graph.findNodeWithId('A')).toBeFalsy();
const n1 = NodeGraphNode.fromJSON({
type: 'test',
id: 'A'
});
graph.addRoot(n1);
expect(graph.findNodeWithId('A')).toBeTruthy();
//and then remove it
graph.removeNode(n1);
expect(graph.findNodeWithId('A')).toBeFalsy();
});
it('can add and remove a node with children', () => {
const graph = new NodeGraphModel();
//add a new root with children and verify that both nodes are found
const n2 = NodeGraphNode.fromJSON({
type: 'test',
id: 'A',
children: [
{
type: 'test',
id: 'B'
}
]
});
graph.addRoot(n2);
expect(graph.findNodeWithId('A')).toBe(n2);
expect(graph.findNodeWithId('B')).toBe(n2.children[0]);
//remove the root and verify that both nodes are removed
graph.removeNode(n2);
expect(graph.findNodeWithId('A')).toBeFalsy();
expect(graph.findNodeWithId('B')).toBeFalsy();
});
it('can add a child node to a root', () => {
const graph = new NodeGraphModel();
const root = NodeGraphNode.fromJSON({
type: 'test',
id: 'A'
});
graph.addRoot(root);
const child = NodeGraphNode.fromJSON({
type: 'test',
id: 'B'
});
root.addChild(child);
expect(graph.findNodeWithId('A')).toBe(root);
expect(graph.findNodeWithId('B')).toBe(child);
const child2 = NodeGraphNode.fromJSON({
type: 'test',
id: 'C'
});
root.insertChild(child2, 0);
expect(graph.findNodeWithId('C')).toBe(child2);
});
it('can remove a child node to a root', () => {
const graph = new NodeGraphModel();
const n2 = NodeGraphNode.fromJSON({
type: 'test',
id: 'A',
children: [
{
type: 'test',
id: 'B'
}
]
});
graph.addRoot(n2);
graph.removeNode(n2.children[0]);
expect(graph.findNodeWithId('B')).toBeFalsy();
expect(graph.findNodeWithId('A')).toBe(n2);
});
});
});

View File

@@ -0,0 +1,318 @@
const NodeLibrary = require('@noodl-models/nodelibrary').NodeLibrary;
const BasicNodeType = require('@noodl-models/nodelibrary/BasicNodeType').BasicNodeType;
const UnknownNodeType = require('@noodl-models/nodelibrary/UnknownNodeType').UnknownNodeType;
const { ComponentModel } = require('@noodl-models/componentmodel');
const { ProjectModel } = require('@noodl-models/projectmodel');
describe('Node library tests', function () {
beforeEach(() => {
//reload fresh library for every test
window.NodeLibraryData = require('../nodegraph/nodelibrary');
for (const module of NodeLibrary.instance.modules) {
NodeLibrary.instance.unregisterModule(module);
}
NodeLibrary.instance.loadLibrary();
});
it('can detect compatibale port types', function () {
// Equal types
expect(NodeLibrary.instance.canCastPortTypes('string', 'string')).toBe(true);
expect(
NodeLibrary.instance.canCastPortTypes(
{
name: 'string'
},
'string'
)
).toBe(true);
expect(
NodeLibrary.instance.canCastPortTypes('string', {
name: 'string'
})
).toBe(true);
// Not equal and cannot cast
expect(
NodeLibrary.instance.canCastPortTypes('string', {
name: 'signal'
})
).toBe(false);
// Star types are compatible with everything
expect(
NodeLibrary.instance.canCastPortTypes('*', {
name: 'number'
})
).toBe(true);
// Casting
expect(
NodeLibrary.instance.canCastPortTypes('number', {
name: 'string'
})
).toBe(true);
});
it('can find compatible port types', function () {
// Single type
expect(
NodeLibrary.instance.findCompatiblePortType([
{
direction: 'to',
type: 'string'
}
])
).toEqual({
name: 'string'
});
// Multiple connections, same type, with modifiers
expect(
NodeLibrary.instance.findCompatiblePortType([
{
direction: 'to',
type: 'string'
},
{
direction: 'from',
type: {
name: 'string',
multiline: true
}
}
])
).toEqual({
name: 'string',
multiline: true
});
// Is enums merged
expect(
NodeLibrary.instance.findCompatiblePortType([
{
direction: 'to',
type: {
name: 'enum',
enums: ['a', 'b']
}
},
{
direction: 'from',
type: {
name: 'enum',
enums: ['b', 'c']
}
}
])
).toEqual({
name: 'enum',
enums: ['a', 'b', 'c']
});
// Find compatible type and merge
expect(
NodeLibrary.instance.findCompatiblePortType([
{
direction: 'to',
type: {
name: 'number',
allowConnectionsOnly: true
}
},
{
direction: 'to',
type: 'boolean'
}
])
).toEqual({
name: 'number',
allowConnectionsOnly: true
});
// Find compatible type, mixed direction
expect(
NodeLibrary.instance.findCompatiblePortType([
{
direction: 'to',
type: {
name: 'number',
allowEditOnly: true
}
},
{
direction: 'to',
type: 'boolean'
},
{
direction: 'from',
type: 'number'
}
])
).toEqual({
name: 'number',
allowEditOnly: true
});
// Find compatible type, mixed direction
expect(
NodeLibrary.instance.findCompatiblePortType([
{
direction: 'to',
type: {
name: 'number'
}
},
{
direction: 'to',
type: 'boolean'
},
{
direction: 'from',
type: 'number'
},
{
direction: 'to',
type: 'color'
}
])
).toEqual({
name: 'string'
});
// Can't find compatible type
expect(
NodeLibrary.instance.findCompatiblePortType([
{
direction: 'to',
type: {
name: 'number',
allowEditOnly: true
}
},
{
direction: 'to',
type: 'boolean'
},
{
direction: 'from',
type: 'number'
},
{
direction: 'to',
type: 'referece'
}
])
).toBe(undefined);
});
it('can reload library', function () {
var p = ProjectModel.fromJSON(getProject());
NodeLibrary.instance.registerModule(p);
var n1 = p.components[0].graph.findNodeWithId('A');
var n2 = p.components[0].graph.findNodeWithId('B');
var n3 = p.components[0].graph.findNodeWithId('C');
var n4 = p.components[0].graph.findNodeWithId('D');
expect(n1.type instanceof BasicNodeType).toBe(true);
expect(n1.type.name).toBe('image');
expect(n2.type instanceof UnknownNodeType).toBe(true);
// Update library
NodeLibraryData.nodetypes.push({
name: 'newimage',
allowAsChild: true,
allowAsExclusiveRootOnly: true,
category: 'visuals',
ports: [
{
name: 'x',
type: {
name: 'number'
},
plug: 'input'
}
]
});
NodeLibrary.instance.reload();
// Both should be OK now
expect(n1.type instanceof BasicNodeType).toBe(true);
expect(n1.type.name).toBe('image');
expect(n2.type instanceof BasicNodeType).toBe(true);
expect(n2.type.name).toBe('newimage');
NodeLibrary.instance.unregisterModule(p);
});
it('can get types by name', () => {
expect(NodeLibrary.instance.getNodeTypeWithName('comp1')).toBeUndefined();
//get basic type from node library
expect(NodeLibrary.instance.getNodeTypeWithName('image')).toBeInstanceOf(BasicNodeType);
//get component type from a project
const p = ProjectModel.fromJSON(getProject());
NodeLibrary.instance.registerModule(p);
expect(NodeLibrary.instance.getNodeTypeWithName('comp1')).toBeInstanceOf(ComponentModel);
//add a new component and get the type
const newComponent = ComponentModel.fromJSON({
name: 'comp2',
graph: {
roots: [
{
id: 'B',
type: 'image'
}
]
}
});
p.addComponent(newComponent);
expect(NodeLibrary.instance.getNodeTypeWithName('comp2')).toBe(newComponent);
//remove the component again
p.removeComponent(newComponent);
expect(NodeLibrary.instance.getNodeTypeWithName('comp2')).toBeUndefined();
//unregister project and make sure the components aren't available anymore
NodeLibrary.instance.unregisterModule(p);
expect(NodeLibrary.instance.getNodeTypeWithName('comp1')).toBeUndefined();
expect(NodeLibrary.instance.getNodeTypeWithName('comp2')).toBeUndefined();
});
function getProject() {
return {
components: [
{
name: 'comp1',
graph: {
roots: [
{
id: 'A',
type: 'image'
},
{
id: 'B',
type: 'newimage'
},
{
id: 'C',
type: 'Event Sender',
parameters: {
channel: 'Channel0'
}
},
{
id: 'D',
type: 'Event Receiver'
}
]
}
}
]
};
}
});

View File

@@ -0,0 +1,751 @@
function createTestNodeLibrary() {
return {
projectsettings: {
dynamicports: [],
ports: [
{
name: 'settingIgnoredInExport',
type: 'string',
ignoreInExport: true
},
{
name: 'someSetting',
type: 'string',
ignoreInExport: false
},
{
name: 'someSetting2',
type: 'string'
}
]
},
typecasts: [
{
from: 'boolean',
to: []
},
{
from: 'number',
to: ['string', 'boolean']
},
{
from: 'string',
to: ['number', 'boolean', 'color']
}
],
dynamicports: [
{
type: 'conditionalports',
name: 'basic'
},
{
type: 'expand',
name: 'basic'
},
{
type: 'portchannel',
name: 'event-sender-channel',
channelPortname: 'channel',
typename: 'Event Sender',
ignorePorts: ['channel']
},
{
type: 'numbered',
name: 'basic-number',
port: {
type: 'number'
}
},
{
type: 'regexp',
name: 'expression-js',
filters: [
{
type: 'replace',
comment: 'Removed javascript style comments',
regexp: '((\\/\\/.*$)|(\\/\\*[\\s\\S]*?\\*\\/))',
args: 'mg',
with: ''
},
{
type: 'replace',
regexp: '"[^"]+"',
args: 'g',
with: ''
},
{
type: 'replace',
regexp: "'[^']+'",
args: 'g',
with: ''
},
{
type: 'replace',
regexp:
'(break|case|class|catch|const|continue|debugger|default|delete|do|else|export|extends|finally|for|function|if|import|in|instanceof|let|new|return|super|switch|this|throw|try|typeof|var|void|while|with|yield)',
args: 'g',
with: ''
},
{
type: 'replace',
regexp: '\\s',
args: 'g',
with: ''
},
{
type: 'ignore',
regexp: '([a-z]|[A-Z])([a-z]|\\.|[A-Z]|[0-9])*(?=\\()'
},
{
type: 'ignore',
regexp: '([a-z]|[A-Z])([a-z]|\\.|[A-Z]|[0-9])*(?=\\=)'
},
{
type: 'ports',
regexp: '([a-z]|[A-Z])([a-z]|\\.|[A-Z]|[0-9])*(?=\\:boolean)',
args: 'g',
port: {
type: {
name: 'boolean'
},
plug: 'input'
}
},
{
type: 'replace',
regexp: '([a-z]|[A-Z])([a-z]|\\.|[A-Z]|[0-9])*\\:boolean',
args: 'g'
},
{
type: 'ports',
regexp: '([a-z]|[A-Z])([a-z]|\\.|[A-Z]|[0-9])*(?=\\:string)',
args: 'g',
port: {
type: {
name: 'string'
},
plug: 'input'
}
},
{
type: 'replace',
regexp: '([a-z]|[A-Z])([a-z]|\\.|[A-Z]|[0-9])*\\:string',
args: 'g'
},
{
type: 'ports',
regexp: '([a-z]|[A-Z])([a-z]|\\.|[A-Z]|[0-9])*(?=\\:number)',
args: 'g',
port: {
type: {
name: 'number'
},
plug: 'input'
}
},
{
type: 'replace',
regexp: '([a-z]|[A-Z])([a-z]|\\.|[A-Z]|[0-9])*\\:number',
args: 'g'
},
{
type: 'ports',
regexp: '([a-z]|[A-Z])([a-z]|\\.|[A-Z]|[0-9])*',
args: 'g',
port: {
type: {
name: '=',
default: 'string',
allowedTypes: ['string', 'boolean', 'number']
},
plug: 'input'
}
}
]
}
],
nodetypes: [
{
name: 'Component Outputs',
haveComponentPorts: true,
color: 'component',
ports: []
},
{
name: 'Component Inputs',
color: 'component',
haveComponentPorts: true,
ports: []
},
{
name: 'Component Children',
color: 'component',
category: 'visuals',
allowSingleInstanceOnly: true,
haveComponentChildren: ['visuals']
},
{
name: 'Component Modifier Children',
color: 'component',
category: 'modifiers',
allowSingleInstanceOnly: true,
haveComponentChildren: ['modifiers']
},
{
name: 'group',
version: 2,
allowAsChild: true,
allowAsExportRoot: true,
category: 'visuals',
allowChildrenWithCategory: ['visuals'],
ports: [
{
name: 'compref',
type: 'component',
plug: 'input'
},
{
group: 'test',
name: 'x',
type: {
name: 'number',
units: ['px', '%']
},
default: {
value: 10,
unit: '%'
},
plug: 'input'
},
{
group: 'test',
name: 'y',
type: {
name: 'number'
},
plug: 'input'
},
{
name: 'opacity',
type: 'number',
plug: 'input'
},
{
displayName: 'AlignX',
name: 'alignX',
type: {
name: 'enum',
enums: [
{
value: 'left',
label: 'Left'
},
{
value: 'center',
label: 'Center'
},
{
value: 'right',
label: 'Right'
}
]
},
plug: 'input'
},
{
name: 'alignY',
type: {
name: 'string',
enums: ['top', 'center', 'bottom']
},
plug: 'input'
},
{
name: 'scaleX',
type: 'number',
plug: 'input'
},
{
name: 'scaleY',
type: 'number',
plug: 'input'
},
{
name: 'width',
type: 'number',
plug: 'input'
},
{
name: 'height',
type: 'number',
plug: 'input'
},
{
name: 'fill',
type: {
name: 'string',
enums: ['parent', 'width', 'height', 'aspectFill', 'aspectFit']
},
plug: 'input'
},
{
name: 'pivotX',
type: 'number',
plug: 'input'
},
{
name: 'pivotY',
type: 'number',
plug: 'input'
},
{
name: 'layoutX',
type: 'number',
plug: 'input'
},
{
name: 'layoutY',
type: 'number',
plug: 'input'
},
{
name: 'depth',
type: {
name: 'number',
casts: ['boolean']
},
plug: 'input'
},
{
name: 'rotationX',
type: 'number',
plug: 'input'
},
{
name: 'rotationY',
type: 'number',
plug: 'input'
},
{
name: 'rotationZ',
type: 'number',
plug: 'input'
},
{
name: 'touchPropagation',
type: {
name: 'boolean',
casts: ['number']
},
plug: 'input'
},
{
name: 'layout',
type: {
name: 'string',
enums: ['stackVertical', 'stackHorizontal', 'flowVertical', 'flowHorizontal', 'none']
},
plug: 'input'
},
{
name: 'clip',
type: 'boolean',
plug: 'input'
},
{
group: 'test',
name: 'screenX',
type: 'number',
plug: 'output'
},
{
group: 'test',
name: 'screenY',
type: 'number',
plug: 'output'
},
{
name: 'width',
type: 'number',
plug: 'output'
},
{
name: 'height',
type: 'number',
plug: 'output'
},
{
name: 'this',
type: 'reference',
plug: 'output'
},
{
name: 'clipOut',
type: 'boolean',
plug: 'output'
}
]
},
{
name: 'image',
allowAsChild: true,
allowAsExclusiveRootOnly: true,
category: 'visuals',
ports: [
{
name: 'x',
type: {
name: 'number'
},
plug: 'input'
},
{
name: 'y',
type: {
name: 'number'
},
plug: 'input'
},
{
name: 'opacity',
type: 'number',
plug: 'input'
},
{
name: 'alignX',
type: {
name: 'string',
enums: ['left', 'center', 'right']
},
plug: 'input'
},
{
name: 'alignY',
type: {
name: 'string',
enums: ['top', 'center', 'bottom']
},
plug: 'input'
},
{
name: 'scaleX',
type: 'number',
plug: 'input'
},
{
name: 'scaleY',
type: 'number',
plug: 'input'
},
{
name: 'width',
type: 'number',
plug: 'input'
},
{
name: 'height',
type: 'number',
plug: 'input'
},
{
name: 'fill',
type: {
name: 'string',
enums: ['parent', 'width', 'height', 'aspectFill', 'aspectFit']
},
plug: 'input'
},
{
name: 'pivotX',
type: 'number',
plug: 'input'
},
{
name: 'pivotY',
type: 'number',
plug: 'input'
},
{
name: 'layoutX',
type: 'number',
plug: 'input'
},
{
name: 'layoutY',
type: 'number',
plug: 'input'
},
{
name: 'depth',
type: {
name: 'number',
casts: ['boolean']
},
plug: 'input'
},
{
name: 'rotationX',
type: 'number',
plug: 'input'
},
{
name: 'rotationY',
type: 'number',
plug: 'input'
},
{
name: 'rotationZ',
type: 'number',
plug: 'input'
},
{
name: 'touchPropagation',
type: {
name: 'boolean',
casts: ['number']
},
plug: 'input'
},
{
name: 'blendMode',
type: {
name: 'string',
enums: ['normal', 'solid', 'additive', 'multiply']
},
plug: 'input'
},
{
name: 'color',
type: 'color',
plug: 'input'
},
{
name: 'image',
type: {
name: 'string'
},
allowEditOnly: true,
plug: 'input/output'
},
{
name: 'image2',
type: {
name: 'image'
},
plug: 'input'
},
{
name: 'font',
type: {
name: 'font'
},
plug: 'input'
},
{
name: 'css',
type: {
name: 'string',
codeeditor: 'css'
},
allowEditOnly: true,
plug: 'input'
},
{
name: 'shader',
type: 'shader',
plug: 'input'
},
{
group: 'gruppen',
name: 'screenX',
type: 'number',
plug: 'output'
},
{
name: 'screenY',
type: 'number',
plug: 'output'
},
{
name: 'width',
type: 'number',
plug: 'output'
},
{
name: 'height',
type: 'number',
plug: 'output'
},
{
name: 'this',
type: 'reference',
plug: 'output'
}
]
},
{
name: 'animation'
},
{
name: 'scaleModifier',
category: 'modifiers'
},
{
name: 'nodeWithNumberedPorts',
dynamicports: [
{
name: 'numbered/basic-number',
prefix: 'my number',
displayPrefix: 'My number',
port: {
group: 'My group'
}
}
]
},
{
name: 'nodeWithNumberedPortsAndSelectors',
dynamicports: [
{
name: 'numbered/basic-number',
prefix: 'my number',
displayPrefix: 'My number',
port: {
group: 'My group'
},
selectors: [
{
name: 'startAt',
displayName: 'Start at',
group: 'My selectors'
}
]
}
]
},
{
name: 'javascript expression',
usePortAsLabel: 'expression',
dynamicports: [
{
name: 'regexp/expression-js',
port: 'expression'
}
],
ports: [
{
name: 'expression',
type: {
name: 'string',
multiline: true
},
plug: 'input'
},
{
name: 'result',
type: '=',
plug: 'output'
}
]
},
{
name: 'Event Sender',
ports: [
{
name: 'channel',
type: 'string',
plug: 'input'
}
]
},
{
name: 'Event Receiver',
dynamicports: [
{
name: 'portchannel/event-sender-channel',
channelPort: {
name: 'channel',
displayName: 'Channel',
plug: 'input'
},
port: {
type: '*',
plug: 'output'
}
}
]
},
{
name: 'Anim',
dynamicports: [
{
name: 'conditionalports/basic',
condition: 'type = typeA',
ports: [
{
name: 'from',
plug: 'input',
type: 'number'
}
]
},
{
name: 'conditionalports/basic',
condition: 'type != typeA',
ports: [
{
name: 'to',
plug: 'input',
type: 'number'
}
]
}
],
ports: [
{
name: 'type',
type: 'string',
plug: 'input'
}
]
},
{
name: 'ExpandPorts',
dynamicports: [
{
name: 'expand/basic',
indexStep: 100,
template: [
{
name: '{{portname}}.A',
plug: 'input',
type: 'number',
index: 1
}
]
},
{
name: 'expand/basic',
indexStep: 100,
condition: "'{{portname}}.A' = test OR '{{portname}}.A' NOT SET",
template: [
{
name: '{{portname}}.B',
plug: 'input',
type: 'number',
index: 2
}
]
}
]
}
]
};
}
module.exports = createTestNodeLibrary();

View File

@@ -0,0 +1,92 @@
const { PropertyEditor } = require('@noodl-views/panels/propertyeditor/propertyeditor');
const { NodeGraphNodeRename } = require('@noodl-views/panels/propertyeditor');
const { ProjectModel } = require('@noodl-models/projectmodel');
const NodeLibrary = require('@noodl-models/nodelibrary').NodeLibrary;
const { UndoQueue } = require('@noodl-models/undo-queue-model');
describe('Property editor panel unit tests', function () {
var pe, n, c;
beforeEach(() => {
ProjectModel.instance = ProjectModel.fromJSON(project);
NodeLibrary.instance.registerModule(ProjectModel.instance);
c = ProjectModel.instance.getComponentWithName('Root');
n = c.graph.findNodeWithId('A');
pe = new PropertyEditor({
model: n
});
pe.render();
});
afterEach((done) => {
//some tests schedule renders with a setTimeout
//so schedule one ourselves before we clean up so the console
//isn't filled with errors
setTimeout(() => {
ProjectModel.instance = undefined;
done();
}, 1);
});
it('can delete node and undo', function () {
pe.performDelete();
expect(c.graph.findNodeWithId('A')).toBe(undefined);
expect(c.graph.connections.length).toBe(0);
UndoQueue.instance.undo();
expect(c.graph.findNodeWithId('A')).not.toBe(undefined);
expect(c.graph.connections.length).toBe(1);
});
it('can rename and undo', function () {
NodeGraphNodeRename(n, 'test');
expect(c.graph.findNodeWithId('A').label).toBe('test');
UndoQueue.instance.undo();
expect(c.graph.findNodeWithId('A').label).toBe('group');
});
it('can edit parameter and undo', function () {
pe.portsView.setParameter('alpha', 0.5);
expect(c.graph.findNodeWithId('A').parameters['alpha']).toBe(0.5);
UndoQueue.instance.undo();
expect(c.graph.findNodeWithId('A').parameters['alpha']).toBe(undefined);
});
var project = {
components: [
{
name: 'Root',
graph: {
roots: [
{
id: 'A',
type: 'group'
},
{
id: 'B',
type: 'group'
}
],
connections: [
{
fromId: 'A',
toId: 'B',
fromProperty: 'x',
toProperty: 'y'
}
]
}
}
]
};
});

View File

@@ -0,0 +1,234 @@
const { ProjectModel } = require('@noodl-models/projectmodel');
const NodeLibrary = require('@noodl-models/nodelibrary').NodeLibrary;
const { ComponentModel } = require('@noodl-models/componentmodel');
describe('Type change propagation', function () {
var p;
it('can load project', function () {
p = ProjectModel.fromJSON(project1);
NodeLibrary.instance.registerModule(p);
expect(p).not.toBe(undefined);
});
xit('can remove connections and type will propagate', function () {
// Remove connections
var comp2 = p.getComponentWithName('/comp2');
comp2.graph.removeConnection(comp2.graph.connections[0]);
comp2.graph.removeConnection(comp2.graph.connections[0]);
// Component types should now have propagated and been set to undefined
var comp1 = p.getComponentWithName('/comp1');
expect(NodeLibrary.nameForPortType(comp1.findPortWithName('in1').type)).toBe(undefined);
expect(NodeLibrary.nameForPortType(comp1.findPortWithName('out2').type)).toBe(undefined);
});
xit('can reconnect and type will propagate', function () {
// Add connections
var comp2 = p.getComponentWithName('/comp2');
var comp1 = p.getComponentWithName('/comp1');
comp2.graph.addConnection({
fromId: 'a05abb49-625e-8bd6-7183-7ea07a46e1d4',
fromProperty: 'screenX',
toId: '6783bee9-1225-5840-354f-348a3d270b6d',
toProperty: 'out2'
});
comp2.graph.addConnection({
fromId: 'b1a816da-5023-18a2-59ba-9c58be5cd073',
fromProperty: 'in1',
toId: 'a05abb49-625e-8bd6-7183-7ea07a46e1d4',
toProperty: 'image'
});
// The types should have propagetd to comp1
expect(NodeLibrary.nameForPortType(comp1.findPortWithName('in1').type)).toBe('string');
expect(NodeLibrary.nameForPortType(comp1.findPortWithName('out2').type)).toBe('number');
});
xit('removing a type results in undefined type', function () {
var n1 = p.getComponentWithName('/comp').graph.findNodeWithId('A');
p.getComponentWithName('/comp').graph.evaluateHealth();
expect(n1.getHealth().healthy).toBe(false);
expect(n1.type.localName).toBe('notfound');
expect(n1.type.fullName).toBe('/notfound');
p.addComponent(
ComponentModel.fromJSON({
name: '/notfound',
graph: {}
})
);
p.getComponentWithName('/comp').graph.evaluateHealth();
expect(n1.getHealth().healthy).toBe(true); // Node type should now be found
});
xit('can remove type and create new one with new name', function () {
var n1 = p.getComponentWithName('/comp').graph.findNodeWithId('A');
var comp = p.getComponentWithName('/notfound');
p.removeComponent(comp);
p.getComponentWithName('/comp').graph.evaluateHealth();
expect(n1.getHealth().healthy).toBe(false); // Health should be bad again
var comp2 = ComponentModel.fromJSON({
name: '/notfound',
graph: {}
});
p.addComponent(comp2);
p.getComponentWithName('/comp').graph.evaluateHealth();
expect(n1.getHealth().healthy).toBe(true); // The new type should be resolved
expect(n1.type).toBe(comp2);
expect(n1.toJSON().type).toBe('/notfound');
});
xit('can rename components, type is changed', function () {
var n1 = p.getComponentWithName('/comp').graph.findNodeWithId('A');
// rename the type
p.renameComponentWithName('/notfound', '/notfound2');
expect(n1.getHealth().healthy).toBe(true); // The new type should be resolved
expect(n1.typename).toBe('/notfound2');
});
var project1 = {
components: [
{
name: '/comp',
graph: {
roots: [
{
id: 'A',
type: '/notfound'
}
]
}
},
{
name: '/comp1',
graph: {
connections: [
{
fromId: '7f6b3382-b5c9-ae85-3614-3a3d0b31e4ce',
fromProperty: 'out2',
toId: 'd72cfb77-8515-8612-efed-c92e3f533b8b',
toProperty: 'out2'
},
{
fromId: '15a67805-e2ee-1fa4-b86a-4efc04e7028d',
fromProperty: 'in1',
toId: '7f6b3382-b5c9-ae85-3614-3a3d0b31e4ce',
toProperty: 'in1'
}
],
roots: [
{
id: 'd72cfb77-8515-8612-efed-c92e3f533b8b',
type: 'Component Outputs',
x: 274,
y: 185,
parameters: {},
children: [],
ports: [
{
name: 'out2',
type: '*',
plug: 'input'
}
]
},
{
id: '7f6b3382-b5c9-ae85-3614-3a3d0b31e4ce',
type: '/comp2',
x: 530,
y: 185,
parameters: {},
children: []
},
{
id: '15a67805-e2ee-1fa4-b86a-4efc04e7028d',
type: 'Component Inputs',
x: 792,
y: 186,
parameters: {},
children: [],
ports: [
{
name: 'in1',
type: '*',
plug: 'output'
}
]
}
]
}
},
{
name: '/comp2',
visual: true,
visualRootId: 'a05abb49-625e-8bd6-7183-7ea07a46e1d4',
canHaveVisualChildren: false,
graph: {
connections: [
{
fromId: 'a05abb49-625e-8bd6-7183-7ea07a46e1d4',
fromProperty: 'this',
toId: '6783bee9-1225-5840-354f-348a3d270b6d',
toProperty: 'out2'
},
{
fromId: 'b1a816da-5023-18a2-59ba-9c58be5cd073',
fromProperty: 'in1',
toId: 'a05abb49-625e-8bd6-7183-7ea07a46e1d4',
toProperty: 'x'
}
],
roots: [
{
id: '6783bee9-1225-5840-354f-348a3d270b6d',
type: 'Component Outputs',
x: 686,
y: 150,
parameters: {},
children: [],
ports: [
{
name: 'out2',
type: '*',
plug: 'input'
}
]
},
{
id: 'a05abb49-625e-8bd6-7183-7ea07a46e1d4',
type: 'image',
x: 426,
y: 147,
parameters: {},
children: []
},
{
id: 'b1a816da-5023-18a2-59ba-9c58be5cd073',
type: 'Component Inputs',
x: 169,
y: 148,
parameters: {},
children: [],
ports: [
{
name: 'in1',
type: '*',
plug: 'output'
}
]
}
]
}
}
],
name: 'proj1'
};
});

View File

@@ -0,0 +1,32 @@
const WarningsModel = require('@noodl-models/warningsmodel').WarningsModel;
describe('Warnings model', function () {
beforeEach(() => {
//since warningsmodel is global there can be warnings left over from other tests, make sure to clear them
WarningsModel.instance.clearAllWarnings();
});
it('can remove warnings using a callback for matching', function () {
WarningsModel.instance.setWarning(
{
key: 'test-warning-remove'
},
{
message: 'test warning'
}
);
WarningsModel.instance.setWarning(
{
key: 'test-warning'
},
{
message: 'test warning'
}
);
expect(WarningsModel.instance.getTotalNumberOfWarnings()).toBe(2);
WarningsModel.instance.clearWarningsForRefMatching((ref) => ref.key.includes('remove'));
expect(WarningsModel.instance.getTotalNumberOfWarnings()).toBe(1);
});
});