import { expect } from '@playwright/test' import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage' test.beforeEach(async ({ comfyPage }) => { await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe( 'Load Workflow in Media', { tag: ['@screenshot', '@workflow'] }, () => { const fileNames = [ 'workflow.webp', 'edited_workflow.webp', 'no_workflow.webp', 'large_workflow.webp', 'workflow_prompt_parameters.png', 'workflow_itxt.png', 'workflow.webm', // Skipped due to 3d widget unstable visual result. // 3d widget shows grid after fully loaded. // 'workflow.glb', 'workflow.mp4', 'workflow.mov', 'workflow.m4v', 'workflow.svg' // TODO: Re-enable after fixing test asset to use core nodes only // Currently opens missing nodes dialog which is outside scope of AVIF loading functionality // 'workflow.avif' ] const filesWithUpload = new Set(['no_workflow.webp']) fileNames.forEach(async (fileName) => { test(`Load workflow in ${fileName} (drop from filesystem)`, async ({ comfyPage }) => { const shouldUpload = filesWithUpload.has(fileName) const uploadRequestPromise = shouldUpload ? comfyPage.page.waitForRequest((req) => req.url().includes('/upload/') ) : null await comfyPage.dragDrop.dragAndDropFile(`workflowInMedia/${fileName}`) if (uploadRequestPromise) { const request = await uploadRequestPromise expect(request.url()).toContain('/upload/') } else { await expect(comfyPage.canvas).toHaveScreenshot(`${fileName}.png`) } }) }) const urls = [ 'https://comfyanonymous.github.io/ComfyUI_examples/hidream/hidream_dev_example.png' ] urls.forEach(async (url) => { test(`Load workflow from URL ${url} (drop from different browser tabs)`, async ({ comfyPage }) => { const initialNodeCount = await comfyPage.nodeOps.getGraphNodesCount() await comfyPage.dragDrop.dragAndDropURL(url) // The drop triggers an async fetch → parse → loadGraphData chain. // Poll until the graph settles with the loaded workflow's nodes. await expect .poll(() => comfyPage.nodeOps.getGraphNodesCount(), { timeout: 15000 }) .toBeGreaterThan(initialNodeCount) }) }) test.describe('Image without workflow', () => { test('places LoadImage at the drop cursor when graph_mouse is stale', async ({ comfyPage }) => { await comfyPage.nodeOps.clearGraph() await expect.poll(() => comfyPage.nodeOps.getGraphNodesCount()).toBe(0) await comfyPage.page.evaluate(() => { window.app!.canvas.graph_mouse[0] = -9999 window.app!.canvas.graph_mouse[1] = -9999 }) const dropPosition = { x: 480, y: 320 } await comfyPage.dragDrop.dragAndDropFile('image32x32.webp', { dropPosition, waitForUpload: true }) await expect.poll(() => comfyPage.nodeOps.getGraphNodesCount()).toBe(1) const { nodePos, expectedPos } = await comfyPage.page.evaluate( (drop) => { const canvas = window.app!.canvas const expected = canvas.convertEventToCanvasOffset( new MouseEvent('drop', { clientX: drop.x, clientY: drop.y }) ) as [number, number] const node = window.app!.graph.nodes[0] return { nodePos: [node.pos[0], node.pos[1]] as [number, number], expectedPos: expected } }, dropPosition ) expect(nodePos[0]).toBeCloseTo(expectedPos[0], 0) expect(nodePos[1]).toBeCloseTo(expectedPos[1], 0) expect(nodePos[0]).not.toBe(-9999) expect(nodePos[1]).not.toBe(-9999) }) test('places LoadImage above existing nodes (zIndex)', async ({ comfyPage }) => { await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true) await comfyPage.vueNodes.waitForNodes() const initialNodeIds = await comfyPage.vueNodes.getNodeIds() expect(initialNodeIds.length).toBeGreaterThan(0) await comfyPage.dragDrop.dragAndDropFile('image32x32.webp', { dropPosition: { x: 540, y: 380 }, waitForUpload: true }) await expect .poll(() => comfyPage.vueNodes.getNodeCount()) .toBe(initialNodeIds.length + 1) const newNodeIds = await comfyPage.vueNodes.getNodeIds() const addedNodeId = newNodeIds.find( (id) => !initialNodeIds.includes(id) ) expect(addedNodeId).toBeDefined() const newNodeZ = await comfyPage.vueNodes .getNodeLocator(addedNodeId!) .evaluate((el) => Number((el as HTMLElement).style.zIndex)) const existingZIndexes = await comfyPage.vueNodes.nodes.evaluateAll( (els, id) => els .filter((el) => el.getAttribute('data-node-id') !== id) .map((el) => Number((el as HTMLElement).style.zIndex)), addedNodeId! ) expect(newNodeZ).toBeGreaterThan(Math.max(0, ...existingZIndexes)) }) }) test('Load workflow from URL dropped onto Vue node', async ({ comfyPage }) => { const fakeUrl = 'https://example.com/workflow.png' await comfyPage.page.route(fakeUrl, (route) => route.fulfill({ path: comfyPage.assetPath('workflowInMedia/workflow_itxt.png') }) ) await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true) await comfyPage.vueNodes.waitForNodes() const initialNodeCount = await comfyPage.nodeOps.getGraphNodesCount() const node = comfyPage.vueNodes.getNodeByTitle('KSampler') await expect.poll(() => node.boundingBox()).toBeTruthy() const box = (await node.boundingBox())! const dropPosition = { x: box.x + box.width / 2, y: box.y + box.height / 2 } await comfyPage.dragDrop.dragAndDropURL(fakeUrl, { dropPosition, preserveNativePropagation: true }) await comfyPage.page.waitForFunction( (prevCount) => window.app!.graph.nodes.length !== prevCount, initialNodeCount, { timeout: 10000 } ) await expect .poll(() => comfyPage.nodeOps.getGraphNodesCount()) .not.toBe(initialNodeCount) }) } )