mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-24 06:35:10 +00:00
## Summary Migrate 132 test files from `@vue/test-utils` (VTU) to `@testing-library/vue` (VTL) with `@testing-library/user-event`, adopting user-centric behavioral testing patterns across the codebase. ## Changes - **What**: Systematic migration of component/unit tests from VTU's `mount`/`wrapper` API to VTL's `render`/`screen`/`userEvent` API across 132 files in `src/` - **Breaking**: None — test-only changes, no production code affected ### Migration breakdown | Batch | Files | Description | |-------|-------|-------------| | 1 | 19 | Simple render/assert tests | | 2A | 16 | Interactive tests with user events | | 2B-1 | 14 | Interactive tests (continued) | | 2B-2 | 32 | Interactive tests (continued) | | 3A–3E | 51 | Complex tests (stores, composables, heavy mocking) | | Lint fix | 7 | `await` on `fireEvent` calls for `no-floating-promises` | | Review fixes | 15 | Address CodeRabbit feedback (3 rounds) | ### Review feedback addressed - Removed class-based assertions (`text-ellipsis`, `pr-3`, `.pi-save`, `.skeleton`, `.bg-black\/15`, Tailwind utilities) in favor of behavioral/accessible queries - Added null guards before `querySelector` casts - Added `expect(roots).toHaveLength(N)` guards before indexed NodeList access - Wrapped fake timer tests in `try/finally` for guaranteed cleanup - Split double-render tests into focused single-render tests - Replaced CSS class selectors with `screen.getByText`/`screen.getByRole` queries - Updated stubs to use semantic `role`/`aria-label` instead of CSS classes - Consolidated redundant edge-case tests - Removed manual `document.body.appendChild` in favor of VTL container management - Used distinct mock return values to verify command wiring ### VTU holdouts (2 files) These files intentionally retain `@vue/test-utils` because their components use `<script setup>` without `defineExpose`, making internal computed properties and methods inaccessible via VTL: 1. **`NodeWidgets.test.ts`** — partial VTU for `vm.processedWidgets` 2. **`WidgetSelectDropdown.test.ts`** — full VTU for heavy `wrapper.vm.*` access ## Follow-up Deferred items (`ComponentProps` typing, camelCase listener props) tracked in #10966. ## Review Focus - Test correctness: all migrated tests preserve original behavioral coverage - VTL idioms: proper use of `screen` queries, `userEvent`, and accessibility-based selectors - The 2 VTU holdout files are intentional, not oversights ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-10965-test-migrate-132-test-files-from-vue-test-utils-to-testing-library-vue-33c6d73d36508199a6a7e513cf5d8296) by [Unito](https://www.unito.io) --------- Co-authored-by: Amp <amp@ampcode.com> Co-authored-by: Christian Byrne <cbyrne@comfy.org>
211 lines
5.9 KiB
TypeScript
211 lines
5.9 KiB
TypeScript
import { createTestingPinia } from '@pinia/testing'
|
|
import { fromPartial } from '@total-typescript/shoehorn'
|
|
import { render } from '@testing-library/vue'
|
|
import { setActivePinia } from 'pinia'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import DomWidgets from '@/components/graph/DomWidgets.vue'
|
|
import { Rectangle } from '@/lib/litegraph/src/infrastructure/Rectangle'
|
|
import { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
|
import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas'
|
|
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
|
import type { BaseDOMWidget } from '@/scripts/domWidget'
|
|
import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
|
|
|
type TestWidget = BaseDOMWidget<object | string>
|
|
|
|
function createNode(
|
|
graph: LGraph,
|
|
id: number,
|
|
title: string,
|
|
pos: [number, number]
|
|
) {
|
|
const node = new LGraphNode(title)
|
|
node.id = id
|
|
node.pos = [...pos]
|
|
node.size = [240, 120]
|
|
graph.add(node)
|
|
return node
|
|
}
|
|
|
|
function createWidget(id: string, node: LGraphNode, y = 12): TestWidget {
|
|
return fromPartial<TestWidget>({
|
|
id,
|
|
node,
|
|
name: 'test_widget',
|
|
type: 'custom',
|
|
value: '',
|
|
options: {},
|
|
y,
|
|
width: 120,
|
|
computedHeight: 40,
|
|
margin: 10,
|
|
isVisible: () => true
|
|
})
|
|
}
|
|
|
|
function createCanvas(graph: LGraph): LGraphCanvas {
|
|
return fromPartial<LGraphCanvas>({
|
|
graph,
|
|
low_quality: false,
|
|
read_only: false,
|
|
isNodeVisible: vi.fn(() => true)
|
|
})
|
|
}
|
|
|
|
function drawFrame(canvas: LGraphCanvas) {
|
|
canvas.onDrawForeground?.({} as CanvasRenderingContext2D, new Rectangle())
|
|
}
|
|
|
|
describe('DomWidgets transition grace characterization', () => {
|
|
beforeEach(() => {
|
|
setActivePinia(createTestingPinia({ stubActions: false }))
|
|
})
|
|
|
|
it('applies transition grace for exactly one frame when override exists but is not active', () => {
|
|
const canvasStore = useCanvasStore()
|
|
const domWidgetStore = useDomWidgetStore()
|
|
|
|
const graphA = new LGraph()
|
|
const graphB = new LGraph()
|
|
const interiorNode = createNode(graphA, 1, 'interior', [100, 200])
|
|
const overrideNode = createNode(graphB, 2, 'override', [600, 700])
|
|
|
|
const widget = createWidget('widget-transition', interiorNode, 14)
|
|
const overrideWidget = createWidget('override-widget', overrideNode, 22)
|
|
|
|
domWidgetStore.registerWidget(widget)
|
|
domWidgetStore.setPositionOverride(widget.id, {
|
|
node: overrideNode,
|
|
widget: overrideWidget
|
|
})
|
|
domWidgetStore.deactivateWidget(widget.id)
|
|
|
|
const widgetState = domWidgetStore.widgetStates.get(widget.id)
|
|
if (!widgetState) throw new Error('Widget state not registered')
|
|
widgetState.visible = true
|
|
widgetState.pos = [321, 654]
|
|
|
|
const canvas = createCanvas(graphA)
|
|
canvasStore.canvas = canvas
|
|
|
|
render(DomWidgets, {
|
|
global: {
|
|
stubs: {
|
|
DomWidget: true
|
|
}
|
|
}
|
|
})
|
|
|
|
drawFrame(canvas)
|
|
expect(widgetState.visible).toBe(true)
|
|
expect(widgetState.pos).toEqual([321, 654])
|
|
|
|
drawFrame(canvas)
|
|
expect(widgetState.visible).toBe(false)
|
|
})
|
|
|
|
it('uses override positioning while override node is in current graph even when widget is inactive', () => {
|
|
const canvasStore = useCanvasStore()
|
|
const domWidgetStore = useDomWidgetStore()
|
|
|
|
const graphA = new LGraph()
|
|
const graphB = new LGraph()
|
|
const interiorNode = createNode(graphA, 1, 'interior', [10, 20])
|
|
const overrideNode = createNode(graphB, 2, 'override', [300, 400])
|
|
|
|
const widget = createWidget('widget-override-active', interiorNode, 8)
|
|
const overrideWidget = createWidget(
|
|
'override-position-source',
|
|
overrideNode,
|
|
18
|
|
)
|
|
|
|
domWidgetStore.registerWidget(widget)
|
|
domWidgetStore.setPositionOverride(widget.id, {
|
|
node: overrideNode,
|
|
widget: overrideWidget
|
|
})
|
|
domWidgetStore.deactivateWidget(widget.id)
|
|
|
|
const widgetState = domWidgetStore.widgetStates.get(widget.id)
|
|
if (!widgetState) throw new Error('Widget state not registered')
|
|
|
|
const canvas = createCanvas(graphB)
|
|
canvasStore.canvas = canvas
|
|
|
|
render(DomWidgets, {
|
|
global: {
|
|
stubs: {
|
|
DomWidget: true
|
|
}
|
|
}
|
|
})
|
|
|
|
drawFrame(canvas)
|
|
|
|
expect(widgetState.visible).toBe(true)
|
|
expect(widgetState.pos).toEqual([310, 428])
|
|
})
|
|
|
|
it('cleans orphaned transition-grace ids after widget removal', () => {
|
|
const canvasStore = useCanvasStore()
|
|
const domWidgetStore = useDomWidgetStore()
|
|
|
|
const graphA = new LGraph()
|
|
const graphB = new LGraph()
|
|
const interiorNode = createNode(graphA, 1, 'interior', [0, 0])
|
|
const overrideNode = createNode(graphB, 2, 'override', [200, 200])
|
|
|
|
const canvas = createCanvas(graphA)
|
|
canvasStore.canvas = canvas
|
|
|
|
render(DomWidgets, {
|
|
global: {
|
|
stubs: {
|
|
DomWidget: true
|
|
}
|
|
}
|
|
})
|
|
|
|
const oldWidget = createWidget('shared-widget-id', interiorNode, 10)
|
|
const overrideWidget = createWidget(
|
|
'shared-override-widget',
|
|
overrideNode,
|
|
14
|
|
)
|
|
|
|
domWidgetStore.registerWidget(oldWidget)
|
|
domWidgetStore.setPositionOverride(oldWidget.id, {
|
|
node: overrideNode,
|
|
widget: overrideWidget
|
|
})
|
|
domWidgetStore.deactivateWidget(oldWidget.id)
|
|
|
|
drawFrame(canvas)
|
|
domWidgetStore.unregisterWidget(oldWidget.id)
|
|
|
|
drawFrame(canvas)
|
|
|
|
const replacementWidget = createWidget('shared-widget-id', interiorNode, 10)
|
|
domWidgetStore.registerWidget(replacementWidget)
|
|
domWidgetStore.setPositionOverride(replacementWidget.id, {
|
|
node: overrideNode,
|
|
widget: overrideWidget
|
|
})
|
|
domWidgetStore.deactivateWidget(replacementWidget.id)
|
|
|
|
const replacementState = domWidgetStore.widgetStates.get(
|
|
replacementWidget.id
|
|
)
|
|
if (!replacementState) throw new Error('Replacement widget missing state')
|
|
replacementState.visible = true
|
|
replacementState.pos = [999, 999]
|
|
|
|
drawFrame(canvas)
|
|
|
|
expect(replacementState.visible).toBe(true)
|
|
expect(replacementState.pos).toEqual([999, 999])
|
|
})
|
|
})
|