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:
dante01yoon
2026-03-28 22:43:58 +09:00
parent 6e26918311
commit b48ed9bc1b
4 changed files with 305 additions and 2 deletions

View 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)
})
}
)

View File

@@ -12,6 +12,7 @@ const makeMinimalSummary = (
): HubWorkflowSummary => ({
share_id: 'abc123',
name: 'My Workflow',
status: 'approved',
profile: { username: 'testuser' },
...overrides
})

View File

@@ -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')
})
})

View File

@@ -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}`)