## Summary Backend validation errors (`node_errors`) are reported against the **flattened** prompt, so an error whose real fix lives on a subgraph host node gets attached to an interior node the user may never open — and, after ADR 0009, often *cannot* meaningfully fix there. This PR re-surfaces a validation error onto the subgraph host node **when — and only when — the error's subject (the specific input/widget named by `extra_info.input_name`) is exposed through the subgraph boundary**. ## Why this is needed Two concrete situations motivated this, both observed in real workflows: 1. **Broken link at the host.** Root node A should feed subgraph host B, whose boundary input is linked to interior node C. If the A→B link is missing, the backend flattens the prompt, sees C with no resolved input, and raises `required_input_missing` **on C** (`"B:C"`). The actual fix — connect B's input — is one level up, on a node the error never points at. 2. **Host-owned widget values.** Per ADR 0009 (subgraph promoted widgets use linked inputs), a promoted widget's value is owned by the host `SubgraphNode`; the interior widget only supplies schema/defaults. When the backend raises `value_not_in_list` (or min/max violations) for that value, attributing it to the interior node is factually wrong — the value that failed validation *is the host's value*. This continues the direction of #13059, which moved **missing-model** detection identity to `{hostExecutionId, hostWidgetName}` with the interior path kept as diagnostics. That was possible in the FE pre-scan; this PR applies the same ownership principle to **backend-received** errors via a receive-side mapping, since the backend cannot know about subgraph boundaries in a flattened prompt. ## The rule (design) > Lift an error from interior node N to host H **iff** N's input slot named by the error is linked to the containing subgraph's boundary (`SubgraphInput`). Apply the same test again at H (boundary-by-boundary, matching ADR 0009's chaining principle) and stop at the first level where the subject is no longer boundary-linked — that node is where the user can actually fix it. The predicate is **structural (boundary exposure), not data-flow**. ### In scope — examples - `required_input_missing` on interior `"12:5"` whose input is fed by the boundary → surfaces on host `12`'s input slot (red slot ring on the host, errors-tab card titled/located at the host, message names the host's `SubgraphInput.name`). - `value_not_in_list` / `value_smaller_than_min` / `value_bigger_than_max` on a promoted interior widget → surfaces on the host's promoted widget. Nested hosts chain: `"1:2:3"` lifts to `"1:2"`, and further to `"1"` only if `1:2`'s own slot is boundary-linked too. - Clearing follows the surface: connecting the highlighted host input or fixing the host widget clears the underlying interior (raw) error — range-guarded per target, so a still-out-of-range host value does **not** clear. ### Out of scope — examples - **No value-flow ancestry.** All in the root graph: A's widget links to B, B's to C, and C rejects the value that originated at A → the error **stays on C**. Following same-graph links to a "root cause" node is explicitly not this feature. - Errors without an `input_name` subject, node-level types (`exception_during_validation`, `dependency_cycle`, image-not-loaded), and unknown validation types — never lifted. Unknown types stay node-scoped to match how the error catalog renders them (the shared `isNodeLevelValidationError` in `executionErrorUtil` encodes this, and the catalog derives its node-level rules from the same set). - Runtime execution errors (exceptions during a run) — validation responses only. - Interior errors whose input is fed by another interior node — fixable in place, stay in place. - Fan-out display dedupe: when one boundary input feeds multiple interior nodes and the host slot is unconnected, each interior error lifts to the same host slot as a separate panel line. A single fix (connecting the host input) clears all of them — the clearing translation already fans out — so the duplication is cosmetic. Display-level dedupe is a follow-up; deduping inside the lift would break the one-source-per-error clearing contract. - Reactive re-lifting on graph topology edits while errors are displayed (invariant documented on the computed; follow-up), and deriving the error catalog's full validation rule table from the shared classification (follow-up; the node-level type set and the image-not-loaded predicate are already single-sourced in `executionErrorUtil` and consumed by both the lift and the catalog). ## Changes - **What**: New pure module `core/graph/subgraph/liftNodeErrorsToBoundary.ts` — per-error, fail-open record transform (unresolvable ids/slots/links leave the error where the backend put it; raw payload is never mutated). `executionErrorStore` derives `surfacedNodeErrors` from it and display consumers switch over (errors tab grouping, canvas node/slot flags, Vue node badges, app-mode/linear hints); raw `lastNodeErrors` remains the source of truth for mutation. Host-side clearing translates through the lift's diagnostics fields (`source_execution_id` / `source_input_name`) with a per-target range guard. - **Breaking**: None. No persistence/serialization changes; interior identity survives as diagnostics metadata only (ADR 0009 language). ## Review Focus - The lift predicate lives entirely on link topology (`LLink.originIsIoNode` → `SubgraphInput`) — no `proxyWidgets`/promotion-store style source authority is reintroduced. - `clearSlotErrorsWithRangeCheck` now resolves clear targets first and range-checks each target's raw errors; the lifted-path twin of the existing range-retention test pins this. - `useProcessedWidgets` deliberately stays on the raw record: host promoted widgets already map to interior errors via `widget.sourceExecutionId`, so an interior widget keeps its local red hint when the user opens the subgraph (hint layer vs surface layer). - Unit coverage is carried by the pure module (real litegraph subgraph fixtures, incl. nested recursion, promoted widgets via `promoteValueWidgetViaSubgraphInput`, ordering, fail-open/no-mutation); one e2e pins the user-visible contract (host ring + host slot dot + interior clean). ## Screenshots ### Before https://github.com/user-attachments/assets/81e5c4db-515d-4f1f-8f8a-e07ac490510f ### After https://github.com/user-attachments/assets/2949da06-a049-41c1-a480-98ee28333bf2
Playwright Testing for ComfyUI_frontend
This document outlines the setup, usage, and common patterns for Playwright browser tests in the ComfyUI_frontend project.
Prerequisites
CRITICAL: Start ComfyUI backend with --multi-user flag:
python main.py --multi-user
Without this flag, parallel tests will conflict and fail randomly.
Setup
ComfyUI devtools
ComfyUI_devtools is included in this repository under tools/devtools/. During CI/CD, these files are automatically copied to the custom_nodes directory.
ComfyUI_devtools adds additional API endpoints and nodes to ComfyUI for browser testing.
For local development, copy the devtools files to your ComfyUI installation:
cp -r tools/devtools/* /path/to/your/ComfyUI/custom_nodes/ComfyUI_devtools/
Node.js & Playwright Prerequisites
Ensure you have the Node.js version specified in .nvmrc installed.
Then, set up the Chromium test driver:
pnpm exec playwright install chromium --with-deps
Environment Configuration
Create .env from the template:
cp .env_example .env
Key settings for debugging:
# Remove Vue dev overlay that blocks UI elements
DISABLE_VUE_PLUGINS=true
# Test against dev server (recommended) or backend directly
PLAYWRIGHT_TEST_URL=http://localhost:5173 # Dev server
# PLAYWRIGHT_TEST_URL=http://localhost:8188 # Direct backend
PLAYWRIGHT_SETUP_API_URL=http://localhost:8188 # Setup/auth API when using the dev server URL above
# Path to ComfyUI for backing up user data/settings before tests
TEST_COMFYUI_DIR=/path/to/your/ComfyUI
Common Setup Issues
Release API Mocking
By default, all tests mock the release API (api.comfy.org/releases) to prevent release notification popups from interfering with test execution. This is necessary because the release notifications can appear over UI elements and block test interactions.
To test with real release data, you can disable mocking:
await comfyPage.setup({ mockReleases: false })
For tests that specifically need to test release functionality, see the example in tests/releaseNotifications.spec.ts.
Running Tests
Always use UI mode for development:
pnpm test:browser:local --ui
UI mode features:
- Locator picker: Click the target icon, then click any element to get the exact locator code to use in your test. The code appears in the Locator tab.
- Step debugging: Step through your test line-by-line by clicking Source tab
- Time travel: In the Actions tab/panel, click any step to see the browser state at that moment
- Console/Network Tabs: View logs and API calls at each step
- Attachments Tab: View all snapshots with expected and actual images
For CI or headless testing:
pnpm test:browser:local # Run all tests
pnpm test:browser:local widget.spec.ts # Run specific test file
Slowing the browser down for debugging
When running with --headed (or --ui), set SLOW_MO to a millisecond delay
to slow every Playwright action down so you can watch what is happening. The
delay only applies when PLAYWRIGHT_LOCAL is set (the default for the
pnpm test:browser:local script).
SLOW_MO=250 pnpm test:browser:local --headed widget.spec.ts
Test Structure
Browser tests in this project follow a specific organization pattern:
-
Fixtures: Located in
fixtures/- These provide test setup and utilitiesComfyPage.ts- The main fixture for interacting with ComfyUIComfyMouse.ts- Utility for mouse interactions with the canvas- Components fixtures in
fixtures/components/- Page object models for UI components
-
Tests: Located in
tests/- The actual test specifications- Organized by functionality (e.g.,
widget.spec.ts,interaction.spec.ts) - Snapshot directories (e.g.,
widget.spec.ts-snapshots/) contain reference screenshots
- Organized by functionality (e.g.,
-
Utilities: Located in
utils/- Common utility functionslitegraphUtils.ts- Utilities for working with LiteGraph nodes
Writing Effective Tests
When writing new tests, follow these patterns:
Test Structure
// Import the test fixture
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
test.describe('Feature Name', () => {
// Set up test environment if needed
test.beforeEach(async ({ comfyPage }) => {
// Common setup
})
test('should do something specific', async ({ comfyPage }) => {
// Test implementation
})
})
Leverage Existing Fixtures and Helpers
Always check for existing helpers and fixtures before implementing new ones:
- ComfyPage: Main fixture with methods for canvas interaction and node management
- ComfyMouse: Helper for precise mouse operations on the canvas
- Component Fixtures: Check
browser_tests/fixtures/components/for UI component page objects (e.g.Actionbar.ts,Templates.ts,ContextMenu.ts) - Helper Classes: Check
browser_tests/fixtures/helpers/for domain-specific helper classes wired into ComfyPage (e.g.CanvasHelper.ts,WorkflowHelper.ts) - Utility Functions: Check
browser_tests/fixtures/utils/for standalone utilities (e.g.fitToView.ts,clipboardSpy.ts,builderTestUtils.ts)
Most common testing needs are already addressed by these helpers, which will make your tests more consistent and reliable.
Import Conventions
- Prefer
@e2e/*for imports withinbrowser_tests/ - Continue using
@/*for imports fromsrc/ - Avoid introducing new deep relative imports within
browser_tests/when the alias is available
Key Testing Patterns
-
Focus elements explicitly: Canvas-based elements often need explicit focus before interaction:
// Click the canvas first to focus it before pressing keys await comfyPage.canvas.click() await comfyPage.page.keyboard.press('a') -
Mark canvas as dirty if needed: Some interactions need explicit canvas updates:
// After programmatically changing node state, mark canvas dirty await comfyPage.page.evaluate(() => { window['app'].graph.setDirtyCanvas(true, true) }) -
Use node references over coordinates: Node references from
fixtures/utils/litegraphUtils.tsprovide stable ways to interact with nodes:// Prefer this: const node = await comfyPage.getNodeRefsByType('LoadImage')[0] await node.click('title') // Over this: await comfyPage.canvas.click({ position: { x: 100, y: 100 } }) -
Wait for canvas to render after UI interactions:
await comfyPage.nextFrame() -
Clean up persistent server state: While most state is reset between tests, anything stored on the server persists:
// Reset settings that affect other tests (these are stored on server) await comfyPage.setSetting('Comfy.ColorPalette', 'dark') await comfyPage.setSetting('Comfy.NodeBadge.NodeIdBadgeMode', 'None') // Clean up uploaded files if needed comfyPage.deleteFileAfterTest({ filename: 'image.png' }) -
Prefer functional assertions over screenshots: Use screenshots only when visual verification is necessary:
// Prefer this: await expect.poll(() => node.isPinned()).toBe(true) await expect.poll(() => node.getProperty('title')).toBe('Expected Title') // Over this - only use when needed: await expect(comfyPage.canvas).toHaveScreenshot('state.png') -
Use minimal test workflows: When creating test workflows, keep them as minimal as possible:
// Include only the components needed for the test await comfyPage.loadWorkflow('single_ksampler') -
Debug helpers for visual debugging (remove before committing):
ComfyPage includes temporary debug methods for troubleshooting:
test('debug failing interaction', async ({ comfyPage }, testInfo) => { // Add visual markers to see click positions await comfyPage.debugAddMarker({ x: 100, y: 200 }) // Attach screenshot with markers to test report await comfyPage.debugAttachScreenshot(testInfo, 'node-positions', { element: 'canvas', markers: [{ position: { x: 100, y: 200 } }] }) // Show canvas overlay for easier debugging await comfyPage.debugShowCanvasOverlay() // Remember to remove debug code before committing! })Available debug methods:
debugAddMarker(position)- Red circle at positiondebugAttachScreenshot(testInfo, name)- Attach to test reportdebugShowCanvasOverlay()- Show canvas as overlaydebugGetCanvasDataURL()- Get canvas as base64
Common Patterns and Utilities
Page Object Pattern
Tests use the Page Object pattern to create abstractions over the UI:
// Using the ComfyPage fixture
test('Can toggle boolean widget', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('widgets/boolean_widget')
const node = (await comfyPage.getFirstNodeRef())!
const widget = await node.getWidget(0)
await widget.click()
})
Node References
The NodeReference class provides helpers for interacting with LiteGraph nodes:
// Getting node by type and interacting with it
const nodes = await comfyPage.getNodeRefsByType('LoadImage')
const loadImageNode = nodes[0]
const widget = await loadImageNode.getWidget(0)
await widget.click()
Visual Regression Testing
Tests use screenshot comparisons to verify UI state:
// Take a screenshot and compare to reference
await expect(comfyPage.canvas).toHaveScreenshot('boolean_widget_toggled.png')
Waiting for Animations
Always call nextFrame() after actions that trigger animations:
await comfyPage.canvas.click({ position: { x: 100, y: 100 } })
await comfyPage.nextFrame() // Wait for canvas to redraw
Mouse Interactions
Canvas operations use special helpers to ensure proper timing:
// Using ComfyMouse for drag and drop
await comfyMouse.dragAndDrop(
{ x: 100, y: 100 }, // From
{ x: 200, y: 200 } // To
)
// Standard ComfyPage helpers
await comfyPage.drag({ x: 100, y: 100 }, { x: 200, y: 200 })
await comfyPage.pan({ x: 200, y: 200 })
await comfyPage.zoom(-100) // Zoom in
Workflow Management
Tests use workflows stored in assets/ for consistent starting points:
// Load a test workflow
await comfyPage.loadWorkflow('single_ksampler')
// Wait for workflow to load and stabilize
await comfyPage.nextFrame()
Custom Assertions
The project includes custom Playwright assertions through comfyExpect:
// Check if a node is in a specific state
await expect(node).toBePinned()
await expect(node).toBeBypassed()
await expect(node).toBeCollapsed()
Troubleshooting Common Issues
Flaky Tests
- Timing Issues: Always wait for animations to complete with
nextFrame() - Coordinate Sensitivity: Canvas coordinates are viewport-relative; use node references when possible
- Test Isolation: Tests run in parallel; avoid dependencies between tests
- Screenshots vary: Ensure your OS and browser match the reference environment (Linux)
- Async / await: Race conditions are a very common cause of test flakiness
Screenshot Testing
Due to variations in system font rendering, screenshot expectations are platform-specific. Please note:
- Do not commit local screenshot expectations to the repository
- We maintain Linux screenshot expectations as our GitHub Action runner operates in a Linux environment
- While developing, you can generate local screenshots for your tests, but these will differ from CI-generated ones
Working with Screenshots Locally
Option 1 - Skip screenshot tests (add to playwright.config.ts):
export default defineConfig({
grep: process.env.CI ? undefined : /^(?!.*screenshot).*$/
})
Option 2 - Generate local baselines for comparison:
pnpm test:browser:local --update-snapshots
Creating New Screenshot Baselines
For PRs from Comfy-Org/ComfyUI_frontend branches:
- Write test with
toHaveScreenshot('filename.png') - Create PR and add
New Browser Test Expectationlabel - CI will generate and commit the Linux baseline screenshots
Note: Fork PRs cannot auto-commit screenshots. A maintainer will need to commit the screenshots manually for you (don't worry, they'll do it).
Viewing Test Reports
Automated Test Deployment
The project automatically deploys Playwright test reports to Cloudflare Pages for every PR and push to main branches.
Accessing Test Reports
- From PR comments: Click the "View Report" links for each browser
- Direct URLs: Reports are available at
https://[branch].comfyui-playwright-[browser].pages.dev(branch-specific deployments) - From GitHub Actions: Download artifacts from failed runs
How It Works
-
Test execution: All browser tests run in parallel across multiple browsers
-
Report generation: HTML reports are generated for each browser configuration
-
Cloudflare deployment: Each browser's report deploys to its own Cloudflare Pages project with branch isolation:
comfyui-playwright-chromium(with branch-specific URLs)comfyui-playwright-mobile-chrome(with branch-specific URLs)comfyui-playwright-chromium-2x(2x scale, with branch-specific URLs)comfyui-playwright-chromium-0-5x(0.5x scale, with branch-specific URLs)
-
PR comments: GitHub automatically updates PR comments with:
- ✅/❌ Test status for each browser
- Direct links to interactive test reports
- Real-time progress updates as tests complete
Resources
- Playwright UI Mode - Interactive test debugging
- Playwright Debugging Guide
- act - Run GitHub Actions locally for CI debugging