diff --git a/browser_tests/assets/node_template_templates.json b/browser_tests/assets/node_template_templates.json new file mode 100644 index 000000000..54d293d24 --- /dev/null +++ b/browser_tests/assets/node_template_templates.json @@ -0,0 +1,10 @@ +[ + { + "name": "Three Nodes Template", + "data": "{\"nodes\":[{\"id\":7,\"type\":\"CLIPTextEncode\",\"pos\":[413,389],\"size\":[425.27801513671875,180.6060791015625],\"flags\":{},\"order\":3,\"mode\":0,\"inputs\":[{\"name\":\"clip\",\"type\":\"CLIP\",\"link\":null}],\"outputs\":[{\"name\":\"CONDITIONING\",\"type\":\"CONDITIONING\",\"links\":[],\"slot_index\":0}],\"properties\":{\"Node name for S&R\":\"CLIPTextEncode\"},\"widgets_values\":[\"text, watermark\"]},{\"id\":6,\"type\":\"CLIPTextEncode\",\"pos\":[415,186],\"size\":[422.84503173828125,164.31304931640625],\"flags\":{},\"order\":2,\"mode\":0,\"inputs\":[{\"name\":\"clip\",\"type\":\"CLIP\",\"link\":null}],\"outputs\":[{\"name\":\"CONDITIONING\",\"type\":\"CONDITIONING\",\"links\":[],\"slot_index\":0}],\"properties\":{\"Node name for S&R\":\"CLIPTextEncode\"},\"widgets_values\":[\"beautiful scenery nature glass bottle landscape, , purple galaxy bottle,\"]},{\"id\":4,\"type\":\"CheckpointLoaderSimple\",\"pos\":[26,474],\"size\":[315,98],\"flags\":{},\"order\":1,\"mode\":0,\"inputs\":[],\"outputs\":[{\"name\":\"MODEL\",\"type\":\"MODEL\",\"links\":[],\"slot_index\":0},{\"name\":\"CLIP\",\"type\":\"CLIP\",\"links\":[],\"slot_index\":1},{\"name\":\"VAE\",\"type\":\"VAE\",\"links\":[],\"slot_index\":2}],\"properties\":{\"Node name for S&R\":\"CheckpointLoaderSimple\"},\"widgets_values\":[\"v1-5-pruned-emaonly.ckpt\"]}],\"groups\":[],\"reroutes\":[],\"links\":[{\"id\":5,\"origin_id\":4,\"origin_slot\":1,\"target_id\":7,\"target_slot\":0,\"type\":\"CLIP\"},{\"id\":3,\"origin_id\":4,\"origin_slot\":1,\"target_id\":6,\"target_slot\":0,\"type\":\"CLIP\"}]}" + }, + { + "name": "Completely empty template", + "data": "{\"nodes\":[],\"groups\":[],\"reroutes\":[],\"links\":[]}" + } +] \ No newline at end of file diff --git a/browser_tests/assets/vintage_clipboard_template.json b/browser_tests/assets/vintage_clipboard_template.json new file mode 100644 index 000000000..d0c27c624 --- /dev/null +++ b/browser_tests/assets/vintage_clipboard_template.json @@ -0,0 +1,6 @@ +[ + { + "name": "vintageClipboard Template", + "data": "{\"nodes\":[{\"id\":-1,\"type\":\"CheckpointLoaderSimple\",\"pos\":[26,474],\"size\":[315,98],\"flags\":{},\"order\":1,\"mode\":0,\"inputs\":[],\"outputs\":[{\"name\":\"MODEL\",\"type\":\"MODEL\",\"links\":[],\"slot_index\":0},{\"name\":\"CLIP\",\"type\":\"CLIP\",\"links\":[],\"slot_index\":1},{\"name\":\"VAE\",\"type\":\"VAE\",\"links\":[],\"slot_index\":2}],\"properties\":{\"Node name for S&R\":\"CheckpointLoaderSimple\"},\"widgets_values\":[\"v1-5-pruned-emaonly.ckpt\"]},{\"id\":-1,\"type\":\"CLIPTextEncode\",\"pos\":[415,186],\"size\":[422.84503173828125,164.31304931640625],\"flags\":{},\"order\":2,\"mode\":0,\"inputs\":[{\"name\":\"clip\",\"type\":\"CLIP\",\"link\":null}],\"outputs\":[{\"name\":\"CONDITIONING\",\"type\":\"CONDITIONING\",\"links\":[],\"slot_index\":0}],\"properties\":{\"Node name for S&R\":\"CLIPTextEncode\"},\"widgets_values\":[\"beautiful scenery nature glass bottle landscape, , purple galaxy bottle,\"]},{\"id\":-1,\"type\":\"CLIPTextEncode\",\"pos\":[413,389],\"size\":[425.27801513671875,180.6060791015625],\"flags\":{},\"order\":3,\"mode\":0,\"inputs\":[{\"name\":\"clip\",\"type\":\"CLIP\",\"link\":null}],\"outputs\":[{\"name\":\"CONDITIONING\",\"type\":\"CONDITIONING\",\"links\":[],\"slot_index\":0}],\"properties\":{\"Node name for S&R\":\"CLIPTextEncode\"},\"widgets_values\":[\"text, watermark\"]}],\"links\":[[0,1,1,0,4],[0,1,2,0,4]]}" + } +] \ No newline at end of file diff --git a/browser_tests/dialog.spec.ts b/browser_tests/dialog.spec.ts index 28f101ede..4d790f8f5 100644 --- a/browser_tests/dialog.spec.ts +++ b/browser_tests/dialog.spec.ts @@ -94,7 +94,7 @@ test.describe('Settings', () => { test('Can change canvas zoom speed setting', async ({ comfyPage }) => { const maxSpeed = 2.5 await comfyPage.setSetting('Comfy.Graph.ZoomSpeed', maxSpeed) - test.step('Setting should persist', async () => { + await test.step('Setting should persist', async () => { expect(await comfyPage.getSetting('Comfy.Graph.ZoomSpeed')).toBe(maxSpeed) }) }) diff --git a/browser_tests/fixtures/ComfyPage.ts b/browser_tests/fixtures/ComfyPage.ts index 3679ca5bd..65df4b91e 100644 --- a/browser_tests/fixtures/ComfyPage.ts +++ b/browser_tests/fixtures/ComfyPage.ts @@ -77,6 +77,7 @@ export class ComfyPage { // All canvas position operations are based on default view of canvas. public readonly canvas: Locator public readonly widgetTextBox: Locator + public readonly contextMenu: Locator // Buttons public readonly resetViewButton: Locator @@ -107,6 +108,7 @@ export class ComfyPage { this.url = process.env.PLAYWRIGHT_TEST_URL || 'http://localhost:8188' this.canvas = page.locator('#graph-canvas') this.widgetTextBox = page.getByPlaceholder('text').nth(1) + this.contextMenu = page.locator('.litegraph.litecontextmenu') this.resetViewButton = page.getByRole('button', { name: 'Reset View' }) this.queueButton = page.getByRole('button', { name: 'Queue Prompt' }) this.workflowUploadInput = page.locator('#comfy-file-input') @@ -148,6 +150,12 @@ export class ComfyPage { }) } + async getGraphSelectedItemsCount(): Promise { + return await this.page.evaluate(() => { + return window['app']?.canvas?.selectedItems?.size + }) + } + async setupWorkflowsDirectory(structure: FolderStructure) { const resp = await this.request.post( `${this.url}/api/devtools/setup_folder_structure`, @@ -191,6 +199,39 @@ export class ComfyPage { return await resp.json() } + async clearNodeTemplates() { + const resp = await this.request.delete( + `${this.url}/api/userdata/comfy.templates.json`, + { + headers: { 'Comfy-User': this.id } + } + ) + + const status = resp.status() + if (status !== 204 && status !== 404) + throw new Error(`Failed to delete node templates: ${await resp.text()}`) + } + + async setNodeTemplates(fileName: string) { + const path = this.assetPath(fileName) + const data = fs.readFileSync(path, 'utf-8') + + const resp = await this.request.post( + `${this.url}/api/userdata/comfy.templates.json`, + { + headers: { + 'Comfy-User': this.id, + overwrite: 'true', + full_info: 'true' + }, + data + } + ) + + if (resp.status() !== 200) + throw new Error(`Failed to upload node templates: ${await resp.text()}`) + } + async setupSettings(settings: Record) { const resp = await this.request.post( `${this.url}/api/devtools/set_settings`, @@ -399,11 +440,17 @@ export class ComfyPage { await this.nextFrame() } - async dragAndDrop(source: Position, target: Position) { + async dragAndDrop( + source: Position, + target: Position, + modifierKey?: 'ControlOrMeta' | 'Control' | 'Alt' | 'Shift' + ) { + if (modifierKey) await this.page.keyboard.down(modifierKey) await this.page.mouse.move(source.x, source.y) await this.page.mouse.down() await this.page.mouse.move(target.x, target.y) await this.page.mouse.up() + if (modifierKey) await this.page.keyboard.up(modifierKey) await this.nextFrame() } @@ -551,8 +598,11 @@ export class ComfyPage { } async rightClickCanvas() { - await this.page.mouse.click(10, 10, { button: 'right' }) - await this.nextFrame() + await this.canvas.click({ + position: { x: 10, y: 10 }, + button: 'right' + }) + await expect(this.contextMenu).toBeVisible() } async doubleClickCanvas() { @@ -567,7 +617,7 @@ export class ComfyPage { y: 625 } }) - this.page.mouse.move(10, 10) + await this.page.mouse.move(10, 10) await this.nextFrame() } @@ -579,10 +629,14 @@ export class ComfyPage { }, button: 'right' }) - this.page.mouse.move(10, 10) + await this.page.mouse.move(10, 10) await this.nextFrame() } + async clickContextMenuItem(name: string): Promise { + await this.page.getByRole('menuitem', { name }).click() + } + async select2Nodes() { // Select 2 CLIP nodes. await this.page.keyboard.down('Control') diff --git a/browser_tests/nodeTemplate.spec.ts b/browser_tests/nodeTemplate.spec.ts new file mode 100644 index 000000000..9a7cb7d87 --- /dev/null +++ b/browser_tests/nodeTemplate.spec.ts @@ -0,0 +1,70 @@ +import { + comfyPageFixture as test, + comfyExpect as expect +} from './fixtures/ComfyPage' + +// Old `nodeTemplate.ts` system +test.describe('Node Template', () => { + test.afterEach(async ({ comfyPage }) => { + await comfyPage.clearNodeTemplates() + }) + + test('Can create and use node template', async ({ comfyPage }) => { + const templateName = 'Can create node template template' + + await comfyPage.clearNodeTemplates() + await comfyPage.reload() + + // TODO: Flaky test. Right click requires delay after reload, but other interactions do not. + await comfyPage.page.waitForTimeout(500) + + // Enter filename when prompt dialog shown + comfyPage.page.on('dialog', (dialog) => dialog.accept(templateName)) + + // Ctrl + drag over 3 nodes + await comfyPage.dragAndDrop( + { x: 175, y: 252 }, + { x: 483, y: 564 }, + 'ControlOrMeta' + ) + expect(await comfyPage.getGraphSelectedItemsCount()).toEqual(3) + + await comfyPage.rightClickCanvas() + await comfyPage.clickContextMenuItem('Save Selected as Template') + await comfyPage.nextFrame() + + await comfyPage.rightClickCanvas() + await comfyPage.clickContextMenuItem('Node Templates >') + await comfyPage.clickContextMenuItem(templateName) + + await expect(comfyPage.canvas).toHaveScreenshot() + }) + + test('Can load old format template', async ({ comfyPage }) => { + await comfyPage.setNodeTemplates('vintage_clipboard_template.json') + await comfyPage.reload() + + // TODO: Flaky test. Right click requires delay after reload, but other interactions do not. + await comfyPage.page.waitForTimeout(500) + + await comfyPage.rightClickCanvas() + await comfyPage.clickContextMenuItem('Node Templates >') + await comfyPage.clickContextMenuItem('vintageClipboard Template') + + await expect(comfyPage.canvas).toHaveScreenshot() + }) + + test('Can load new format template', async ({ comfyPage }) => { + await comfyPage.setNodeTemplates('node_template_templates.json') + await comfyPage.reload() + + // TODO: Flaky test. Right click requires delay after reload, but other interactions do not. + await comfyPage.page.waitForTimeout(500) + + await comfyPage.rightClickCanvas() + await comfyPage.clickContextMenuItem('Node Templates >') + await comfyPage.clickContextMenuItem('Three Nodes Template') + + await expect(comfyPage.canvas).toHaveScreenshot() + }) +})