diff --git a/e2e/breadcrumb.spec.ts b/e2e/breadcrumb.spec.ts index 5dfdc99..79f49c6 100644 --- a/e2e/breadcrumb.spec.ts +++ b/e2e/breadcrumb.spec.ts @@ -102,5 +102,92 @@ test.describe('Breadcrumb navigation', () => { 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 }); + }); });