tests/ (Python backend tests, .mjs frontend rule tests) is no longer ignored. tools/run_tests.mjs runs the structure check, every tests/*.mjs and the Python tests with $PYTHON, the portable python_embeded or python. docs/guides/testing.md says how to run and write them; README links it. Developer notes no longer point at 18 test files that were removed earlier (AGENTS.md table, backend, frontend and update-guide notes). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
94 lines
6.0 KiB
JavaScript
94 lines
6.0 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { applyNodeMaterialValues, selectedMaterialNode } from '../web/modules/node_material_actions.js';
|
|
import { promptBoxes } from '../web/modules/prompt_boxes.js';
|
|
import { composePromptPlan } from '../web/modules/prompt_composition.js';
|
|
|
|
// A saved block's values into a node of its type (the library's block apply, removed with the
|
|
// Material Library page), to exercise applyNodeMaterialValues' value and hash rules.
|
|
function applyMaterialBlock(app, node, block, workflowHashes) {
|
|
if (node?.type !== block?.type || !Array.isArray(block.widgets_values)
|
|
|| block.widgets_values.length > (node.widgets?.length || 0)) throw new Error('materialNoCompatibleValues');
|
|
return applyNodeMaterialValues(app, node, block.widgets_values.map((value, index) => ({ index, value })),
|
|
{ sourceNodeId: block.node_id, workflowHashes });
|
|
}
|
|
|
|
function fixture() {
|
|
const node = { id: 7, type: 'KSampler', pos: [100, 20], inputs: [{ link: 88 }],
|
|
widgets: [{ name: 'seed', value: 123 }, { name: 'steps', value: 20 }, { name: 'sampler_name', value: 'euler', options: { values: ['euler', 'heun'] } }],
|
|
widgets_values: [123, 20, 'euler'] };
|
|
const calls = [];
|
|
node.onWidgetChanged = (name, value, old, widget) => { assert.equal(widget.name, name); calls.push([node.widgets.indexOf(widget), value, old]); };
|
|
const graph = { extra: { keep: true, anomalous_hashes: { '7_old': 'old', '8_other': 'other' } },
|
|
getNodeById: id => id === node.id ? node : null,
|
|
beforeChange: () => calls.push('before'), afterChange: () => calls.push('after'), change() {}, setDirtyCanvas() {} };
|
|
const app = { graph, canvas: { selected_nodes: { 7: node }, setDirty() {} } };
|
|
const block = { node_id: 2, type: 'KSampler', widgets_values: [999, 30, 'heun'] };
|
|
return { node, graph, app, block, calls };
|
|
}
|
|
{
|
|
const { node, graph, app, block, calls } = fixture();
|
|
assert.equal(selectedMaterialNode(app), node);
|
|
const before = structuredClone({ values: node.widgets_values, extra: graph.extra, pos: node.pos, inputs: node.inputs });
|
|
const result = applyMaterialBlock(app, node, block, { '2_model': { hash: 'abc' }, '22_secret': 'ignore' });
|
|
assert.equal(result.widgets, 2);
|
|
assert.deepEqual(node.widgets_values, [123, 30, 'heun']);
|
|
assert.deepEqual(graph.extra.anomalous_hashes, { '7_model': { hash: 'abc' }, '8_other': 'other' });
|
|
assert.deepEqual(calls, ['before', [1, 30, 20], [2, 'heun', 'euler'], 'after']);
|
|
result.undo();
|
|
assert.deepEqual({ values: node.widgets_values, extra: graph.extra, pos: node.pos, inputs: node.inputs }, before);
|
|
assert.throws(() => result.undo(), /materialUndoChanged/);
|
|
}
|
|
for (const invalid of [ { type: 'Other' }, { widgets_values: [99, 30, 'foreign'] }, { widgets_values: [99, 'bad', 'heun'] }, { widgets_values: [99, NaN, 'heun'] }, { widgets_values: [99, 30, 'heun', 4] } ]) {
|
|
const { app, node, block, graph, calls } = fixture();
|
|
const before = JSON.stringify([node.widgets_values, graph.extra]);
|
|
assert.throws(() => applyMaterialBlock(app, node, { ...block, ...invalid }, {}));
|
|
assert.equal(JSON.stringify([node.widgets_values, graph.extra]), before);
|
|
assert.deepEqual(calls, [], 'validation must finish before graph mutation');
|
|
}
|
|
{
|
|
const { app, node, block, graph, calls } = fixture();
|
|
node.widgets[2].callback = () => { throw new Error('host hook failed'); };
|
|
const before = JSON.stringify([node.widgets_values, graph.extra]);
|
|
assert.throws(() => applyMaterialBlock(app, node, block, { '2_new': 'new' }), /host hook/);
|
|
assert.deepEqual([node.widgets_values, graph.extra], JSON.parse(before));
|
|
assert.deepEqual(node.widgets.map(w => w.value), node.widgets_values);
|
|
assert.equal(calls.at(-1), 'after');
|
|
}
|
|
for (const change of ['widget', 'serialized', 'hash', 'graph']) {
|
|
const { app, node, graph, block } = fixture();
|
|
const result = applyMaterialBlock(app, node, block, {});
|
|
if (change === 'widget') node.widgets[1].value = 55;
|
|
if (change === 'serialized') node.widgets_values[1] = 55;
|
|
if (change === 'hash') graph.extra.anomalous_hashes['7_other'] = 'new';
|
|
if (change === 'graph') app.graph = { ...graph };
|
|
assert.throws(() => result.undo(), /materialUndoChanged/);
|
|
}
|
|
{
|
|
const { app, node, block, graph } = fixture();
|
|
delete graph.extra;
|
|
const result = applyMaterialBlock(app, node, block, { '2_model': 'new' });
|
|
result.undo(); assert.equal(Object.hasOwn(graph, 'extra'), false);
|
|
app.canvas.selected_nodes.other = {};
|
|
assert.equal(selectedMaterialNode(app), null);
|
|
app.canvas.selected_nodes = { 7: { ...node } };
|
|
assert.equal(selectedMaterialNode(app), null);
|
|
assert.throws(() => applyNodeMaterialValues(app, { ...node }, [{ index: 1, value: 30 }]), /materialTargetChanged/);
|
|
}
|
|
{
|
|
const { app, node } = fixture();
|
|
node.type = 'CLIPTextEncode';
|
|
node.widgets = [{ name: 'text', value: 'old' }, { name: 'text_l', value: '' }, { name: 'model', value: 'not a prompt' }];
|
|
globalThis.LiteGraph = { registered_node_types: { CLIPTextEncode: { nodeData: { input: { required: { text: ['STRING', { multiline: true }], text_l: ['STRING', { multiline: true }], model: ['STRING', {}] } } } } } };
|
|
assert.deepEqual(promptBoxes(node).map(box => [box.index, box.name]), [[0, 'text'], [1, 'text_l']]);
|
|
const result = applyNodeMaterialValues(app, node, [{ index: 0, value: 'new\n(weights:1.2)' }]);
|
|
assert.equal(node.widgets[1].value, ''); result.undo(); assert.equal(node.widgets[0].value, 'old');
|
|
}
|
|
const plan = { parts: [
|
|
{ enabled: true, positive: ' (quality:1.2), BREAK\n', negative: 'bad' },
|
|
{ enabled: false, positive: 'disabled', negative: 'disabled' },
|
|
{ enabled: true, positive: 'style', negative: 'bad' },
|
|
], positive: 'scene', negative: ' ' };
|
|
assert.deepEqual(composePromptPlan(plan), { positive: ' (quality:1.2), BREAK\n\nstyle\nscene', negative: 'bad\nbad' });
|
|
assert.deepEqual(composePromptPlan({ parts: [] }), { positive: '', negative: '' });
|
|
console.log('node materials: validation, scoped hashes, rollback, guarded undo and prompt composition OK');
|