diff --git a/browser_tests/assets/vueNodes/simple-triple.json b/browser_tests/assets/vueNodes/simple-triple.json new file mode 100644 index 0000000000..9b665191db --- /dev/null +++ b/browser_tests/assets/vueNodes/simple-triple.json @@ -0,0 +1 @@ +{"id":"4412323e-2509-4258-8abc-68ddeea8f9e1","revision":0,"last_node_id":39,"last_link_id":29,"nodes":[{"id":37,"type":"KSampler","pos":[3635.923095703125,870.237548828125],"size":[428,437],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"model","name":"model","type":"MODEL","link":null},{"localized_name":"positive","name":"positive","type":"CONDITIONING","link":null},{"localized_name":"negative","name":"negative","type":"CONDITIONING","link":null},{"localized_name":"latent_image","name":"latent_image","type":"LATENT","link":null},{"localized_name":"seed","name":"seed","type":"INT","widget":{"name":"seed"},"link":null},{"localized_name":"steps","name":"steps","type":"INT","widget":{"name":"steps"},"link":null},{"localized_name":"cfg","name":"cfg","type":"FLOAT","widget":{"name":"cfg"},"link":null},{"localized_name":"sampler_name","name":"sampler_name","type":"COMBO","widget":{"name":"sampler_name"},"link":null},{"localized_name":"scheduler","name":"scheduler","type":"COMBO","widget":{"name":"scheduler"},"link":null},{"localized_name":"denoise","name":"denoise","type":"FLOAT","widget":{"name":"denoise"},"link":null}],"outputs":[{"localized_name":"LATENT","name":"LATENT","type":"LATENT","links":null}],"properties":{"Node name for S&R":"KSampler"},"widgets_values":[0,"randomize",20,8,"euler","simple",1]},{"id":38,"type":"VAEDecode","pos":[4164.01611328125,925.5230712890625],"size":[193.25,107],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"samples","name":"samples","type":"LATENT","link":null},{"localized_name":"vae","name":"vae","type":"VAE","link":null}],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":null}],"properties":{"Node name for S&R":"VAEDecode"}},{"id":39,"type":"CLIPTextEncode","pos":[3259.289794921875,927.2508544921875],"size":[239.9375,155],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"clip","name":"clip","type":"CLIP","link":null},{"localized_name":"text","name":"text","type":"STRING","widget":{"name":"text"},"link":null}],"outputs":[{"localized_name":"CONDITIONING","name":"CONDITIONING","type":"CONDITIONING","links":null}],"properties":{"Node name for S&R":"CLIPTextEncode"},"widgets_values":[""]}],"links":[],"groups":[],"config":{},"extra":{"ds":{"scale":1.1576250000000001,"offset":[-2808.366467322067,-478.34316506594797]}},"version":0.4} \ No newline at end of file diff --git a/browser_tests/fixtures/ComfyPage.ts b/browser_tests/fixtures/ComfyPage.ts index c9a8820f5e..2db8e9de57 100644 --- a/browser_tests/fixtures/ComfyPage.ts +++ b/browser_tests/fixtures/ComfyPage.ts @@ -4,7 +4,11 @@ import { test as base } from '@playwright/test' import dotenv from 'dotenv' import * as fs from 'fs' -import type { LGraphNode } from '../../src/lib/litegraph/src/litegraph' +import type { HasBoundingRect } from '../../src/lib/litegraph/src/interfaces' +import { + type LGraphNode, + createBounds +} from '../../src/lib/litegraph/src/litegraph' import type { NodeId } from '../../src/platform/workflow/validation/schemas/workflowSchema' import type { KeyCombo } from '../../src/schemas/keyBindingSchema' import type { useWorkspaceStore } from '../../src/stores/workspaceStore' @@ -29,6 +33,8 @@ dotenv.config() type WorkspaceStore = ReturnType +type Bounds = readonly [number, number, number, number] + class ComfyMenu { private _nodeLibraryTab: NodeLibrarySidebarTab | null = null private _workflowsTab: WorkflowsSidebarTab | null = null @@ -338,6 +344,87 @@ export class ComfyPage { return `./browser_tests/assets/${fileName}` } + async fitToView( + options: { + selectionOnly?: boolean + zoom?: number + padding?: number + } = {} + ) { + const { selectionOnly = false, zoom = 0.75, padding = 10 } = options + + const rectangles = await this.page.evaluate< + Bounds[] | null, + { selectionOnly: boolean } + >( + ({ selectionOnly }) => { + const app = window['app'] + if (!app?.canvas) return null + + const canvas = app.canvas + const items = (() => { + if (selectionOnly && canvas.selectedItems?.size) { + return Array.from(canvas.selectedItems) + } + try { + return Array.from(canvas.positionableItems ?? []) + } catch { + return [] + } + })() + + if (!items.length) return null + + const rects: Bounds[] = [] + for (const item of items) { + const rect = item?.boundingRect + if (!rect) continue + + const x = Number(rect[0]) + const y = Number(rect[1]) + const width = Number(rect[2]) + const height = Number(rect[3]) + + rects.push([x, y, width, height] as Bounds) + } + + return rects.length ? rects : null + }, + { selectionOnly } + ) + + if (!rectangles?.length) return + + const sources: HasBoundingRect[] = rectangles.map( + (rect) => ({ boundingRect: rect }) satisfies HasBoundingRect + ) + + const bounds = createBounds(sources, padding) + + if (!bounds) return + + const boundsArray = [ + Number(bounds[0]), + Number(bounds[1]), + Number(bounds[2]), + Number(bounds[3]) + ] as Bounds + + await this.page.evaluate( + ({ bounds, zoom }) => { + const app = window['app'] + if (!app?.canvas) return + + const canvas = app.canvas + canvas.ds.fitToBounds(bounds, { zoom }) + canvas.setDirty(true, true) + }, + { bounds: boundsArray, zoom } + ) + + await this.nextFrame() + } + async executeCommand(commandId: string) { await this.page.evaluate((id: string) => { return window['app'].extensionManager.command.execute(id) diff --git a/browser_tests/tests/vueNodes/NodeHeader.spec.ts b/browser_tests/tests/vueNodes/NodeHeader.spec.ts index 7a8ae5dd2b..336e2672de 100644 --- a/browser_tests/tests/vueNodes/NodeHeader.spec.ts +++ b/browser_tests/tests/vueNodes/NodeHeader.spec.ts @@ -6,7 +6,7 @@ import { VueNodeFixture } from '../../fixtures/utils/vueNodeFixtures' test.describe('NodeHeader', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Enabled') + await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false) await comfyPage.setSetting('Comfy.EnableTooltips', true) await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) diff --git a/browser_tests/tests/vueNodes/linkInteraction.spec.ts b/browser_tests/tests/vueNodes/linkInteraction.spec.ts new file mode 100644 index 0000000000..33d754b6d9 --- /dev/null +++ b/browser_tests/tests/vueNodes/linkInteraction.spec.ts @@ -0,0 +1,66 @@ +import type { Locator } from '@playwright/test' + +import { getSlotKey } from '../../../src/renderer/core/layout/slots/slotIdentifier' +import { + comfyExpect as expect, + comfyPageFixture as test +} from '../../fixtures/ComfyPage' + +async function getCenter(locator: Locator): Promise<{ x: number; y: number }> { + const box = await locator.boundingBox() + if (!box) throw new Error('Slot bounding box not available') + return { + x: box.x + box.width / 2, + y: box.y + box.height / 2 + } +} + +test.describe('Vue Node Link Interaction', () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.setup() + await comfyPage.loadWorkflow('vueNodes/simple-triple') + await comfyPage.vueNodes.waitForNodes() + await comfyPage.fitToView() + }) + + test('should show a link dragging out from a slot when dragging on a slot', async ({ + comfyPage, + comfyMouse + }) => { + const samplerNodes = await comfyPage.getNodeRefsByType('KSampler') + expect(samplerNodes.length).toBeGreaterThan(0) + + const samplerNode = samplerNodes[0] + const outputSlot = await samplerNode.getOutput(0) + await outputSlot.removeLinks() + await comfyPage.nextFrame() + + const slotKey = getSlotKey(String(samplerNode.id), 0, false) + const slotLocator = comfyPage.page.locator(`[data-slot-key="${slotKey}"]`) + await expect(slotLocator).toBeVisible() + + const start = await getCenter(slotLocator) + const canvasBox = await comfyPage.canvas.boundingBox() + if (!canvasBox) throw new Error('Canvas bounding box not available') + + // Arbitrary value + const dragTarget = { + x: start.x + 180, + y: start.y - 140 + } + + await comfyMouse.move(start) + await comfyMouse.drag(dragTarget) + await comfyPage.nextFrame() + + try { + await expect(comfyPage.canvas).toHaveScreenshot( + 'vue-node-dragging-link.png' + ) + } finally { + await comfyMouse.drop() + } + }) +})