Files
ComfyUI_frontend/tests-ui/tests/composables/useTemplateWorkflows.test.ts
Johnpaul Chiwetelu d954336973 New Workflow Templates Modal (#5142)
This pull request refactors and simplifies the template workflow card
components and related UI in the codebase. The main changes focus on
removing unused or redundant components, improving visual and
interaction consistency, and enhancing error handling for images. Below
are the most important changes grouped by theme:

**Template Workflow Card Refactor and Cleanup**

* Removed the `TemplateWorkflowCard.vue` component and its associated
test file `TemplateWorkflowCard.spec.ts`, as well as the
`TemplateWorkflowCardSkeleton.vue` and `TemplateWorkflowList.vue`
components, indicating a shift away from the previous card-based
template workflow UI.
[[1]](diffhunk://#diff-49569af0404058e8257f3cc0716b066517ce7397dd58744b02aa0d0c61f2a815L1-L139)
[[2]](diffhunk://#diff-9fa6fc1470371f0b520d4deda4129fb313b1bea69888a376556f4bd824f9d751L1-L263)
[[3]](diffhunk://#diff-bc35b6f77d1cee6e86b05d0da80b7bd40013c7a6a97a89706d3bc52573e1c574L1-L30)
[[4]](diffhunk://#diff-48171f792b22022526fca411d3c3a366d48b675dab77943a20846ae079cbaf3bL1-L68)
* Removed the `TemplateSearchBar.vue` component, suggesting a redesign
or replacement of the search/filter UI for templates.

**UI and Interaction Improvements**

* Improved the `CardBottom.vue` component by making its height
configurable via a `fullHeight` prop, enhancing layout flexibility.
* Updated the `CardContainer.vue` component to add hover effects
(background, border, shadow, and padding) and support a new `none`
aspect ratio for more flexible card layouts.

**Image and Input Enhancements**

* Enhanced the `LazyImage.vue` component to display a default
placeholder image when an image fails to load, improving error handling
and user experience.
* Improved the `SearchBox.vue` component by making the input focusable
when clicking anywhere on the wrapper, and added a template ref for
better accessibility and usability.
[[1]](diffhunk://#diff-08f3b0c51fbfe63171509b9944bf7558228f6c2596a1ef5338e88ab64585791bL2-R5)
[[2]](diffhunk://#diff-08f3b0c51fbfe63171509b9944bf7558228f6c2596a1ef5338e88ab64585791bL16-R17)
[[3]](diffhunk://#diff-08f3b0c51fbfe63171509b9944bf7558228f6c2596a1ef5338e88ab64585791bR33-R39)

**Minor UI Tweaks**

* Adjusted label styling in `SingleSelect.vue` to remove unnecessary
overflow handling, simplifying the visual layout.

---------

Co-authored-by: Benjamin Lu <benceruleanlu@proton.me>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Christian Byrne <cbyrne@comfy.org>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: snomiao <snomiao@gmail.com>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: filtered <176114999+webfiltered@users.noreply.github.com>
Co-authored-by: Comfy Org PR Bot <snomiao+comfy-pr@gmail.com>
Co-authored-by: Jin Yi <jin12cc@gmail.com>
2025-09-26 11:52:19 -07:00

299 lines
8.3 KiB
TypeScript

import { flushPromises } from '@vue/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useTemplateWorkflows } from '@/platform/workflow/templates/composables/useTemplateWorkflows'
import { useWorkflowTemplatesStore } from '@/platform/workflow/templates/repositories/workflowTemplatesStore'
// Mock the store
vi.mock(
'@/platform/workflow/templates/repositories/workflowTemplatesStore',
() => ({
useWorkflowTemplatesStore: vi.fn()
})
)
// Mock the API
vi.mock('@/scripts/api', () => ({
api: {
fileURL: vi.fn((path) => `mock-file-url${path}`),
apiURL: vi.fn((path) => `mock-api-url${path}`)
}
}))
// Mock the app
vi.mock('@/scripts/app', () => ({
app: {
loadGraphData: vi.fn()
}
}))
// Mock Vue I18n
vi.mock('vue-i18n', () => ({
useI18n: () => ({
t: vi.fn((key, fallback) => fallback || key)
})
}))
// Mock the dialog store
vi.mock('@/stores/dialogStore', () => ({
useDialogStore: vi.fn(() => ({
closeDialog: vi.fn()
}))
}))
// Mock fetch
global.fetch = vi.fn()
describe('useTemplateWorkflows', () => {
let mockWorkflowTemplatesStore: any
beforeEach(() => {
mockWorkflowTemplatesStore = {
isLoaded: false,
loadWorkflowTemplates: vi.fn().mockResolvedValue(true),
groupedTemplates: [
{
label: 'ComfyUI Examples',
modules: [
{
moduleName: 'all',
title: 'All',
localizedTitle: 'All Templates',
templates: [
{
name: 'template1',
mediaType: 'image',
mediaSubtype: 'jpg',
sourceModule: 'default',
localizedTitle: 'Template 1'
},
{
name: 'template2',
mediaType: 'image',
mediaSubtype: 'jpg',
sourceModule: 'custom-module',
description: 'A custom template'
}
]
},
{
moduleName: 'default',
title: 'Default',
localizedTitle: 'Default Templates',
templates: [
{
name: 'template1',
mediaType: 'image',
mediaSubtype: 'jpg',
localizedTitle: 'Template 1',
localizedDescription: 'A default template'
}
]
}
]
}
]
}
vi.mocked(useWorkflowTemplatesStore).mockReturnValue(
mockWorkflowTemplatesStore
)
// Mock fetch response
vi.mocked(fetch).mockResolvedValue({
json: vi.fn().mockResolvedValue({ workflow: 'data' })
} as unknown as Response)
})
it('should load templates from store', async () => {
const { loadTemplates, isTemplatesLoaded } = useTemplateWorkflows()
expect(isTemplatesLoaded.value).toBe(false)
await loadTemplates()
expect(mockWorkflowTemplatesStore.loadWorkflowTemplates).toHaveBeenCalled()
})
it('should select the first template category', () => {
const { selectFirstTemplateCategory, selectedTemplate } =
useTemplateWorkflows()
selectFirstTemplateCategory()
expect(selectedTemplate.value).toEqual(
mockWorkflowTemplatesStore.groupedTemplates[0].modules[0]
)
})
it('should select a template category', () => {
const { selectTemplateCategory, selectedTemplate } = useTemplateWorkflows()
const category = mockWorkflowTemplatesStore.groupedTemplates[0].modules[1] // Default category
const result = selectTemplateCategory(category)
expect(result).toBe(true)
expect(selectedTemplate.value).toEqual(category)
})
it('should format template thumbnails correctly for default templates', () => {
const { getTemplateThumbnailUrl } = useTemplateWorkflows()
const template = {
name: 'test-template',
mediaSubtype: 'jpg',
mediaType: 'image',
description: 'Test template'
}
const url = getTemplateThumbnailUrl(template, 'default', '1')
expect(url).toBe('mock-file-url/templates/test-template-1.jpg')
})
it('should format template thumbnails correctly for custom templates', () => {
const { getTemplateThumbnailUrl } = useTemplateWorkflows()
const template = {
name: 'test-template',
mediaSubtype: 'jpg',
mediaType: 'image',
description: 'Test template'
}
const url = getTemplateThumbnailUrl(template, 'custom-module')
expect(url).toBe(
'mock-api-url/workflow_templates/custom-module/test-template.jpg'
)
})
it('should format template titles correctly', () => {
const { getTemplateTitle } = useTemplateWorkflows()
// Default template with localized title
const titleWithLocalized = getTemplateTitle(
{
name: 'test',
localizedTitle: 'Localized Title',
mediaType: 'image',
mediaSubtype: 'jpg',
description: 'Test'
},
'default'
)
expect(titleWithLocalized).toBe('Localized Title')
// Default template without localized title
const titleWithFallback = getTemplateTitle(
{
name: 'test',
title: 'Title',
mediaType: 'image',
mediaSubtype: 'jpg',
description: 'Test'
},
'default'
)
expect(titleWithFallback).toBe('Title')
// Custom template
const customTitle = getTemplateTitle(
{
name: 'test-template',
title: 'Custom Title',
mediaType: 'image',
mediaSubtype: 'jpg',
description: 'Test'
},
'custom-module'
)
expect(customTitle).toBe('Custom Title')
// Fallback to name
const nameOnly = getTemplateTitle(
{
name: 'name-only',
mediaType: 'image',
mediaSubtype: 'jpg',
description: 'Test'
},
'custom-module'
)
expect(nameOnly).toBe('name-only')
})
it('should format template descriptions correctly', () => {
const { getTemplateDescription } = useTemplateWorkflows()
// Default template with localized description
const descWithLocalized = getTemplateDescription({
name: 'test',
localizedDescription: 'Localized Description',
mediaType: 'image',
mediaSubtype: 'jpg',
description: 'Test'
})
expect(descWithLocalized).toBe('Localized Description')
// Custom template with description
const customDesc = getTemplateDescription({
name: 'test',
description: 'custom-template_description',
mediaType: 'image',
mediaSubtype: 'jpg'
})
expect(customDesc).toBe('custom template description')
})
it('should load a template from the "All" category', async () => {
const { loadWorkflowTemplate, loadingTemplateId } = useTemplateWorkflows()
// Set the store as loaded
mockWorkflowTemplatesStore.isLoaded = true
// Load a template from the "All" category
const result = await loadWorkflowTemplate('template1', 'all')
await flushPromises()
expect(result).toBe(true)
expect(fetch).toHaveBeenCalledWith('mock-file-url/templates/template1.json')
expect(loadingTemplateId.value).toBe(null) // Should reset after loading
})
it('should load a template from a regular category', async () => {
const { loadWorkflowTemplate } = useTemplateWorkflows()
// Set the store as loaded
mockWorkflowTemplatesStore.isLoaded = true
// Load a template from the default category
const result = await loadWorkflowTemplate('template1', 'default')
await flushPromises()
expect(result).toBe(true)
expect(fetch).toHaveBeenCalledWith('mock-file-url/templates/template1.json')
})
it('should handle errors when loading templates', async () => {
const { loadWorkflowTemplate, loadingTemplateId } = useTemplateWorkflows()
// Set the store as loaded
mockWorkflowTemplatesStore.isLoaded = true
// Mock fetch to throw an error
vi.mocked(fetch).mockRejectedValueOnce(new Error('Failed to fetch'))
// Spy on console.error
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
// Load a template that will fail
const result = await loadWorkflowTemplate('error-template', 'default')
expect(result).toBe(false)
expect(consoleSpy).toHaveBeenCalled()
expect(loadingTemplateId.value).toBe(null) // Should reset even after error
// Restore console.error
consoleSpy.mockRestore()
})
})