diff --git a/browser_tests/fixtures/helpers/SubgraphHelper.ts b/browser_tests/fixtures/helpers/SubgraphHelper.ts index 2570e5858df..e8a613e0a66 100644 --- a/browser_tests/fixtures/helpers/SubgraphHelper.ts +++ b/browser_tests/fixtures/helpers/SubgraphHelper.ts @@ -528,6 +528,49 @@ export class SubgraphHelper { return id } + async getBoundaryLinkSnapshot() { + return this.page.evaluate(() => { + const graph = window.app!.graph! + const host = graph.nodes.find((node) => node.isSubgraphNode()) + if (!host) { + return { + rootLinks: ['no subgraph node'], + incompatibleHostInputLinks: ['no subgraph node'], + incompatibleHostOutputLinks: ['no subgraph node'] + } + } + + const hostId = host.id + function label(id: string | number) { + return id === hostId ? 'HOST' : String(id) + } + + const links = [...graph.links.values()] + return { + rootLinks: links + .map( + (link) => + `${label(link.origin_id)}:${link.origin_slot}->${label(link.target_id)}:${link.target_slot}` + ) + .sort(), + incompatibleHostInputLinks: links + .filter((link) => link.target_id === host.id) + .filter((link) => host.inputs[link.target_slot]?.type !== link.type) + .map( + (link) => + `${link.type} link landed on slot ${link.target_slot} typed ${host.inputs[link.target_slot]?.type}` + ), + incompatibleHostOutputLinks: links + .filter((link) => link.origin_id === host.id) + .filter((link) => host.outputs[link.origin_slot]?.type !== link.type) + .map( + (link) => + `${link.type} link left slot ${link.origin_slot} typed ${host.outputs[link.origin_slot]?.type}` + ) + } + }) + } + async serializeAndReload(): Promise { const serialized = await this.page.evaluate(() => window.app!.graph!.serialize() diff --git a/browser_tests/tests/subgraph/subgraphCreationBoundaryLinks.spec.ts b/browser_tests/tests/subgraph/subgraphCreationBoundaryLinks.spec.ts new file mode 100644 index 00000000000..0ee3b210771 --- /dev/null +++ b/browser_tests/tests/subgraph/subgraphCreationBoundaryLinks.spec.ts @@ -0,0 +1,59 @@ +import { expect } from '@playwright/test' + +import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage' + +const SUBGRAPH_LINKS_EXPECTED = { + rootLinks: [ + '4:0->HOST:0', + '4:1->6:0', + '4:1->7:0', + '4:2->HOST:4', + '5:0->HOST:3', + '6:0->HOST:1', + '7:0->HOST:2', + 'HOST:0->9:0' + ], + incompatibleHostInputLinks: [], + incompatibleHostOutputLinks: [] +} as const + +test( + 'Subgraph creation rewires boundary links to compatible slots across reload', + { tag: ['@slow', '@subgraph', '@vue-nodes'] }, + async ({ comfyPage }) => { + await test.step('Select both nodes in the default workflow', async () => { + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.workflow.loadWorkflow('default') + + // VAE Decode sits past the right edge of the 1280px canvas at the default + // view, so clicking its title silently misses and the selection is left + // holding KSampler alone. + await comfyPage.command.executeCommand('Comfy.Canvas.FitView') + const vaeDecode = await comfyPage.vueNodes.getFixtureByTitle('VAE Decode') + await expect(vaeDecode.header).toBeInViewport({ ratio: 1 }) + + await comfyPage.nodeOps.selectNodes(['KSampler', 'VAE Decode']) + expect( + await comfyPage.nodeOps.getSelectedNodeIds(), + 'both nodes must be selected, or the conversion under test is not the one being asserted' + ).toEqual(['3', '8']) + }) + + await test.step('Convert and verify the boundary links', async () => { + const ksampler = await comfyPage.nodeOps.getNodeRefById('3') + await ksampler.convertToSubgraph() + + await expect + .poll(() => comfyPage.subgraph.getBoundaryLinkSnapshot()) + .toEqual(SUBGRAPH_LINKS_EXPECTED) + }) + + await test.step('Reload and verify the boundary links', async () => { + await comfyPage.subgraph.serializeAndReload() + + await expect + .poll(() => comfyPage.subgraph.getBoundaryLinkSnapshot()) + .toEqual(SUBGRAPH_LINKS_EXPECTED) + }) + } +)