Add more testing

This commit is contained in:
Benjamin Lu
2026-01-24 07:14:02 -08:00
parent f9583dcf4e
commit 0debd00b65
3 changed files with 404 additions and 0 deletions

View File

@@ -0,0 +1,200 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ref } from 'vue'
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
import { useOutputStacks } from '@/platform/assets/composables/useOutputStacks'
const mocks = vi.hoisted(() => ({
resolveOutputAssetItems: vi.fn()
}))
vi.mock('@/platform/assets/utils/outputAssetUtil', () => ({
resolveOutputAssetItems: mocks.resolveOutputAssetItems
}))
type Deferred<T> = {
promise: Promise<T>
resolve: (value: T) => void
reject: (reason?: unknown) => void
}
function createDeferred<T>(): Deferred<T> {
let resolve!: (value: T) => void
let reject!: (reason?: unknown) => void
const promise = new Promise<T>((resolveFn, rejectFn) => {
resolve = resolveFn
reject = rejectFn
})
return { promise, resolve, reject }
}
function createAsset(overrides: Partial<AssetItem> = {}): AssetItem {
return {
id: 'asset-1',
name: 'parent.png',
tags: [],
created_at: '2025-01-01T00:00:00.000Z',
user_metadata: {
promptId: 'prompt-1',
nodeId: 'node-1',
subfolder: 'outputs'
},
...overrides
}
}
describe('useOutputStacks', () => {
beforeEach(() => {
vi.resetAllMocks()
})
it('expands stacks and exposes children as selectable assets', async () => {
const parent = createAsset({ id: 'parent', name: 'parent.png' })
const childA = createAsset({
id: 'child-a',
name: 'child-a.png',
user_metadata: undefined
})
const childB = createAsset({
id: 'child-b',
name: 'child-b.png',
user_metadata: undefined
})
vi.mocked(mocks.resolveOutputAssetItems).mockResolvedValue([childA, childB])
const { assetItems, isStackExpanded, selectableAssets, toggleStack } =
useOutputStacks({ assets: ref([parent]) })
await toggleStack(parent)
expect(mocks.resolveOutputAssetItems).toHaveBeenCalledWith(
expect.objectContaining({ promptId: 'prompt-1' }),
{
createdAt: parent.created_at,
excludeOutputKey: parent.name
}
)
expect(isStackExpanded(parent)).toBe(true)
expect(assetItems.value.map((item) => item.asset.id)).toEqual([
parent.id,
childA.id,
childB.id
])
expect(assetItems.value[1]).toMatchObject({
asset: childA,
isChild: true
})
expect(assetItems.value[2]).toMatchObject({
asset: childB,
isChild: true
})
expect(selectableAssets.value).toEqual([parent, childA, childB])
})
it('collapses an expanded stack when toggled again', async () => {
const parent = createAsset({ id: 'parent', name: 'parent.png' })
const child = createAsset({
id: 'child',
name: 'child.png',
user_metadata: undefined
})
vi.mocked(mocks.resolveOutputAssetItems).mockResolvedValue([child])
const { assetItems, isStackExpanded, toggleStack } = useOutputStacks({
assets: ref([parent])
})
await toggleStack(parent)
await toggleStack(parent)
expect(isStackExpanded(parent)).toBe(false)
expect(assetItems.value.map((item) => item.asset.id)).toEqual([parent.id])
})
it('ignores assets without stack metadata', async () => {
const asset = createAsset({
id: 'no-meta',
name: 'no-meta.png',
user_metadata: undefined
})
const { assetItems, isStackExpanded, toggleStack } = useOutputStacks({
assets: ref([asset])
})
await toggleStack(asset)
expect(mocks.resolveOutputAssetItems).not.toHaveBeenCalled()
expect(isStackExpanded(asset)).toBe(false)
expect(assetItems.value).toHaveLength(1)
expect(assetItems.value[0].asset).toMatchObject(asset)
})
it('does not expand when no children are resolved', async () => {
const parent = createAsset({ id: 'parent', name: 'parent.png' })
vi.mocked(mocks.resolveOutputAssetItems).mockResolvedValue([])
const { assetItems, isStackExpanded, toggleStack } = useOutputStacks({
assets: ref([parent])
})
await toggleStack(parent)
expect(isStackExpanded(parent)).toBe(false)
expect(assetItems.value.map((item) => item.asset.id)).toEqual([parent.id])
})
it('does not expand when resolving children throws', async () => {
const parent = createAsset({ id: 'parent', name: 'parent.png' })
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
vi.mocked(mocks.resolveOutputAssetItems).mockRejectedValue(
new Error('resolve failed')
)
const { assetItems, isStackExpanded, toggleStack } = useOutputStacks({
assets: ref([parent])
})
await toggleStack(parent)
expect(isStackExpanded(parent)).toBe(false)
expect(assetItems.value.map((item) => item.asset.id)).toEqual([parent.id])
errorSpy.mockRestore()
})
it('guards against duplicate loads while a stack is resolving', async () => {
const parent = createAsset({ id: 'parent', name: 'parent.png' })
const child = createAsset({
id: 'child',
name: 'child.png',
user_metadata: undefined
})
const deferred = createDeferred<AssetItem[]>()
vi.mocked(mocks.resolveOutputAssetItems).mockReturnValue(deferred.promise)
const { assetItems, toggleStack } = useOutputStacks({
assets: ref([parent])
})
const firstToggle = toggleStack(parent)
const secondToggle = toggleStack(parent)
expect(mocks.resolveOutputAssetItems).toHaveBeenCalledTimes(1)
deferred.resolve([child])
await firstToggle
await secondToggle
expect(assetItems.value.map((item) => item.asset.id)).toEqual([
parent.id,
child.id
])
})
})

View File

@@ -0,0 +1,123 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { OutputAssetMetadata } from '@/platform/assets/schemas/assetMetadataSchema'
import type { ResultItemImpl } from '@/stores/queueStore'
import { resolveOutputAssetItems } from './outputAssetUtil'
const mocks = vi.hoisted(() => ({
getJobDetail: vi.fn(),
getPreviewableOutputsFromJobDetail: vi.fn()
}))
vi.mock('@/services/jobOutputCache', () => ({
getJobDetail: mocks.getJobDetail,
getPreviewableOutputsFromJobDetail: mocks.getPreviewableOutputsFromJobDetail
}))
type OutputOverrides = Partial<{
filename: string
subfolder: string
nodeId: string
url: string
}>
function createOutput(overrides: OutputOverrides = {}): ResultItemImpl {
return {
filename: 'file.png',
subfolder: 'sub',
nodeId: '1',
url: 'https://example.com/file.png',
...overrides
} as ResultItemImpl
}
describe('resolveOutputAssetItems', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('maps outputs and excludes a filename', async () => {
const outputA = createOutput({
filename: 'a.png',
nodeId: '1',
url: 'https://example.com/a.png'
})
const outputB = createOutput({
filename: 'b.png',
nodeId: '2',
url: 'https://example.com/b.png'
})
const metadata: OutputAssetMetadata = {
promptId: 'prompt-1',
nodeId: '1',
subfolder: 'sub',
executionTimeInSeconds: 12.5,
outputCount: 2,
allOutputs: [outputA, outputB]
}
const results = await resolveOutputAssetItems(metadata, {
createdAt: '2025-01-01T00:00:00.000Z',
excludeOutputKey: 'b.png'
})
expect(mocks.getJobDetail).not.toHaveBeenCalled()
expect(results).toHaveLength(1)
expect(results[0]).toEqual(
expect.objectContaining({
id: 'prompt-1-1-a.png',
name: 'a.png',
created_at: '2025-01-01T00:00:00.000Z',
tags: ['output'],
preview_url: 'https://example.com/a.png'
})
)
expect(results[0].user_metadata).toEqual(
expect.objectContaining({
promptId: 'prompt-1',
nodeId: '1',
subfolder: 'sub',
executionTimeInSeconds: 12.5
})
)
})
it('loads full outputs when metadata indicates more outputs', async () => {
const previewOutput = createOutput({
filename: 'preview.png',
nodeId: '1',
url: 'https://example.com/preview.png'
})
const fullOutput = createOutput({
filename: 'full.png',
nodeId: '2',
url: 'https://example.com/full.png'
})
const metadata: OutputAssetMetadata = {
promptId: 'prompt-2',
nodeId: '1',
subfolder: 'sub',
outputCount: 3,
allOutputs: [previewOutput]
}
const jobDetail = { id: 'job-1' }
mocks.getJobDetail.mockResolvedValue(jobDetail)
mocks.getPreviewableOutputsFromJobDetail.mockReturnValue([
fullOutput,
previewOutput
])
const results = await resolveOutputAssetItems(metadata)
expect(mocks.getJobDetail).toHaveBeenCalledWith('prompt-2')
expect(mocks.getPreviewableOutputsFromJobDetail).toHaveBeenCalledWith(
jobDetail
)
expect(results.map((asset) => asset.name)).toEqual([
'full.png',
'preview.png'
])
})
})

View File

@@ -180,6 +180,87 @@ describe('jobOutputCache', () => {
})
})
describe('getPreviewableOutputsFromJobDetail', () => {
it('returns empty array when job detail or outputs are missing', async () => {
const { getPreviewableOutputsFromJobDetail } =
await import('@/services/jobOutputCache')
expect(getPreviewableOutputsFromJobDetail(undefined)).toEqual([])
const jobDetail: JobDetail = {
id: 'job-empty',
status: 'completed',
create_time: Date.now(),
priority: 0
}
expect(getPreviewableOutputsFromJobDetail(jobDetail)).toEqual([])
})
it('maps previewable outputs and skips animated/text entries', async () => {
const { getPreviewableOutputsFromJobDetail } =
await import('@/services/jobOutputCache')
const jobDetail: JobDetail = {
id: 'job-previewable',
status: 'completed',
create_time: Date.now(),
priority: 0,
outputs: {
'node-1': {
images: [
{ filename: 'image.png', subfolder: '', type: 'output' },
{ filename: 'image.webp', subfolder: '', type: 'temp' }
],
animated: [true],
text: 'hello'
},
'node-2': {
video: [{ filename: 'clip.mp4', subfolder: '', type: 'output' }],
audio: [{ filename: 'sound.mp3', subfolder: '', type: 'output' }]
}
}
}
const result = getPreviewableOutputsFromJobDetail(jobDetail)
expect(result).toHaveLength(4)
expect(result.map((item) => item.filename).sort()).toEqual(
['image.png', 'image.webp', 'clip.mp4', 'sound.mp3'].sort()
)
const image = result.find((item) => item.filename === 'image.png')
const video = result.find((item) => item.filename === 'clip.mp4')
expect(image).toBeInstanceOf(ResultItemImpl)
expect(image?.nodeId).toBe('node-1')
expect(image?.mediaType).toBe('images')
expect(video?.nodeId).toBe('node-2')
expect(video?.mediaType).toBe('video')
})
it('filters non-previewable outputs and non-object items', async () => {
const { getPreviewableOutputsFromJobDetail } =
await import('@/services/jobOutputCache')
const jobDetail: JobDetail = {
id: 'job-filter',
status: 'completed',
create_time: Date.now(),
priority: 0,
outputs: {
'node-3': {
images: [{ filename: 'valid.png', subfolder: '', type: 'output' }],
text: ['not-object'],
unknown: [{ filename: 'data.bin', subfolder: '', type: 'output' }]
}
}
}
const result = getPreviewableOutputsFromJobDetail(jobDetail)
expect(result.map((item) => item.filename)).toEqual(['valid.png'])
})
})
describe('getJobDetail', () => {
it('fetches and caches job detail', async () => {
const { getJobDetail } = await import('@/services/jobOutputCache')