|
| 1 | +// @vitest-environment jsdom |
| 2 | +/** |
| 3 | + * @ai-context AI 侧栏列接线守卫(批 3 T9 / 规格 §6.2「AI 对话侧栏:接入列基础设施」+ M3)。 |
| 4 | + * |
| 5 | + * Why 用**真执行器 + 真注册表规格**(不是给组件塞假 props):本文件要证的正是 |
| 6 | + * 「`useColumnLayout("chat-sidebar", columnSpec("chat-sidebar"))` 的结果真的走到了 DOM」 |
| 7 | + * —— 假 props 只能证明组件认这个 prop,证不了接线(同 `notes/NotesReadingColumn.outline.test.tsx` 口径)。 |
| 8 | + * |
| 9 | + * 判据(每条都配了能红的变异体,见 task-9-report §变异体实测): |
| 10 | + * ① 未记忆时渲染宽度 = 注册表默认 260(接线前本组件写死 240 ⇒ 红); |
| 11 | + * ② localStorage 记的列宽 → 渲染宽度(写死宽度 ⇒ 记忆无效 ⇒ 红); |
| 12 | + * ③ 窄窗(< `BREAKPOINTS.twoCol`)自动折叠 → 26px 窄条;**点窄条走 expand() 能展开** |
| 13 | + * (J1-3 死局形态:只翻 setManualFolded 则自动折叠下点了不展开 ⇒ 红); |
| 14 | + * ④ 手动折叠记忆 + 页面不注入任何 prop 时仍然自足(缺省不回落 240)。 |
| 15 | + * |
| 16 | + * ⚠️ 仪器边界:jsdom **不排版** —— 只判 `style.width` 与元素有无,**不判像素**(像素归 T14 探针); |
| 17 | + * 且组件测试底座全局 `environment: "node"` ⇒ 首行必须是指令行(本行之上不得有注释)。 |
| 18 | + * 本仓未装 jest-dom / user-event ⇒ 断言用原生 API、交互用 `fireEvent`;无 `globals` |
| 19 | + * ⇒ 必须显式 `afterEach(cleanup)`(先例 `NoteColorPicker.test.tsx:11`)。 |
| 20 | + */ |
| 21 | +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; |
| 22 | +import { cleanup, fireEvent, render, screen } from "@testing-library/react"; |
| 23 | +import { useColumnLayout } from "../hooks/useColumnLayout"; |
| 24 | +import { columnSpec } from "../shell/columnRegistry"; |
| 25 | +import ChatSidebar from "./ChatSidebar"; |
| 26 | +import type { ChatSidebarProps } from "./ChatSidebar"; |
| 27 | + |
| 28 | +const KEY = "chat-sidebar"; |
| 29 | +const base: Omit<ChatSidebarProps, "width" | "folded" | "onExpand"> = { |
| 30 | + sessions: [], tasks: [], activeChatId: null, activeTaskId: null, |
| 31 | + onSelectChat: vi.fn(), onSelectTask: vi.fn(), onNewChat: vi.fn(), |
| 32 | + onRenameChat: vi.fn(), onDeleteChat: vi.fn(), |
| 33 | + sessionTitles: new Map<number, string>(), noteTitles: new Map<number, string>(), |
| 34 | +}; |
| 35 | + |
| 36 | +/** 页面形态的宿主:真 hook + 真注册表规格(= `ChatPage.tsx` 的那一行 + props 注入) */ |
| 37 | +function Harness() { |
| 38 | + const chatCol = useColumnLayout(KEY, columnSpec(KEY)); |
| 39 | + return <ChatSidebar {...base} width={chatCol.width} folded={chatCol.folded} onExpand={chatCol.expand} />; |
| 40 | +} |
| 41 | + |
| 42 | +/** 展开态的根元素宽度(折叠态根元素是 ColumnBar 窄条,无 width 样式) */ |
| 43 | +const rootWidth = (el: HTMLElement | null) => el?.style.width ?? ""; |
| 44 | + |
| 45 | +beforeEach(() => { |
| 46 | + // jsdom 默认视口恰为 1024 < twoCol(1100) ⇒ 不显式声明会让每一列都静默变成折叠态 |
| 47 | + // (批 3 T5 实测;1280 = 规格 §1 决策 17 的默认窗宽)。断言一条不改,改的只是测试环境。 |
| 48 | + Object.defineProperty(window, "innerWidth", { writable: true, configurable: true, value: 1280 }); |
| 49 | + window.localStorage.clear(); |
| 50 | +}); |
| 51 | +afterEach(() => { |
| 52 | + cleanup(); |
| 53 | + window.localStorage.clear(); |
| 54 | + // 与 `SelectionActionMenu.test.tsx` 同款:视口改动必须复原,否则同文件后续用例跑在错的窗口下 |
| 55 | + Object.defineProperty(window, "innerWidth", { writable: true, configurable: true, value: 1024 }); |
| 56 | +}); |
| 57 | + |
| 58 | +describe("ChatSidebar 列接线(规格 §6.2)", () => { |
| 59 | + it("① 未记忆时 = 注册表默认(260,接线前写死 240)", () => { |
| 60 | + expect(columnSpec(KEY).default).toBe(260); // 规格 §6.2 锚点(注册表被改错时先在这里红) |
| 61 | + const { container } = render(<Harness />); |
| 62 | + expect(rootWidth(container.firstElementChild as HTMLElement)).toBe("260px"); |
| 63 | + expect(screen.queryByTestId("column-bar")).toBeNull(); |
| 64 | + }); |
| 65 | + |
| 66 | + it("② localStorage 记忆的列宽走到 DOM(写死宽度则记忆无效)", () => { |
| 67 | + window.localStorage.setItem(`layout:col-width:${KEY}`, "300"); |
| 68 | + const { container } = render(<Harness />); |
| 69 | + expect(rootWidth(container.firstElementChild as HTMLElement)).toBe("300px"); |
| 70 | + }); |
| 71 | + |
| 72 | + it("③ 窄窗自动折叠渲染窄条;点窄条走 expand() 能展开", () => { |
| 73 | + Object.defineProperty(window, "innerWidth", { writable: true, configurable: true, value: 1024 }); |
| 74 | + const { container } = render(<Harness />); |
| 75 | + expect(screen.getByTestId("column-bar")).toBeTruthy(); |
| 76 | + expect(container.firstElementChild?.getAttribute("data-testid")).toBe("column-bar"); |
| 77 | + fireEvent.click(screen.getByTestId("column-bar")); |
| 78 | + expect(screen.queryByTestId("column-bar")).toBeNull(); |
| 79 | + expect(rootWidth(container.firstElementChild as HTMLElement)).toBe("260px"); |
| 80 | + }); |
| 81 | + |
| 82 | + it("④ 手动折叠记忆生效;且页面零注入时组件仍自足(不回落 240)", () => { |
| 83 | + window.localStorage.setItem(`layout:col-fold:${KEY}`, "1"); |
| 84 | + const folded = render(<Harness />); |
| 85 | + expect(screen.getByTestId("column-bar")).toBeTruthy(); |
| 86 | + folded.unmount(); |
| 87 | + const bare = render(<ChatSidebar {...base} />); |
| 88 | + expect(rootWidth(bare.container.firstElementChild as HTMLElement)).toBe( |
| 89 | + `${columnSpec(KEY).default}px`, |
| 90 | + ); |
| 91 | + }); |
| 92 | +}); |
0 commit comments