bugfix: fixed subgraph not being preserved after navigation
This commit is contained in:
142
e2e/subgraph-preservation.spec.ts
Normal file
142
e2e/subgraph-preservation.spec.ts
Normal file
@@ -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 });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -68,8 +68,11 @@ export default function Graph({ setGraphPath }: { setGraphPath: React.Dispatch<R
|
|||||||
const [contextMenuOpened, openContextMenu] = useState(false);
|
const [contextMenuOpened, openContextMenu] = useState(false);
|
||||||
const [renameModalOpened, openRenameModal] = useState(false);
|
const [renameModalOpened, openRenameModal] = useState(false);
|
||||||
const [graphId, selectGraphId] = useState('main');
|
const [graphId, selectGraphId] = useState('main');
|
||||||
|
const graphIdRef = useRef(graphId);
|
||||||
const [graphsPath, setGraphsPath] = useState([createPathSegment('main', 'Main')])
|
const [graphsPath, setGraphsPath] = useState([createPathSegment('main', 'Main')])
|
||||||
const [nodeContext, openNodeContext] = useState<null | NodeContext>(null);
|
const [nodeContext, openNodeContext] = useState<null | NodeContext>(null);
|
||||||
|
graphIdRef.current = graphId;
|
||||||
|
|
||||||
const graphContextValue = {
|
const graphContextValue = {
|
||||||
graphId: graphId,
|
graphId: graphId,
|
||||||
selectGraphId: selectGraphId,
|
selectGraphId: selectGraphId,
|
||||||
@@ -92,6 +95,15 @@ export default function Graph({ setGraphPath }: { setGraphPath: React.Dispatch<R
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [graph]);
|
}, [graph]);
|
||||||
|
|
||||||
|
// Persist graph edits to the store so they survive breadcrumb navigation.
|
||||||
|
// We intentionally read graphId via ref (not as a dep) so this effect only
|
||||||
|
// fires on graph content changes, never on navigation (graphId changes).
|
||||||
|
useEffect(() => {
|
||||||
|
const state = useGraphsStore.getState() as { graphsById: Map<string, GraphModel> };
|
||||||
|
state.graphsById.set(graphIdRef.current, graph);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [graph]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (nodeContext) {
|
if (nodeContext) {
|
||||||
openContextMenu(true);
|
openContextMenu(true);
|
||||||
|
|||||||
Reference in New Issue
Block a user