The fix in issue #2 resolved a stale-closure bug in createPathSegment where onClick captured graphsPath at render time; after further navigation findIndex returned -1 and splice(0) wiped the entire breadcrumb array. New test suite 'Stale-closure regression (issue #2)' covers: - Root breadcrumb remains functional after navigating three levels deep - Level-1 breadcrumb resolves correctly when clicked from level 3 - Successive back-clicks each trim exactly one breadcrumb level - Clicking the currently active breadcrumb does not alter the path Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
194 lines
8.1 KiB
TypeScript
194 lines
8.1 KiB
TypeScript
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 });
|
|
// Wait for the dropdown animation to fully settle before clicking
|
|
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('Breadcrumb navigation', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto('/');
|
|
await waitForGraph(page);
|
|
});
|
|
|
|
test('shows "Main" as the only breadcrumb in the initial state', async ({ page }) => {
|
|
await expect(breadcrumbLinks(page)).toHaveCount(1);
|
|
await expect(page.locator('.ant-breadcrumb')).toContainText('Main');
|
|
});
|
|
|
|
test('adds a breadcrumb segment when entering a subgraph', async ({ page }) => {
|
|
await openSubgraph(page, 'Start');
|
|
|
|
await expect(breadcrumbLinks(page)).toHaveCount(2);
|
|
await expect(page.locator('.ant-breadcrumb')).toContainText('Main');
|
|
await expect(page.locator('.ant-breadcrumb')).toContainText('Start');
|
|
});
|
|
|
|
test('breadcrumb grows for each additional level of nesting', async ({ page }) => {
|
|
// Level 1: enter Start's subgraph
|
|
await openSubgraph(page, 'Start');
|
|
await expect(breadcrumbLinks(page)).toHaveCount(2);
|
|
|
|
// Level 2: enter Start's subgraph again from within level 1
|
|
await openSubgraph(page, 'Start');
|
|
await expect(breadcrumbLinks(page)).toHaveCount(3);
|
|
});
|
|
|
|
test('clicking the root breadcrumb from two levels deep returns to main graph', async ({ page }) => {
|
|
// Navigate 2 levels deep
|
|
await openSubgraph(page, 'Start');
|
|
await openSubgraph(page, 'Start');
|
|
await expect(breadcrumbLinks(page)).toHaveCount(3);
|
|
|
|
// Click "Main" (first item)
|
|
await breadcrumbLinks(page).first().click();
|
|
|
|
await expect(breadcrumbLinks(page)).toHaveCount(1, { timeout: 3000 });
|
|
await expect(page.locator('.ant-breadcrumb')).toContainText('Main');
|
|
await waitForGraph(page);
|
|
await expect(nodeByLabel(page, 'Start')).toBeVisible();
|
|
await expect(nodeByLabel(page, 'End')).toBeVisible();
|
|
});
|
|
|
|
test('clicking a middle breadcrumb navigates to that level and removes subsequent segments', async ({ page }) => {
|
|
// Navigate to level 1 (Start's subgraph)
|
|
await openSubgraph(page, 'Start');
|
|
|
|
// Add a node at level 1 so we can recognise it when we return
|
|
await nodeByLabel(page, 'End').click();
|
|
await expect(page.locator('g.node')).toHaveCount(3, { timeout: 5000 });
|
|
|
|
// Navigate to level 2
|
|
await openSubgraph(page, 'Start');
|
|
await expect(breadcrumbLinks(page)).toHaveCount(3);
|
|
|
|
// Click the level-1 breadcrumb (second item, "Start")
|
|
await breadcrumbLinks(page).nth(1).click();
|
|
|
|
// Breadcrumb should be truncated to 2 items
|
|
await expect(breadcrumbLinks(page)).toHaveCount(2, { timeout: 3000 });
|
|
// The level-1 graph still has the extra node we added
|
|
await waitForGraph(page);
|
|
await expect(page.locator('g.node')).toHaveCount(3, { timeout: 5000 });
|
|
});
|
|
|
|
test('navigating forward after going back starts a fresh path from that level', async ({ page }) => {
|
|
// Navigate to level 1
|
|
await openSubgraph(page, 'Start');
|
|
await expect(breadcrumbLinks(page)).toHaveCount(2);
|
|
|
|
// Go back to main
|
|
await breadcrumbLinks(page).first().click();
|
|
await expect(breadcrumbLinks(page)).toHaveCount(1, { timeout: 3000 });
|
|
|
|
// Navigate into End's subgraph (a different node than before)
|
|
await openSubgraph(page, 'End');
|
|
|
|
// Breadcrumb should be Main / End — no leftover "Start" segment
|
|
await expect(breadcrumbLinks(page)).toHaveCount(2, { timeout: 3000 });
|
|
await expect(page.locator('.ant-breadcrumb')).toContainText('End');
|
|
await expect(page.locator('.ant-breadcrumb')).not.toContainText('Start');
|
|
});
|
|
});
|
|
|
|
// Regression tests for issue #2: stale closure caused breadcrumbs to disappear on back-navigation.
|
|
//
|
|
// The bug: createPathSegment's onClick closed over `graphsPath` from the render in which
|
|
// the segment was created. After further navigation the closure was stale, so findIndex
|
|
// returned -1 and splice(0) wiped the entire breadcrumb array.
|
|
//
|
|
// The fix: use the functional updater form of setGraphsPath so findIndex always runs
|
|
// against the *current* state rather than a captured snapshot.
|
|
test.describe('Stale-closure regression (issue #2)', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto('/');
|
|
await waitForGraph(page);
|
|
});
|
|
|
|
test('root breadcrumb remains functional after navigating three levels deep', async ({ page }) => {
|
|
// Navigate 3 levels deep — each additional level re-renders breadcrumbs with new
|
|
// closures; before the fix, clicking the root would hit a stale closure from level 1
|
|
// and wipe everything.
|
|
await openSubgraph(page, 'Start'); // level 1
|
|
await openSubgraph(page, 'Start'); // level 2
|
|
await openSubgraph(page, 'Start'); // level 3
|
|
await expect(breadcrumbLinks(page)).toHaveCount(4);
|
|
|
|
// Click the root ("Main") breadcrumb
|
|
await breadcrumbLinks(page).first().click();
|
|
|
|
// Breadcrumbs must NOT disappear — exactly one "Main" segment remains
|
|
await expect(breadcrumbLinks(page)).toHaveCount(1, { timeout: 3000 });
|
|
await expect(page.locator('.ant-breadcrumb')).toContainText('Main');
|
|
await waitForGraph(page);
|
|
// The root graph is rendered correctly
|
|
await expect(nodeByLabel(page, 'Start')).toBeVisible();
|
|
await expect(nodeByLabel(page, 'End')).toBeVisible();
|
|
});
|
|
|
|
test('level-1 breadcrumb remains functional when clicked from level 3', async ({ page }) => {
|
|
// This is the core regression scenario: a breadcrumb segment created at level 1
|
|
// must still resolve correctly after two more navigations have updated the path state.
|
|
await openSubgraph(page, 'Start'); // level 1
|
|
await openSubgraph(page, 'Start'); // level 2
|
|
await openSubgraph(page, 'Start'); // level 3
|
|
await expect(breadcrumbLinks(page)).toHaveCount(4);
|
|
|
|
// Click the level-1 breadcrumb (second item)
|
|
await breadcrumbLinks(page).nth(1).click();
|
|
|
|
// Should trim to exactly 2 items — NOT clear everything
|
|
await expect(breadcrumbLinks(page)).toHaveCount(2, { timeout: 3000 });
|
|
await waitForGraph(page);
|
|
// The graph at level 1 is shown
|
|
await expect(nodeByLabel(page, 'Start')).toBeVisible();
|
|
});
|
|
|
|
test('successive back-clicks each trim exactly one breadcrumb level', async ({ page }) => {
|
|
await openSubgraph(page, 'Start'); // level 1
|
|
await openSubgraph(page, 'Start'); // level 2
|
|
await openSubgraph(page, 'Start'); // level 3
|
|
await expect(breadcrumbLinks(page)).toHaveCount(4);
|
|
|
|
// Click level-2 breadcrumb
|
|
await breadcrumbLinks(page).nth(2).click();
|
|
await expect(breadcrumbLinks(page)).toHaveCount(3, { timeout: 3000 });
|
|
|
|
// Then click level-1 breadcrumb
|
|
await breadcrumbLinks(page).nth(1).click();
|
|
await expect(breadcrumbLinks(page)).toHaveCount(2, { timeout: 3000 });
|
|
|
|
// Then click root breadcrumb
|
|
await breadcrumbLinks(page).first().click();
|
|
await expect(breadcrumbLinks(page)).toHaveCount(1, { timeout: 3000 });
|
|
await expect(page.locator('.ant-breadcrumb')).toContainText('Main');
|
|
});
|
|
|
|
test('clicking the active (last) breadcrumb does not alter the path', async ({ page }) => {
|
|
// Clicking the segment you are already on should be a no-op — it must not clear
|
|
// breadcrumbs by accidentally slicing at index -1.
|
|
await openSubgraph(page, 'Start'); // level 1
|
|
await openSubgraph(page, 'Start'); // level 2
|
|
await expect(breadcrumbLinks(page)).toHaveCount(3);
|
|
|
|
// The last breadcrumb link is the currently active level
|
|
await breadcrumbLinks(page).last().click();
|
|
|
|
await expect(breadcrumbLinks(page)).toHaveCount(3, { timeout: 3000 });
|
|
});
|
|
});
|