mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-25 08:49:36 +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>
353 lines
12 KiB
TypeScript
353 lines
12 KiB
TypeScript
import type { Page } from '@playwright/test'
|
|
import { expect } from '@playwright/test'
|
|
|
|
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
|
|
|
async function checkTemplateFileExists(
|
|
page: Page,
|
|
filename: string
|
|
): Promise<boolean> {
|
|
const response = await page.request.head(
|
|
new URL(`/templates/${filename}`, page.url()).toString()
|
|
)
|
|
return response.ok()
|
|
}
|
|
|
|
test.describe('Templates', () => {
|
|
test.beforeEach(async ({ comfyPage }) => {
|
|
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
|
await comfyPage.setSetting('Comfy.Workflow.ShowMissingModelsWarning', false)
|
|
})
|
|
|
|
test('should have a JSON workflow file for each template', async ({
|
|
comfyPage
|
|
}) => {
|
|
test.slow()
|
|
const templates = await comfyPage.templates.getAllTemplates()
|
|
for (const template of templates) {
|
|
const exists = await checkTemplateFileExists(
|
|
comfyPage.page,
|
|
`${template.name}.json`
|
|
)
|
|
expect(exists, `Missing workflow: ${template.name}`).toBe(true)
|
|
}
|
|
})
|
|
|
|
// TODO: Re-enable this test once issue resolved
|
|
// https://github.com/Comfy-Org/ComfyUI_frontend/issues/3992
|
|
test.skip('should have all required thumbnail media for each template', async ({
|
|
comfyPage
|
|
}) => {
|
|
test.slow()
|
|
const templates = await comfyPage.templates.getAllTemplates()
|
|
for (const template of templates) {
|
|
const { name, mediaSubtype, thumbnailVariant } = template
|
|
const baseMedia = `${name}-1.${mediaSubtype}`
|
|
|
|
// Check base thumbnail
|
|
const baseExists = await checkTemplateFileExists(
|
|
comfyPage.page,
|
|
baseMedia
|
|
)
|
|
expect(baseExists, `Missing base thumbnail: ${baseMedia}`).toBe(true)
|
|
|
|
// Check second thumbnail for variants that need it
|
|
if (
|
|
thumbnailVariant === 'compareSlider' ||
|
|
thumbnailVariant === 'hoverDissolve'
|
|
) {
|
|
const secondMedia = `${name}-2.${mediaSubtype}`
|
|
const secondExists = await checkTemplateFileExists(
|
|
comfyPage.page,
|
|
secondMedia
|
|
)
|
|
expect(
|
|
secondExists,
|
|
`Missing second thumbnail: ${secondMedia} required for ${thumbnailVariant}`
|
|
).toBe(true)
|
|
}
|
|
}
|
|
})
|
|
|
|
test('Can load template workflows', async ({ comfyPage }) => {
|
|
// Clear the workflow
|
|
await comfyPage.menu.workflowsTab.open()
|
|
await comfyPage.executeCommand('Comfy.NewBlankWorkflow')
|
|
await expect(async () => {
|
|
expect(await comfyPage.getGraphNodesCount()).toBe(0)
|
|
}).toPass({ timeout: 250 })
|
|
|
|
// Load a template
|
|
await comfyPage.executeCommand('Comfy.BrowseTemplates')
|
|
await expect(comfyPage.templates.content).toBeVisible()
|
|
|
|
await comfyPage.page
|
|
.locator(
|
|
'nav > div:nth-child(2) > div > span:has-text("Getting Started")'
|
|
)
|
|
.click()
|
|
await comfyPage.templates.loadTemplate('default')
|
|
await expect(comfyPage.templates.content).toBeHidden()
|
|
|
|
// Ensure we now have some nodes
|
|
await expect(async () => {
|
|
expect(await comfyPage.getGraphNodesCount()).toBeGreaterThan(0)
|
|
}).toPass({ timeout: 250 })
|
|
})
|
|
|
|
test('dialog should be automatically shown to first-time users', async ({
|
|
comfyPage
|
|
}) => {
|
|
// Set the tutorial as not completed to mark the user as a first-time user
|
|
await comfyPage.setSetting('Comfy.TutorialCompleted', false)
|
|
|
|
// Load the page
|
|
await comfyPage.setup({ clearStorage: true })
|
|
|
|
// Expect the templates dialog to be shown
|
|
expect(await comfyPage.templates.content.isVisible()).toBe(true)
|
|
})
|
|
|
|
test('Uses proper locale files for templates', async ({ comfyPage }) => {
|
|
// Set locale to French before opening templates
|
|
await comfyPage.setSetting('Comfy.Locale', 'fr')
|
|
|
|
// Load the templates dialog and wait for the French index file request
|
|
const requestPromise = comfyPage.page.waitForRequest(
|
|
'**/templates/index.fr.json'
|
|
)
|
|
|
|
await comfyPage.executeCommand('Comfy.BrowseTemplates')
|
|
|
|
const request = await requestPromise
|
|
|
|
// Verify French index was requested
|
|
expect(request.url()).toContain('templates/index.fr.json')
|
|
|
|
await expect(comfyPage.templates.content).toBeVisible()
|
|
})
|
|
|
|
test('Falls back to English templates when locale file not found', async ({
|
|
comfyPage
|
|
}) => {
|
|
// Set locale to a language that doesn't have a template file
|
|
await comfyPage.setSetting('Comfy.Locale', 'de') // German - no index.de.json exists
|
|
|
|
// Wait for the German request (expected to 404)
|
|
const germanRequestPromise = comfyPage.page.waitForRequest(
|
|
'**/templates/index.de.json'
|
|
)
|
|
|
|
// Wait for the fallback English request
|
|
const englishRequestPromise = comfyPage.page.waitForRequest(
|
|
'**/templates/index.json'
|
|
)
|
|
|
|
// Intercept the German file to simulate a 404
|
|
await comfyPage.page.route('**/templates/index.de.json', async (route) => {
|
|
await route.fulfill({
|
|
status: 404,
|
|
headers: { 'Content-Type': 'text/plain' },
|
|
body: 'Not Found'
|
|
})
|
|
})
|
|
|
|
// Allow the English index to load normally
|
|
await comfyPage.page.route('**/templates/index.json', (route) =>
|
|
route.continue()
|
|
)
|
|
|
|
// Load the templates dialog
|
|
await comfyPage.executeCommand('Comfy.BrowseTemplates')
|
|
await expect(comfyPage.templates.content).toBeVisible()
|
|
|
|
// Verify German was requested first, then English as fallback
|
|
const germanRequest = await germanRequestPromise
|
|
const englishRequest = await englishRequestPromise
|
|
|
|
expect(germanRequest.url()).toContain('templates/index.de.json')
|
|
expect(englishRequest.url()).toContain('templates/index.json')
|
|
|
|
// Verify English titles are shown as fallback
|
|
await expect(
|
|
comfyPage.templates.content.getByRole('heading', {
|
|
name: 'Image Generation'
|
|
})
|
|
).toBeVisible()
|
|
})
|
|
|
|
test('template cards are dynamically sized and responsive', async ({
|
|
comfyPage
|
|
}) => {
|
|
// Open templates dialog
|
|
await comfyPage.executeCommand('Comfy.BrowseTemplates')
|
|
await expect(comfyPage.templates.content).toBeVisible()
|
|
|
|
const firstCard = comfyPage.page
|
|
.locator('[data-testid^="template-workflow-"]')
|
|
.first()
|
|
await expect(firstCard).toBeVisible({ timeout: 5000 })
|
|
|
|
// Get the template grid
|
|
const templateGrid = comfyPage.page.locator(
|
|
'[data-testid="template-workflows-content"]'
|
|
)
|
|
await expect(templateGrid).toBeVisible()
|
|
|
|
// Check grid layout at desktop size (default)
|
|
const desktopGridClass = await templateGrid.getAttribute('class')
|
|
expect(desktopGridClass).toContain('grid')
|
|
expect(desktopGridClass).toContain(
|
|
'grid-cols-[repeat(auto-fill,minmax(16rem,1fr))]'
|
|
)
|
|
|
|
// Count visible cards at desktop size
|
|
const desktopCardCount = await comfyPage.page
|
|
.locator('[data-testid^="template-workflow-"]')
|
|
.count()
|
|
expect(desktopCardCount).toBeGreaterThan(0)
|
|
|
|
// Check cards at mobile viewport size
|
|
await comfyPage.page.setViewportSize({ width: 640, height: 800 })
|
|
await expect(templateGrid).toBeVisible()
|
|
// Grid should still be responsive at mobile size
|
|
const mobileGridClass = await templateGrid.getAttribute('class')
|
|
expect(mobileGridClass).toContain('grid')
|
|
|
|
// Check cards at tablet size
|
|
await comfyPage.page.setViewportSize({ width: 1024, height: 800 })
|
|
await expect(templateGrid).toBeVisible()
|
|
// Grid should still be responsive at tablet size
|
|
const tabletGridClass = await templateGrid.getAttribute('class')
|
|
expect(tabletGridClass).toContain('grid')
|
|
})
|
|
|
|
test('hover effects work on template cards', async ({ comfyPage }) => {
|
|
// Open templates dialog
|
|
await comfyPage.executeCommand('Comfy.BrowseTemplates')
|
|
await expect(comfyPage.templates.content).toBeVisible()
|
|
|
|
// Get a template card using data-testid
|
|
const firstCard = comfyPage.page
|
|
.locator('[data-testid^="template-workflow-"]')
|
|
.first()
|
|
await expect(firstCard).toBeVisible({ timeout: 5000 })
|
|
|
|
// Check initial state - card should have transition classes
|
|
// Take snapshot before hover
|
|
await expect(firstCard).toHaveScreenshot('template-card-before-hover.png')
|
|
|
|
// Hover over the card
|
|
await firstCard.hover()
|
|
|
|
// Take snapshot after hover to verify hover effect
|
|
await expect(firstCard).toHaveScreenshot('template-card-after-hover.png')
|
|
})
|
|
|
|
test('template cards descriptions adjust height dynamically', async ({
|
|
comfyPage
|
|
}) => {
|
|
// Setup test by intercepting templates response to inject cards with varying description lengths
|
|
await comfyPage.page.route('**/templates/index.json', async (route, _) => {
|
|
const response = [
|
|
{
|
|
moduleName: 'default',
|
|
title: 'Test Templates',
|
|
type: 'image',
|
|
templates: [
|
|
{
|
|
name: 'short-description',
|
|
title: 'Short Description',
|
|
mediaType: 'image',
|
|
mediaSubtype: 'webp',
|
|
description: 'This is a short description.'
|
|
},
|
|
{
|
|
name: 'medium-description',
|
|
title: 'Medium Description',
|
|
mediaType: 'image',
|
|
mediaSubtype: 'webp',
|
|
description:
|
|
'This is a medium length description that should take up two lines on most displays.'
|
|
},
|
|
{
|
|
name: 'long-description',
|
|
title: 'Long Description',
|
|
mediaType: 'image',
|
|
mediaSubtype: 'webp',
|
|
description:
|
|
'This is a much longer description that should definitely wrap to multiple lines. It contains enough text to demonstrate how the cards handle varying amounts of content while maintaining a consistent layout grid.'
|
|
}
|
|
]
|
|
}
|
|
]
|
|
await route.fulfill({
|
|
status: 200,
|
|
body: JSON.stringify(response),
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Cache-Control': 'no-store'
|
|
}
|
|
})
|
|
})
|
|
|
|
// Mock the thumbnail images to avoid 404s
|
|
await comfyPage.page.route('**/templates/**.webp', async (route) => {
|
|
const headers = {
|
|
'Content-Type': 'image/webp',
|
|
'Cache-Control': 'no-store'
|
|
}
|
|
await route.fulfill({
|
|
status: 200,
|
|
path: 'browser_tests/assets/example.webp',
|
|
headers
|
|
})
|
|
})
|
|
|
|
// Open templates dialog
|
|
await comfyPage.executeCommand('Comfy.BrowseTemplates')
|
|
await expect(comfyPage.templates.content).toBeVisible()
|
|
|
|
// Wait for cards to load
|
|
await expect(
|
|
comfyPage.page.locator(
|
|
'[data-testid="template-workflow-short-description"]'
|
|
)
|
|
).toBeVisible({ timeout: 5000 })
|
|
|
|
// Verify all three cards with different descriptions are visible
|
|
const shortDescCard = comfyPage.page.locator(
|
|
'[data-testid="template-workflow-short-description"]'
|
|
)
|
|
const mediumDescCard = comfyPage.page.locator(
|
|
'[data-testid="template-workflow-medium-description"]'
|
|
)
|
|
const longDescCard = comfyPage.page.locator(
|
|
'[data-testid="template-workflow-long-description"]'
|
|
)
|
|
|
|
await expect(shortDescCard).toBeVisible()
|
|
await expect(mediumDescCard).toBeVisible()
|
|
await expect(longDescCard).toBeVisible()
|
|
|
|
// Verify descriptions are visible and have line-clamp class
|
|
// The description is in a p tag with text-muted class
|
|
const shortDesc = shortDescCard.locator('p.text-muted.line-clamp-2')
|
|
const mediumDesc = mediumDescCard.locator('p.text-muted.line-clamp-2')
|
|
const longDesc = longDescCard.locator('p.text-muted.line-clamp-2')
|
|
|
|
await expect(shortDesc).toContainText('short description')
|
|
await expect(mediumDesc).toContainText('medium length description')
|
|
await expect(longDesc).toContainText('much longer description')
|
|
|
|
// Verify grid layout maintains consistency
|
|
const templateGrid = comfyPage.page.locator(
|
|
'[data-testid="template-workflows-content"]'
|
|
)
|
|
await expect(templateGrid).toBeVisible()
|
|
await expect(templateGrid).toHaveScreenshot(
|
|
'template-grid-varying-content.png'
|
|
)
|
|
})
|
|
})
|