Pack-mode breaks are delivered via manifest pin swaps (CI clones packs at pins, so in-repo pack-file edits cannot reach it); corpus-derived red messages promise the tier and failure class, not byte-identical offender text; remaining citations and mechanism descriptions tightened to what the code and captured runs actually show. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Custom-node regression suite
Proves community custom-node packs work against this frontend across both renderers: nodes register, render under LiteGraph (canvas) AND Vue Nodes 2.0 (DOM), and execute real workflows end to end. Manifest-driven: adding a pack is one JSON row, no new test code.
System design, data flow, and the reasoning behind every invariant: ARCHITECTURE.md. Onboarding a new pack: ADDING_CUSTOM_NODES.md.
Prerequisites
- A ComfyUI backend on
127.0.0.1:8288with every manifest pack (thepackentries inbrowser_tests/fixtures/data/customNodeManifest.json) and ComfyUI_devtools installed. Launch it with--multi-user(the repo-wide browser-test prerequisite; the fixture writes per-worker user settings and the suite depends on them landing),--cache-none(repeat runs must re-execute every node or the executed-set check fails honestly withPARTIAL), and withbrowser_tests/assets/plain_video.mp4copied into itsinput/dir. - The dev server proxying that backend:
DEV_SERVER_COMFYUI_URL=http://127.0.0.1:8288 pnpm dev
Running
| Script | What it does |
|---|---|
pnpm test:custom-nodes |
whole suite headless against the Vite dev server - the fast local loop for suite-code iteration. NOT the gate: the dev server never loads pack frontend JS (see Gotchas) |
pnpm test:custom-nodes:ci |
whole suite headless against the backend-served BUILT frontend - the gate-equivalent run (every tier passes, zero skips). Requires a backend serving the built dist on :8188 (a separate endpoint from the :8288 dev-proxy backend in Prerequisites); set PLAYWRIGHT_TEST_URL if yours differs |
pnpm test:custom-nodes:watch |
headed slow-motion run of the browser tiers, hands-off watching |
pnpm test:custom-nodes:debug |
step through the browser tiers in the Playwright Inspector (F10 step, F8 resume) |
pnpm test:custom-nodes:impact-render |
Impact nodes render in both renderers (Inspector) |
pnpm test:custom-nodes:impact-run |
Impact group workflow executes on the backend (Inspector) |
pnpm test:custom-nodes:vhs-render |
VHS nodes render in both renderers (Inspector) |
pnpm test:custom-nodes:vhs-run |
VHS decodes a real video through its node chain (Inspector) |
pnpm test:custom-nodes:connectivity |
slot/type contract: type-paired links + real slot drags in both renderers (Inspector) |
pnpm test:custom-nodes:self-check |
watches the harness catch a deliberate execution error |
Example - watch the VHS video-decode run step by step:
pnpm test:custom-nodes:vhs-run
Two windows open: the app under test and the Playwright Inspector. Press F10 to execute one robot action at a time (workflow loads, queue fires, backend decodes the video), F8 to run to the end. While paused, look but do not click inside the app window - your clicks change the state the next assertion checks.
Any -g pattern works against the generic scripts, e.g.
pnpm test:custom-nodes:debug -g "Impact-Pack.*T0".
What the tests assert
- T0 load: pack nodes are registered in
/object_info, added to a cleared graph, counted exactly, and each added node's own[data-node-id]element mounts under Vue Nodes 2.0. Both renderer passes - unless the pack declaresvueNodesCompatible: falsein the manifest (evidence required; see ADDING_CUSTOM_NODES.md), in which case its tests run their LiteGraph-canvas assertions only. Never a skip. - T1 run: the manifest workflow is loaded and queued; the backend's
executingevent stream must contain every expected node id, and the run must end inexecution_success. - Every-node tiers (
allNodes.spec.ts): the pack's FULL node list, discovered live from/object_info, is exercised with zero configuration - every registered node mounts in both renderers (chunked at an empirically measured batch size), survives a serialize/configure save-reload round-trip, and executes for real on the backend when self-sufficient (all required inputs are widgets with valid defaults). Nodes that cannot run alone are classified and logged (NEEDS_WIRES/NEEDS_MODELS/NO_OBSERVABLE_OUTPUT/ rejected-at-validation), never silently dropped; the documented exception ledgers (see ADDING_CUSTOM_NODES.md) carry a written mechanism for every escape hatch. - connectivity (contract): wiring-only, no execution. A
type-pairing generator (
fixtures/customNode/typePairing.ts) indexes/object_infoproducers/consumers and plans one representative typed edge per slot (wildcard*slots excluded - they bypass the real type compare and prove nothing). Each planned edge must connect through the realisValidConnectionveto, then surviveserialize()->configure()and appear ingraphToPrompt()output. A curated subset is additionally dragged for real - slot dot to slot dot - under both renderers. Orphan types (no partner in the corpus) are reported, never fake-failed. One representative edge per slot bounds cost; it does not prove all pairs. - Zero visible errors: the mount, persistence, connectivity, core smoke, and curated workflow tests assert the app's error surfaces (error overlay, error dialog, node render errors, error toasts) are absent at start and after every pass - green means a human watching those runs sees no errors. Two deliberate exceptions: the auto-run execution tier provokes expected failures (baselined cannotRunAlone nodes surface as real error UI by design), and the self-check inverts the invariant - it forces a real execution error and asserts the overlay IS visible, proving the selectors stay live.
- Console-error window: the console/page-error ledger (curated run, save/reload) starts collecting inside each tier, so it covers the tier's own actions - load, run, wire, save. Pure console noise a pack logs at app boot, before the first tier action, is out of that window by design: the shared app fixture navigates once at setup, so boot output predates any per-pack collector. Boot breakage that MATTERS still fails the gate - the zero-visible-errors check runs at startup and catches any boot error that reaches a visible surface; only invisible, functionally-inert boot console noise (the ledger's whole reason to exist) is out of scope.
Adding a pack
One manifest row plus one small workflow JSON - no new test code. The
authoritative step-by-step process (verifying the pack's real node keys,
authoring the run workflow, the vueNodesCompatible evidence rule, what CI
does with the row) lives in ADDING_CUSTOM_NODES.md. Follow it
exactly; the traps it lists all shipped in real packs.
Gotchas
- Pack frontend JS does not load under the Vite dev server. The dev
server's
/extensionsendpoint lists core extensions only, so nodes render vanilla locally even when the backend has the packs installed. CI serves the built frontend from the backend, where every pack's JS loads and can restyle nodes, rebuild widgets, or inject page chrome. Before pushing changes that could interact with pack JS, reproduce CI locally:pnpm build, relaunch the backend with--front-end-root <repo>/dist, and run the suite withPLAYWRIGHT_TEST_URLpointed at the backend. - Do not run with
--trace onagainst system Chrome (playwright.chrome.config.tspins trace off): the trace recorder crashes pages under the branded Chrome channel and every test reports a bogus 15s timeout. - In a git worktree whose
node_modulesis symlinked from another checkout, prefix scripts withpnpm --config.verify-deps-before-run=false ...to skip pnpm's auto-install check. - First run against a cold dev server can exceed the 15s per-test setup budget while Vite compiles; just run again.