Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 166 additions & 0 deletions browser_tests/tests/vueNodes/layout/ecsBridgeHistory.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import {
comfyExpect as expect,
comfyPageFixture as test
} from '@e2e/fixtures/ComfyPage'
import { toNodeId } from '@/types/nodeId'

test.describe(
'ECS bridge history',
{ tag: ['@slow', '@subgraph', '@vue-nodes'] },
() => {
test.slow()

test('restores promoted subgraph state after delete, undo, and redo', async ({
comfyPage
}) => {
await comfyPage.workflow.loadWorkflow(
'subgraphs/subgraph-with-promoted-text-widget'
)

const { baseline, promotedText } =
await test.step('Capture the initial promoted subgraph state', async () => {
const baseline = await comfyPage.page.evaluate(() =>
window.app!.graph!.serialize()
)
const promotedText = comfyPage.vueNodes
.getNodeLocator('11')
.getByRole('textbox', { name: 'text' })
await expect(promotedText).toBeVisible()
return { baseline, promotedText }
})

await test.step('Delete the subgraph', async () => {
const host = await comfyPage.vueNodes.getFixtureByTitle('New Subgraph')
await host.title.click()
await comfyPage.keyboard.delete()

await expect(comfyPage.vueNodes.nodes).toHaveCount(0)
await expect(promotedText).toBeHidden()
})

await test.step('Undo and verify the restored subgraph', async () => {
await comfyPage.keyboard.undo()
await comfyPage.vueNodes.waitForNodes()

await expect(comfyPage.vueNodes.nodes).toHaveCount(1)
await expect(promotedText).toBeVisible()
await expect
.poll(() =>
comfyPage.page.evaluate(() => window.app!.graph!.serialize())
)
.toEqual(baseline)

await comfyPage.vueNodes.enterSubgraph('11')
await expect(comfyPage.vueNodes.nodes).toHaveCount(2)
await expect
.poll(() =>
comfyPage.page.evaluate(() => window.app!.canvas.graph!.links.size)
)
.toBe(6)

const interiorGeometry = await comfyPage.canvasOps.getNodeGeometry(
toNodeId('10')
)
comfyPage.canvasOps.expectSlotsOnNode(
interiorGeometry,
'after restoring the subgraph'
)

await comfyPage.subgraph.exitViaBreadcrumb()
})

await test.step('Redo the deletion', async () => {
await comfyPage.keyboard.redo()
await expect(comfyPage.vueNodes.nodes).toHaveCount(0)
})
})

test('preserves geometry through navigation, renderer toggle, and history', async ({
comfyPage
}) => {
await comfyPage.workflow.loadWorkflow(
'subgraphs/subgraph-with-promoted-text-widget'
)

const nodeId = toNodeId('11')

const { before, moved } =
await test.step('Move the subgraph and capture its geometry', async () => {
const before = await comfyPage.canvasOps.getNodeGeometry(nodeId)
const { header } =
await comfyPage.vueNodes.getFixtureByTitle('New Subgraph')
const headerBox = await header.boundingBox()
if (!headerBox) throw new Error('Subgraph header not found')

const start = {
x: headerBox.x + headerBox.width / 2,
y: headerBox.y + headerBox.height / 2
}
await comfyPage.canvasOps.dragAndDrop(start, {
x: start.x + 120,
y: start.y + 90
})
await comfyPage.nextFrame()

const moved = await comfyPage.canvasOps.getNodeGeometry(nodeId)
comfyPage.canvasOps.expectSlotsTrackedNode(moved, before)
await expect.poll(() => comfyPage.workflow.getUndoQueueSize()).toBe(1)
return { before, moved }
})

await test.step('Verify geometry while navigating the subgraph', async () => {
await comfyPage.vueNodes.enterSubgraph(nodeId)
const interiorGeometry = await comfyPage.canvasOps.getNodeGeometry(
toNodeId('10')
)
comfyPage.canvasOps.expectSlotsOnNode(
interiorGeometry,
'while navigating the subgraph'
)
await comfyPage.subgraph.exitViaBreadcrumb()
})

await test.step('Undo and redo in the legacy renderer', async () => {
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', false)
await expect(comfyPage.vueNodes.nodes).toHaveCount(0)

await comfyPage.keyboard.undo()
const undone = await comfyPage.canvasOps.getNodeGeometry(nodeId)
expect(undone.pos[0]).toBeCloseTo(before.pos[0], 0)
expect(undone.pos[1]).toBeCloseTo(before.pos[1], 0)
expect(undone.size).toEqual(before.size)
comfyPage.canvasOps.expectSlotsOnNode(
undone,
'after undo in the legacy renderer'
)

await comfyPage.keyboard.redo()
const redone = await comfyPage.canvasOps.getNodeGeometry(nodeId)
expect(redone.pos[0]).toBeCloseTo(moved.pos[0], 0)
expect(redone.pos[1]).toBeCloseTo(moved.pos[1], 0)
expect(redone.size).toEqual(moved.size)
comfyPage.canvasOps.expectSlotsOnNode(
redone,
'after redo in the legacy renderer'
)
})

await test.step('Restore Vue nodes and reload the workflow', async () => {
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.vueNodes.waitForNodes()
await expect(
comfyPage.vueNodes
.getNodeLocator(nodeId)
.getByRole('textbox', { name: 'text' })
).toBeVisible()

await comfyPage.subgraph.serializeAndReload()
comfyPage.canvasOps.expectNodeGeometryPreserved(
await comfyPage.canvasOps.getNodeGeometry(nodeId),
moved,
'after serialization and reload'
)
})
})
}
)
13 changes: 11 additions & 2 deletions docs/architecture/ecs/ecs-migration-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,10 +147,19 @@ but they are not blockers for completing this data-centralization phase.

### 1. Prove the current bridge

Follow-up coverage in `ecsBridgeHistory.spec.ts` now proves that a promoted
subgraph with nested links and layout survives delete/undo and can be removed
again by redo. It also exercises geometry through subgraph navigation,
Vue-to-legacy switching, undo/redo, and serialization/reload. Focused tests now
pin virtual-consumer role inference and verify that first-run coach targets read
the root-scoped layout without creating layout state.

Before broadening or removing compatibility paths:

- Add mixed undo/redo coverage for node replacement or removal involving
links, reroutes, promoted widgets, and layout.
- Extend mixed undo/redo coverage to replacement or removal involving reroutes
and groups, with direct store and callback-visible assertions. Promoted
widgets, nested links, layout, renderer switching, and serialization now have
browser coverage across delete/undo/redo.
- Prove failed workflow configuration leaves no node, link, reroute, widget,
or layout ownership behind.
- Exercise recursive mixed-ID collisions through load, insertion, copy/paste,
Expand Down
Loading
Loading