Files
DemonGatanjieuandClaude Opus 5.5 7b09909e5b test: publish the plugin's tests, one command to run them
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>
2026-10-06 17:47:35 +08:00

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');