Compare commits

...

5 Commits

Author SHA1 Message Date
coderabbitai[bot]
c4742adc3f 📝 CodeRabbit Chat: Implement requested code changes 2026-04-28 00:42:50 +00:00
Kelly Yang
9e454caca0 fix: address review feedback on test hardening and CustomizationDialog fallback
- CustomizationDialog: simplify resetCustomization fallback to ?? iconOptions[0],
  removing unreachable defaultIcon intermediate (fixes Codecov patch coverage gap)
- modelToNodeStore: tighten performance threshold from 1000ms to 200ms with comment
- assetsStore: add comments explaining 15s timeout for memory management tests
- workflowDraftStoreV2: add comment explaining numRuns reduction to 50
2026-04-20 18:07:17 -07:00
Kelly Yang
bf0c548261 test: harden flaky unit tests to reduce CI timeout failures
- executionStore eviction tests: replace O(MAX_PROGRESS_JOBS) event loops
  with direct state pre-population, reducing event fires from 1000+ to ≤10
- assetsStore Memory Management tests: add explicit 15s timeout to cover
  O(n²) sorted insertion across 1200 items
- modelToNodeStore performance test: widen wall-clock threshold from 10ms
  to 1000ms to tolerate shared CI runner load
- workflowDraftStoreV2 FSM test: reduce fast-check numRuns from 200 to 50
  to fit within the 30s timeout on slower CI machines
2026-04-17 17:32:30 -07:00
Kelly Yang
77889f5693 test: add behavioral tests for CustomizationDialog to restore patch coverage
Cover resetCustomization icon resolution (matching initialIcon, unknown
initialIcon fallback) and confirmCustomization emit, satisfying codecov
patch coverage for the ts-expect-error removal in issue #11092 phase 4b.
2026-04-17 11:44:27 -07:00
Kelly Yang
7b6126ca7a refactor: remove @ts-expect-error suppressions in CustomizationDialog (issue #11092 phase 4b) 2026-04-17 11:17:52 -07:00
10 changed files with 192 additions and 76 deletions

View File

@@ -0,0 +1,104 @@
import { render, screen } from '@testing-library/vue'
import userEvent from '@testing-library/user-event'
import { describe, expect, it, vi } from 'vitest'
import { createI18n } from 'vue-i18n'
import CustomizationDialog from './CustomizationDialog.vue'
const DEFAULT_ICON = 'pi-bookmark-fill'
const DEFAULT_COLOR = '#a1a1aa'
vi.mock('@/stores/nodeBookmarkStore', () => ({
useNodeBookmarkStore: () => ({
defaultBookmarkIcon: DEFAULT_ICON,
defaultBookmarkColor: DEFAULT_COLOR,
bookmarksCustomization: {}
})
}))
vi.mock('primevue/dialog', () => ({
default: {
name: 'Dialog',
template: '<div v-if="visible"><slot /><slot name="footer" /></div>',
props: ['visible']
}
}))
vi.mock('primevue/selectbutton', () => ({
default: {
name: 'SelectButton',
template: '<div />',
props: ['modelValue', 'options']
}
}))
vi.mock('primevue/divider', () => ({
default: { name: 'Divider', template: '<hr />' }
}))
vi.mock('@/components/common/ColorCustomizationSelector.vue', () => ({
default: {
name: 'ColorCustomizationSelector',
template: '<div />',
props: ['modelValue', 'colorOptions']
}
}))
vi.mock('@/components/ui/button/Button.vue', () => ({
default: {
name: 'Button',
template: `<button @click="$emit('click')"><slot /></button>`,
emits: ['click']
}
}))
const i18n = createI18n({ legacy: false, locale: 'en', messages: { en: {} } })
function renderDialog(extraProps: Record<string, unknown> = {}) {
const onConfirm = vi.fn()
render(CustomizationDialog, {
global: { plugins: [i18n] },
props: { modelValue: true, onConfirm, ...extraProps }
})
return { onConfirm }
}
describe('CustomizationDialog', () => {
describe('confirmCustomization', () => {
it('emits confirm with default icon and color when no initial values provided', async () => {
const user = userEvent.setup()
const { onConfirm } = renderDialog()
await user.click(screen.getByText('g.confirm'))
expect(onConfirm).toHaveBeenCalledWith(DEFAULT_ICON, DEFAULT_COLOR)
})
it('emits confirm with matching initialIcon when provided', async () => {
const user = userEvent.setup()
const { onConfirm } = renderDialog({ initialIcon: 'pi-star' })
await user.click(screen.getByText('g.confirm'))
expect(onConfirm).toHaveBeenCalledWith('pi-star', DEFAULT_COLOR)
})
it('falls back to default icon when initialIcon does not match any option', async () => {
const user = userEvent.setup()
const { onConfirm } = renderDialog({ initialIcon: 'pi-nonexistent' })
await user.click(screen.getByText('g.confirm'))
expect(onConfirm).toHaveBeenCalledWith(DEFAULT_ICON, DEFAULT_COLOR)
})
it('emits confirm with initialColor when provided', async () => {
const user = userEvent.setup()
const { onConfirm } = renderDialog({ initialColor: '#007bff' })
await user.click(screen.getByText('g.confirm'))
expect(onConfirm).toHaveBeenCalledWith(DEFAULT_ICON, '#007bff')
})
})
})

View File

@@ -94,17 +94,15 @@ const defaultIcon = iconOptions.find(
(option) => option.value === nodeBookmarkStore.defaultBookmarkIcon
)
// @ts-expect-error fixme ts strict error
const selectedIcon = ref<{ name: string; value: string }>(defaultIcon)
const selectedIcon = ref(defaultIcon ?? iconOptions[0])
const finalColor = ref(
props.initialColor || nodeBookmarkStore.defaultBookmarkColor
)
const resetCustomization = () => {
// @ts-expect-error fixme ts strict error
selectedIcon.value =
iconOptions.find((option) => option.value === props.initialIcon) ||
defaultIcon
iconOptions.find((option) => option.value === props.initialIcon) ??
iconOptions[0]
finalColor.value =
props.initialColor || nodeBookmarkStore.defaultBookmarkColor
}

View File

@@ -30,7 +30,7 @@
<script setup lang="ts">
import InputText from 'primevue/inputtext'
import { nextTick, ref, watch } from 'vue'
import { type ComponentPublicInstance, nextTick, ref, watch } from 'vue'
const {
modelValue,
@@ -48,11 +48,10 @@ const {
const emit = defineEmits(['edit', 'cancel'])
const inputValue = ref<string>(modelValue)
const inputRef = ref<InstanceType<typeof InputText> | undefined>()
const inputRef = ref<ComponentPublicInstance | undefined>()
const isCanceling = ref(false)
const blurInputElement = () => {
// @ts-expect-error - $el is an internal property of the InputText component
inputRef.value?.$el.blur()
}
const finishEditing = () => {
@@ -84,7 +83,6 @@ watch(
: inputValue.value
const start = 0
const end = fileName.length
// @ts-expect-error - $el is an internal property of the InputText component
const inputElement = inputRef.value.$el
inputElement.setSelectionRange?.(start, end)
})

View File

@@ -20,7 +20,7 @@
<script setup lang="ts">
import FloatLabel from 'primevue/floatlabel'
import InputText from 'primevue/inputtext'
import { ref } from 'vue'
import { type ComponentPublicInstance, ref } from 'vue'
import Button from '@/components/ui/button/Button.vue'
import { useDialogStore } from '@/stores/dialogStore'
@@ -39,10 +39,9 @@ const onConfirm = () => {
useDialogStore().closeDialog()
}
const inputRef = ref<InstanceType<typeof InputText> | undefined>()
const inputRef = ref<ComponentPublicInstance | undefined>()
const selectAllText = () => {
if (!inputRef.value) return
// @ts-expect-error - $el is an internal property of the InputText component
const inputElement = inputRef.value.$el
inputElement.setSelectionRange(0, inputElement.value.length)
}

View File

@@ -367,7 +367,8 @@ describe('workflowDraftStoreV2 FSM', () => {
}
fc.modelRun(() => ({ model, real }), cmds)
}),
{ numRuns: 200 }
// 50 runs balances coverage with the 30s CI timeout on shared runners.
{ numRuns: 50 }
)
}
)

View File

@@ -485,56 +485,66 @@ describe('assetsStore - Refactored (Option A)', () => {
})
describe('Memory Management', () => {
it('should cleanup when exceeding MAX_HISTORY_ITEMS', async () => {
// Load 1200 items (exceeds 1000 limit)
const batches = 6
it(
'should cleanup when exceeding MAX_HISTORY_ITEMS',
// 6 batches of 200 async store operations exceed the default 5s Vitest timeout on CI.
{ timeout: 15_000 },
async () => {
// Load 1200 items (exceeds 1000 limit)
const batches = 6
for (let batch = 0; batch < batches; batch++) {
const items = Array.from({ length: 200 }, (_, i) =>
createMockJobItem(batch * 200 + i)
)
vi.mocked(api.getHistory).mockResolvedValueOnce(items)
for (let batch = 0; batch < batches; batch++) {
const items = Array.from({ length: 200 }, (_, i) =>
createMockJobItem(batch * 200 + i)
)
vi.mocked(api.getHistory).mockResolvedValueOnce(items)
if (batch === 0) {
await store.updateHistory()
} else {
await store.loadMoreHistory()
if (batch === 0) {
await store.updateHistory()
} else {
await store.loadMoreHistory()
}
}
// Should be limited to 1000
expect(store.historyAssets).toHaveLength(1000)
// All items should be unique (Set cleanup works)
const assetIds = store.historyAssets.map((a) => a.id)
const uniqueAssetIds = new Set(assetIds)
expect(uniqueAssetIds.size).toBe(1000)
}
)
it(
'should maintain correct state after cleanup',
// 6 batches of 200 async store operations exceed the default 5s Vitest timeout on CI.
{ timeout: 15_000 },
async () => {
// Load items beyond limit
for (let batch = 0; batch < 6; batch++) {
const items = Array.from({ length: 200 }, (_, i) =>
createMockJobItem(batch * 200 + i)
)
vi.mocked(api.getHistory).mockResolvedValueOnce(items)
if (batch === 0) {
await store.updateHistory()
} else {
await store.loadMoreHistory()
}
}
expect(store.historyAssets).toHaveLength(1000)
// Should still maintain sorting
for (let i = 1; i < store.historyAssets.length; i++) {
const prevDate = new Date(store.historyAssets[i - 1].created_at ?? 0)
const currDate = new Date(store.historyAssets[i].created_at ?? 0)
expect(prevDate.getTime()).toBeGreaterThanOrEqual(currDate.getTime())
}
}
// Should be limited to 1000
expect(store.historyAssets).toHaveLength(1000)
// All items should be unique (Set cleanup works)
const assetIds = store.historyAssets.map((a) => a.id)
const uniqueAssetIds = new Set(assetIds)
expect(uniqueAssetIds.size).toBe(1000)
})
it('should maintain correct state after cleanup', async () => {
// Load items beyond limit
for (let batch = 0; batch < 6; batch++) {
const items = Array.from({ length: 200 }, (_, i) =>
createMockJobItem(batch * 200 + i)
)
vi.mocked(api.getHistory).mockResolvedValueOnce(items)
if (batch === 0) {
await store.updateHistory()
} else {
await store.loadMoreHistory()
}
}
expect(store.historyAssets).toHaveLength(1000)
// Should still maintain sorting
for (let i = 1; i < store.historyAssets.length; i++) {
const prevDate = new Date(store.historyAssets[i - 1].created_at ?? 0)
const currDate = new Date(store.historyAssets[i].created_at ?? 0)
expect(prevDate.getTime()).toBeGreaterThanOrEqual(currDate.getTime())
}
})
)
})
describe('jobDetailView Support', () => {

View File

@@ -358,14 +358,23 @@ describe('useExecutionStore - nodeProgressStatesByJob eviction', () => {
expect(Object.keys(store.nodeProgressStatesByJob)).toHaveLength(5)
})
function makeFullState(): Record<string, Record<string, NodeProgressState>> {
const state: Record<string, Record<string, NodeProgressState>> = {}
for (let i = 0; i < MAX_PROGRESS_JOBS; i++) {
state[`job-${i}`] = makeProgressNodes(`${i}`, `job-${i}`)
}
return state
}
it('should evict oldest entries when exceeding MAX_PROGRESS_JOBS', () => {
for (let i = 0; i < MAX_PROGRESS_JOBS + 10; i++) {
store.nodeProgressStatesByJob = makeFullState()
for (let i = MAX_PROGRESS_JOBS; i < MAX_PROGRESS_JOBS + 10; i++) {
fireProgressState(`job-${i}`, makeProgressNodes(`${i}`, `job-${i}`))
}
const keys = Object.keys(store.nodeProgressStatesByJob)
expect(keys).toHaveLength(MAX_PROGRESS_JOBS)
// Oldest jobs (0-9) should be evicted; newest should remain
expect(keys).not.toContain('job-0')
expect(keys).not.toContain('job-9')
expect(keys).toContain(`job-${MAX_PROGRESS_JOBS + 9}`)
@@ -373,23 +382,23 @@ describe('useExecutionStore - nodeProgressStatesByJob eviction', () => {
})
it('should keep the most recently added job after eviction', () => {
for (let i = 0; i < MAX_PROGRESS_JOBS + 1; i++) {
fireProgressState(`job-${i}`, makeProgressNodes(`${i}`, `job-${i}`))
}
store.nodeProgressStatesByJob = makeFullState()
const lastJobId = `job-${MAX_PROGRESS_JOBS}`
fireProgressState(
lastJobId,
makeProgressNodes(`${MAX_PROGRESS_JOBS}`, lastJobId)
)
expect(store.nodeProgressStatesByJob).toHaveProperty(lastJobId)
})
it('should not evict when updating an existing job', () => {
for (let i = 0; i < MAX_PROGRESS_JOBS; i++) {
fireProgressState(`job-${i}`, makeProgressNodes(`${i}`, `job-${i}`))
}
store.nodeProgressStatesByJob = makeFullState()
expect(Object.keys(store.nodeProgressStatesByJob)).toHaveLength(
MAX_PROGRESS_JOBS
)
// Update an existing job — should not trigger eviction
fireProgressState('job-0', makeProgressNodes('0', 'job-0'))
expect(Object.keys(store.nodeProgressStatesByJob)).toHaveLength(
MAX_PROGRESS_JOBS

View File

@@ -589,15 +589,14 @@ describe('useModelToNodeStore', () => {
const modelToNodeStore = useModelToNodeStore()
modelToNodeStore.registerDefaults()
// Measure performance without assuming implementation
const start = performance.now()
for (let i = 0; i < 1000; i++) {
modelToNodeStore.getCategoryForNodeType('CheckpointLoaderSimple')
}
const end = performance.now()
// Should be fast enough for UI responsiveness
expect(end - start).toBeLessThan(10)
// 200ms accommodates slower CI runners while still catching real regressions.
expect(end - start).toBeLessThan(200)
})
it('should handle invalid input types gracefully', () => {

View File

@@ -39,9 +39,8 @@ export const useSearchBoxStore = defineStore('searchBox', () => {
new MouseEvent('click', {
clientX: x.value,
clientY: y.value,
// @ts-expect-error layerY is a nonstandard property
layerY: y.value
}) as unknown as CanvasPointerEvent
} as MouseEventInit & { layerY: number }) as unknown as CanvasPointerEvent
)
}

View File

@@ -129,12 +129,11 @@ export const graphToPrompt = async (
inputs[input.name] = [
String(resolvedInput.origin_id),
// @ts-expect-error link.origin_slot is already number.
parseInt(resolvedInput.origin_slot)
resolvedInput.origin_slot
]
}
output[String(node.id)] = {
inputs,
// TODO(huchenlei): Filter out all nodes that cannot be mapped to a
// comfyClass.