From 4f8a74c46ca7a7cbd131d5241bdfbd437d2a290d Mon Sep 17 00:00:00 2001 From: bymyself Date: Tue, 17 Mar 2026 11:05:11 -0700 Subject: [PATCH] test: add Playwright e2e tests for SubgraphNode copy-paste - Copy-paste SubgraphNode preserves promoted widgets - Pasted SubgraphNode retains proxyWidgets in serialized properties - Interior widget values survive copy-paste round-trip through loadGraphData Covers the _serializeItems serialize-not-clone path introduced in this PR. Requested by DrJKL and CodeRabbit. --- browser_tests/tests/subgraphCopyPaste.spec.ts | 143 ++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 browser_tests/tests/subgraphCopyPaste.spec.ts diff --git a/browser_tests/tests/subgraphCopyPaste.spec.ts b/browser_tests/tests/subgraphCopyPaste.spec.ts new file mode 100644 index 0000000000..5a1d9a72cd --- /dev/null +++ b/browser_tests/tests/subgraphCopyPaste.spec.ts @@ -0,0 +1,143 @@ +import { expect } from '@playwright/test' + +import type { ComfyPage } from '../fixtures/ComfyPage' +import { comfyPageFixture as test } from '../fixtures/ComfyPage' +import { TestIds } from '../fixtures/selectors' +import { + getPromotedWidgetNames, + getPromotedWidgets +} from '../helpers/promotedWidgets' + +async function getSubgraphNodeIds(comfyPage: ComfyPage): Promise { + return comfyPage.page.evaluate(() => { + const graph = window.app!.canvas.graph! + return graph.nodes + .filter( + (n) => typeof n.isSubgraphNode === 'function' && n.isSubgraphNode() + ) + .map((n) => String(n.id)) + }) +} + +test.describe('Subgraph Copy-Paste', { tag: ['@subgraph', '@widget'] }, () => { + test('Copy-paste SubgraphNode preserves promoted widgets', async ({ + comfyPage + }) => { + await comfyPage.workflow.loadWorkflow( + 'subgraphs/subgraph-with-promoted-text-widget' + ) + await comfyPage.nextFrame() + + const originalNode = await comfyPage.nodeOps.getNodeRefById('11') + const originalPromoted = await getPromotedWidgetNames(comfyPage, '11') + expect(originalPromoted).toContain('text') + + // Select the subgraph node + await originalNode.click('title') + await comfyPage.nextFrame() + + // Copy via Ctrl+C, then paste via Ctrl+V + await comfyPage.clipboard.copy() + await comfyPage.clipboard.paste() + await comfyPage.nextFrame() + + // Should now have 2 subgraph nodes + const nodeIds = await getSubgraphNodeIds(comfyPage) + expect(nodeIds).toHaveLength(2) + + // Both should have promoted widgets with 'text' + for (const nodeId of nodeIds) { + const promotedWidgets = await getPromotedWidgets(comfyPage, nodeId) + expect(promotedWidgets.length).toBeGreaterThan(0) + expect( + promotedWidgets.some(([, widgetName]) => widgetName === 'text') + ).toBe(true) + } + }) + + test('Copy-paste SubgraphNode preserves proxyWidgets in serialized data', async ({ + comfyPage + }) => { + await comfyPage.workflow.loadWorkflow( + 'subgraphs/subgraph-with-promoted-text-widget' + ) + await comfyPage.nextFrame() + + const originalNode = await comfyPage.nodeOps.getNodeRefById('11') + await originalNode.click('title') + await comfyPage.nextFrame() + + await comfyPage.clipboard.copy() + await comfyPage.clipboard.paste() + await comfyPage.nextFrame() + + // The pasted node should have proxyWidgets in its properties + const nodeIds = await getSubgraphNodeIds(comfyPage) + const pastedId = nodeIds.find((id) => id !== '11') + expect(pastedId).toBeDefined() + + const pastedProxyWidgets = await comfyPage.page.evaluate((id) => { + const node = window.app!.canvas.graph!.getNodeById(id) + const pw = node?.properties?.proxyWidgets + if (!Array.isArray(pw)) return [] + return pw as [string, string][] + }, pastedId!) + + expect(pastedProxyWidgets.length).toBeGreaterThan(0) + + // The proxyWidgets should reference the 'text' widget + const hasTextWidget = pastedProxyWidgets.some( + ([, widgetName]) => widgetName === 'text' + ) + expect(hasTextWidget).toBe(true) + }) + + test('Pasted SubgraphNode interior widget values survive round-trip', async ({ + comfyPage + }) => { + await comfyPage.workflow.loadWorkflow( + 'subgraphs/subgraph-with-promoted-text-widget' + ) + await comfyPage.nextFrame() + + const testContent = 'copy-paste-round-trip-test' + + // Set a value on the promoted textarea + const textarea = comfyPage.page.getByTestId( + TestIds.widgets.domWidgetTextarea + ) + await textarea.first().fill(testContent) + await comfyPage.nextFrame() + + // Select and copy the SubgraphNode + const originalNode = await comfyPage.nodeOps.getNodeRefById('11') + await originalNode.click('title') + await comfyPage.nextFrame() + + await comfyPage.clipboard.copy() + await comfyPage.clipboard.paste() + await comfyPage.nextFrame() + + // Serialize the whole graph and reload to test full round-trip + const serialized = await comfyPage.page.evaluate(() => { + return window.app!.graph!.serialize() + }) + + await comfyPage.page.evaluate( + (workflow) => { + return window.app!.loadGraphData(workflow) + }, + serialized as Parameters[1] + ) + await comfyPage.nextFrame() + + // Both subgraph nodes should still have promoted widgets + const nodeIds = await getSubgraphNodeIds(comfyPage) + expect(nodeIds.length).toBeGreaterThanOrEqual(2) + + for (const nodeId of nodeIds) { + const promoted = await getPromotedWidgetNames(comfyPage, nodeId) + expect(promoted).toContain('text') + } + }) +})