Files
ComfyUI_frontend/src/utils/executionErrorUtil.test.ts
jaeone94 46c40c755e feat: node-specific error tab with selection-aware grouping and error overlay (#8956)
## Summary
Enhances the error panel with node-specific views: single-node selection
shows errors grouped by message in compact mode, container nodes
(subgraph/group) expose child errors via a badge and "See Error" button,
and a floating ErrorOverlay appears after execution failure with a
deduplicated summary and quick navigation to the errors tab.

## Changes
- **Consolidate error tab**: Remove `TabError.vue`; merge all error
display into `TabErrors.vue` and drop the separate `error` tab type from
`rightSidePanelStore`
- **Selection-aware grouping**: Single-node selection regroups errors by
message (not `class_type`) and renders `ErrorNodeCard` in compact mode
- **Container node support**: Detect child-node errors in subgraph/group
nodes via execution ID prefix matching; show error badge and "See Error"
button in `SectionWidgets`
- **ErrorOverlay**: New floating card shown after execution failure with
deduplicated error messages, "Dismiss" and "See Errors" actions;
`isErrorOverlayOpen` / `showErrorOverlay` / `dismissErrorOverlay` added
to `executionStore`
- **Refactor**: Centralize error ID collection in `executionStore`
(`allErrorExecutionIds`, `hasInternalErrorForNode`); split `errorGroups`
into `allErrorGroups` (unfiltered) and `tabErrorGroups`
(selection-filtered); move `ErrorOverlay` business logic into
`useErrorGroups`

## Review Focus
- `useErrorGroups.ts`: split into `allErrorGroups` / `tabErrorGroups`
and the new `filterBySelection` parameter flow
- `executionStore.ts`: `hasInternalErrorForNode` helper and
`allErrorExecutionIds` computed
- `ErrorOverlay.vue`: integration with `executionStore` overlay state
and `useErrorGroups`

## Screenshots
<img width="853" height="461" alt="image"
src="https://github.com/user-attachments/assets/a49ab620-4209-4ae7-b547-fba13da0c633"
/>
<img width="854" height="203" alt="image"
src="https://github.com/user-attachments/assets/c119da54-cd78-4e7a-8b7a-456cfd348f1d"
/>
<img width="497" height="361" alt="image"
src="https://github.com/user-attachments/assets/74b16161-cf45-454b-ae60-24922fe36931"
/>

---------

Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: github-actions <github-actions@github.com>
2026-02-20 12:14:52 -08:00

191 lines
5.9 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import {
isCloudValidationError,
tryExtractValidationError,
classifyCloudValidationError
} from '@/utils/executionErrorUtil'
describe('executionErrorUtil', () => {
describe('isCloudValidationError', () => {
it('should return true when object has error field', () => {
expect(isCloudValidationError({ error: 'some error' })).toBe(true)
})
it('should return true when object has node_errors field', () => {
expect(isCloudValidationError({ node_errors: {} })).toBe(true)
})
it('should return true when object has both fields', () => {
expect(isCloudValidationError({ error: 'err', node_errors: {} })).toBe(
true
)
})
it('should return false for null', () => {
expect(isCloudValidationError(null)).toBe(false)
})
it('should return false for non-object', () => {
expect(isCloudValidationError('string')).toBe(false)
})
it('should return false for object without error or node_errors', () => {
expect(isCloudValidationError({ foo: 'bar' })).toBe(false)
})
})
describe('tryExtractValidationError', () => {
it('should extract JSON from a message with embedded validation error', () => {
const embedded = JSON.stringify({
error: {
type: 'prompt_no_outputs',
message: 'No outputs',
details: ''
},
node_errors: {}
})
const message = `Failed to send prompt request: status 400: ${embedded}`
const result = tryExtractValidationError(message)
expect(result).not.toBeNull()
expect(result?.error).toEqual({
type: 'prompt_no_outputs',
message: 'No outputs',
details: ''
})
})
it('should return null when message has no JSON', () => {
expect(tryExtractValidationError('plain error message')).toBeNull()
})
it('should return null when JSON is not a validation error shape', () => {
const message = 'error: {"foo": "bar"}'
expect(tryExtractValidationError(message)).toBeNull()
})
it('should return null when JSON is malformed', () => {
const message = 'error: {invalid json'
expect(tryExtractValidationError(message)).toBeNull()
})
})
describe('classifyCloudValidationError', () => {
it('should classify node errors when node_errors is present', () => {
const nodeErrors = {
'11:1': {
errors: [
{
type: 'required_input_missing',
message: 'Required input is missing',
details: 'clip',
extra_info: { input_name: 'clip' }
}
],
dependent_outputs: ['9'],
class_type: 'CLIPTextEncode'
}
}
const embedded = JSON.stringify({
error: {
type: 'prompt_outputs_failed_validation',
message: 'Prompt outputs failed validation',
details: ''
},
node_errors: nodeErrors
})
const message = `Failed to send prompt request: status 400: ${embedded}`
const result = classifyCloudValidationError(message)
expect(result).not.toBeNull()
expect(result?.kind).toBe('nodeErrors')
if (result?.kind === 'nodeErrors') {
expect(result.nodeErrors['11:1'].class_type).toBe('CLIPTextEncode')
}
})
it('should classify prompt error when error is an object and no node_errors', () => {
const embedded = JSON.stringify({
error: {
type: 'prompt_no_outputs',
message: 'Prompt has no outputs',
details: ''
}
})
const message = `Failed: ${embedded}`
const result = classifyCloudValidationError(message)
expect(result).not.toBeNull()
expect(result?.kind).toBe('promptError')
if (result?.kind === 'promptError') {
expect(result.promptError.type).toBe('prompt_no_outputs')
expect(result.promptError.message).toBe('Prompt has no outputs')
}
})
it('should classify prompt error when error is a string', () => {
const embedded = JSON.stringify({ error: 'Something went wrong' })
const message = `Failed: ${embedded}`
const result = classifyCloudValidationError(message)
expect(result).not.toBeNull()
expect(result?.kind).toBe('promptError')
if (result?.kind === 'promptError') {
expect(result.promptError.type).toBe('error')
expect(result.promptError.message).toBe('Something went wrong')
}
})
it('should return null when message has no embedded JSON', () => {
expect(classifyCloudValidationError('plain error')).toBeNull()
})
it('should return null when embedded JSON has no error or node_errors', () => {
const message = 'error: {"foo": "bar"}'
expect(classifyCloudValidationError(message)).toBeNull()
})
it('should return null when error field is neither object nor string', () => {
const embedded = JSON.stringify({ error: 123 })
const message = `Failed: ${embedded}`
expect(classifyCloudValidationError(message)).toBeNull()
})
it('should prefer node_errors over error when both present', () => {
const embedded = JSON.stringify({
error: { type: 'validation', message: 'fail', details: '' },
node_errors: {
'5': {
errors: [{ type: 'err', message: 'bad', details: '' }],
dependent_outputs: [],
class_type: 'KSampler'
}
}
})
const message = `Failed: ${embedded}`
const result = classifyCloudValidationError(message)
expect(result?.kind).toBe('nodeErrors')
})
it('should treat empty node_errors as prompt error', () => {
const embedded = JSON.stringify({
error: { type: 'no_prompt', message: 'No prompt', details: '' },
node_errors: {}
})
const message = `Failed: ${embedded}`
const result = classifyCloudValidationError(message)
expect(result?.kind).toBe('promptError')
})
})
})