From 7208ffc10f0f52f95f63b7f57e5c9d745530bd68 Mon Sep 17 00:00:00 2001 From: Guilhem Lemouel Date: Wed, 30 Sep 2026 10:27:33 -0700 Subject: [PATCH] fix: a path that names nothing yet has no breadcrumb leaf MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `splitItemPath` documents that it gives up on a path still being typed, but `f/demo/` is three parts and returned a leaf with an empty name — a blank segment in the trail. The editor binds the path live, so the breadcrumb sees every keystroke. Tested here rather than `navPageFor`'s sort: no two `NAV_PAGES` paths nest, so the longest-prefix case that test claimed to pin has only one candidate. Renamed it to what it actually checks. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/lib/components/breadcrumbPath.test.ts | 20 +++++++++++++++++++ frontend/src/lib/components/breadcrumbPath.ts | 3 +++ .../lib/components/sidebar/navPages.test.ts | 2 +- 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 frontend/src/lib/components/breadcrumbPath.test.ts diff --git a/frontend/src/lib/components/breadcrumbPath.test.ts b/frontend/src/lib/components/breadcrumbPath.test.ts new file mode 100644 index 0000000000..987403e2e2 --- /dev/null +++ b/frontend/src/lib/components/breadcrumbPath.test.ts @@ -0,0 +1,20 @@ +import { describe, it, expect } from 'vitest' +import { splitItemPath } from './breadcrumbPath' + +describe('splitItemPath', () => { + it('walks the scope, each subfolder, then the item, each with its cumulative path', () => { + expect(splitItemPath('f/demo/sub/weather_report')).toEqual({ + dirs: [ + { name: 'f/demo', fullPath: 'f/demo' }, + { name: 'sub', fullPath: 'f/demo/sub' } + ], + leaf: { name: 'weather_report', fullPath: 'f/demo/sub/weather_report' } + }) + }) + + it('gives up on a path that names nothing yet, trailing separator included', () => { + // The editor binds the path live, so every keystroke reaches the breadcrumb. + expect(splitItemPath('f/demo')).toBeUndefined() + expect(splitItemPath('f/demo/')).toBeUndefined() + }) +}) diff --git a/frontend/src/lib/components/breadcrumbPath.ts b/frontend/src/lib/components/breadcrumbPath.ts index 8c041518f5..25834181ad 100644 --- a/frontend/src/lib/components/breadcrumbPath.ts +++ b/frontend/src/lib/components/breadcrumbPath.ts @@ -13,6 +13,9 @@ export function splitItemPath(path: string): { dirs: PathDir[]; leaf: PathDir } if (parts.length < 3) return undefined const scope = parts.slice(0, 2).join('/') const slug = parts.slice(2) + // `f/demo/` is three parts but names nothing yet, and a leaf with an empty name draws a blank + // segment. The editor's path is bound live, so the breadcrumb sees every keystroke. + if (slug[slug.length - 1] === '') return undefined const dirs: PathDir[] = [{ name: scope, fullPath: scope }] let acc = scope for (let i = 0; i < slug.length - 1; i++) { diff --git a/frontend/src/lib/components/sidebar/navPages.test.ts b/frontend/src/lib/components/sidebar/navPages.test.ts index 73f5ea456e..6e68912a15 100644 --- a/frontend/src/lib/components/sidebar/navPages.test.ts +++ b/frontend/src/lib/components/sidebar/navPages.test.ts @@ -6,7 +6,7 @@ describe('navPageFor', () => { expect(navPageFor('/base/runs', '/base')?.label).toBe('Runs') }) - it('keeps an item route on its page (longest prefix wins)', () => { + it('keeps an item route on its page', () => { expect(navPageFor('/runs/f/demo/x', '')?.label).toBe('Runs') })