import assert from "node:assert/strict"; import fs from "node:fs"; import vm from "node:vm"; const sourcePath = new URL("../web/js/videoTrim.js", import.meta.url); const source = fs.readFileSync(sourcePath, "utf8").replace(/^import .*;\s*$/gm, ""); class Element { constructor(tagName) { this.tagName = tagName.toUpperCase(); this.children = []; this.listeners = new Map(); this.style = {}; this.textContent = ""; this.className = ""; this.classList = { add() {}, remove() {} }; this.paused = true; this.currentTime = 0; } append(...children) { this.children.push(...children); } appendChild(child) { this.children.push(child); } replaceChildren(...children) { this.children = [...children]; } addEventListener(type, listener) { this.listeners.set(type, listener); } removeAttribute(name) { delete this[name]; } querySelector(selector) { return selector === "span" ? this._span ?? null : null; } pause() { this.paused = true; } load() {} remove() { this.removed = true; } set innerHTML(value) { this._innerHTML = value; if (this.tagName === "BUTTON" && value.includes("")) { this._span = new Element("span"); } } get innerHTML() { return this._innerHTML ?? ""; } } const createdElements = []; const elementsById = new Map(); const document = { head: { appendChild(element) { if (element.id) elementsById.set(element.id, element); }, }, body: { appendChild(element) { createdElements.push(element); }, }, createElement(tagName) { const element = new Element(tagName); createdElements.push(element); return element; }, getElementById(id) { return elementsById.get(id) ?? null; }, }; let registeredExtension; const app = { registerExtension(extension) { registeredExtension = extension; }, }; const api = { apiURL(value) { return value; }, async fetchApi(path) { assert.equal(path, "/o1key/input_dir"); return { ok: true, async json() { return { path: "F:/o1key_windows_portable/ComfyUI/input" }; }, }; }, }; const context = vm.createContext({ app, api, console, document, FormData, URLSearchParams, isFinite, requestAnimationFrame: (callback) => callback(), setTimeout, }); vm.runInContext(source, context, { filename: sourcePath.pathname }); const callbackReceiver = { kind: "comfy-widget-component" }; let callbackArguments; function originalFixedCallback(...args) { assert.equal(this, callbackReceiver); callbackArguments = args; return "original-result"; } class VideoTrimNode { constructor() { this.size = [300, 300]; this.widgets = [ { name: "视频路径", value: "" }, { name: "开始时间", value: 0 }, { name: "结束时间", value: 0 }, { name: "固定时长", value: 0, callback: originalFixedCallback }, ]; } addDOMWidget(name, type, element, options) { const widget = { name, type, element, options }; this.widgets.push(widget); return widget; } computeSize() { return [this.size[0], 480]; } setSize(size) { this.size = size; } setDirtyCanvas() {} } await registeredExtension.beforeRegisterNodeDef(VideoTrimNode, { name: "O1keyVideoTrim" }); const node = new VideoTrimNode(); node.onNodeCreated(); const pathWidget = node.widgets.find((widget) => widget.name === "视频路径"); const fixedWidget = node.widgets.find((widget) => widget.name === "固定时长"); assert.equal(pathWidget.hidden, true, "视频路径必须作为内部字段隐藏"); assert.equal(pathWidget.options.hidden, true, "Nodes 2.0 必须收到正式隐藏标记"); assert.equal(pathWidget.computeSize, undefined, "不得用负高度隐藏路径控件"); fixedWidget.value = 4; const callbackResult = fixedWidget.callback.call( callbackReceiver, 4, "canvas", node, [10, 20], { type: "change" }, ); assert.equal(callbackResult, "original-result"); assert.deepEqual(callbackArguments, [4, "canvas", node, [10, 20], { type: "change" }]); pathWidget.value = "F:/o1key_windows_portable/ComfyUI/input/clips/demo.mp4"; await node._o1vtRefreshPath(pathWidget.value); const preview = createdElements.find((element) => element.tagName === "VIDEO"); assert.equal(preview.src, "/view?filename=demo.mp4&type=input&subfolder=clips"); assert.equal(preview.style.display, "block"); pathWidget.value = "D:/external/demo.mp4"; await node._o1vtRefreshPath(pathWidget.value); assert.equal(preview.src, undefined, "input 目录外路径不能暴露为 /view 地址"); assert.equal(preview.style.display, "none"); node.onRemoved(); const fileInput = createdElements.find((element) => element.tagName === "INPUT"); assert.equal(fileInput.removed, true); console.log("video trim frontend tests passed");