mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 06:20:11 +00:00
test(templates): add store unit tests and E2E regression tests
- Remove temporary status override for testing - Add 6 unit tests for workflowTemplatesStore cloud/local paths (hub loading, field adaptation, shareId lookup, error handling) - Add E2E regression tests for template dialog: open/cards, filters, search, template loading, navigation, reopen behavior
This commit is contained in:
128
browser_tests/tests/templateHubMigration.spec.ts
Normal file
128
browser_tests/tests/templateHubMigration.spec.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
/**
|
||||
* Regression tests for the template dialog hub API migration.
|
||||
*
|
||||
* These tests verify that the template dialog continues to work correctly
|
||||
* after migrating the data source from static index.json to the hub API
|
||||
* on cloud, and that local behavior is unaffected.
|
||||
*/
|
||||
test.describe(
|
||||
'Template Hub Migration — Regression',
|
||||
{ tag: ['@slow', '@workflow'] },
|
||||
() => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
})
|
||||
|
||||
test('template dialog opens and shows cards', async ({ comfyPage }) => {
|
||||
await comfyPage.command.executeCommand('Comfy.BrowseTemplates')
|
||||
await expect(comfyPage.templates.content).toBeVisible()
|
||||
await comfyPage.templates.expectMinimumCardCount(1)
|
||||
})
|
||||
|
||||
test('template dialog has filter controls', async ({ comfyPage }) => {
|
||||
await comfyPage.command.executeCommand('Comfy.BrowseTemplates')
|
||||
await expect(comfyPage.templates.content).toBeVisible()
|
||||
|
||||
const dialog = comfyPage.page.getByRole('dialog')
|
||||
|
||||
// Sort control should be present
|
||||
const sortBySelect = dialog.getByRole('combobox', { name: /Sort/ })
|
||||
await expect(sortBySelect).toBeVisible()
|
||||
|
||||
// Search input should be present
|
||||
const searchInput = dialog.getByRole('searchbox')
|
||||
await expect(searchInput).toBeVisible()
|
||||
})
|
||||
|
||||
test('search filters templates', async ({ comfyPage }) => {
|
||||
await comfyPage.command.executeCommand('Comfy.BrowseTemplates')
|
||||
await expect(comfyPage.templates.content).toBeVisible()
|
||||
await comfyPage.templates.expectMinimumCardCount(1)
|
||||
|
||||
const dialog = comfyPage.page.getByRole('dialog')
|
||||
const searchInput = dialog.getByRole('searchbox')
|
||||
|
||||
// Count templates before search
|
||||
const beforeCount = await comfyPage.templates.allTemplateCards.count()
|
||||
|
||||
// Search for something very specific that should narrow results
|
||||
await searchInput.fill('zzz_nonexistent_template_xyz')
|
||||
// Wait for debounce
|
||||
await comfyPage.page.waitForTimeout(300)
|
||||
|
||||
// Should have fewer (or zero) results
|
||||
const afterCount = await comfyPage.templates.allTemplateCards.count()
|
||||
expect(afterCount).toBeLessThan(beforeCount)
|
||||
|
||||
// Clear search should restore results
|
||||
await searchInput.clear()
|
||||
await comfyPage.page.waitForTimeout(300)
|
||||
|
||||
await comfyPage.templates.expectMinimumCardCount(1)
|
||||
})
|
||||
|
||||
test('loading a template populates the graph', async ({ comfyPage }) => {
|
||||
// Start with empty canvas
|
||||
await comfyPage.menu.workflowsTab.open()
|
||||
await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow')
|
||||
await expect(async () => {
|
||||
expect(await comfyPage.nodeOps.getGraphNodesCount()).toBe(0)
|
||||
}).toPass({ timeout: 250 })
|
||||
|
||||
// Open dialog and load a template
|
||||
await comfyPage.command.executeCommand('Comfy.BrowseTemplates')
|
||||
await expect(comfyPage.templates.content).toBeVisible()
|
||||
await comfyPage.templates.expectMinimumCardCount(1)
|
||||
|
||||
// Click the first template card
|
||||
const firstCard = comfyPage.templates.allTemplateCards.first()
|
||||
await firstCard.scrollIntoViewIfNeeded()
|
||||
await firstCard.getByRole('img').click()
|
||||
|
||||
// Dialog should close
|
||||
await expect(comfyPage.templates.content).toBeHidden()
|
||||
|
||||
// Graph should have nodes
|
||||
await expect(async () => {
|
||||
expect(await comfyPage.nodeOps.getGraphNodesCount()).toBeGreaterThan(0)
|
||||
}).toPass({ timeout: 5000 })
|
||||
})
|
||||
|
||||
test('navigation categories are visible', async ({ comfyPage }) => {
|
||||
await comfyPage.command.executeCommand('Comfy.BrowseTemplates')
|
||||
await expect(comfyPage.templates.content).toBeVisible()
|
||||
|
||||
const dialog = comfyPage.page.getByRole('dialog')
|
||||
|
||||
// "All Templates" nav item should always be present
|
||||
await expect(
|
||||
dialog.getByRole('button', { name: /All Templates/i })
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('closing and reopening dialog preserves functionality', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Open
|
||||
await comfyPage.command.executeCommand('Comfy.BrowseTemplates')
|
||||
await expect(comfyPage.templates.content).toBeVisible()
|
||||
await comfyPage.templates.expectMinimumCardCount(1)
|
||||
|
||||
// Close via X button
|
||||
const closeButton = comfyPage.page
|
||||
.getByRole('dialog')
|
||||
.getByRole('button', { name: /close/i })
|
||||
await closeButton.click()
|
||||
await expect(comfyPage.templates.content).toBeHidden()
|
||||
|
||||
// Reopen
|
||||
await comfyPage.command.executeCommand('Comfy.BrowseTemplates')
|
||||
await expect(comfyPage.templates.content).toBeVisible()
|
||||
await comfyPage.templates.expectMinimumCardCount(1)
|
||||
})
|
||||
}
|
||||
)
|
||||
@@ -12,6 +12,7 @@ const makeMinimalSummary = (
|
||||
): HubWorkflowSummary => ({
|
||||
share_id: 'abc123',
|
||||
name: 'My Workflow',
|
||||
status: 'approved',
|
||||
profile: { username: 'testuser' },
|
||||
...overrides
|
||||
})
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { HubWorkflowSummary } from '@comfyorg/ingest-types'
|
||||
|
||||
import { useWorkflowTemplatesStore } from './workflowTemplatesStore'
|
||||
|
||||
// Mock isCloud — default to true for hub tests
|
||||
let mockIsCloud = true
|
||||
vi.mock('@/platform/distribution/types', () => ({
|
||||
get isCloud() {
|
||||
return mockIsCloud
|
||||
}
|
||||
}))
|
||||
|
||||
// Mock i18n
|
||||
vi.mock('@/i18n', () => ({
|
||||
i18n: { global: { locale: { value: 'en' } } },
|
||||
st: (_key: string, fallback: string) => fallback
|
||||
}))
|
||||
|
||||
// Mock API
|
||||
const mockListAllHubWorkflows = vi.fn()
|
||||
const mockGetWorkflowTemplates = vi.fn().mockResolvedValue({})
|
||||
const mockGetCoreWorkflowTemplates = vi.fn().mockResolvedValue([])
|
||||
const mockFileURL = vi.fn((path: string) => `mock${path}`)
|
||||
|
||||
vi.mock('@/scripts/api', () => ({
|
||||
api: {
|
||||
listAllHubWorkflows: (...args: unknown[]) =>
|
||||
mockListAllHubWorkflows(...args),
|
||||
getWorkflowTemplates: (...args: unknown[]) =>
|
||||
mockGetWorkflowTemplates(...args),
|
||||
getCoreWorkflowTemplates: (...args: unknown[]) =>
|
||||
mockGetCoreWorkflowTemplates(...args),
|
||||
fileURL: (path: string) => mockFileURL(path)
|
||||
}
|
||||
}))
|
||||
|
||||
const makeSummary = (
|
||||
overrides?: Partial<HubWorkflowSummary>
|
||||
): HubWorkflowSummary => ({
|
||||
share_id: 'share-1',
|
||||
name: 'Test Workflow',
|
||||
status: 'approved',
|
||||
profile: { username: 'user1' },
|
||||
...overrides
|
||||
})
|
||||
|
||||
describe('workflowTemplatesStore — cloud hub path', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
vi.clearAllMocks()
|
||||
mockIsCloud = true
|
||||
})
|
||||
|
||||
it('loads templates from hub API on cloud', async () => {
|
||||
const summaries: HubWorkflowSummary[] = [
|
||||
makeSummary({ share_id: 'a', name: 'Workflow A' }),
|
||||
makeSummary({
|
||||
share_id: 'b',
|
||||
name: 'Workflow B',
|
||||
tags: [{ name: 'video', display_name: 'Video' }]
|
||||
})
|
||||
]
|
||||
mockListAllHubWorkflows.mockResolvedValue(summaries)
|
||||
|
||||
const store = useWorkflowTemplatesStore()
|
||||
await store.loadWorkflowTemplates()
|
||||
|
||||
expect(mockListAllHubWorkflows).toHaveBeenCalledOnce()
|
||||
expect(mockGetCoreWorkflowTemplates).not.toHaveBeenCalled()
|
||||
expect(store.isLoaded).toBe(true)
|
||||
expect(store.enhancedTemplates).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('adapts HubWorkflowSummary fields correctly', async () => {
|
||||
mockListAllHubWorkflows.mockResolvedValue([
|
||||
makeSummary({
|
||||
share_id: 'abc',
|
||||
name: 'My Workflow',
|
||||
description: 'A description',
|
||||
tags: [{ name: 'img', display_name: 'Image Gen' }],
|
||||
models: [{ name: 'flux', display_name: 'Flux' }],
|
||||
thumbnail_url: 'https://cdn.example.com/thumb.webp',
|
||||
metadata: { vram: 8000, open_source: true }
|
||||
})
|
||||
])
|
||||
|
||||
const store = useWorkflowTemplatesStore()
|
||||
await store.loadWorkflowTemplates()
|
||||
|
||||
const template = store.enhancedTemplates[0]
|
||||
expect(template.name).toBe('abc')
|
||||
expect(template.title).toBe('My Workflow')
|
||||
expect(template.description).toBe('A description')
|
||||
expect(template.tags).toEqual(['Image Gen'])
|
||||
expect(template.models).toEqual(['Flux'])
|
||||
expect(template.thumbnailUrl).toBe('https://cdn.example.com/thumb.webp')
|
||||
expect(template.shareId).toBe('abc')
|
||||
expect(template.vram).toBe(8000)
|
||||
expect(template.openSource).toBe(true)
|
||||
})
|
||||
|
||||
it('getTemplateByShareId finds the correct template', async () => {
|
||||
mockListAllHubWorkflows.mockResolvedValue([
|
||||
makeSummary({ share_id: 'x1', name: 'First' }),
|
||||
makeSummary({ share_id: 'x2', name: 'Second' })
|
||||
])
|
||||
|
||||
const store = useWorkflowTemplatesStore()
|
||||
await store.loadWorkflowTemplates()
|
||||
|
||||
expect(store.getTemplateByShareId('x2')?.title).toBe('Second')
|
||||
expect(store.getTemplateByShareId('nonexistent')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('registers hub template names in knownTemplateNames', async () => {
|
||||
mockListAllHubWorkflows.mockResolvedValue([
|
||||
makeSummary({ share_id: 'id1' }),
|
||||
makeSummary({ share_id: 'id2' })
|
||||
])
|
||||
|
||||
const store = useWorkflowTemplatesStore()
|
||||
await store.loadWorkflowTemplates()
|
||||
|
||||
expect(store.knownTemplateNames.has('id1')).toBe(true)
|
||||
expect(store.knownTemplateNames.has('id2')).toBe(true)
|
||||
})
|
||||
|
||||
it('handles API errors gracefully', async () => {
|
||||
mockListAllHubWorkflows.mockRejectedValue(new Error('Network error'))
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
|
||||
const store = useWorkflowTemplatesStore()
|
||||
await store.loadWorkflowTemplates()
|
||||
|
||||
expect(store.isLoaded).toBe(false)
|
||||
expect(store.enhancedTemplates).toHaveLength(0)
|
||||
consoleSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe('workflowTemplatesStore — local static path', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
vi.clearAllMocks()
|
||||
mockIsCloud = false
|
||||
})
|
||||
|
||||
it('loads templates from static files on local', async () => {
|
||||
mockGetCoreWorkflowTemplates.mockResolvedValue([
|
||||
{
|
||||
moduleName: 'default',
|
||||
title: 'Default',
|
||||
templates: [
|
||||
{
|
||||
name: 'local-template',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'A local template'
|
||||
}
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
const store = useWorkflowTemplatesStore()
|
||||
await store.loadWorkflowTemplates()
|
||||
|
||||
expect(mockListAllHubWorkflows).not.toHaveBeenCalled()
|
||||
expect(mockGetCoreWorkflowTemplates).toHaveBeenCalled()
|
||||
expect(store.isLoaded).toBe(true)
|
||||
expect(store.enhancedTemplates).toHaveLength(1)
|
||||
expect(store.enhancedTemplates[0].name).toBe('local-template')
|
||||
})
|
||||
})
|
||||
@@ -846,8 +846,6 @@ export class ComfyApi extends EventTarget {
|
||||
const query = new URLSearchParams()
|
||||
query.set('limit', String(limit))
|
||||
if (cursor) query.set('cursor', cursor)
|
||||
// TODO: Remove after production has approved data — fetch all statuses for testing
|
||||
query.set('status', 'pending,approved,rejected,deprecated')
|
||||
const res = await this.fetchApi(`/hub/workflows?${query.toString()}`)
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to list hub workflows: ${res.status}`)
|
||||
|
||||
Reference in New Issue
Block a user