Files
comfyui_o1key/tests/test_o1key_update_button.mjs
T

272 lines
11 KiB
JavaScript

import assert from "node:assert/strict";
import fs from "node:fs";
import vm from "node:vm";
const sourcePath = new URL("../web/js/o1keyUpdateButton.js", import.meta.url);
const source = fs.readFileSync(sourcePath, "utf8").replace(/^import .*;\s*$/gm, "");
class Element {
constructor(tagName) {
this.tagName = tagName;
this.children = [];
this.listeners = new Map();
this.attributes = new Map();
this.style = {};
this.dataset = {};
this.className = "";
this.textContent = "";
this.disabled = false;
this.open = false;
this.parentElement = null;
this.classList = {
add: (...names) => this.updateClasses(names, []),
remove: (...names) => this.updateClasses([], names),
};
}
updateClasses(add, remove) {
const names = new Set(this.className.split(/\s+/).filter(Boolean));
for (const name of remove) names.delete(name);
for (const name of add) names.add(name);
this.className = [...names].join(" ");
}
append(...elements) {
for (const element of elements) {
if (element.parentElement) {
const siblings = element.parentElement.children;
siblings.splice(siblings.indexOf(element), 1);
}
element.parentElement = this;
this.children.push(element);
}
}
appendChild(element) { this.append(element); }
after(element) {
if (element.parentElement) {
const siblings = element.parentElement.children;
siblings.splice(siblings.indexOf(element), 1);
}
const siblings = this.parentElement.children;
siblings.splice(siblings.indexOf(this) + 1, 0, element);
element.parentElement = this.parentElement;
}
get nextElementSibling() {
const siblings = this.parentElement?.children ?? [];
return siblings[siblings.indexOf(this) + 1] ?? null;
}
cloneNode(deep) {
const clone = new Element(this.tagName);
clone.id = this.id;
clone.type = this.type;
clone.className = this.className;
clone.textContent = this.textContent;
clone.attributes = new Map(this.attributes);
if (deep) clone.append(...this.children.map((child) => child.cloneNode(true)));
return clone;
}
querySelector(selector) {
const test = (element) => selector.startsWith("#")
? element.id === selector.slice(1)
: element.className.split(/\s+/).includes(selector.slice(1));
for (const child of this.children) {
if (test(child)) return child;
const nested = child.querySelector(selector);
if (nested) return nested;
}
return null;
}
setAttribute(name, value) { this.attributes.set(name, value); }
removeAttribute(name) { this.attributes.delete(name); }
addEventListener(name, listener) { this.listeners.set(name, listener); }
showModal() { this.open = true; }
close() { this.open = false; }
}
const body = new Element("body");
const head = new Element("head");
const toolbar = new Element("div");
body.append(toolbar);
const document = {
body,
head,
createElement: (tag) => new Element(tag),
querySelector: (selector) => body.querySelector(selector) || head.querySelector(selector),
};
let extension;
let observer;
const requests = [];
const responses = [];
let fetchFailure = false;
let restartCalls = 0;
let restartFailure = false;
const reply = (body, status = 200) => ({ ok: status < 400, status, async json() { return body; } });
const app = { registerExtension(value) { extension = value; } };
const api = {
async fetchApi(path, options) {
requests.push({ path, options });
if (fetchFailure) throw new Error("offline");
return responses.shift();
},
};
class MutationObserver {
constructor(callback) { this.callback = callback; observer = this; }
observe() {}
disconnect() { this.disconnected = true; }
}
async function startComfyUIRestart() {
restartCalls++;
if (restartFailure) throw new Error("restart failed");
}
vm.runInNewContext(source, { app, api, document, startComfyUIRestart, MutationObserver });
extension.setup();
assert.ok(observer);
const token = new Element("button");
token.id = "o1key-token-manager-button";
token.className = "side-bar-button o1key-token-manager-button";
const tokenIcon = new Element("span");
tokenIcon.className = "pi pi-key side-bar-button-icon";
const tokenLabel = new Element("span");
tokenLabel.className = "side-bar-button-label";
tokenLabel.textContent = "令牌管理";
token.append(tokenIcon, tokenLabel);
const restart = new Element("button");
restart.id = "o1key-restart-button";
toolbar.append(token, restart);
observer.callback();
const update = document.querySelector("#o1key-update-button");
assert.equal(toolbar.children[0], token);
assert.equal(toolbar.children[1], update);
assert.equal(toolbar.children[2], restart);
assert.equal(update.querySelector(".side-bar-button-label").textContent, "更新");
assert.match(update.querySelector(".side-bar-button-icon").className, /pi-download/);
assert.equal(document.querySelector("#o1key-detect-button"), null);
assert.ok(observer.disconnected);
extension.setup();
assert.equal(toolbar.children.length, 3);
responses.push(reply({ update_available: false }));
update.listeners.get("click")();
const checkingDialog = document.querySelector("#o1key-update-dialog");
const checkingIcon = checkingDialog.children[0].children[1].children[1].children[0];
assert.equal(checkingDialog.dataset.state, "checking");
assert.match(checkingIcon.className, /is-loading/);
assert.doesNotMatch(checkingIcon.className, /pi-spin/);
assert.match(head.children[0].textContent, /flex: 0 0 18px/);
assert.match(head.children[0].textContent, /animation: o1key-update-spin/);
assert.match(head.children[0].textContent, /--p-primary-500/);
await new Promise(setImmediate);
const dialog = document.querySelector("#o1key-update-dialog");
assert.equal(dialog.open, true);
const [header, content, actions] = dialog.children[0].children;
const [description, resultCard] = content.children;
const [status, suggestion] = resultCard.children[1].children;
const [secondary, primary] = actions.children;
assert.equal(header.children[1].children[1].textContent, "更新 O1Key 节点包");
assert.equal(description.textContent, "检查是否有新版本可用。");
assert.doesNotMatch(source, /git\.o1key\.com|发布仓库|main 分支|requirements\.txt/);
assert.doesNotMatch(source, /window\.confirm/);
assert.equal(requests.length, 1);
assert.equal(requests[0].path, "/o1key/update/check");
assert.equal(requests[0].options.cache, "no-store");
assert.match(status.textContent, /已是最新版本/);
assert.equal(dialog.dataset.state, "current");
assert.equal(resultCard.dataset.kind, "success");
assert.equal(primary.textContent, "重新检查");
assert.equal(head.children.length, 1);
responses.push(reply({ update_available: false }));
await primary.listeners.get("click")();
assert.equal(requests[1].path, "/o1key/update/check");
assert.match(status.textContent, /已是最新版本/);
responses.push(reply({ update_available: true }));
await primary.listeners.get("click")();
assert.equal(requests.length, 3);
assert.match(status.textContent, /发现新版本/);
assert.equal(dialog.dataset.state, "available");
assert.equal(primary.textContent, "立即更新");
assert.equal(secondary.textContent, "稍后再说");
assert.equal(resultCard.dataset.kind, "available");
const requestsBeforeDecline = requests.length;
secondary.listeners.get("click")();
assert.equal(dialog.open, false);
assert.equal(requests.length, requestsBeforeDecline);
responses.push(reply({ update_available: true }));
update.listeners.get("click")();
await new Promise(setImmediate);
assert.equal(dialog.open, true);
assert.equal(dialog.dataset.state, "available");
responses.push(reply({ updated: true, version: "abc1234", requirements_changed: true }));
await primary.listeners.get("click")();
assert.equal(requests.at(-1).path, "/o1key/update");
assert.equal(requests.at(-1).options.method, "POST");
assert.equal(requests.at(-1).options.headers["X-O1Key-Update"], "1");
assert.match(status.textContent, /更新完成/);
assert.match(suggestion.textContent, /联系技术支持/);
assert.doesNotMatch(suggestion.textContent, /requirements\.txt/);
assert.equal(primary.textContent, "完成");
assert.equal(dialog.dataset.state, "success");
assert.equal(restartCalls, 0);
responses.push(reply({ update_available: true }));
await dialog.checkForUpdate();
restartFailure = true;
responses.push(reply({ updated: true, requirements_changed: false }));
await primary.listeners.get("click")();
assert.equal(restartCalls, 1);
assert.equal(dialog.dataset.state, "restart_failed");
assert.match(status.textContent, /自动重启失败/);
assert.match(suggestion.textContent, /左侧“重启”按钮/);
assert.equal(primary.textContent, "关闭");
await primary.listeners.get("click")();
assert.equal(dialog.open, false);
responses.push(reply({ update_available: true }));
update.listeners.get("click")();
await new Promise(setImmediate);
assert.equal(dialog.dataset.state, "available");
const requestsBeforeError = requests.length;
responses.push(reply({ code: "local_changes", error: "插件目录有未提交的代码修改。", suggestion: "请先提交" }, 409));
await dialog.checkForUpdate();
assert.equal(requests.length, requestsBeforeError + 1);
assert.match(status.textContent, /自定义修改/);
assert.match(suggestion.textContent, /联系技术支持/);
assert.doesNotMatch(status.textContent + suggestion.textContent, /插件目录|提交|Git/);
assert.equal(dialog.dataset.state, "error");
assert.equal(resultCard.dataset.kind, "error");
responses.push({ ok: false, status: 404 });
await primary.listeners.get("click")();
assert.match(status.textContent, /尚未就绪/);
responses.push({ ok: false, status: 500, async json() { throw new Error("invalid JSON"); } });
await primary.listeners.get("click")();
assert.match(status.textContent, /无法读取检查结果/);
fetchFailure = true;
await primary.listeners.get("click")();
assert.match(status.textContent, /暂时无法完成操作/);
assert.equal(primary.disabled, false);
assert.equal(head.children.length, 1);
header.children[2].listeners.get("click")();
assert.equal(dialog.open, false);
fetchFailure = false;
restartFailure = false;
responses.push(reply({ update_available: true }));
update.listeners.get("click")();
await new Promise(setImmediate);
responses.push(reply({ updated: true, requirements_changed: false }));
await primary.listeners.get("click")();
assert.equal(restartCalls, 2);
assert.equal(dialog.dataset.state, "restarting");
assert.match(status.textContent, /正在重启 ComfyUI/);
assert.match(suggestion.textContent, /页面会自动刷新/);
assert.equal(primary.disabled, true);
assert.equal(secondary.disabled, true);
assert.equal(header.children[2].disabled, true);
assert.equal(primary.textContent, "重启中…");