Files
ComfyUI_frontend/browser_tests/tests/customNodes
Nathaniel Parson Koroso baa2e046b7 docs(custom-nodes): Detection Proof accuracy pass
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>
2026-07-08 18:19:11 -07:00
..

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

  1. A ComfyUI backend on 127.0.0.1:8288 with every manifest pack (the pack entries in browser_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 with PARTIAL), and with browser_tests/assets/plain_video.mp4 copied into its input/ dir.
  2. 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 declares vueNodesCompatible: false in 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 executing event stream must contain every expected node id, and the run must end in execution_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_info producers/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 real isValidConnection veto, then survive serialize() -> configure() and appear in graphToPrompt() 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 /extensions endpoint 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 with PLAYWRIGHT_TEST_URL pointed at the backend.
  • Do not run with --trace on against system Chrome (playwright.chrome.config.ts pins 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_modules is symlinked from another checkout, prefix scripts with pnpm --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.