test(ui): 侧栏只读模式、共享外壳与列表缓存的合同回归
新增 8 条:`sidebar-data-cache.test.ts`(命中不重拉 / 过期重拉一次 / 写操作后 拿到新标题并逐条锁住五个写路径 / 双账户不串 / 只存内存 / 401 清空)、 `sidebar-state.test.ts` +2(收起后重挂仍收起,含三种降级;移动端不读不写)、 `sidebar-contract.test.ts` +1(只读模式只少三样)、`chart-page-view.test.tsx` +1 (`(secondary)` layout 恰好挂一份只读侧栏,数据 hook 无写方法)。 改写 9 处既有断言,每处带「原值 / 新值 / 原因」三栏注释,均未削弱: `window.location.assign(path)` → `<SidebarMenuLink href>` 并追加反向断言; `onOpenReports` / `useRouter` 改成 doesNotMatch;`SecondaryShell` → `SecondaryHeader`; 导航顺序改在 `NAV_PAGES` 常量里量;两个 render 辅助改为裹 `SidebarProvider` (provider 上移到 layout);三处源码路径跟随路由组移动。 测试总数 3391 → 3399,失败清单与基线逐条一致(47 条均为无 Docker 的既有缺口)。 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
d9d347236f
commit
fb77c86585
@@ -193,18 +193,54 @@ test("keeps the sidebar brand row free of a duplicate collapse trigger", () => {
|
||||
assert.doesNotMatch(appSidebar, /SidebarTrigger/);
|
||||
});
|
||||
|
||||
test("leaves the chat document for chart, ephemeris, and reports", () => {
|
||||
test("reaches chart, ephemeris and reports with links, not a document load", () => {
|
||||
const appSidebar = readProjectFile("src/components/app-sidebar.tsx");
|
||||
// 对话首页用 history.pushState 维护 ?c=,App Router 的 router.push 无法离开。
|
||||
assert.match(appSidebar, /persistLoginSessionReturn\(\);\s*window\.location\.assign\(path\)/);
|
||||
assert.match(appSidebar, /leaveChat\("\/chart"\)/);
|
||||
assert.match(appSidebar, /leaveChat\("\/ephemeris"\)/);
|
||||
assert.match(appSidebar, /leaveChat\("\/reports"\)/);
|
||||
// 原值:要求 `persistLoginSessionReturn(); window.location.assign(path)`,
|
||||
// 以及三个 `leaveChat("/chart" | "/ephemeris" | "/reports")` 调用点。
|
||||
// 新值:要求三项由 `NAV_PAGES` 渲染成 `<SidebarMenuLink href=…>`,`leaveChat()`
|
||||
// 只保留 `persistLoginSessionReturn()` 与关抽屉,并反向禁止任何
|
||||
// `window.location.assign`。
|
||||
// 原因:TASK-sidebar-unify D3/T4。原写法是**有意**的整页刷新,理由写在这里的旧注释
|
||||
// 里:首页用 `history.pushState` 维护 `?c=`,`router.push` 离不开首页。
|
||||
// `<Link>` 没有这个限制——它是 App Router 的导航原语,而不是 `router.push`
|
||||
// ——所以产品拍板换成客户端跳转,React 树与内存缓存因此能活过这一跳。
|
||||
// `?c=` 的保存(`persistLoginSessionReturn`)一步没少,仍在跳转之前。
|
||||
// `/login` 仍是硬跳转,由 chat-navigation-a11y-contract 守着。
|
||||
assert.match(appSidebar, /function leaveChat\(\) \{\s*persistLoginSessionReturn\(\);/);
|
||||
assert.match(appSidebar, /\{ href: "\/chart", label: "星盘"/);
|
||||
assert.match(appSidebar, /\{ href: "\/ephemeris", label: "星历"/);
|
||||
assert.match(appSidebar, /\{ href: "\/reports", label: "我的报告"/);
|
||||
assert.match(appSidebar, /<SidebarMenuLink\n\s*className="report-nav-button"/);
|
||||
assert.doesNotMatch(appSidebar, /window\.location/);
|
||||
assert.doesNotMatch(appSidebar, /router\.push\("\/chart"\)/);
|
||||
assert.doesNotMatch(appSidebar, /router\.push\("\/ephemeris"\)/);
|
||||
assert.doesNotMatch(appSidebar, /useRouter/);
|
||||
});
|
||||
|
||||
test("the same component renders read-only when `/` is not the one mounting it", () => {
|
||||
const appSidebar = readProjectFile("src/components/app-sidebar.tsx");
|
||||
|
||||
// One component, two modes: `controls` present on `/`, absent everywhere else.
|
||||
assert.match(appSidebar, /export type AppSidebarControls/);
|
||||
assert.match(appSidebar, /controls\?: AppSidebarControls;/);
|
||||
|
||||
// Read-only rows keep the interactive row's markup — `.session-row` wrapping a
|
||||
// `.session-main` — and drop only the menu trigger and its reserved column.
|
||||
const sessionRow = readProjectFile("src/components/sidebar-session-row.tsx");
|
||||
assert.match(sessionRow, /<div className="session-row" data-readonly="true">/);
|
||||
assert.match(sessionRow, /<Link\n\s*className="session-main"/);
|
||||
assert.match(cssBlock('.session-row[data-readonly="true"]'), /grid-template-columns:\s*minmax\(0,\s*1fr\)/);
|
||||
const readonlyRow = sessionRow.slice(sessionRow.indexOf('data-readonly="true"'), sessionRow.indexOf("const {\n disabled,"));
|
||||
assert.doesNotMatch(readonlyRow, /session-menu-trigger/);
|
||||
|
||||
// The footer is the same 56px `.profile-trigger`, as a link with no chevron.
|
||||
assert.match(appSidebar, /<Link\n\s*className="profile-trigger"\n\s*href="\/"/);
|
||||
|
||||
// And nothing in read-only mode can write: the write callbacks only exist
|
||||
// inside `controls`, which the secondary layout never passes.
|
||||
assert.doesNotMatch(appSidebar, /supabase|fetch\(/i);
|
||||
});
|
||||
|
||||
test("uses one collapsed history action instead of icon-only session rows", () => {
|
||||
const appSidebar = readProjectFile("src/components/app-sidebar.tsx");
|
||||
assert.match(appSidebar, /MessageSquareText/);
|
||||
@@ -486,6 +522,12 @@ test("history renders recency group labels and a silent load-more sentinel", ()
|
||||
assert.match(appSidebar, /groupSessionsByRecency/);
|
||||
assert.match(appSidebar, /sidebar-group-label/);
|
||||
assert.match(appSidebar, /session-list-sentinel/);
|
||||
assert.match(appSidebar, /sessionControls\.hasMore \? <div ref=\{loadMoreRef\}/);
|
||||
// 原值:`sessionControls.hasMore ? <div ref={loadMoreRef}`
|
||||
// 新值:`hasMoreSessions ? <div ref={loadMoreRef}`
|
||||
// 原因:`sessionControls` 在只读模式下不存在,改从可选的 `controls` 里解构;
|
||||
// `hasMoreSessions` 是它派生出的稳定布尔值,effect 的依赖数组也用它,
|
||||
// 否则 exhaustive-deps 会要求整个对象、让观察器每次渲染重建。哨兵本身
|
||||
// (有下一页才渲染、渲染出来是静默的 sentinel)没变。
|
||||
assert.match(appSidebar, /hasMoreSessions \? <div ref=\{loadMoreRef\}/);
|
||||
assert.doesNotMatch(appSidebar, /加载更多|没有更多/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user