Files
ComfyUI_frontend/browser_tests/tests/nodeTemplates.spec.ts
pythongosssss 0052cdadd4 test: e2e coverage for node templates (#11564)
## Summary

Add E2E tests for node templates

## Changes

- **What**: 
- add tests for save, insert, delete, import export
- vue and litegraph
- add testid to dialog
- update `clickLitegraphMenuItem` to enable clicking children with the
same name as parent

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11564-test-e2e-coverage-for-node-templates-34b6d73d365081a39ce5c713f05a2a92)
by [Unito](https://www.unito.io)
2026-04-23 19:33:18 +00:00

165 lines
5.4 KiB
TypeScript

import fs from 'node:fs'
import type { TestInfo } from '@playwright/test'
import { expect } from '@playwright/test'
import { nodeTemplatesFixture as test } from '@e2e/fixtures/nodeTemplatesFixture'
type NodeMode = 'vue' | 'litegraph'
function templateName(mode: NodeMode | 'shared', testInfo: TestInfo) {
return `tpl-${mode}-${testInfo.parallelIndex}-${testInfo.title.replace(/\W+/g, '-')}`
}
function isStoreTemplatesRequest(url: string, method: string): boolean {
return method === 'POST' && url.includes('/api/userdata/comfy.templates.json')
}
function defineNodeTemplatesTests(mode: NodeMode) {
test.afterEach(async ({ comfyPage }) => {
await comfyPage.canvasOps.resetView()
})
test('Save Selected as Template persists the selection under the given name', async ({
nodeTemplates
}, testInfo) => {
const name = templateName(mode, testInfo)
const { manageDialog } = nodeTemplates
await nodeTemplates.saveKSamplerAsTemplate(name)
await test.step('Manage dialog lists the saved template', async () => {
await nodeTemplates.openManageDialog()
await expect(manageDialog.rowByName(name)).toHaveCount(1)
await manageDialog.close()
})
})
test('Saved template appears in the submenu and pastes nodes on click', async ({
comfyPage,
nodeTemplates
}, testInfo) => {
const name = templateName(mode, testInfo)
await nodeTemplates.saveKSamplerAsTemplate(name)
const initialNodeCount = await comfyPage.nodeOps.getGraphNodesCount()
await test.step('Insert template from canvas submenu', async () => {
await nodeTemplates.insertTemplate(name)
})
await expect
.poll(() => comfyPage.nodeOps.getGraphNodesCount())
.toBe(initialNodeCount + 1)
})
test('Deleting a template in the manage dialog removes it from the list', async ({
comfyPage,
nodeTemplates
}, testInfo) => {
const name = templateName(mode, testInfo)
const { manageDialog } = nodeTemplates
await nodeTemplates.saveKSamplerAsTemplate(name)
await test.step('Delete the template and assert it is persisted server-side', async () => {
await nodeTemplates.openManageDialog()
const row = manageDialog.rowByName(name)
await expect(row).toHaveCount(1)
const storeResponse = comfyPage.page.waitForResponse((res) =>
isStoreTemplatesRequest(res.url(), res.request().method())
)
await row.getByRole('button', { name: 'Delete' }).click()
const response = await storeResponse
expect(response.ok()).toBe(true)
const stored = JSON.parse(response.request().postData() ?? '') as {
name: string
data: string
}[]
expect(stored.map((t) => t.name)).not.toContain(name)
await expect(row).toHaveCount(0)
await manageDialog.close()
})
})
}
test.describe('Node Templates', { tag: ['@canvas'] }, () => {
test.describe('Vue nodes', { tag: ['@vue-nodes'] }, () => {
defineNodeTemplatesTests('vue')
})
test.describe('Litegraph nodes', () => {
defineNodeTemplatesTests('litegraph')
})
// Import/Export are dialog-only flows unaffected by node rendering mode;
// run once outside the Vue/Litegraph matrix.
test.describe('Dialog import/export', () => {
test.afterEach(async ({ comfyPage }) => {
await comfyPage.canvasOps.resetView()
})
test('Export downloads a JSON file containing the saved template', async ({
comfyPage,
nodeTemplates
}, testInfo) => {
const name = templateName('shared', testInfo)
const { manageDialog } = nodeTemplates
await nodeTemplates.saveKSamplerAsTemplate(name)
await test.step('Export and verify the downloaded file contents', async () => {
await nodeTemplates.openManageDialog()
const downloadPromise = comfyPage.page.waitForEvent('download')
await manageDialog
.rowByName(name)
.getByRole('button', { name: 'Export' })
.click()
const download = await downloadPromise
expect(download.suggestedFilename()).toContain(name)
const downloadPath = await download.path()
if (!downloadPath) throw new Error('Download path unavailable')
const parsed = JSON.parse(fs.readFileSync(downloadPath, 'utf-8')) as {
templates: { name: string; data: string }[]
}
expect(parsed.templates.map((t) => t.name)).toEqual([name])
await manageDialog.close()
})
})
test('Import adds templates from a JSON file', async ({
comfyPage,
nodeTemplates
}, testInfo) => {
const name = templateName('shared', testInfo)
const { manageDialog } = nodeTemplates
const payload = {
templates: [{ name, data: JSON.stringify({ nodes: [] }) }]
}
await nodeTemplates.openManageDialog()
await expect(manageDialog.rowByName(name)).toHaveCount(0)
const storeResponse = comfyPage.page.waitForResponse((res) =>
isStoreTemplatesRequest(res.url(), res.request().method())
)
await manageDialog.importInput.setInputFiles({
name: 'templates.json',
mimeType: 'application/json',
buffer: Buffer.from(JSON.stringify(payload))
})
expect((await storeResponse).ok()).toBe(true)
await manageDialog.waitForHidden()
await nodeTemplates.openManageDialog()
await expect(manageDialog.rowByName(name)).toHaveCount(1)
await manageDialog.close()
})
})
})