From e1adf6b9b0e691cb493e05994c514659011c59bc Mon Sep 17 00:00:00 2001 From: tymurbaniak Date: Fri, 6 Mar 2026 08:44:19 +0100 Subject: [PATCH] bugfix: fixed subgraph not being preserved after navigation --- e2e/subgraph-preservation.spec.ts | 142 ++++++++++++++++++++++++++++++ src/components/Graph.tsx | 14 ++- 2 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 e2e/subgraph-preservation.spec.ts diff --git a/e2e/subgraph-preservation.spec.ts b/e2e/subgraph-preservation.spec.ts new file mode 100644 index 0000000..44b27bd --- /dev/null +++ b/e2e/subgraph-preservation.spec.ts @@ -0,0 +1,142 @@ +import { test, expect, type Page } from '@playwright/test'; + +async function waitForGraph(page: Page) { + await page.waitForSelector('svg g.node', { timeout: 15000 }); +} + +function nodeByLabel(page: Page, label: string) { + return page.locator('g.node').filter({ hasText: label }); +} + +async function openSubgraph(page: Page, nodeLabel: string) { + await nodeByLabel(page, nodeLabel).click({ button: 'right' }); + await expect(page.locator('.ant-dropdown:visible')).toBeVisible({ timeout: 3000 }); + await page.waitForTimeout(300); + await page.locator('.ant-dropdown-menu-item').filter({ hasText: 'Subgraph' }).click(); + await waitForGraph(page); +} + +const breadcrumbLinks = (page: Page) => page.locator('.ant-breadcrumb-link'); + +test.describe('Subgraph preservation during breadcrumb navigation', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/'); + await waitForGraph(page); + }); + + test('nodes added to a subgraph are preserved after navigating to root and back', async ({ page }) => { + // Enter Start's subgraph and add a node + await openSubgraph(page, 'Start'); + await nodeByLabel(page, 'End').click(); + await expect(page.locator('g.node')).toHaveCount(3, { timeout: 5000 }); + + // Go back to main + await breadcrumbLinks(page).first().click(); + await expect(breadcrumbLinks(page)).toHaveCount(1, { timeout: 3000 }); + await waitForGraph(page); + + // Re-enter the same subgraph + await openSubgraph(page, 'Start'); + + // The extra node should still be there + await expect(page.locator('g.node')).toHaveCount(3, { timeout: 5000 }); + }); + + test('main graph is preserved when navigating into a subgraph and back', async ({ page }) => { + // Add a node to the main graph + await nodeByLabel(page, 'Start').click(); + await expect(page.locator('g.node')).toHaveCount(3, { timeout: 5000 }); + + // Navigate into End's subgraph + await openSubgraph(page, 'End'); + + // Go back to main + await breadcrumbLinks(page).first().click(); + await expect(breadcrumbLinks(page)).toHaveCount(1, { timeout: 3000 }); + await waitForGraph(page); + + // Main graph should still have 3 nodes + await expect(page.locator('g.node')).toHaveCount(3, { timeout: 5000 }); + await expect(nodeByLabel(page, 'Start')).toBeVisible(); + await expect(nodeByLabel(page, 'End')).toBeVisible(); + }); + + test('two sibling subgraphs preserve their states independently', async ({ page }) => { + // Enter Start's subgraph and add 1 extra node (3 total) + await openSubgraph(page, 'Start'); + await nodeByLabel(page, 'End').click(); + await expect(page.locator('g.node')).toHaveCount(3, { timeout: 5000 }); + + // Back to main + await breadcrumbLinks(page).first().click(); + await expect(breadcrumbLinks(page)).toHaveCount(1, { timeout: 3000 }); + await waitForGraph(page); + + // Enter End's subgraph and add 2 extra nodes (4 total) + await openSubgraph(page, 'End'); + await nodeByLabel(page, 'End').click(); + await expect(page.locator('g.node')).toHaveCount(3, { timeout: 5000 }); + await nodeByLabel(page, 'End').click(); + await expect(page.locator('g.node')).toHaveCount(4, { timeout: 5000 }); + + // Back to main + await breadcrumbLinks(page).first().click(); + await expect(breadcrumbLinks(page)).toHaveCount(1, { timeout: 3000 }); + await waitForGraph(page); + + // Re-enter Start's subgraph — should still have 3 nodes + await openSubgraph(page, 'Start'); + await expect(page.locator('g.node')).toHaveCount(3, { timeout: 5000 }); + + // Back to main + await breadcrumbLinks(page).first().click(); + await waitForGraph(page); + + // Re-enter End's subgraph — should still have 4 nodes + await openSubgraph(page, 'End'); + await expect(page.locator('g.node')).toHaveCount(4, { timeout: 5000 }); + }); + + test('intermediate level subgraph is preserved when navigating back from deeper nesting', async ({ page }) => { + // Enter Start's subgraph (level 1) and add a node + await openSubgraph(page, 'Start'); + await nodeByLabel(page, 'End').click(); + await expect(page.locator('g.node')).toHaveCount(3, { timeout: 5000 }); + + // Go one level deeper (level 2) + await openSubgraph(page, 'Start'); + await expect(breadcrumbLinks(page)).toHaveCount(3, { timeout: 3000 }); + + // Navigate all the way back to root + await breadcrumbLinks(page).first().click(); + await expect(breadcrumbLinks(page)).toHaveCount(1, { timeout: 3000 }); + await waitForGraph(page); + + // Re-enter Start's subgraph (level 1) — the 3-node state must be intact + await openSubgraph(page, 'Start'); + await expect(page.locator('g.node')).toHaveCount(3, { timeout: 5000 }); + }); + + test('edges added inside a subgraph are preserved after navigating away and back', async ({ page }) => { + // Enter Start's subgraph, create a node by clicking End, then link them + await openSubgraph(page, 'Start'); + const edgeCountBefore = await page.locator('g.edge').count(); + + // Ctrl+click Start to select it as a parent for linking + await page.keyboard.down('Control'); + await nodeByLabel(page, 'Start').click(); + await page.keyboard.up('Control'); + // Click End without Ctrl to link selected Start → End + await nodeByLabel(page, 'End').click(); + await expect(page.locator('g.edge')).toHaveCount(edgeCountBefore + 1, { timeout: 5000 }); + + // Navigate back to main + await breadcrumbLinks(page).first().click(); + await expect(breadcrumbLinks(page)).toHaveCount(1, { timeout: 3000 }); + await waitForGraph(page); + + // Re-enter the subgraph — extra edge must still be there + await openSubgraph(page, 'Start'); + await expect(page.locator('g.edge')).toHaveCount(edgeCountBefore + 1, { timeout: 5000 }); + }); +}); diff --git a/src/components/Graph.tsx b/src/components/Graph.tsx index bd203dc..1694e34 100644 --- a/src/components/Graph.tsx +++ b/src/components/Graph.tsx @@ -68,8 +68,11 @@ export default function Graph({ setGraphPath }: { setGraphPath: React.Dispatch(null); + const [nodeContext, openNodeContext] = useState(null); + graphIdRef.current = graphId; + const graphContextValue = { graphId: graphId, selectGraphId: selectGraphId, @@ -92,6 +95,15 @@ export default function Graph({ setGraphPath }: { setGraphPath: React.Dispatch { + const state = useGraphsStore.getState() as { graphsById: Map }; + state.graphsById.set(graphIdRef.current, graph); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [graph]); + useEffect(() => { if (nodeContext) { openContextMenu(true);