diff --git a/browser_tests/tests/templates.spec.ts b/browser_tests/tests/templates.spec.ts index a891cc098..c40529740 100644 --- a/browser_tests/tests/templates.spec.ts +++ b/browser_tests/tests/templates.spec.ts @@ -142,4 +142,136 @@ test.describe('Templates', () => { // Expect the title to be used as fallback for the template categories await expect(comfyPage.page.getByLabel('FALLBACK CATEGORY')).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() + + // Wait for at least one template card to appear + await expect(comfyPage.page.locator('.template-card').first()).toBeVisible({ + timeout: 5000 + }) + + // Take snapshot of the template grid + const templateGrid = comfyPage.templates.content.locator('.grid').first() + await expect(templateGrid).toBeVisible() + await expect(templateGrid).toHaveScreenshot('template-grid-desktop.png') + + // Check cards at mobile viewport size + await comfyPage.page.setViewportSize({ width: 640, height: 800 }) + await expect(templateGrid).toBeVisible() + await expect(templateGrid).toHaveScreenshot('template-grid-mobile.png') + + // Check cards at tablet size + await comfyPage.page.setViewportSize({ width: 1024, height: 800 }) + await expect(templateGrid).toBeVisible() + await expect(templateGrid).toHaveScreenshot('template-grid-tablet.png') + }) + + 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 + const firstCard = comfyPage.page.locator('.template-card').first() + await expect(firstCard).toBeVisible({ timeout: 5000 }) + + // 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() + + // Verify cards are visible with varying content lengths + await expect( + comfyPage.page.getByText('This is a short description.') + ).toBeVisible({ timeout: 5000 }) + await expect( + comfyPage.page.getByText('This is a medium length description') + ).toBeVisible({ timeout: 5000 }) + await expect( + comfyPage.page.getByText('This is a much longer description') + ).toBeVisible({ timeout: 5000 }) + + // Take snapshot of a grid with specific cards + const templateGrid = comfyPage.templates.content + .locator('.grid:has-text("Short Description")') + .first() + await expect(templateGrid).toBeVisible() + await expect(templateGrid).toHaveScreenshot( + 'template-grid-varying-content.png' + ) + }) }) diff --git a/browser_tests/tests/templates.spec.ts-snapshots/template-card-after-hover-chromium-linux.png b/browser_tests/tests/templates.spec.ts-snapshots/template-card-after-hover-chromium-linux.png new file mode 100644 index 000000000..b767dbe20 Binary files /dev/null and b/browser_tests/tests/templates.spec.ts-snapshots/template-card-after-hover-chromium-linux.png differ diff --git a/browser_tests/tests/templates.spec.ts-snapshots/template-card-before-hover-chromium-linux.png b/browser_tests/tests/templates.spec.ts-snapshots/template-card-before-hover-chromium-linux.png new file mode 100644 index 000000000..cfebed5da Binary files /dev/null and b/browser_tests/tests/templates.spec.ts-snapshots/template-card-before-hover-chromium-linux.png differ diff --git a/browser_tests/tests/templates.spec.ts-snapshots/template-grid-desktop-chromium-linux.png b/browser_tests/tests/templates.spec.ts-snapshots/template-grid-desktop-chromium-linux.png new file mode 100644 index 000000000..92cb6b4f6 Binary files /dev/null and b/browser_tests/tests/templates.spec.ts-snapshots/template-grid-desktop-chromium-linux.png differ diff --git a/browser_tests/tests/templates.spec.ts-snapshots/template-grid-mobile-chromium-linux.png b/browser_tests/tests/templates.spec.ts-snapshots/template-grid-mobile-chromium-linux.png new file mode 100644 index 000000000..9bad33cd8 Binary files /dev/null and b/browser_tests/tests/templates.spec.ts-snapshots/template-grid-mobile-chromium-linux.png differ diff --git a/browser_tests/tests/templates.spec.ts-snapshots/template-grid-tablet-chromium-linux.png b/browser_tests/tests/templates.spec.ts-snapshots/template-grid-tablet-chromium-linux.png new file mode 100644 index 000000000..f4d2ead5c Binary files /dev/null and b/browser_tests/tests/templates.spec.ts-snapshots/template-grid-tablet-chromium-linux.png differ diff --git a/browser_tests/tests/templates.spec.ts-snapshots/template-grid-varying-content-chromium-linux.png b/browser_tests/tests/templates.spec.ts-snapshots/template-grid-varying-content-chromium-linux.png new file mode 100644 index 000000000..40452d452 Binary files /dev/null and b/browser_tests/tests/templates.spec.ts-snapshots/template-grid-varying-content-chromium-linux.png differ diff --git a/src/components/templates/TemplateWorkflowCard.spec.ts b/src/components/templates/TemplateWorkflowCard.spec.ts new file mode 100644 index 000000000..795bb66a7 --- /dev/null +++ b/src/components/templates/TemplateWorkflowCard.spec.ts @@ -0,0 +1,205 @@ +import { mount } from '@vue/test-utils' +import { describe, expect, it, vi } from 'vitest' +import { ref } from 'vue' + +import TemplateWorkflowCard from '@/components/templates/TemplateWorkflowCard.vue' +import { TemplateInfo } from '@/types/workflowTemplateTypes' + +vi.mock('@/components/templates/thumbnails/AudioThumbnail.vue', () => ({ + default: { + name: 'AudioThumbnail', + template: '
', + props: ['src'] + } +})) + +vi.mock('@/components/templates/thumbnails/CompareSliderThumbnail.vue', () => ({ + default: { + name: 'CompareSliderThumbnail', + template: + '
', + props: ['baseImageSrc', 'overlayImageSrc', 'alt', 'isHovered'] + } +})) + +vi.mock('@/components/templates/thumbnails/DefaultThumbnail.vue', () => ({ + default: { + name: 'DefaultThumbnail', + template: '
', + props: ['src', 'alt', 'isHovered', 'isVideo', 'hoverZoom'] + } +})) + +vi.mock('@/components/templates/thumbnails/HoverDissolveThumbnail.vue', () => ({ + default: { + name: 'HoverDissolveThumbnail', + template: + '
', + props: ['baseImageSrc', 'overlayImageSrc', 'alt', 'isHovered'] + } +})) + +vi.mock('@vueuse/core', () => ({ + useElementHover: () => ref(false) +})) + +vi.mock('@/scripts/api', () => ({ + api: { + fileURL: (path: string) => `/fileURL${path}`, + apiURL: (path: string) => `/apiURL${path}` + } +})) + +describe('TemplateWorkflowCard', () => { + const createTemplate = (overrides = {}): TemplateInfo => ({ + name: 'test-template', + mediaType: 'image', + mediaSubtype: 'png', + thumbnailVariant: 'default', + description: 'Test description', + ...overrides + }) + + const mountCard = (props = {}) => { + return mount(TemplateWorkflowCard, { + props: { + sourceModule: 'default', + categoryTitle: 'Test Category', + loading: false, + template: createTemplate(), + ...props + }, + global: { + stubs: { + Card: { + template: + '
', + props: ['dataTestid', 'pt'] + }, + ProgressSpinner: { + template: '
' + } + } + } + }) + } + + it('emits loadWorkflow event when clicked', async () => { + const wrapper = mountCard({ + template: createTemplate({ name: 'test-workflow' }) + }) + await wrapper.find('.card').trigger('click') + expect(wrapper.emitted('loadWorkflow')).toBeTruthy() + expect(wrapper.emitted('loadWorkflow')?.[0]).toEqual(['test-workflow']) + }) + + it('shows loading spinner when loading is true', () => { + const wrapper = mountCard({ loading: true }) + expect(wrapper.find('.progress-spinner').exists()).toBe(true) + }) + + it('renders audio thumbnail for audio media type', () => { + const wrapper = mountCard({ + template: createTemplate({ mediaType: 'audio' }) + }) + expect(wrapper.find('.mock-audio-thumbnail').exists()).toBe(true) + }) + + it('renders compare slider thumbnail for compareSlider variant', () => { + const wrapper = mountCard({ + template: createTemplate({ thumbnailVariant: 'compareSlider' }) + }) + expect(wrapper.find('.mock-compare-slider').exists()).toBe(true) + }) + + it('renders hover dissolve thumbnail for hoverDissolve variant', () => { + const wrapper = mountCard({ + template: createTemplate({ thumbnailVariant: 'hoverDissolve' }) + }) + expect(wrapper.find('.mock-hover-dissolve').exists()).toBe(true) + }) + + it('renders default thumbnail by default', () => { + const wrapper = mountCard() + expect(wrapper.find('.mock-default-thumbnail').exists()).toBe(true) + }) + + it('passes correct props to default thumbnail for video', () => { + const wrapper = mountCard({ + template: createTemplate({ mediaType: 'video' }) + }) + const thumbnail = wrapper.find('.mock-default-thumbnail') + expect(thumbnail.exists()).toBe(true) + }) + + it('uses zoomHover scale when variant is zoomHover', () => { + const wrapper = mountCard({ + template: createTemplate({ thumbnailVariant: 'zoomHover' }) + }) + expect(wrapper.find('.mock-default-thumbnail').exists()).toBe(true) + }) + + it('displays localized title for default source module', () => { + const wrapper = mountCard({ + sourceModule: 'default', + template: createTemplate({ localizedTitle: 'My Localized Title' }) + }) + expect(wrapper.text()).toContain('My Localized Title') + }) + + it('displays template name as title for non-default source modules', () => { + const wrapper = mountCard({ + sourceModule: 'custom', + template: createTemplate({ name: 'custom-template' }) + }) + expect(wrapper.text()).toContain('custom-template') + }) + + it('displays localized description for default source module', () => { + const wrapper = mountCard({ + sourceModule: 'default', + template: createTemplate({ + localizedDescription: 'My Localized Description' + }) + }) + expect(wrapper.text()).toContain('My Localized Description') + }) + + it('processes description for non-default source modules', () => { + const wrapper = mountCard({ + sourceModule: 'custom', + template: createTemplate({ description: 'custom_module-description' }) + }) + expect(wrapper.text()).toContain('custom module description') + }) + + it('generates correct thumbnail URLs for default source module', () => { + const wrapper = mountCard({ + sourceModule: 'default', + template: createTemplate({ + name: 'my-template', + mediaSubtype: 'jpg' + }) + }) + const vm = wrapper.vm as any + expect(vm.baseThumbnailSrc).toBe('/fileURL/templates/my-template-1.jpg') + expect(vm.overlayThumbnailSrc).toBe('/fileURL/templates/my-template-2.jpg') + }) + + it('generates correct thumbnail URLs for custom source module', () => { + const wrapper = mountCard({ + sourceModule: 'custom-module', + template: createTemplate({ + name: 'my-template', + mediaSubtype: 'png' + }) + }) + const vm = wrapper.vm as any + expect(vm.baseThumbnailSrc).toBe( + '/apiURL/workflow_templates/custom-module/my-template.png' + ) + expect(vm.overlayThumbnailSrc).toBe( + '/apiURL/workflow_templates/custom-module/my-template.png' + ) + }) +}) diff --git a/src/components/templates/TemplateWorkflowCard.vue b/src/components/templates/TemplateWorkflowCard.vue index e1ce70ba1..8b79081ce 100644 --- a/src/components/templates/TemplateWorkflowCard.vue +++ b/src/components/templates/TemplateWorkflowCard.vue @@ -20,6 +20,10 @@ :overlay-image-src="overlayThumbnailSrc" :alt="title" :is-hovered="isHovered" + :is-video=" + template.mediaType === 'video' || + template.mediaSubtype === 'webp' + " />