diff --git a/browser_tests/tests/templates.spec.ts b/browser_tests/tests/templates.spec.ts index 9141e9135b..16f9b24a62 100644 --- a/browser_tests/tests/templates.spec.ts +++ b/browser_tests/tests/templates.spec.ts @@ -80,6 +80,12 @@ test.describe('Templates', () => { // 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() @@ -102,48 +108,72 @@ test.describe('Templates', () => { expect(await comfyPage.templates.content.isVisible()).toBe(true) }) - test('Uses title field as fallback when the key is not found in locales', async ({ + 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 }) => { - // Capture request for the index.json - await comfyPage.page.route('**/templates/index.json', async (route, _) => { - // Add a new template that won't have a translation pre-generated - const response = [ - { - moduleName: 'default', - title: 'FALLBACK CATEGORY', - type: 'image', - templates: [ - { - name: 'unknown_key_has_no_translation_available', - title: 'FALLBACK TEMPLATE NAME', - mediaType: 'image', - mediaSubtype: 'webp', - description: 'No translations found' - } - ] - } - ] + // 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: 200, - body: JSON.stringify(response), - headers: { - 'Content-Type': 'application/json', - 'Cache-Control': 'no-store' - } + 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') - // Expect the title to be used as fallback for template cards + // Verify English titles are shown as fallback await expect( - comfyPage.templates.content.getByText('FALLBACK TEMPLATE NAME') + comfyPage.templates.content.getByRole('heading', { + name: 'Image Generation' + }) ).toBeVisible() - - // 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 ({ @@ -153,25 +183,43 @@ test.describe('Templates', () => { 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 - }) + const firstCard = comfyPage.page + .locator('[data-testid^="template-workflow-"]') + .first() + await expect(firstCard).toBeVisible({ timeout: 5000 }) - // Take snapshot of the template grid - const templateGrid = comfyPage.templates.content.locator('.grid').first() + // Get the template grid + const templateGrid = comfyPage.page.locator( + '[data-testid="template-workflows-content"]' + ) await expect(templateGrid).toBeVisible() - await expect(templateGrid).toHaveScreenshot('template-grid-desktop.png') + + // 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() - await expect(templateGrid).toHaveScreenshot('template-grid-mobile.png') + // 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() - await expect(templateGrid).toHaveScreenshot('template-grid-tablet.png') + // 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 }) => { @@ -179,10 +227,13 @@ test.describe('Templates', () => { await comfyPage.executeCommand('Comfy.BrowseTemplates') await expect(comfyPage.templates.content).toBeVisible() - // Get a template card - const firstCard = comfyPage.page.locator('.template-card').first() + // 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') @@ -257,21 +308,42 @@ test.describe('Templates', () => { 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 }) + // Wait for cards to load 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') + comfyPage.page.locator( + '[data-testid="template-workflow-short-description"]' + ) ).toBeVisible({ timeout: 5000 }) - // Take snapshot of a grid with specific cards - const templateGrid = comfyPage.templates.content - .locator('.grid:has-text("Short Description")') - .first() + // 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' 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 index ce88325aad..6ca212261f 100644 Binary files a/browser_tests/tests/templates.spec.ts-snapshots/template-card-after-hover-chromium-linux.png 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 index bf1d18934d..3af64e7905 100644 Binary files a/browser_tests/tests/templates.spec.ts-snapshots/template-card-before-hover-chromium-linux.png 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-varying-content-chromium-linux.png b/browser_tests/tests/templates.spec.ts-snapshots/template-grid-varying-content-chromium-linux.png index 6c330c2f46..a3635bdaf9 100644 Binary files a/browser_tests/tests/templates.spec.ts-snapshots/template-grid-varying-content-chromium-linux.png and b/browser_tests/tests/templates.spec.ts-snapshots/template-grid-varying-content-chromium-linux.png differ diff --git a/package.json b/package.json index ec94c881bb..9f0cc1ebd8 100644 --- a/package.json +++ b/package.json @@ -123,6 +123,7 @@ "@tiptap/extension-table-row": "^2.10.4", "@tiptap/starter-kit": "^2.10.4", "@vueuse/core": "^11.0.0", + "@vueuse/integrations": "^13.9.0", "@xterm/addon-fit": "^0.10.0", "@xterm/addon-serialize": "^0.13.0", "@xterm/xterm": "^5.5.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6ce1f4d341..dd30b30d92 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -68,6 +68,9 @@ importers: '@vueuse/core': specifier: ^11.0.0 version: 11.0.0(vue@3.5.13(typescript@5.9.2)) + '@vueuse/integrations': + specifier: ^13.9.0 + version: 13.9.0(axios@1.11.0)(fuse.js@7.0.0)(vue@3.5.13(typescript@5.9.2)) '@xterm/addon-fit': specifier: ^0.10.0 version: 0.10.0(@xterm/xterm@5.5.0) @@ -2895,18 +2898,73 @@ packages: '@vueuse/core@12.8.2': resolution: {integrity: sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==} + '@vueuse/core@13.9.0': + resolution: {integrity: sha512-ts3regBQyURfCE2BcytLqzm8+MmLlo5Ln/KLoxDVcsZ2gzIwVNnQpQOL/UKV8alUqjSZOlpFZcRNsLRqj+OzyA==} + peerDependencies: + vue: ^3.5.0 + + '@vueuse/integrations@13.9.0': + resolution: {integrity: sha512-SDobKBbPIOe0cVL7QxMzGkuUGHvWTdihi9zOrrWaWUgFKe15cwEcwfWmgrcNzjT6kHnNmWuTajPHoIzUjYNYYQ==} + peerDependencies: + async-validator: ^4 + axios: ^1 + change-case: ^5 + drauu: ^0.4 + focus-trap: ^7 + fuse.js: ^7 + idb-keyval: ^6 + jwt-decode: ^4 + nprogress: ^0.2 + qrcode: ^1.5 + sortablejs: ^1 + universal-cookie: ^7 || ^8 + vue: ^3.5.0 + peerDependenciesMeta: + async-validator: + optional: true + axios: + optional: true + change-case: + optional: true + drauu: + optional: true + focus-trap: + optional: true + fuse.js: + optional: true + idb-keyval: + optional: true + jwt-decode: + optional: true + nprogress: + optional: true + qrcode: + optional: true + sortablejs: + optional: true + universal-cookie: + optional: true + '@vueuse/metadata@11.0.0': resolution: {integrity: sha512-0TKsAVT0iUOAPWyc9N79xWYfovJVPATiOPVKByG6jmAYdDiwvMVm9xXJ5hp4I8nZDxpCcYlLq/Rg9w1Z/jrGcg==} '@vueuse/metadata@12.8.2': resolution: {integrity: sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==} + '@vueuse/metadata@13.9.0': + resolution: {integrity: sha512-1AFRvuiGphfF7yWixZa0KwjYH8ulyjDCC0aFgrGRz8+P4kvDFSdXLVfTk5xAN9wEuD1J6z4/myMoYbnHoX07zg==} + '@vueuse/shared@11.0.0': resolution: {integrity: sha512-i4ZmOrIEjSsL94uAEt3hz88UCz93fMyP/fba9S+vypX90fKg3uYX9cThqvWc9aXxuTzR0UGhOKOTQd//Goh1nQ==} '@vueuse/shared@12.8.2': resolution: {integrity: sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==} + '@vueuse/shared@13.9.0': + resolution: {integrity: sha512-e89uuTLMh0U5cZ9iDpEI2senqPGfbPRTHM/0AaQkcxnpqjkZqDYP8rpfm7edOz8s+pOCOROEy1PIveSW8+fL5g==} + peerDependencies: + vue: ^3.5.0 + '@webgpu/types@0.1.51': resolution: {integrity: sha512-ktR3u64NPjwIViNCck+z9QeyN0iPkQCUOQ07ZCV1RzlkfP+olLTeEZ95O1QHS+v4w9vJeY9xj/uJuSphsHy5rQ==} @@ -6323,6 +6381,9 @@ packages: vue-component-type-helpers@3.0.7: resolution: {integrity: sha512-TvyUcFXmjZcXUvU+r1MOyn4/vv4iF+tPwg5Ig33l/FJ3myZkxeQpzzQMLMFWcQAjr6Xs7BRwVy/TwbmNZUA/4w==} + vue-component-type-helpers@3.0.8: + resolution: {integrity: sha512-WyR30Eq15Y/+odrUUMax6FmPbZwAp/HnC7qgR1r3lVFAcqwQ4wUoV79Mbh4SxDy3NiqDa+G4TOKD5xXSgBHo5A==} + vue-demi@0.14.10: resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} engines: {node: '>=12'} @@ -8879,7 +8940,7 @@ snapshots: storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)) type-fest: 2.19.0 vue: 3.5.13(typescript@5.9.2) - vue-component-type-helpers: 3.0.7 + vue-component-type-helpers: 3.0.8 '@swc/helpers@0.5.17': dependencies: @@ -9662,10 +9723,28 @@ snapshots: transitivePeerDependencies: - typescript + '@vueuse/core@13.9.0(vue@3.5.13(typescript@5.9.2))': + dependencies: + '@types/web-bluetooth': 0.0.21 + '@vueuse/metadata': 13.9.0 + '@vueuse/shared': 13.9.0(vue@3.5.13(typescript@5.9.2)) + vue: 3.5.13(typescript@5.9.2) + + '@vueuse/integrations@13.9.0(axios@1.11.0)(fuse.js@7.0.0)(vue@3.5.13(typescript@5.9.2))': + dependencies: + '@vueuse/core': 13.9.0(vue@3.5.13(typescript@5.9.2)) + '@vueuse/shared': 13.9.0(vue@3.5.13(typescript@5.9.2)) + vue: 3.5.13(typescript@5.9.2) + optionalDependencies: + axios: 1.11.0 + fuse.js: 7.0.0 + '@vueuse/metadata@11.0.0': {} '@vueuse/metadata@12.8.2': {} + '@vueuse/metadata@13.9.0': {} + '@vueuse/shared@11.0.0(vue@3.5.13(typescript@5.9.2))': dependencies: vue-demi: 0.14.10(vue@3.5.13(typescript@5.9.2)) @@ -9679,6 +9758,10 @@ snapshots: transitivePeerDependencies: - typescript + '@vueuse/shared@13.9.0(vue@3.5.13(typescript@5.9.2))': + dependencies: + vue: 3.5.13(typescript@5.9.2) + '@webgpu/types@0.1.51': {} '@xterm/addon-fit@0.10.0(@xterm/xterm@5.5.0)': @@ -13545,6 +13628,8 @@ snapshots: vue-component-type-helpers@3.0.7: {} + vue-component-type-helpers@3.0.8: {} + vue-demi@0.14.10(vue@3.5.13(typescript@5.9.2)): dependencies: vue: 3.5.13(typescript@5.9.2) diff --git a/public/assets/images/default-template.png b/public/assets/images/default-template.png new file mode 100644 index 0000000000..8aeab89cc0 Binary files /dev/null and b/public/assets/images/default-template.png differ diff --git a/src/assets/icons/custom/dark-info.svg b/src/assets/icons/custom/dark-info.svg new file mode 100644 index 0000000000..26c05560f2 --- /dev/null +++ b/src/assets/icons/custom/dark-info.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/components/card/CardBottom.vue b/src/components/card/CardBottom.vue index 7f35754e6c..4a0ae10475 100644 --- a/src/components/card/CardBottom.vue +++ b/src/components/card/CardBottom.vue @@ -1,7 +1,19 @@ - + diff --git a/src/components/card/CardContainer.vue b/src/components/card/CardContainer.vue index 1a17d5659b..6b20db975a 100644 --- a/src/components/card/CardContainer.vue +++ b/src/components/card/CardContainer.vue @@ -1,5 +1,5 @@