improve back and frontend testing
This commit is contained in:
371
frontend/tests/connectionUtils.test.mjs
Normal file
371
frontend/tests/connectionUtils.test.mjs
Normal file
@@ -0,0 +1,371 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
import {
|
||||
getHandleType,
|
||||
getInputName,
|
||||
getOutputSlot,
|
||||
encodeProxyHandleRef,
|
||||
decodeProxyHandleRef,
|
||||
parseGroupProxyHandle,
|
||||
getConnectionHandleType,
|
||||
getResolvedHandleRef,
|
||||
getNodeInputSpecForHandle,
|
||||
outputTypeCanConnectToTarget,
|
||||
resolveOutputTypeForTarget,
|
||||
checkConnectionValid,
|
||||
} from '../src/connectionUtils.js';
|
||||
|
||||
// ── Handle ID helpers ─────────────────────────────────────────────────
|
||||
|
||||
test('getHandleType extracts the type segment from a handle ID', () => {
|
||||
assert.equal(getHandleType('output::0::DATA_FIELD'), 'DATA_FIELD');
|
||||
assert.equal(getHandleType('input::field::DATA_FIELD'), 'DATA_FIELD');
|
||||
assert.equal(getHandleType('output::2::LINE'), 'LINE');
|
||||
});
|
||||
|
||||
test('getInputName extracts the name segment from an input handle ID', () => {
|
||||
assert.equal(getInputName('input::field::DATA_FIELD'), 'field');
|
||||
assert.equal(getInputName('input::colormap::COLORMAP'), 'colormap');
|
||||
});
|
||||
|
||||
test('getOutputSlot extracts the slot index from an output handle ID', () => {
|
||||
assert.equal(getOutputSlot('output::0::DATA_FIELD'), 0);
|
||||
assert.equal(getOutputSlot('output::3::LINE'), 3);
|
||||
});
|
||||
|
||||
test('encodeProxyHandleRef round-trips with decodeProxyHandleRef', () => {
|
||||
const handle = 'output::0::DATA_FIELD';
|
||||
assert.equal(decodeProxyHandleRef(encodeProxyHandleRef(handle)), handle);
|
||||
});
|
||||
|
||||
test('encodeProxyHandleRef handles null/undefined gracefully', () => {
|
||||
assert.equal(decodeProxyHandleRef(encodeProxyHandleRef(null)), '');
|
||||
assert.equal(decodeProxyHandleRef(encodeProxyHandleRef(undefined)), '');
|
||||
});
|
||||
|
||||
test('decodeProxyHandleRef falls back for malformed percent-encoding', () => {
|
||||
// '%zz' is not valid percent-encoding; should return the raw string
|
||||
const bad = '%zz';
|
||||
const result = decodeProxyHandleRef(bad);
|
||||
assert.equal(result, bad);
|
||||
});
|
||||
|
||||
test('decodeProxyHandleRef handles a handle with colons preserved after round-trip', () => {
|
||||
const handle = 'output::1::RECORD_TABLE';
|
||||
const encoded = encodeProxyHandleRef(handle);
|
||||
assert.ok(!encoded.includes('::'), 'colons should be encoded');
|
||||
assert.equal(decodeProxyHandleRef(encoded), handle);
|
||||
});
|
||||
|
||||
// ── parseGroupProxyHandle ─────────────────────────────────────────────
|
||||
|
||||
test('parseGroupProxyHandle returns null for regular handles', () => {
|
||||
assert.equal(parseGroupProxyHandle('output::0::DATA_FIELD'), null);
|
||||
assert.equal(parseGroupProxyHandle('input::field::DATA_FIELD'), null);
|
||||
assert.equal(parseGroupProxyHandle(''), null);
|
||||
assert.equal(parseGroupProxyHandle(null), null);
|
||||
});
|
||||
|
||||
test('parseGroupProxyHandle returns null for too-short proxy handles', () => {
|
||||
assert.equal(parseGroupProxyHandle('group-proxy::out::nodeA'), null);
|
||||
assert.equal(parseGroupProxyHandle('group-proxy::out::nodeA::DATA_FIELD'), null);
|
||||
});
|
||||
|
||||
test('parseGroupProxyHandle parses a valid outbound proxy handle', () => {
|
||||
const inner = 'output::0::DATA_FIELD';
|
||||
const encoded = encodeProxyHandleRef(inner);
|
||||
const proxy = parseGroupProxyHandle(`group-proxy::out::node42::DATA_FIELD::${encoded}`);
|
||||
assert.ok(proxy !== null);
|
||||
assert.equal(proxy.direction, 'out');
|
||||
assert.equal(proxy.nodeId, 'node42');
|
||||
assert.equal(proxy.type, 'DATA_FIELD');
|
||||
assert.equal(proxy.realHandle, inner);
|
||||
});
|
||||
|
||||
test('parseGroupProxyHandle parses a valid inbound proxy handle', () => {
|
||||
const inner = 'input::field::DATA_FIELD';
|
||||
const encoded = encodeProxyHandleRef(inner);
|
||||
const proxy = parseGroupProxyHandle(`group-proxy::in::node7::DATA_FIELD::${encoded}`);
|
||||
assert.ok(proxy !== null);
|
||||
assert.equal(proxy.direction, 'in');
|
||||
assert.equal(proxy.nodeId, 'node7');
|
||||
assert.equal(proxy.realHandle, inner);
|
||||
});
|
||||
|
||||
test('parseGroupProxyHandle handles colons inside the encoded real handle', () => {
|
||||
// The real handle contains '::' which gets encoded; the join should reconstruct correctly
|
||||
const inner = 'output::2::LINE';
|
||||
const encoded = encodeProxyHandleRef(inner);
|
||||
const proxyId = `group-proxy::out::n1::LINE::${encoded}`;
|
||||
const proxy = parseGroupProxyHandle(proxyId);
|
||||
assert.equal(proxy.realHandle, inner);
|
||||
});
|
||||
|
||||
// ── getConnectionHandleType ───────────────────────────────────────────
|
||||
|
||||
test('getConnectionHandleType returns type from a regular handle', () => {
|
||||
assert.equal(getConnectionHandleType('output::0::DATA_FIELD'), 'DATA_FIELD');
|
||||
assert.equal(getConnectionHandleType('input::x::LINE'), 'LINE');
|
||||
});
|
||||
|
||||
test('getConnectionHandleType returns type from a proxy handle', () => {
|
||||
const encoded = encodeProxyHandleRef('output::0::IMAGE');
|
||||
const proxy = `group-proxy::out::nodeX::IMAGE::${encoded}`;
|
||||
assert.equal(getConnectionHandleType(proxy), 'IMAGE');
|
||||
});
|
||||
|
||||
// ── getResolvedHandleRef ──────────────────────────────────────────────
|
||||
|
||||
test('getResolvedHandleRef returns identity for a regular handle', () => {
|
||||
const ref = getResolvedHandleRef('node1', 'input::field::DATA_FIELD');
|
||||
assert.equal(ref.nodeId, 'node1');
|
||||
assert.equal(ref.handleId, 'input::field::DATA_FIELD');
|
||||
assert.equal(ref.type, 'DATA_FIELD');
|
||||
});
|
||||
|
||||
test('getResolvedHandleRef unwraps a proxy handle to the real node and handle', () => {
|
||||
const inner = 'input::field::DATA_FIELD';
|
||||
const encoded = encodeProxyHandleRef(inner);
|
||||
const proxyId = `group-proxy::in::realNode::DATA_FIELD::${encoded}`;
|
||||
const ref = getResolvedHandleRef('groupNode', proxyId);
|
||||
assert.equal(ref.nodeId, 'realNode');
|
||||
assert.equal(ref.handleId, inner);
|
||||
assert.equal(ref.type, 'DATA_FIELD');
|
||||
});
|
||||
|
||||
// ── getNodeInputSpecForHandle ─────────────────────────────────────────
|
||||
|
||||
test('getNodeInputSpecForHandle returns null for missing node', () => {
|
||||
assert.equal(getNodeInputSpecForHandle(null, 'input::field::DATA_FIELD'), null);
|
||||
assert.equal(getNodeInputSpecForHandle(undefined, 'input::field::DATA_FIELD'), null);
|
||||
});
|
||||
|
||||
test('getNodeInputSpecForHandle returns null when definition has no input', () => {
|
||||
const node = { data: { definition: {} } };
|
||||
assert.equal(getNodeInputSpecForHandle(node, 'input::field::DATA_FIELD'), null);
|
||||
});
|
||||
|
||||
test('getNodeInputSpecForHandle finds a required input spec', () => {
|
||||
const spec = ['DATA_FIELD', {}];
|
||||
const node = { data: { definition: { input: { required: { field: spec }, optional: {} } } } };
|
||||
assert.deepEqual(getNodeInputSpecForHandle(node, 'input::field::DATA_FIELD'), spec);
|
||||
});
|
||||
|
||||
test('getNodeInputSpecForHandle finds an optional input spec', () => {
|
||||
const spec = ['LINE', { accepted_types: ['DATA_FIELD'] }];
|
||||
const node = { data: { definition: { input: { required: {}, optional: { profile: spec } } } } };
|
||||
assert.deepEqual(getNodeInputSpecForHandle(node, 'input::profile::LINE'), spec);
|
||||
});
|
||||
|
||||
test('getNodeInputSpecForHandle returns null for unknown input name', () => {
|
||||
const node = { data: { definition: { input: { required: { field: ['DATA_FIELD', {}] }, optional: {} } } } };
|
||||
assert.equal(getNodeInputSpecForHandle(node, 'input::nonexistent::DATA_FIELD'), null);
|
||||
});
|
||||
|
||||
// ── outputTypeCanConnectToTarget ──────────────────────────────────────
|
||||
|
||||
test('outputTypeCanConnectToTarget allows exact type match', () => {
|
||||
assert.equal(outputTypeCanConnectToTarget('DATA_FIELD', 'DATA_FIELD'), true);
|
||||
assert.equal(outputTypeCanConnectToTarget('LINE', 'LINE'), true);
|
||||
assert.equal(outputTypeCanConnectToTarget('FLOAT', 'FLOAT'), true);
|
||||
});
|
||||
|
||||
test('outputTypeCanConnectToTarget allows INT↔FLOAT via socket compatibility', () => {
|
||||
assert.equal(outputTypeCanConnectToTarget('INT', 'FLOAT'), true);
|
||||
assert.equal(outputTypeCanConnectToTarget('FLOAT', 'INT'), true);
|
||||
});
|
||||
|
||||
test('outputTypeCanConnectToTarget rejects incompatible types with no accepted_types', () => {
|
||||
assert.equal(outputTypeCanConnectToTarget('DATA_FIELD', 'LINE'), false);
|
||||
assert.equal(outputTypeCanConnectToTarget('LINE', 'IMAGE'), false);
|
||||
assert.equal(outputTypeCanConnectToTarget('FLOAT', 'DATA_FIELD'), false);
|
||||
});
|
||||
|
||||
test('outputTypeCanConnectToTarget allows polymorphic match via outputAcceptedTypes', () => {
|
||||
// Output socket declares LINE but can also emit DATA_FIELD
|
||||
assert.equal(outputTypeCanConnectToTarget('LINE', 'DATA_FIELD', ['DATA_FIELD']), true);
|
||||
assert.equal(outputTypeCanConnectToTarget('LINE', 'IMAGE', ['DATA_FIELD']), false);
|
||||
});
|
||||
|
||||
test('outputTypeCanConnectToTarget allows polymorphic match from spec array', () => {
|
||||
const spec = ['DATA_FIELD', {}];
|
||||
assert.equal(outputTypeCanConnectToTarget('LINE', spec, ['DATA_FIELD']), true);
|
||||
});
|
||||
|
||||
test('outputTypeCanConnectToTarget: ANNOTATION_SOURCE connects to DATA_FIELD and IMAGE targets', () => {
|
||||
assert.equal(outputTypeCanConnectToTarget('ANNOTATION_SOURCE', 'DATA_FIELD'), true);
|
||||
assert.equal(outputTypeCanConnectToTarget('ANNOTATION_SOURCE', 'IMAGE'), true);
|
||||
});
|
||||
|
||||
test('outputTypeCanConnectToTarget: ANNOTATION_SOURCE connects to ANNOTATION_SOURCE target', () => {
|
||||
assert.equal(outputTypeCanConnectToTarget('ANNOTATION_SOURCE', 'ANNOTATION_SOURCE'), true);
|
||||
});
|
||||
|
||||
test('outputTypeCanConnectToTarget: ANNOTATION_SOURCE does not connect to LINE', () => {
|
||||
assert.equal(outputTypeCanConnectToTarget('ANNOTATION_SOURCE', 'LINE'), false);
|
||||
});
|
||||
|
||||
test('outputTypeCanConnectToTarget: empty outputAcceptedTypes does not affect result', () => {
|
||||
assert.equal(outputTypeCanConnectToTarget('DATA_FIELD', 'LINE', []), false);
|
||||
});
|
||||
|
||||
test('outputTypeCanConnectToTarget: accepts target given as spec array', () => {
|
||||
assert.equal(
|
||||
outputTypeCanConnectToTarget('DATA_FIELD', ['DATA_FIELD', { label: 'input' }]),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
// ── resolveOutputTypeForTarget ────────────────────────────────────────
|
||||
|
||||
test('resolveOutputTypeForTarget returns non-ANNOTATION_SOURCE types unchanged', () => {
|
||||
assert.equal(resolveOutputTypeForTarget('DATA_FIELD', 'DATA_FIELD'), 'DATA_FIELD');
|
||||
assert.equal(resolveOutputTypeForTarget('LINE', 'LINE'), 'LINE');
|
||||
assert.equal(resolveOutputTypeForTarget('IMAGE', 'IMAGE'), 'IMAGE');
|
||||
});
|
||||
|
||||
test('resolveOutputTypeForTarget keeps ANNOTATION_SOURCE when target accepts it', () => {
|
||||
assert.equal(resolveOutputTypeForTarget('ANNOTATION_SOURCE', 'ANNOTATION_SOURCE'), 'ANNOTATION_SOURCE');
|
||||
});
|
||||
|
||||
test('resolveOutputTypeForTarget resolves ANNOTATION_SOURCE to DATA_FIELD when target is DATA_FIELD', () => {
|
||||
assert.equal(resolveOutputTypeForTarget('ANNOTATION_SOURCE', 'DATA_FIELD'), 'DATA_FIELD');
|
||||
});
|
||||
|
||||
test('resolveOutputTypeForTarget resolves ANNOTATION_SOURCE to IMAGE when target is IMAGE', () => {
|
||||
assert.equal(resolveOutputTypeForTarget('ANNOTATION_SOURCE', 'IMAGE'), 'IMAGE');
|
||||
});
|
||||
|
||||
test('resolveOutputTypeForTarget falls back to ANNOTATION_SOURCE for unknown target', () => {
|
||||
assert.equal(resolveOutputTypeForTarget('ANNOTATION_SOURCE', 'LINE'), 'ANNOTATION_SOURCE');
|
||||
assert.equal(resolveOutputTypeForTarget('ANNOTATION_SOURCE', 'RECORD_TABLE'), 'ANNOTATION_SOURCE');
|
||||
});
|
||||
|
||||
// ── checkConnectionValid ──────────────────────────────────────────────
|
||||
|
||||
function makeNode(id, inputs, outputAcceptedTypes = []) {
|
||||
return {
|
||||
id,
|
||||
data: {
|
||||
definition: {
|
||||
input: inputs,
|
||||
output_accepted_types: outputAcceptedTypes,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function makeGetNode(nodes) {
|
||||
const map = Object.fromEntries(nodes.map((n) => [n.id, n]));
|
||||
return (id) => map[id] ?? null;
|
||||
}
|
||||
|
||||
test('checkConnectionValid accepts a direct type match', () => {
|
||||
const srcNode = makeNode('src', {});
|
||||
const tgtNode = makeNode('tgt', { required: { field: ['DATA_FIELD', {}] } });
|
||||
const getNode = makeGetNode([srcNode, tgtNode]);
|
||||
const conn = {
|
||||
source: 'src',
|
||||
sourceHandle: 'output::0::DATA_FIELD',
|
||||
target: 'tgt',
|
||||
targetHandle: 'input::field::DATA_FIELD',
|
||||
};
|
||||
assert.equal(checkConnectionValid(conn, getNode), true);
|
||||
});
|
||||
|
||||
test('checkConnectionValid rejects a type mismatch with no accepted_types', () => {
|
||||
const srcNode = makeNode('src', {});
|
||||
const tgtNode = makeNode('tgt', { required: { field: ['LINE', {}] } });
|
||||
const getNode = makeGetNode([srcNode, tgtNode]);
|
||||
const conn = {
|
||||
source: 'src',
|
||||
sourceHandle: 'output::0::DATA_FIELD',
|
||||
target: 'tgt',
|
||||
targetHandle: 'input::field::LINE',
|
||||
};
|
||||
assert.equal(checkConnectionValid(conn, getNode), false);
|
||||
});
|
||||
|
||||
test('checkConnectionValid accepts INT→FLOAT via intrinsic compatibility', () => {
|
||||
const srcNode = makeNode('src', {});
|
||||
const tgtNode = makeNode('tgt', { required: { v: ['FLOAT', {}] } });
|
||||
const getNode = makeGetNode([srcNode, tgtNode]);
|
||||
const conn = {
|
||||
source: 'src',
|
||||
sourceHandle: 'output::0::INT',
|
||||
target: 'tgt',
|
||||
targetHandle: 'input::v::FLOAT',
|
||||
};
|
||||
assert.equal(checkConnectionValid(conn, getNode), true);
|
||||
});
|
||||
|
||||
test('checkConnectionValid accepts polymorphic output_accepted_types match', () => {
|
||||
// Source slot 0 declares it can also produce DATA_FIELD (its primary type is LINE)
|
||||
const srcNode = makeNode('src', {}, [['DATA_FIELD']]);
|
||||
const tgtNode = makeNode('tgt', { required: { field: ['DATA_FIELD', {}] } });
|
||||
const getNode = makeGetNode([srcNode, tgtNode]);
|
||||
const conn = {
|
||||
source: 'src',
|
||||
sourceHandle: 'output::0::LINE',
|
||||
target: 'tgt',
|
||||
targetHandle: 'input::field::DATA_FIELD',
|
||||
};
|
||||
assert.equal(checkConnectionValid(conn, getNode), true);
|
||||
});
|
||||
|
||||
test('checkConnectionValid rejects when accepted_types does not include target type', () => {
|
||||
const srcNode = makeNode('src', {}, [['IMAGE']]);
|
||||
const tgtNode = makeNode('tgt', { required: { field: ['DATA_FIELD', {}] } });
|
||||
const getNode = makeGetNode([srcNode, tgtNode]);
|
||||
const conn = {
|
||||
source: 'src',
|
||||
sourceHandle: 'output::0::LINE',
|
||||
target: 'tgt',
|
||||
targetHandle: 'input::field::DATA_FIELD',
|
||||
};
|
||||
assert.equal(checkConnectionValid(conn, getNode), false);
|
||||
});
|
||||
|
||||
test('checkConnectionValid falls back to handle type when target node is missing', () => {
|
||||
const srcNode = makeNode('src', {});
|
||||
const getNode = makeGetNode([srcNode]);
|
||||
const conn = {
|
||||
source: 'src',
|
||||
sourceHandle: 'output::0::DATA_FIELD',
|
||||
target: 'missing',
|
||||
targetHandle: 'input::field::DATA_FIELD',
|
||||
};
|
||||
// resolvedTarget.type will be DATA_FIELD, which matches the source
|
||||
assert.equal(checkConnectionValid(conn, getNode), true);
|
||||
});
|
||||
|
||||
test('checkConnectionValid handles group proxy source handles', () => {
|
||||
const realSrcNode = makeNode('realSrc', {}, [[]]);
|
||||
const tgtNode = makeNode('tgt', { required: { field: ['DATA_FIELD', {}] } });
|
||||
const getNode = makeGetNode([realSrcNode, tgtNode]);
|
||||
const realHandle = 'output::0::DATA_FIELD';
|
||||
const proxyHandle = `group-proxy::out::realSrc::DATA_FIELD::${encodeProxyHandleRef(realHandle)}`;
|
||||
const conn = {
|
||||
source: 'groupNode',
|
||||
sourceHandle: proxyHandle,
|
||||
target: 'tgt',
|
||||
targetHandle: 'input::field::DATA_FIELD',
|
||||
};
|
||||
assert.equal(checkConnectionValid(conn, getNode), true);
|
||||
});
|
||||
|
||||
test('checkConnectionValid handles group proxy target handles', () => {
|
||||
const srcNode = makeNode('src', {});
|
||||
const realTgtNode = makeNode('realTgt', { required: { field: ['DATA_FIELD', {}] } });
|
||||
const getNode = makeGetNode([srcNode, realTgtNode]);
|
||||
const realHandle = 'input::field::DATA_FIELD';
|
||||
const proxyHandle = `group-proxy::in::realTgt::DATA_FIELD::${encodeProxyHandleRef(realHandle)}`;
|
||||
const conn = {
|
||||
source: 'src',
|
||||
sourceHandle: 'output::0::DATA_FIELD',
|
||||
target: 'groupNode',
|
||||
targetHandle: proxyHandle,
|
||||
};
|
||||
assert.equal(checkConnectionValid(conn, getNode), true);
|
||||
});
|
||||
@@ -24,7 +24,7 @@ test('accepted_types extend canonical socket compatibility without reintroducing
|
||||
assert.equal(isDataSocketSpec(spec), true);
|
||||
assert.deepEqual(
|
||||
Array.from(getAcceptedSocketTypes(spec)).sort(),
|
||||
['RECORD_TABLE', 'DATA_TABLE'],
|
||||
['DATA_TABLE', 'RECORD_TABLE'],
|
||||
);
|
||||
assert.equal(socketSpecAcceptsType('DATA_TABLE', spec), true);
|
||||
assert.equal(socketSpecAcceptsType('LINE', spec), false);
|
||||
|
||||
Reference in New Issue
Block a user