diff --git a/browser_tests/fixtures/data/templateFixtures.ts b/browser_tests/fixtures/data/templateFixtures.ts index fdf40d9528..af6efa7783 100644 --- a/browser_tests/fixtures/data/templateFixtures.ts +++ b/browser_tests/fixtures/data/templateFixtures.ts @@ -2,6 +2,11 @@ import type { TemplateInfo, WorkflowTemplates } from '@/platform/workflow/templates/types/template' +import { TemplateIncludeOnDistributionEnum } from '@/platform/workflow/templates/types/template' + +const Cloud = TemplateIncludeOnDistributionEnum.Cloud +const Desktop = TemplateIncludeOnDistributionEnum.Desktop +const Local = TemplateIncludeOnDistributionEnum.Local export function makeTemplate( overrides: Partial & Pick @@ -26,3 +31,33 @@ export function mockTemplateIndex( } ] } + +export const STABLE_CLOUD_TEMPLATE: TemplateInfo = makeTemplate({ + name: 'cloud-stable', + title: 'Cloud Stable', + includeOnDistributions: [Cloud] +}) + +export const STABLE_DESKTOP_TEMPLATE: TemplateInfo = makeTemplate({ + name: 'desktop-stable', + title: 'Desktop Stable', + includeOnDistributions: [Desktop] +}) + +export const STABLE_LOCAL_TEMPLATE: TemplateInfo = makeTemplate({ + name: 'local-stable', + title: 'Local Stable', + includeOnDistributions: [Local] +}) + +export const STABLE_UNRESTRICTED_TEMPLATE: TemplateInfo = makeTemplate({ + name: 'unrestricted-stable', + title: 'Unrestricted Stable' +}) + +export const ALL_DISTRIBUTION_TEMPLATES: TemplateInfo[] = [ + STABLE_CLOUD_TEMPLATE, + STABLE_DESKTOP_TEMPLATE, + STABLE_LOCAL_TEMPLATE, + STABLE_UNRESTRICTED_TEMPLATE +] diff --git a/browser_tests/fixtures/helpers/TemplateHelper.ts b/browser_tests/fixtures/helpers/TemplateHelper.ts new file mode 100644 index 0000000000..7ae904e890 --- /dev/null +++ b/browser_tests/fixtures/helpers/TemplateHelper.ts @@ -0,0 +1,198 @@ +import type { Page, Route } from '@playwright/test' + +import type { + TemplateInfo, + WorkflowTemplates +} from '@/platform/workflow/templates/types/template' +import { TemplateIncludeOnDistributionEnum } from '@/platform/workflow/templates/types/template' +import { + makeTemplate, + mockTemplateIndex +} from '@e2e/fixtures/data/templateFixtures' + +/** + * Generate N deterministic templates, optionally restricted to a distribution. + * + * Lives here (not in `fixtures/data/`) because `fixtures/data/` is reserved + * for static test data with no executable fixture logic. + */ +function generateTemplates( + count: number, + distribution?: TemplateIncludeOnDistributionEnum +): TemplateInfo[] { + const slug = distribution ?? 'unrestricted' + return Array.from({ length: count }, (_, i) => + makeTemplate({ + name: `gen-${slug}-${String(i + 1).padStart(3, '0')}`, + title: `Generated ${slug} ${i + 1}`, + ...(distribution ? { includeOnDistributions: [distribution] } : {}) + }) + ) +} + +export interface TemplateConfig { + readonly templates: readonly TemplateInfo[] + readonly index: readonly WorkflowTemplates[] | null +} + +function emptyConfig(): TemplateConfig { + return { templates: [], index: null } +} + +export type TemplateOperator = (config: TemplateConfig) => TemplateConfig + +function cloneTemplates(templates: readonly TemplateInfo[]): TemplateInfo[] { + return templates.map((t) => structuredClone(t)) +} + +function cloneIndex( + index: readonly WorkflowTemplates[] | null +): WorkflowTemplates[] | null { + return index ? index.map((m) => structuredClone(m)) : null +} + +function addTemplates( + config: TemplateConfig, + templates: TemplateInfo[] +): TemplateConfig { + return { ...config, templates: [...config.templates, ...templates] } +} + +export function withTemplates(templates: TemplateInfo[]): TemplateOperator { + return (config) => addTemplates(config, templates) +} + +export function withTemplate(template: TemplateInfo): TemplateOperator { + return (config) => addTemplates(config, [template]) +} + +export function withCloudTemplates(count: number): TemplateOperator { + return (config) => + addTemplates( + config, + generateTemplates(count, TemplateIncludeOnDistributionEnum.Cloud) + ) +} + +export function withDesktopTemplates(count: number): TemplateOperator { + return (config) => + addTemplates( + config, + generateTemplates(count, TemplateIncludeOnDistributionEnum.Desktop) + ) +} + +export function withLocalTemplates(count: number): TemplateOperator { + return (config) => + addTemplates( + config, + generateTemplates(count, TemplateIncludeOnDistributionEnum.Local) + ) +} + +export function withUnrestrictedTemplates(count: number): TemplateOperator { + return (config) => addTemplates(config, generateTemplates(count)) +} + +/** + * Override the index payload entirely. Useful when a test needs a custom + * `WorkflowTemplates[]` shape (e.g. multiple modules). + */ +export function withRawIndex(index: WorkflowTemplates[]): TemplateOperator { + return (config) => ({ ...config, index }) +} + +export class TemplateHelper { + private templates: TemplateInfo[] + private index: WorkflowTemplates[] | null + private routeHandlers: Array<{ + pattern: string + handler: (route: Route) => Promise + }> = [] + + constructor( + private readonly page: Page, + config: TemplateConfig = emptyConfig() + ) { + this.templates = cloneTemplates(config.templates) + this.index = cloneIndex(config.index) + } + + configure(...operators: TemplateOperator[]): void { + const config = operators.reduce( + (cfg, op) => op(cfg), + emptyConfig() + ) + this.templates = cloneTemplates(config.templates) + this.index = cloneIndex(config.index) + } + + async mock(): Promise { + await this.mockIndex() + await this.mockThumbnails() + } + + async mockIndex(): Promise { + const indexHandler = async (route: Route) => { + const payload = this.index ?? mockTemplateIndex(this.templates) + await route.fulfill({ + status: 200, + body: JSON.stringify(payload), + headers: { + 'Content-Type': 'application/json', + 'Cache-Control': 'no-store' + } + }) + } + const indexPattern = '**/templates/index.json' + this.routeHandlers.push({ pattern: indexPattern, handler: indexHandler }) + await this.page.route(indexPattern, indexHandler) + } + + async mockThumbnails(): Promise { + const thumbnailHandler = async (route: Route) => { + await route.fulfill({ + status: 200, + path: 'browser_tests/assets/example.webp', + headers: { + 'Content-Type': 'image/webp', + 'Cache-Control': 'no-store' + } + }) + } + const thumbnailPattern = '**/templates/**.webp' + this.routeHandlers.push({ + pattern: thumbnailPattern, + handler: thumbnailHandler + }) + await this.page.route(thumbnailPattern, thumbnailHandler) + } + + getTemplates(): TemplateInfo[] { + return cloneTemplates(this.templates) + } + + get templateCount(): number { + return this.templates.length + } + + async clearMocks(): Promise { + for (const { pattern, handler } of this.routeHandlers) { + await this.page.unroute(pattern, handler) + } + this.routeHandlers = [] + this.templates = [] + this.index = null + } +} + +export function createTemplateHelper( + page: Page, + ...operators: TemplateOperator[] +): TemplateHelper { + const config = operators.reduce( + (cfg, op) => op(cfg), + emptyConfig() + ) + return new TemplateHelper(page, config) +} diff --git a/browser_tests/fixtures/templateApiFixture.ts b/browser_tests/fixtures/templateApiFixture.ts new file mode 100644 index 0000000000..eaa9f8d669 --- /dev/null +++ b/browser_tests/fixtures/templateApiFixture.ts @@ -0,0 +1,16 @@ +import { test as base } from '@playwright/test' + +import type { TemplateHelper } from '@e2e/fixtures/helpers/TemplateHelper' +import { createTemplateHelper } from '@e2e/fixtures/helpers/TemplateHelper' + +export const templateApiFixture = base.extend<{ + templateApi: TemplateHelper +}>({ + templateApi: async ({ page }, use) => { + const templateApi = createTemplateHelper(page) + + await use(templateApi) + + await templateApi.clearMocks() + } +}) diff --git a/browser_tests/tests/templateFilteringCount.spec.ts b/browser_tests/tests/templateFilteringCount.spec.ts index 21165b0715..59f7d898b6 100644 --- a/browser_tests/tests/templateFilteringCount.spec.ts +++ b/browser_tests/tests/templateFilteringCount.spec.ts @@ -1,13 +1,13 @@ -import { expect } from '@playwright/test' +import { expect, mergeTests } from '@playwright/test' -import type { TemplateInfo } from '@/platform/workflow/templates/types/template' import { TemplateIncludeOnDistributionEnum } from '@/platform/workflow/templates/types/template' -import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage' -import { - makeTemplate, - mockTemplateIndex -} from '@e2e/fixtures/data/templateFixtures' +import { comfyPageFixture } from '@e2e/fixtures/ComfyPage' +import { makeTemplate } from '@e2e/fixtures/data/templateFixtures' +import { withTemplates } from '@e2e/fixtures/helpers/TemplateHelper' import { TestIds } from '@e2e/fixtures/selectors' +import { templateApiFixture } from '@e2e/fixtures/templateApiFixture' + +const test = mergeTests(comfyPageFixture, templateApiFixture) const Cloud = TemplateIncludeOnDistributionEnum.Cloud const Desktop = TemplateIncludeOnDistributionEnum.Desktop @@ -17,7 +17,7 @@ test.describe( 'Template distribution filtering count', { tag: '@cloud' }, () => { - test.beforeEach(async ({ comfyPage }) => { + test.beforeEach(async ({ comfyPage, templateApi }) => { await comfyPage.settings.setSetting('Comfy.Templates.SelectedModels', []) await comfyPage.settings.setSetting( 'Comfy.Templates.SelectedUseCases', @@ -26,53 +26,37 @@ test.describe( await comfyPage.settings.setSetting('Comfy.Templates.SelectedRunsOn', []) await comfyPage.settings.setSetting('Comfy.Templates.SortBy', 'default') - await comfyPage.page.route('**/templates/**.webp', async (route) => { - await route.fulfill({ - status: 200, - path: 'browser_tests/assets/example.webp', - headers: { - 'Content-Type': 'image/webp', - 'Cache-Control': 'no-store' - } - }) - }) + await templateApi.mockThumbnails() }) test('displayed count matches visible cards when distribution filter excludes templates', async ({ - comfyPage + comfyPage, + templateApi }) => { - const templates: TemplateInfo[] = [ - makeTemplate({ - name: 'cloud-1', - title: 'Cloud One', - includeOnDistributions: [Cloud] - }), - makeTemplate({ - name: 'cloud-2', - title: 'Cloud Two', - includeOnDistributions: [Cloud] - }), - makeTemplate({ - name: 'desktop-hidden', - title: 'Desktop Hidden', - includeOnDistributions: [Desktop] - }), - makeTemplate({ - name: 'universal', - title: 'Universal' - }) - ] - - await comfyPage.page.route('**/templates/index.json', async (route) => { - await route.fulfill({ - status: 200, - body: JSON.stringify(mockTemplateIndex(templates)), - headers: { - 'Content-Type': 'application/json', - 'Cache-Control': 'no-store' - } - }) - }) + templateApi.configure( + withTemplates([ + makeTemplate({ + name: 'cloud-1', + title: 'Cloud One', + includeOnDistributions: [Cloud] + }), + makeTemplate({ + name: 'cloud-2', + title: 'Cloud Two', + includeOnDistributions: [Cloud] + }), + makeTemplate({ + name: 'desktop-hidden', + title: 'Desktop Hidden', + includeOnDistributions: [Desktop] + }), + makeTemplate({ + name: 'universal', + title: 'Universal' + }) + ]) + ) + await templateApi.mockIndex() await comfyPage.command.executeCommand('Comfy.BrowseTemplates') await expect(comfyPage.templates.content).toBeVisible() @@ -86,45 +70,38 @@ test.describe( }) test('filtered count reflects distribution + model filter together', async ({ - comfyPage + comfyPage, + templateApi }) => { - const templates: TemplateInfo[] = [ - makeTemplate({ - name: 'wan-cloud-1', - title: 'Wan Cloud 1', - models: ['Wan 2.2'], - includeOnDistributions: [Cloud] - }), - makeTemplate({ - name: 'wan-cloud-2', - title: 'Wan Cloud 2', - models: ['Wan 2.2'], - includeOnDistributions: [Cloud] - }), - makeTemplate({ - name: 'wan-desktop', - title: 'Wan Desktop', - models: ['Wan 2.2'], - includeOnDistributions: [Desktop] - }), - makeTemplate({ - name: 'flux-cloud', - title: 'Flux Cloud', - models: ['Flux'], - includeOnDistributions: [Cloud] - }) - ] - - await comfyPage.page.route('**/templates/index.json', async (route) => { - await route.fulfill({ - status: 200, - body: JSON.stringify(mockTemplateIndex(templates)), - headers: { - 'Content-Type': 'application/json', - 'Cache-Control': 'no-store' - } - }) - }) + templateApi.configure( + withTemplates([ + makeTemplate({ + name: 'wan-cloud-1', + title: 'Wan Cloud 1', + models: ['Wan 2.2'], + includeOnDistributions: [Cloud] + }), + makeTemplate({ + name: 'wan-cloud-2', + title: 'Wan Cloud 2', + models: ['Wan 2.2'], + includeOnDistributions: [Cloud] + }), + makeTemplate({ + name: 'wan-desktop', + title: 'Wan Desktop', + models: ['Wan 2.2'], + includeOnDistributions: [Desktop] + }), + makeTemplate({ + name: 'flux-cloud', + title: 'Flux Cloud', + models: ['Flux'], + includeOnDistributions: [Cloud] + }) + ]) + ) + await templateApi.mockIndex() await comfyPage.command.executeCommand('Comfy.BrowseTemplates') await expect(comfyPage.templates.content).toBeVisible() @@ -144,36 +121,29 @@ test.describe( }) test('desktop-only templates never leak into DOM on cloud distribution', async ({ - comfyPage + comfyPage, + templateApi }) => { - const templates: TemplateInfo[] = [ - makeTemplate({ - name: 'cloud-visible', - title: 'Cloud Visible', - includeOnDistributions: [Cloud] - }), - makeTemplate({ - name: 'desktop-leak-check', - title: 'Desktop Leak Check', - includeOnDistributions: [Desktop] - }), - makeTemplate({ - name: 'local-leak-check', - title: 'Local Leak Check', - includeOnDistributions: [Local] - }) - ] - - await comfyPage.page.route('**/templates/index.json', async (route) => { - await route.fulfill({ - status: 200, - body: JSON.stringify(mockTemplateIndex(templates)), - headers: { - 'Content-Type': 'application/json', - 'Cache-Control': 'no-store' - } - }) - }) + templateApi.configure( + withTemplates([ + makeTemplate({ + name: 'cloud-visible', + title: 'Cloud Visible', + includeOnDistributions: [Cloud] + }), + makeTemplate({ + name: 'desktop-leak-check', + title: 'Desktop Leak Check', + includeOnDistributions: [Desktop] + }), + makeTemplate({ + name: 'local-leak-check', + title: 'Local Leak Check', + includeOnDistributions: [Local] + }) + ]) + ) + await templateApi.mockIndex() await comfyPage.command.executeCommand('Comfy.BrowseTemplates') await expect(comfyPage.templates.content).toBeVisible() @@ -200,28 +170,21 @@ test.describe( }) test('templates without includeOnDistributions are visible on cloud', async ({ - comfyPage + comfyPage, + templateApi }) => { - const templates: TemplateInfo[] = [ - makeTemplate({ name: 'unrestricted-1', title: 'Unrestricted 1' }), - makeTemplate({ name: 'unrestricted-2', title: 'Unrestricted 2' }), - makeTemplate({ - name: 'cloud-only', - title: 'Cloud Only', - includeOnDistributions: [Cloud] - }) - ] - - await comfyPage.page.route('**/templates/index.json', async (route) => { - await route.fulfill({ - status: 200, - body: JSON.stringify(mockTemplateIndex(templates)), - headers: { - 'Content-Type': 'application/json', - 'Cache-Control': 'no-store' - } - }) - }) + templateApi.configure( + withTemplates([ + makeTemplate({ name: 'unrestricted-1', title: 'Unrestricted 1' }), + makeTemplate({ name: 'unrestricted-2', title: 'Unrestricted 2' }), + makeTemplate({ + name: 'cloud-only', + title: 'Cloud Only', + includeOnDistributions: [Cloud] + }) + ]) + ) + await templateApi.mockIndex() await comfyPage.command.executeCommand('Comfy.BrowseTemplates') await expect(comfyPage.templates.content).toBeVisible() @@ -234,39 +197,32 @@ test.describe( }) test('clear filters button resets to correct distribution-filtered total', async ({ - comfyPage + comfyPage, + templateApi }) => { - const templates: TemplateInfo[] = [ - makeTemplate({ - name: 'wan-cloud', - title: 'Wan Cloud', - models: ['Wan 2.2'], - includeOnDistributions: [Cloud] - }), - makeTemplate({ - name: 'flux-cloud', - title: 'Flux Cloud', - models: ['Flux'], - includeOnDistributions: [Cloud] - }), - makeTemplate({ - name: 'wan-desktop', - title: 'Wan Desktop', - models: ['Wan 2.2'], - includeOnDistributions: [Desktop] - }) - ] - - await comfyPage.page.route('**/templates/index.json', async (route) => { - await route.fulfill({ - status: 200, - body: JSON.stringify(mockTemplateIndex(templates)), - headers: { - 'Content-Type': 'application/json', - 'Cache-Control': 'no-store' - } - }) - }) + templateApi.configure( + withTemplates([ + makeTemplate({ + name: 'wan-cloud', + title: 'Wan Cloud', + models: ['Wan 2.2'], + includeOnDistributions: [Cloud] + }), + makeTemplate({ + name: 'flux-cloud', + title: 'Flux Cloud', + models: ['Flux'], + includeOnDistributions: [Cloud] + }), + makeTemplate({ + name: 'wan-desktop', + title: 'Wan Desktop', + models: ['Wan 2.2'], + includeOnDistributions: [Desktop] + }) + ]) + ) + await templateApi.mockIndex() await comfyPage.command.executeCommand('Comfy.BrowseTemplates') await expect(comfyPage.templates.content).toBeVisible()