mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 10:59:53 +00:00
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>
299 lines
8.3 KiB
TypeScript
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()
|
|
})
|
|
})
|