Files
filliptm-ComfyUI_Fill-Nodes/tests/test_context_window_progress.mjs

126 lines
5.7 KiB
JavaScript

import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
import vm from "node:vm";
const source = await readFile(
new URL("../web/nodes/ksamplers/FL_KsamplerContextWindow.js", import.meta.url),
"utf8",
);
test("context-window progress uses stable string node keys", () => {
assert.match(source, /const nodeKey = \(value\) => String\(value\);/);
assert.match(source, /INSTANCES\.set\(nodeKey\(node\.id\), inst\);/);
assert.match(source, /INSTANCES\.get\(nodeKey\(detail\.node\)\)/);
assert.doesNotMatch(source, /parseInt\(detail\.node/);
});
function harness() {
class Element {
constructor() { this.style = {}; this.children = []; this.roles = new Map(); this.events = {}; }
appendChild(child) { this.children.push(child); }
querySelector(selector) {
if (!this.roles.has(selector)) this.roles.set(selector, new Element());
return this.roles.get(selector);
}
addEventListener(name, callback) { this.events[name] = callback; }
setAttribute(name, value) { this[name] = value; }
remove() { this.removed = true; }
}
const handlers = new Map();
const timers = new Map();
const app = { registerExtension(extension) { this.extension = extension; } };
const context = vm.createContext({
app,
api: { addEventListener: (name, callback) => handlers.set(name, callback) },
document: { createElement: () => new Element(), getElementById: () => null, head: new Element() },
setTimeout(callback) { const id = timers.size + 1; timers.set(id, callback); return id; },
clearTimeout(id) { timers.delete(id); },
});
vm.runInContext(source.replace(/^import .*;\r?$/gm, "").replace(/^export /gm, ""), context);
const defaults = vm.runInContext("({...ADVANCED_DEFAULTS})", context);
const widgets = Object.entries({ seed: 7, steps: 20, context_length: 81, context_overlap: 30, ...defaults }).map(([name, value]) => ({
name, value, type: "number", computeSize: () => [200, 20], options: {},
}));
const node = {
id: "subgraph:7", constructor: { comfyClass: "FL_KsamplerContextWindow" }, properties: {},
widgets, inputs: widgets.map((w) => ({name: w.name, link: null, widget: {name: w.name}})), size: [330, 730], setDirtyCanvas() {}, graph: { change() {} },
addDOMWidget(name, type, container, options) {
const widget = { name, type, container, options };
this.widgets.push(widget);
return widget;
},
computeSize() { return [330, 100 + this.widgets.filter((w) => !w.hidden).length * 24]; },
setSize(size) { this.size = size; },
};
app.extension.nodeCreated(node);
const init = () => { for (const [id, callback] of timers) { timers.delete(id); callback(); } };
const widget = (name) => widgets.find((w) => w.name === name);
const role = (name) => widget("context_progress").container.children[0].querySelector(`[data-role="${name}"]`);
return { context, node, widget, role, init, handlers, timers };
}
test("advanced collapse preserves widget values and serialization order", () => {
const h = harness();
const before = h.node.widgets.map((w) => [w.name, w.value]);
h.init();
const compactHeight = h.node.size[1];
assert.equal(h.widget("temporal_unit").hidden, true);
assert.equal(h.widget("context_length").hidden, undefined);
h.role("advanced").events.click();
assert.equal(h.widget("temporal_unit").type, "number");
assert.equal(h.widget("temporal_unit").hidden, undefined);
assert.equal(h.widget("context_stride").hidden, true);
assert.ok(h.node.size[1] > compactHeight);
h.role("advanced").events.click();
assert.deepEqual(h.node.widgets.map((w) => [w.name, w.value]), before);
assert.equal(h.node.size[1], compactHeight);
assert.ok(compactHeight < 730);
});
test("schedule-dependent controls follow the actual ComfyUI enum values", () => {
const h = harness();
h.init();
h.role("advanced").events.click();
const schedule = h.widget("context_schedule");
schedule.value = "standard_uniform";
schedule.callback();
assert.equal(h.widget("context_stride").hidden, undefined);
assert.equal(h.widget("closed_loop").hidden, true);
schedule.value = "looped_uniform";
schedule.callback();
assert.equal(h.widget("closed_loop").hidden, undefined);
schedule.value = "batched";
schedule.callback();
assert.equal(h.widget("context_overlap").hidden, true);
assert.equal(h.widget("context_overlap").value, 30);
});
test("saved legacy values survive configure and connected advanced sockets stay present", () => {
const h = harness();
h.init();
h.node.id = "nested:42";
h.widget("temporal_unit").value = "video_frames_4n_plus_1";
h.node.inputs.find((input) => input.name === "context_stride").link = 3;
h.node.onConfigure();
assert.equal(h.widget("temporal_unit").value, "video_frames_4n_plus_1");
assert.equal(h.node.inputs.find((input) => input.name === "context_stride").link, 3);
assert.match(h.role("advanced").textContent, /2 overrides/);
h.handlers.get("fl_context_window_progress")({ detail: { node: "nested:42", status: "resolved", profile: "ltx", video_frames: 81, latent_length: 11 } });
assert.match(h.role("model").textContent, /LTX.*81 frames \/ 11 latents/);
h.node.onConnectionsChange();
assert.match(h.role("model").textContent, /Settings changed/);
});
test("progress accepts string execution IDs and removal cancels pending initialization", () => {
const h = harness();
h.init();
h.handlers.get("executing")({ detail: "subgraph:7" });
assert.equal(h.role("percent").textContent, "0%");
h.handlers.get("execution_error")({ detail: { node_id: "subgraph:7", exception_message: "Invalid overlap" } });
assert.equal(h.role("percent").textContent, "error");
const pending = harness();
pending.widget("context_progress").onRemove();
assert.equal(pending.timers.size, 0);
});