Files
DumiFlex-ComfyUI-Wildcard-P…/src/components/context/editors/constraint/sections/TargetReachSection.test.ts
T
DumiFlex 73a00a3760 fix(context): constraint-reach QA, node codenames, bundle-aware toolbar
QA + polish on the unmerged SP3 constraint-reach work, plus the node-id
redesign and bundle-aware bulk toolbar.

- reach badge reactivity: pairingBadgeEqual compares reach + pairingMapEqual
  compares contributors (context.ts) so a target_select edit re-renders.
- TargetReachSection: neutral section highlight + custom up/down count stepper.
- nested carrier renders ONE badge (constraint-pairs.ts: nested coverage goes
  to viaInbound only, not also contributors).
- instance target_select override reaches the badge: cross-node-pairings.ts
  folds the instance value over payload; the modal collapses to null only when
  it equals the library default (preserves an explicit "all"). Engine
  precedence (instance over payload) locked by tests.
- graph.ts writeBindings honors instance.variable_binding, fixing the duplicate
  shadow ("override") badge + missing-var scan + preview for renamed bindings.
- node identity: fixed, unique, POV-independent adjective-noun codename from the
  litegraph node id (node-codename.ts); read-only neutral chip; dropped the
  editable node_label + A/B/C letters.
- nested-pick label reads "@<target> in <carrier>" (target by display name).
- bulk toolbar: toggle-all flips bundle masters + top-level modules (not bundle
  children); collapse/expand-all flips bundle frames; buttons resized to the
  row-icon size.
2026-06-11 16:38:56 +03:00

196 lines
8.0 KiB
TypeScript

// Constraint TargetReachSection — reach selector for the edited
// constraint's `target_select`. Segmented control first | next N | all |
// pick; `next` reveals a min-1 stepper, `pick` reveals a checklist of
// reachable downstream target occurrences (direct + nested-via-carrier)
// built from `chainModules`. Ticking a row toggles its `picks` entry.
//
// Pick identity MUST match what the engine + computePairingsFull match
// on: a direct occurrence is its target instance row's `rowKey`; a nested
// occurrence is `(carrier rowKey, option_id)`.
import { describe, it, expect } from "vitest";
import { mount } from "@vue/test-utils";
import TargetReachSection from "./TargetReachSection.vue";
import type { ChainModule, TargetSelect } from "../../../../../extension/constraint-pairs";
const TARGET_UUID = "ffffffff";
/** Minimal chain: a constraint, a direct downstream target instance, and
* a downstream carrier whose option transitively refs the same target.
* Ordered execution-order — the constraint sits first so both the
* direct + carrier rows are DOWNSTREAM of it. */
function makeChain(): ChainModule[] {
return [
{
id: "cn0",
rowKey: "1#cn0",
type: "constraint",
payload: { source_wildcard_id: "src0", target_wildcard_id: TARGET_UUID },
displayName: "color-mood",
nodeLabel: "A",
},
{
id: TARGET_UUID,
rowKey: "1#mood-a",
type: "wildcard",
payload: { var_binding: "mood", options: [{ id: "m0", value: "calm" }] },
displayName: "mood",
nodeLabel: "A",
},
{
id: "carrier0",
rowKey: "2#backdrop-b",
type: "wildcard",
payload: { options: [{ id: "opt_a", value: `@{${TARGET_UUID}}` }] },
displayName: "backdrop",
nodeLabel: "B",
},
];
}
const CONSTRAINT_UID = "1#cn0";
/** Last emitted value (project TS lib predates Array.prototype.at). */
function last(xs: TargetSelect[]): TargetSelect | undefined {
return xs[xs.length - 1];
}
function mountSection(
modelValue: TargetSelect,
opts: Partial<{ chainModules: ChainModule[]; targetName: string }> = {},
) {
const updates: TargetSelect[] = [];
const w = mount(TargetReachSection, {
props: {
modelValue,
constraintUid: CONSTRAINT_UID,
targetWildcardId: TARGET_UUID,
targetName: opts.targetName ?? "test-target",
chainModules: opts.chainModules ?? makeChain(),
// Reflect emitted values back into the prop — the modal owns
// `target_select` state, so the section re-renders from the new
// model on every change (segmented mode reveals stepper/checklist).
"onUpdate:modelValue": (v: TargetSelect) => {
updates.push(v);
void w.setProps({ modelValue: v });
},
},
});
return { w, updates };
}
describe("TargetReachSection — segmented control", () => {
it("emits {mode:'next', count:2} after clicking 'next N' then setting the stepper to 2", async () => {
const { w, updates } = mountSection({ mode: "all" });
await w.find('[data-test="reach-mode-next"]').trigger("click");
// Entering next defaults count:1.
expect(last(updates)).toEqual({ mode: "next", count: 1 });
await w.vm.$nextTick();
const stepper = w.find<HTMLInputElement>('[data-test="reach-count"]');
expect(stepper.exists()).toBe(true);
await stepper.setValue("2");
expect(last(updates)).toEqual({ mode: "next", count: 2 });
});
it("stepper ▲ button increments the count", async () => {
const { w, updates } = mountSection({ mode: "next", count: 2 });
await w.find('[data-test="reach-count-up"]').trigger("click");
expect(last(updates)).toEqual({ mode: "next", count: 3 });
});
it("stepper ▼ button decrements the count", async () => {
const { w, updates } = mountSection({ mode: "next", count: 3 });
await w.find('[data-test="reach-count-down"]').trigger("click");
expect(last(updates)).toEqual({ mode: "next", count: 2 });
});
it("stepper ▼ button is disabled at the minimum (count 1)", () => {
const { w } = mountSection({ mode: "next", count: 1 });
const down = w.find('[data-test="reach-count-down"]');
expect(down.exists()).toBe(true);
expect(down.attributes("disabled")).toBeDefined();
});
it("emits {mode:'all'} when 'all' clicked", async () => {
const { w, updates } = mountSection({ mode: "next", count: 3 });
await w.find('[data-test="reach-mode-all"]').trigger("click");
expect(last(updates)).toEqual({ mode: "all" });
});
it("default all → no checklist + no stepper rendered (minimal)", () => {
const { w } = mountSection({ mode: "all" });
expect(w.find('[data-test="reach-count"]').exists()).toBe(false);
expect(w.find('[data-test="reach-picklist"]').exists()).toBe(false);
});
});
describe("TargetReachSection — pick checklist", () => {
it("pick mode lists reachable instances (direct + nested), nested tagged + showing node label; ticking emits the right picks entry", async () => {
const { w, updates } = mountSection({ mode: "pick", picks: [] });
const list = w.find('[data-test="reach-picklist"]');
expect(list.exists()).toBe(true);
// Direct row for the mood instance + nested row for the backdrop carrier.
const directRow = w.find('[data-test="reach-pick-1#mood-a"]');
const nestedRow = w.find('[data-test="reach-pick-2#backdrop-b::opt_a"]');
expect(directRow.exists()).toBe(true);
expect(nestedRow.exists()).toBe(true);
// Nested row carries the `nested` tag + the carrier's node label (B).
expect(nestedRow.text().toLowerCase()).toContain("nested");
expect(nestedRow.text()).toContain("B");
// Direct row tagged `direct`.
expect(directRow.text().toLowerCase()).toContain("direct");
// Tick the direct row → picks gains the direct entry keyed by the
// BARE _uid (engine identity), NOT the rowKey. The row's data-test
// still uses the full rowKey (`1#mood-a`); the persisted uid strips
// the `1#` node prefix → `mood-a`.
await directRow.find(".wp-check").trigger("click");
expect(last(updates)).toEqual({
mode: "pick",
picks: [{ kind: "direct", uid: "mood-a" }],
});
});
it("nested row labels the @TARGET ref (display name) hosted in the carrier (display name, no @)", () => {
const { w } = mountSection({ mode: "pick", picks: [] }, { targetName: "test-target" });
const nestedRow = w.find('[data-test="reach-pick-2#backdrop-b::opt_a"]');
expect(nestedRow.exists()).toBe(true);
const txt = nestedRow.text();
// The nested ref is `@{target}` — the `@` belongs to the TARGET, shown by
// display name: "@test-target".
expect(txt).toContain("@test-target");
// Host carrier named by DISPLAY NAME, NOT @-prefixed (you don't @ the host).
expect(txt).toContain("in backdrop");
expect(txt).not.toContain("@backdrop");
expect(txt).not.toContain("@subject");
});
it("ticking a nested row emits a nested picks entry keyed by (bare carrier _uid, option_id)", async () => {
const { w, updates } = mountSection({ mode: "pick", picks: [] });
await w.find('[data-test="reach-pick-2#backdrop-b::opt_a"] .wp-check').trigger("click");
// carrier_uid is the BARE _uid (`backdrop-b`), stripped of the `2#`
// node prefix — the engine's nested carrier_ctx stamps bare uids.
expect(last(updates)).toEqual({
mode: "pick",
picks: [{ kind: "nested", carrier_uid: "backdrop-b", option_id: "opt_a" }],
});
});
it("un-ticking a picked row removes its entry (bare-uid pick matches the row)", async () => {
const { w, updates } = mountSection({
mode: "pick",
picks: [{ kind: "direct", uid: "mood-a" }],
});
await w.find('[data-test="reach-pick-1#mood-a"] .wp-check').trigger("click");
expect(last(updates)).toEqual({ mode: "pick", picks: [] });
});
it("empty chainModules → empty checklist + a hint, no crash", () => {
const { w } = mountSection({ mode: "pick", picks: [] }, { chainModules: [] });
expect(w.find('[data-test="reach-pick-empty"]').exists()).toBe(true);
expect(w.find('[data-test="reach-pick-1#mood-a"]').exists()).toBe(false);
});
});