Saving a character, object, or scene reused an existing name and overwrote that preset without asking. Updating the preset already linked to the region still saves directly.
165 lines
5.3 KiB
JavaScript
165 lines
5.3 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { afterEach, describe, it } from "node:test";
|
|
|
|
globalThis.window = {
|
|
comfyAPI: {
|
|
app: {},
|
|
api: { addEventListener() {} },
|
|
},
|
|
};
|
|
|
|
const {
|
|
loadStores, deleteStoreEntry, saveStoreEntry, normalizeStoreName, savedPresetCollision,
|
|
} = await import("../web/js/ideogram4_shared.js");
|
|
|
|
describe("loadStores", () => {
|
|
const originalFetch = globalThis.fetch;
|
|
|
|
afterEach(() => {
|
|
globalThis.fetch = originalFetch;
|
|
});
|
|
|
|
it("returns the three buckets from a successful response", async () => {
|
|
globalThis.fetch = async () => ({
|
|
ok: true,
|
|
json: async () => ({
|
|
characters: { Ada: { desc: "a" } },
|
|
objects: {},
|
|
scenes: { Scene: { boxes: [] } },
|
|
}),
|
|
});
|
|
const stores = await loadStores();
|
|
assert.deepEqual(stores.characters.Ada, { desc: "a" });
|
|
assert.deepEqual(stores.objects, {});
|
|
assert.deepEqual(stores.scenes.Scene, { boxes: [] });
|
|
});
|
|
|
|
it("returns empty buckets when the server has no presets", async () => {
|
|
globalThis.fetch = async () => ({
|
|
ok: true,
|
|
json: async () => ({}),
|
|
});
|
|
const stores = await loadStores();
|
|
assert.deepEqual(stores, { characters: {}, objects: {}, scenes: {} });
|
|
});
|
|
|
|
it("throws when the store request fails instead of looking empty", async () => {
|
|
globalThis.fetch = async () => ({ ok: false, status: 500, json: async () => ({}) });
|
|
await assert.rejects(loadStores(), /HTTP 500/);
|
|
});
|
|
|
|
it("throws when the network request fails", async () => {
|
|
globalThis.fetch = async () => {
|
|
throw new Error("network down");
|
|
};
|
|
await assert.rejects(loadStores(), /network down/);
|
|
});
|
|
});
|
|
|
|
describe("savedPresetCollision", () => {
|
|
it("names a different preset that would be replaced", () => {
|
|
assert.equal(savedPresetCollision(["Ada", "Bea"], "Bea"), "Bea");
|
|
assert.equal(savedPresetCollision(["Ada"], "Cara"), null);
|
|
assert.equal(savedPresetCollision(["Ada"], " "), null);
|
|
assert.equal(savedPresetCollision([], "Ada"), null);
|
|
});
|
|
|
|
it("does not treat an update of the linked preset as a replacement", () => {
|
|
assert.equal(savedPresetCollision(["Ada"], "Ada", "Ada"), null);
|
|
assert.equal(savedPresetCollision(["Ada", "Bea"], "Bea", "Ada"), "Bea");
|
|
});
|
|
|
|
it("matches a composed name to the stored spelling", () => {
|
|
const nfd = "Émilie".normalize("NFD");
|
|
assert.equal(savedPresetCollision(["Émilie"], nfd), "Émilie");
|
|
assert.equal(savedPresetCollision(["Émilie"], ` ${nfd} `, "Émilie"), null);
|
|
});
|
|
});
|
|
|
|
describe("saveStoreEntry", () => {
|
|
const originalFetch = globalThis.fetch;
|
|
|
|
afterEach(() => {
|
|
globalThis.fetch = originalFetch;
|
|
});
|
|
|
|
it("sends the composed name and returns the stored name", async () => {
|
|
const nfd = "Émilie".normalize("NFD");
|
|
let called;
|
|
globalThis.fetch = async (url, opts) => {
|
|
called = { url, method: opts.method, body: opts.body };
|
|
return { ok: true, json: async () => ({ ok: true, name: "Émilie" }) };
|
|
};
|
|
const saved = await saveStoreEntry("characters", nfd, { desc: "a" });
|
|
assert.equal(called.method, "PUT");
|
|
assert.equal(called.url, "/byae/ideogram/stores/characters/%C3%89milie");
|
|
assert.equal(called.body, JSON.stringify({ desc: "a" }));
|
|
assert.equal(saved, "Émilie");
|
|
});
|
|
});
|
|
|
|
describe("deleteStoreEntry", () => {
|
|
const originalFetch = globalThis.fetch;
|
|
|
|
afterEach(() => {
|
|
globalThis.fetch = originalFetch;
|
|
});
|
|
|
|
it("sends DELETE to the encoded preset name", async () => {
|
|
let called;
|
|
globalThis.fetch = async (url, opts) => {
|
|
called = { url, method: opts.method };
|
|
return { ok: true, json: async () => ({ ok: true }) };
|
|
};
|
|
await deleteStoreEntry("characters", "Émilie");
|
|
assert.equal(called.method, "DELETE");
|
|
assert.equal(called.url, "/byae/ideogram/stores/characters/%C3%89milie");
|
|
});
|
|
|
|
it("sends a composed form of a unicode name", async () => {
|
|
const nfd = "Émilie".normalize("NFD");
|
|
const nfc = "Émilie".normalize("NFC");
|
|
assert.notEqual(nfd, nfc);
|
|
assert.equal(normalizeStoreName(` ${nfd} `), nfc);
|
|
let called;
|
|
globalThis.fetch = async (url, opts) => {
|
|
called = { url, method: opts.method };
|
|
return { ok: true, json: async () => ({ ok: true }) };
|
|
};
|
|
await deleteStoreEntry("characters", nfd);
|
|
assert.equal(called.method, "DELETE");
|
|
assert.equal(called.url, "/byae/ideogram/stores/characters/%C3%89milie");
|
|
});
|
|
|
|
it("encodes spaces in scene names", async () => {
|
|
let called;
|
|
globalThis.fetch = async (url, opts) => {
|
|
called = { url, method: opts.method };
|
|
return { ok: true, json: async () => ({ ok: true }) };
|
|
};
|
|
await deleteStoreEntry("scenes", "My Scene");
|
|
assert.equal(called.method, "DELETE");
|
|
assert.equal(called.url, "/byae/ideogram/stores/scenes/My%20Scene");
|
|
});
|
|
|
|
it("uses the server error message", async () => {
|
|
globalThis.fetch = async () => ({
|
|
ok: false,
|
|
status: 404,
|
|
json: async () => ({ error: "Entry not found" }),
|
|
});
|
|
await assert.rejects(deleteStoreEntry("scenes", "Gone"), /Entry not found/);
|
|
});
|
|
|
|
it("falls back to the status when the body is not JSON", async () => {
|
|
globalThis.fetch = async () => ({
|
|
ok: false,
|
|
status: 500,
|
|
json: async () => {
|
|
throw new Error("nope");
|
|
},
|
|
});
|
|
await assert.rejects(deleteStoreEntry("objects", "Box"), /HTTP 500/);
|
|
});
|
|
});
|