mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-05 13:41:59 +00:00
## Summary Extract internal logic from the 2 remaining VTU holdout components into composables, enabling full VTL migration. ## Changes - **What**: Extract `useProcessedWidgets` from `NodeWidgets.vue` (486→135 LOC) and `useWidgetSelectItems`/`useWidgetSelectActions` from `WidgetSelectDropdown.vue` (563→170 LOC). Rewrite both component test files as composable unit tests + slim behavioral VTL tests. Remove `@vue/test-utils` devDependency. - **Dependencies**: Removes `@vue/test-utils` ## Review Focus - Composable extraction is mechanical — no logic changes, just moving code into testable units - `useProcessedWidgets` handles widget deduplication, promotion border styling, error detection, and identity resolution (~290 LOC) - `useWidgetSelectItems` handles the full computed chain from widget values → dropdown items including cloud asset mode and multi-output job resolution (~350 LOC) - `useWidgetSelectActions` handles selection resolution and file upload (~120 LOC) - 40 new composable-level unit tests replace 13 `wrapper.vm.*` accesses across the 2 holdout files ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-10966-refactor-extract-composables-from-VTU-holdout-components-complete-VTL-migration-33c6d73d36508148a3a4ccf346722d6d) by [Unito](https://www.unito.io) --------- Co-authored-by: Amp <amp@ampcode.com>
669 lines
19 KiB
TypeScript
669 lines
19 KiB
TypeScript
import { createTestingPinia } from '@pinia/testing'
|
|
import { setActivePinia } from 'pinia'
|
|
import { computed, nextTick, ref } from 'vue'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
|
import { useWidgetSelectItems } from '@/renderer/extensions/vueNodes/widgets/composables/useWidgetSelectItems'
|
|
|
|
const mockAssetsData = vi.hoisted(() => ({ items: [] as AssetItem[] }))
|
|
|
|
vi.mock(
|
|
'@/renderer/extensions/vueNodes/widgets/composables/useAssetWidgetData',
|
|
() => ({
|
|
useAssetWidgetData: () => ({
|
|
category: computed(() => 'checkpoints'),
|
|
assets: computed(() => mockAssetsData.items),
|
|
isLoading: computed(() => false),
|
|
error: computed(() => null)
|
|
})
|
|
})
|
|
)
|
|
|
|
const mockResolveOutputAssetItems = vi.fn()
|
|
|
|
function createMockMediaAssets() {
|
|
return {
|
|
media: ref<AssetItem[]>([]),
|
|
loading: ref(false),
|
|
error: ref(null),
|
|
fetchMediaList: vi.fn().mockResolvedValue([]),
|
|
refresh: vi.fn().mockResolvedValue([]),
|
|
loadMore: vi.fn(),
|
|
hasMore: ref(false),
|
|
isLoadingMore: ref(false)
|
|
}
|
|
}
|
|
|
|
let mockMediaAssets = createMockMediaAssets()
|
|
|
|
vi.mock('@/platform/assets/composables/media/useMediaAssets', () => ({
|
|
useMediaAssets: () => mockMediaAssets
|
|
}))
|
|
|
|
vi.mock('@/platform/assets/composables/useAssetFilterOptions', () => ({
|
|
useAssetFilterOptions: () => ({
|
|
ownershipOptions: computed(() => []),
|
|
availableBaseModels: computed(() => []),
|
|
availableFileFormats: computed(() => [])
|
|
})
|
|
}))
|
|
|
|
vi.mock('@/platform/assets/utils/outputAssetUtil', () => ({
|
|
resolveOutputAssetItems: (...args: unknown[]) =>
|
|
mockResolveOutputAssetItems(...args)
|
|
}))
|
|
|
|
function createDefaultOptions(
|
|
overrides: Partial<Parameters<typeof useWidgetSelectItems>[0]> = {}
|
|
) {
|
|
return {
|
|
values: () => ['img_001.png', 'photo_abc.jpg', 'hash789.png'],
|
|
getOptionLabel: () =>
|
|
undefined as ((value?: string | null) => string) | undefined,
|
|
modelValue: ref<string | undefined>('img_001.png'),
|
|
assetKind: () => 'image' as const,
|
|
outputMediaAssets: mockMediaAssets,
|
|
assetData: null,
|
|
isAssetMode: () => false,
|
|
...overrides
|
|
}
|
|
}
|
|
|
|
describe('display label behavior', () => {
|
|
beforeEach(() => {
|
|
setActivePinia(createTestingPinia({ stubActions: false }))
|
|
})
|
|
|
|
it('uses values as labels when no label function provided', () => {
|
|
const { dropdownItems } = useWidgetSelectItems(createDefaultOptions())
|
|
expect(dropdownItems.value[0]).toMatchObject({
|
|
name: 'img_001.png',
|
|
label: 'img_001.png'
|
|
})
|
|
})
|
|
|
|
it('applies custom label function', () => {
|
|
const getOptionLabel = (v?: string | null) => `Custom: ${v}`
|
|
const { dropdownItems } = useWidgetSelectItems(
|
|
createDefaultOptions({ getOptionLabel: () => getOptionLabel })
|
|
)
|
|
expect(dropdownItems.value[0].label).toBe('Custom: img_001.png')
|
|
})
|
|
|
|
it('falls back to value on label function error', () => {
|
|
const consoleWarnSpy = vi
|
|
.spyOn(console, 'warn')
|
|
.mockImplementation(() => {})
|
|
const getOptionLabel = (v?: string | null) => {
|
|
if (v === 'photo_abc.jpg') throw new Error('fail')
|
|
return `Labeled: ${v}`
|
|
}
|
|
const { dropdownItems } = useWidgetSelectItems(
|
|
createDefaultOptions({ getOptionLabel: () => getOptionLabel })
|
|
)
|
|
expect(dropdownItems.value[0].label).toBe('Labeled: img_001.png')
|
|
expect(dropdownItems.value[1].label).toBe('photo_abc.jpg')
|
|
expect(dropdownItems.value[2].label).toBe('Labeled: hash789.png')
|
|
expect(consoleWarnSpy).toHaveBeenCalled()
|
|
consoleWarnSpy.mockRestore()
|
|
})
|
|
|
|
it('falls back to value when label function returns empty string', () => {
|
|
const getOptionLabel = (v?: string | null) => {
|
|
if (v === 'photo_abc.jpg') return ''
|
|
return `Labeled: ${v}`
|
|
}
|
|
const { dropdownItems } = useWidgetSelectItems(
|
|
createDefaultOptions({ getOptionLabel: () => getOptionLabel })
|
|
)
|
|
expect(dropdownItems.value[1].label).toBe('photo_abc.jpg')
|
|
})
|
|
|
|
it('falls back to value when label function returns undefined', () => {
|
|
const getOptionLabel = (v?: string | null) => {
|
|
if (v === 'hash789.png') return undefined as unknown as string
|
|
return `Labeled: ${v}`
|
|
}
|
|
const { dropdownItems } = useWidgetSelectItems(
|
|
createDefaultOptions({ getOptionLabel: () => getOptionLabel })
|
|
)
|
|
expect(dropdownItems.value[2].label).toBe('hash789.png')
|
|
})
|
|
})
|
|
|
|
describe('useWidgetSelectItems', () => {
|
|
beforeEach(() => {
|
|
setActivePinia(createTestingPinia({ stubActions: false }))
|
|
mockMediaAssets = createMockMediaAssets()
|
|
mockResolveOutputAssetItems.mockReset()
|
|
mockAssetsData.items = []
|
|
})
|
|
|
|
describe('dropdownItems', () => {
|
|
it('maps values to items with names as labels', () => {
|
|
const { dropdownItems } = useWidgetSelectItems(createDefaultOptions())
|
|
expect(dropdownItems.value).toHaveLength(3)
|
|
expect(dropdownItems.value[0]).toMatchObject({
|
|
name: 'img_001.png',
|
|
label: 'img_001.png'
|
|
})
|
|
})
|
|
|
|
it('returns empty when values is undefined and no modelValue', () => {
|
|
const { dropdownItems } = useWidgetSelectItems(
|
|
createDefaultOptions({
|
|
values: () => undefined,
|
|
modelValue: ref(undefined)
|
|
})
|
|
)
|
|
expect(dropdownItems.value).toHaveLength(0)
|
|
})
|
|
})
|
|
|
|
describe('missing value handling', () => {
|
|
it('creates fallback item when modelValue not in inputs', () => {
|
|
const { dropdownItems } = useWidgetSelectItems(
|
|
createDefaultOptions({
|
|
values: () => ['img_001.png', 'photo_abc.jpg'],
|
|
modelValue: ref('template_image.png')
|
|
})
|
|
)
|
|
expect(
|
|
dropdownItems.value.some((item) => item.name === 'template_image.png')
|
|
).toBe(true)
|
|
expect(dropdownItems.value[0].id).toBe('missing-template_image.png')
|
|
})
|
|
|
|
it('does not include fallback when filter is inputs', async () => {
|
|
const { dropdownItems, filterSelected } = useWidgetSelectItems(
|
|
createDefaultOptions({
|
|
values: () => ['img_001.png', 'photo_abc.jpg'],
|
|
modelValue: ref('template_image.png')
|
|
})
|
|
)
|
|
filterSelected.value = 'inputs'
|
|
await nextTick()
|
|
|
|
expect(dropdownItems.value).toHaveLength(2)
|
|
expect(
|
|
dropdownItems.value.every(
|
|
(item) => !String(item.id).startsWith('missing-')
|
|
)
|
|
).toBe(true)
|
|
})
|
|
|
|
it('does not include fallback when filter is outputs', async () => {
|
|
const { dropdownItems, filterSelected } = useWidgetSelectItems(
|
|
createDefaultOptions({
|
|
values: () => ['img_001.png', 'photo_abc.jpg'],
|
|
modelValue: ref('template_image.png')
|
|
})
|
|
)
|
|
filterSelected.value = 'outputs'
|
|
await nextTick()
|
|
|
|
expect(
|
|
dropdownItems.value.every(
|
|
(item) => !String(item.id).startsWith('missing-')
|
|
)
|
|
).toBe(true)
|
|
})
|
|
|
|
it('no fallback when modelValue exists in inputs', () => {
|
|
const { dropdownItems } = useWidgetSelectItems(
|
|
createDefaultOptions({
|
|
values: () => ['img_001.png', 'photo_abc.jpg'],
|
|
modelValue: ref('img_001.png')
|
|
})
|
|
)
|
|
expect(dropdownItems.value).toHaveLength(2)
|
|
expect(
|
|
dropdownItems.value.every(
|
|
(item) => !String(item.id).startsWith('missing-')
|
|
)
|
|
).toBe(true)
|
|
})
|
|
|
|
it('no fallback when modelValue is undefined', () => {
|
|
const { dropdownItems } = useWidgetSelectItems(
|
|
createDefaultOptions({
|
|
values: () => ['img_001.png', 'photo_abc.jpg'],
|
|
modelValue: ref(undefined)
|
|
})
|
|
)
|
|
expect(dropdownItems.value).toHaveLength(2)
|
|
expect(
|
|
dropdownItems.value.every(
|
|
(item) => !String(item.id).startsWith('missing-')
|
|
)
|
|
).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('cloud asset mode', () => {
|
|
const createTestAsset = (
|
|
id: string,
|
|
name: string,
|
|
preview_url: string
|
|
): AssetItem => ({
|
|
id,
|
|
name,
|
|
preview_url,
|
|
tags: []
|
|
})
|
|
|
|
it('excludes missing items from cloud dropdown', () => {
|
|
mockAssetsData.items = [
|
|
createTestAsset(
|
|
'asset-1',
|
|
'existing_model.safetensors',
|
|
'https://example.com/preview.jpg'
|
|
)
|
|
]
|
|
|
|
const assetData = {
|
|
category: computed(() => 'checkpoints'),
|
|
assets: computed(() => mockAssetsData.items),
|
|
isLoading: computed(() => false),
|
|
error: computed(() => null)
|
|
}
|
|
|
|
const { dropdownItems } = useWidgetSelectItems(
|
|
createDefaultOptions({
|
|
values: () => [],
|
|
modelValue: ref('missing_model.safetensors'),
|
|
assetKind: () => 'model',
|
|
isAssetMode: () => true,
|
|
assetData
|
|
})
|
|
)
|
|
|
|
expect(dropdownItems.value).toHaveLength(1)
|
|
expect(dropdownItems.value[0].name).toBe('existing_model.safetensors')
|
|
})
|
|
|
|
it('shows only available cloud assets', () => {
|
|
mockAssetsData.items = [
|
|
createTestAsset(
|
|
'asset-1',
|
|
'model_a.safetensors',
|
|
'https://example.com/a.jpg'
|
|
),
|
|
createTestAsset(
|
|
'asset-2',
|
|
'model_b.safetensors',
|
|
'https://example.com/b.jpg'
|
|
)
|
|
]
|
|
|
|
const assetData = {
|
|
category: computed(() => 'checkpoints'),
|
|
assets: computed(() => mockAssetsData.items),
|
|
isLoading: computed(() => false),
|
|
error: computed(() => null)
|
|
}
|
|
|
|
const { dropdownItems } = useWidgetSelectItems(
|
|
createDefaultOptions({
|
|
values: () => [],
|
|
modelValue: ref('model_a.safetensors'),
|
|
assetKind: () => 'model',
|
|
isAssetMode: () => true,
|
|
assetData
|
|
})
|
|
)
|
|
|
|
expect(dropdownItems.value).toHaveLength(2)
|
|
expect(dropdownItems.value.map((i) => i.name)).toEqual([
|
|
'model_a.safetensors',
|
|
'model_b.safetensors'
|
|
])
|
|
})
|
|
|
|
it('returns empty dropdown when no cloud assets', () => {
|
|
const assetData = {
|
|
category: computed(() => 'checkpoints'),
|
|
assets: computed(() => [] as AssetItem[]),
|
|
isLoading: computed(() => false),
|
|
error: computed(() => null)
|
|
}
|
|
|
|
const { dropdownItems } = useWidgetSelectItems(
|
|
createDefaultOptions({
|
|
values: () => [],
|
|
modelValue: ref('missing.safetensors'),
|
|
assetKind: () => 'model',
|
|
isAssetMode: () => true,
|
|
assetData
|
|
})
|
|
)
|
|
|
|
expect(dropdownItems.value).toHaveLength(0)
|
|
})
|
|
|
|
it('includes missing cloud asset in displayItems', () => {
|
|
mockAssetsData.items = [
|
|
createTestAsset(
|
|
'asset-1',
|
|
'existing_model.safetensors',
|
|
'https://example.com/preview.jpg'
|
|
)
|
|
]
|
|
|
|
const assetData = {
|
|
category: computed(() => 'checkpoints'),
|
|
assets: computed(() => mockAssetsData.items),
|
|
isLoading: computed(() => false),
|
|
error: computed(() => null)
|
|
}
|
|
|
|
const { displayItems, selectedSet } = useWidgetSelectItems(
|
|
createDefaultOptions({
|
|
values: () => [],
|
|
modelValue: ref('missing_model.safetensors'),
|
|
assetKind: () => 'model',
|
|
isAssetMode: () => true,
|
|
assetData
|
|
})
|
|
)
|
|
|
|
expect(displayItems.value).toHaveLength(2)
|
|
expect(displayItems.value[0].name).toBe('missing_model.safetensors')
|
|
expect(displayItems.value[0].id).toBe('missing-missing_model.safetensors')
|
|
expect(selectedSet.value.has('missing-missing_model.safetensors')).toBe(
|
|
true
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('multi-output jobs', () => {
|
|
function makeMultiOutputAsset(
|
|
jobId: string,
|
|
name: string,
|
|
nodeId: string,
|
|
outputCount: number
|
|
) {
|
|
return {
|
|
id: jobId,
|
|
name,
|
|
preview_url: `/api/view?filename=${name}&type=output`,
|
|
tags: ['output'],
|
|
user_metadata: {
|
|
jobId,
|
|
nodeId,
|
|
subfolder: '',
|
|
outputCount,
|
|
allOutputs: [
|
|
{
|
|
filename: name,
|
|
subfolder: '',
|
|
type: 'output',
|
|
nodeId,
|
|
mediaType: 'images'
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
|
|
it('shows all outputs after resolving multi-output jobs', async () => {
|
|
mockMediaAssets.media.value = [
|
|
makeMultiOutputAsset('job-1', 'preview.png', '5', 3)
|
|
]
|
|
|
|
mockResolveOutputAssetItems.mockResolvedValue([
|
|
{
|
|
id: 'job-1-5-output_001.png',
|
|
name: 'output_001.png',
|
|
preview_url: '/api/view?filename=output_001.png&type=output',
|
|
tags: ['output']
|
|
},
|
|
{
|
|
id: 'job-1-5-output_002.png',
|
|
name: 'output_002.png',
|
|
preview_url: '/api/view?filename=output_002.png&type=output',
|
|
tags: ['output']
|
|
},
|
|
{
|
|
id: 'job-1-5-output_003.png',
|
|
name: 'output_003.png',
|
|
preview_url: '/api/view?filename=output_003.png&type=output',
|
|
tags: ['output']
|
|
}
|
|
])
|
|
|
|
const { dropdownItems, filterSelected } = useWidgetSelectItems(
|
|
createDefaultOptions({
|
|
values: () => [],
|
|
modelValue: ref('output_001.png')
|
|
})
|
|
)
|
|
filterSelected.value = 'outputs'
|
|
|
|
await vi.waitFor(() => {
|
|
expect(dropdownItems.value).toHaveLength(3)
|
|
})
|
|
|
|
expect(dropdownItems.value.map((i) => i.name)).toEqual([
|
|
'output_001.png [output]',
|
|
'output_002.png [output]',
|
|
'output_003.png [output]'
|
|
])
|
|
})
|
|
|
|
it('shows preview when job has only one output', async () => {
|
|
mockMediaAssets.media.value = [
|
|
makeMultiOutputAsset('job-2', 'single.png', '3', 1)
|
|
]
|
|
|
|
const { dropdownItems, filterSelected } = useWidgetSelectItems(
|
|
createDefaultOptions({
|
|
values: () => [],
|
|
modelValue: ref('single.png')
|
|
})
|
|
)
|
|
filterSelected.value = 'outputs'
|
|
await nextTick()
|
|
|
|
expect(dropdownItems.value).toHaveLength(1)
|
|
expect(dropdownItems.value[0].name).toBe('single.png [output]')
|
|
expect(mockResolveOutputAssetItems).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('resolves two multi-output jobs independently', async () => {
|
|
mockMediaAssets.media.value = [
|
|
makeMultiOutputAsset('job-A', 'previewA.png', '1', 2),
|
|
makeMultiOutputAsset('job-B', 'previewB.png', '2', 2)
|
|
]
|
|
|
|
mockResolveOutputAssetItems.mockImplementation(
|
|
async (meta: { jobId: string }) => {
|
|
if (meta.jobId === 'job-A') {
|
|
return [
|
|
{
|
|
id: 'A-1',
|
|
name: 'a1.png',
|
|
preview_url: '',
|
|
tags: ['output']
|
|
},
|
|
{
|
|
id: 'A-2',
|
|
name: 'a2.png',
|
|
preview_url: '',
|
|
tags: ['output']
|
|
}
|
|
]
|
|
}
|
|
return [
|
|
{
|
|
id: 'B-1',
|
|
name: 'b1.png',
|
|
preview_url: '',
|
|
tags: ['output']
|
|
},
|
|
{
|
|
id: 'B-2',
|
|
name: 'b2.png',
|
|
preview_url: '',
|
|
tags: ['output']
|
|
}
|
|
]
|
|
}
|
|
)
|
|
|
|
const { dropdownItems, filterSelected } = useWidgetSelectItems(
|
|
createDefaultOptions({
|
|
values: () => [],
|
|
modelValue: ref(undefined)
|
|
})
|
|
)
|
|
filterSelected.value = 'outputs'
|
|
|
|
await vi.waitFor(() => {
|
|
expect(dropdownItems.value).toHaveLength(4)
|
|
})
|
|
|
|
const names = dropdownItems.value.map((i) => i.name)
|
|
expect(names).toContain('a1.png [output]')
|
|
expect(names).toContain('a2.png [output]')
|
|
expect(names).toContain('b1.png [output]')
|
|
expect(names).toContain('b2.png [output]')
|
|
})
|
|
|
|
it('resolves outputs when allOutputs already contains all items', async () => {
|
|
mockMediaAssets.media.value = [
|
|
{
|
|
id: 'job-complete',
|
|
name: 'preview.png',
|
|
preview_url: '/api/view?filename=preview.png&type=output',
|
|
tags: ['output'],
|
|
user_metadata: {
|
|
jobId: 'job-complete',
|
|
nodeId: '1',
|
|
subfolder: '',
|
|
outputCount: 2,
|
|
allOutputs: [
|
|
{
|
|
filename: 'out1.png',
|
|
subfolder: '',
|
|
type: 'output',
|
|
nodeId: '1',
|
|
mediaType: 'images'
|
|
},
|
|
{
|
|
filename: 'out2.png',
|
|
subfolder: '',
|
|
type: 'output',
|
|
nodeId: '1',
|
|
mediaType: 'images'
|
|
}
|
|
]
|
|
}
|
|
}
|
|
]
|
|
|
|
mockResolveOutputAssetItems.mockResolvedValue([
|
|
{
|
|
id: 'c-1',
|
|
name: 'out1.png',
|
|
preview_url: '',
|
|
tags: ['output']
|
|
},
|
|
{
|
|
id: 'c-2',
|
|
name: 'out2.png',
|
|
preview_url: '',
|
|
tags: ['output']
|
|
}
|
|
])
|
|
|
|
const { dropdownItems, filterSelected } = useWidgetSelectItems(
|
|
createDefaultOptions({
|
|
values: () => [],
|
|
modelValue: ref(undefined)
|
|
})
|
|
)
|
|
filterSelected.value = 'outputs'
|
|
|
|
await vi.waitFor(() => {
|
|
expect(dropdownItems.value).toHaveLength(2)
|
|
})
|
|
|
|
expect(mockResolveOutputAssetItems).toHaveBeenCalledWith(
|
|
expect.objectContaining({ jobId: 'job-complete' }),
|
|
expect.any(Object)
|
|
)
|
|
const names = dropdownItems.value.map((i) => i.name)
|
|
expect(names).toEqual(['out1.png [output]', 'out2.png [output]'])
|
|
})
|
|
|
|
it('falls back to preview when resolver rejects', async () => {
|
|
const consoleWarnSpy = vi
|
|
.spyOn(console, 'warn')
|
|
.mockImplementation(() => {})
|
|
|
|
mockMediaAssets.media.value = [
|
|
makeMultiOutputAsset('job-fail', 'preview.png', '1', 3)
|
|
]
|
|
mockResolveOutputAssetItems.mockRejectedValue(new Error('network error'))
|
|
|
|
const { dropdownItems, filterSelected } = useWidgetSelectItems(
|
|
createDefaultOptions({
|
|
values: () => [],
|
|
modelValue: ref(undefined)
|
|
})
|
|
)
|
|
filterSelected.value = 'outputs'
|
|
|
|
await vi.waitFor(() => {
|
|
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
'Failed to resolve multi-output job',
|
|
'job-fail',
|
|
expect.any(Error)
|
|
)
|
|
})
|
|
|
|
expect(dropdownItems.value).toHaveLength(1)
|
|
expect(dropdownItems.value[0].name).toBe('preview.png [output]')
|
|
consoleWarnSpy.mockRestore()
|
|
})
|
|
})
|
|
|
|
describe('selectedSet', () => {
|
|
beforeEach(() => {
|
|
setActivePinia(createTestingPinia({ stubActions: false }))
|
|
})
|
|
|
|
it('returns empty set when modelValue is undefined', () => {
|
|
const { selectedSet } = useWidgetSelectItems(
|
|
createDefaultOptions({
|
|
modelValue: ref(undefined)
|
|
})
|
|
)
|
|
expect(selectedSet.value.size).toBe(0)
|
|
})
|
|
|
|
it('returns set with matching item id when modelValue matches', () => {
|
|
const { selectedSet } = useWidgetSelectItems(
|
|
createDefaultOptions({
|
|
modelValue: ref('img_001.png')
|
|
})
|
|
)
|
|
expect(selectedSet.value.size).toBe(1)
|
|
expect(selectedSet.value.has('input-0')).toBe(true)
|
|
})
|
|
|
|
it('returns set with missing item id when modelValue matches no input', () => {
|
|
const { selectedSet } = useWidgetSelectItems(
|
|
createDefaultOptions({
|
|
modelValue: ref('nonexistent.png'),
|
|
values: () => ['img_001.png']
|
|
})
|
|
)
|
|
expect(selectedSet.value.size).toBe(1)
|
|
expect(selectedSet.value.has('missing-nonexistent.png')).toBe(true)
|
|
})
|
|
})
|
|
})
|