Files
DumiFlex-ComfyUI-Wildcard-P…/src/components/context-loop/ContextLoopWidget.test.ts
T

151 lines
7.3 KiB
TypeScript

import { describe, it, expect, beforeEach } from "vitest";
import { mount } from "@vue/test-utils";
import ContextLoopWidget from "./ContextLoopWidget.vue";
import SeedListModal from "../shared/SeedListModal.vue";
import { emptyContextLoopConfig } from "./types";
import { currentFrame } from "./frame-cursor";
describe("ContextLoopWidget", () => {
it("renders defaults — hash chip active, both switches off, internal toggles on", () => {
const w = mount(ContextLoopWidget, { props: { modelValue: emptyContextLoopConfig() } });
expect(w.find('[data-test="loop-strategy-hash_index"]').classes()).toContain("wp-loop__chip--active");
expect(w.find('[data-test="loop-strategy-sequential"]').classes()).not.toContain("wp-loop__chip--active");
expect(w.find('[data-test="loop-override-toggle"]').classes()).not.toContain("wp-loop__switch--on");
expect(w.find('[data-test="loop-bypass-toggle"]').classes()).not.toContain("wp-loop__switch--on");
// Both internal toggles default to ON.
expect(w.find('[data-test="loop-iteration-internal"]').classes()).toContain("wp-loop__pi-btn--on");
expect(w.find('[data-test="loop-total-internal"]').classes()).toContain("wp-loop__pi-btn--on");
});
it("emits new strategy on chip click", async () => {
const w = mount(ContextLoopWidget, { props: { modelValue: emptyContextLoopConfig() } });
await w.find('[data-test="loop-strategy-sequential"]').trigger("click");
const emitted = w.emitted("update:modelValue")?.[0]?.[0] as { strategy: string };
expect(emitted.strategy).toBe("sequential");
});
it("skip emit when picking the already-active strategy", async () => {
const w = mount(ContextLoopWidget, { props: { modelValue: emptyContextLoopConfig() } });
await w.find('[data-test="loop-strategy-hash_index"]').trigger("click");
expect(w.emitted("update:modelValue")).toBeUndefined();
});
it("emits override_seed toggle on switch click", async () => {
const w = mount(ContextLoopWidget, { props: { modelValue: emptyContextLoopConfig() } });
await w.find('[data-test="loop-override-toggle"]').trigger("click");
const emitted = w.emitted("update:modelValue")?.[0]?.[0] as { override_seed: boolean };
expect(emitted.override_seed).toBe(true);
});
it("emits bypass toggle on switch click", async () => {
const w = mount(ContextLoopWidget, { props: { modelValue: emptyContextLoopConfig() } });
await w.find('[data-test="loop-bypass-toggle"]').trigger("click");
const emitted = w.emitted("update:modelValue")?.[0]?.[0] as { bypass: boolean };
expect(emitted.bypass).toBe(true);
});
it("dims strategy/override/var rows when bypass=on", () => {
const w = mount(ContextLoopWidget, { props: { modelValue: { ...emptyContextLoopConfig(), bypass: true } } });
expect(w.classes()).toContain("wp-loop--bypass-on");
});
it("emits iteration_var_name update on text input", async () => {
const w = mount(ContextLoopWidget, { props: { modelValue: emptyContextLoopConfig() } });
const input = w.find<HTMLInputElement>('[data-test="loop-iteration-var"]');
input.element.value = "idx";
await input.trigger("input");
const emitted = w.emitted("update:modelValue")?.[0]?.[0] as { iteration_var_name: string };
expect(emitted.iteration_var_name).toBe("idx");
});
it("applies wp-loop--muted class when nodeMode=2", () => {
const w = mount(ContextLoopWidget, { props: { modelValue: emptyContextLoopConfig(), nodeMode: 2 } });
expect(w.classes()).toContain("wp-loop--muted");
});
it("applies wp-loop--bypassed class when nodeMode=4", () => {
const w = mount(ContextLoopWidget, { props: { modelValue: emptyContextLoopConfig(), nodeMode: 4 } });
expect(w.classes()).toContain("wp-loop--bypassed");
});
it("emits iteration_internal toggle on its button click (true → false)", async () => {
// Default is true; clicking toggles to false.
const w = mount(ContextLoopWidget, { props: { modelValue: emptyContextLoopConfig() } });
await w.find('[data-test="loop-iteration-internal"]').trigger("click");
const emitted = w.emitted("update:modelValue")?.[0]?.[0] as { iteration_internal: boolean };
expect(emitted.iteration_internal).toBe(false);
});
it("emits total_internal toggle on its button click (true → false)", async () => {
// Default is true; clicking toggles to false.
const w = mount(ContextLoopWidget, { props: { modelValue: emptyContextLoopConfig() } });
await w.find('[data-test="loop-total-internal"]').trigger("click");
const emitted = w.emitted("update:modelValue")?.[0]?.[0] as { total_internal: boolean };
expect(emitted.total_internal).toBe(false);
});
it("renders internal-on style when flag set", () => {
const w = mount(ContextLoopWidget, {
props: { modelValue: { ...emptyContextLoopConfig(), iteration_internal: true } },
});
expect(w.find('[data-test="loop-iteration-internal"]').classes()).toContain("wp-loop__pi-btn--on");
});
});
function ws(extra: Partial<ReturnType<typeof emptyContextLoopConfig>> = {}) {
return mount(ContextLoopWidget, {
props: { modelValue: { ...emptyContextLoopConfig(), ...extra }, baseSeed: 42, count: 4 },
attachTo: document.body,
global: { stubs: { teleport: true } },
});
}
describe("ContextLoopWidget seeds button", () => {
it("renders the Per-iteration seeds button", () => {
expect(ws().find('[data-test="loop-seeds-btn"]').exists()).toBe(true);
});
it("badge shows the locked count when >0", () => {
expect(ws({ seed_locks: { "1": 9, "2": 8 } }).find('[data-test="loop-seeds-badge"]').text()).toMatch(/2 locked/i);
});
it("hides the badge when nothing is locked", () => {
expect(ws().find('[data-test="loop-seeds-badge"]').exists()).toBe(false);
});
it("clicking opens the modal", async () => {
const wr = ws();
await wr.find('[data-test="loop-seeds-btn"]').trigger("click");
expect(wr.findComponent(SeedListModal).exists()).toBe(true);
});
it("capitalizes the bypass row label", () => {
expect(ws().text()).toContain("Bypass loop");
});
it("passes the override hint to the modal only when Override Context seed is off", async () => {
const off = ws(); // override_seed false by default → nudge to turn it on
await off.find('[data-test="loop-seeds-btn"]').trigger("click");
expect(off.findComponent(SeedListModal).props("overrideHint")).toBeTruthy();
const on = ws({ override_seed: true });
await on.find('[data-test="loop-seeds-btn"]').trigger("click");
expect(on.findComponent(SeedListModal).props("overrideHint")).toBeFalsy();
});
});
describe("ContextLoopWidget frame selector", () => {
beforeEach(() => { currentFrame.value = null; });
function fw(count = 3) {
return mount(ContextLoopWidget, {
props: { modelValue: emptyContextLoopConfig(), count },
attachTo: document.body, global: { stubs: { teleport: true } },
});
}
it("renders base + one chip per iteration", () => {
expect(fw(3).findAll('[data-test^="loop-frame-"]')).toHaveLength(4);
});
it("clicking a frame chip sets the shared cursor; base clears it", async () => {
const wr = fw(3);
await wr.find('[data-test="loop-frame-2"]').trigger("click");
expect(currentFrame.value).toBe(1);
await wr.find('[data-test="loop-frame-base"]').trigger("click");
expect(currentFrame.value).toBe(null);
});
});