// tests/unit/renderer/layout/library-reader-flex-chain.test.tsx // // 守「文献列 → Reader」的**flex 高度链**契约 // (src/renderer/src/components/layout/WorkbenchContentPanel.tsx r120 头注指向本文件)。 // // 背景(audit-r120 的真实故障):文献列的承载容器一度是**块级**容器, // 子 reader(PDF / Image / Markdown / Code / Plain)的根节点高度全靠 // flex-1 撑 —— 块级父下 flex-1 直接失效,iframe 的 h-full 百分比解析 // 失败,回退 UA 默认 300×150 ⇒ PDF 塌成一条 150px 横条并报障。 // // 契约(每一层都在用户可见的后果上): // // 1. 承载 Reader 的那层容器必须是 **flex + flex-col**: // - 少 flex ⇒ 子 reader 的 flex-1 不生效(高度塌成内容高) // - 少 flex-col ⇒ flex-1 沿主轴(横轴)分配,高度反而撑不开 // 2. 该容器还要带 **flex-1 + min-h-0**: // - 少 flex-1 ⇒ 它不吃掉剩余高度,reader 被挤到容器顶部 // - 少 min-h-0 ⇒ overflow 逃逸,PDF 内容把主区撑出滚动条 // 3. 从 reader 根往上的**每一层祖先都必须是 flex 容器** —— // 中间任何一层退化成块级,整条链的高度计算就断在那儿。 // 4. 双列形态下,文献列容器自身也要是 flex 列(min-h-0 同样不能少)。 // // 变异思路(下列任一改动会红): // - 把 LibraryPanel 里 Reader 外层的 className 去掉 flex / flex-col // - 去掉该层的 flex-1 或 min-h-0 // - 双列分支里给文献列容器换成块级(去掉 flex flex-col) // - 把单列分支的包裹层换成块级 import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'; import { render, cleanup, screen, act } from '@testing-library/react'; // i18n:只关心布局链,不关心文案 vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (k: string) => k }), })); // --- 三个 store 的最小可订阅替身 ------------------------------------------- const stores = vi.hoisted(() => { function makeStore(initial: T) { let state = initial; const listeners = new Set<() => void>(); return { get: () => state, set: (patch: Partial) => { state = { ...state, ...patch }; for (const l of listeners) l(); }, subscribe: (l: () => void) => { listeners.add(l); return () => { listeners.delete(l); }; }, }; } const workspace = makeStore<{ active: 'library' | 'notes' }>({ active: 'library' }); const notes = makeStore<{ selectedId: string | null; select: (id: string | null) => void }>({ selectedId: null, select: () => {}, }); notes.set({ select: (id: string | null) => { notes.set({ selectedId: id }); }, }); const literature = makeStore<{ items: { id: string; absPath: string }[]; selectedId: string | null; select: (id: string | null) => void; }>({ items: [], selectedId: null, select: () => {} }); literature.set({ select: (id: string | null) => { literature.set({ selectedId: id }); }, }); return { workspace, notes, literature }; }); vi.mock('../../../../src/renderer/src/store/workspace', async () => { const { useSyncExternalStore } = await import('react'); const store = stores.workspace; const useWorkspaceStore = (selector: (s: { active: 'library' | 'notes' }) => T): T => useSyncExternalStore(store.subscribe, () => selector(store.get())); return { useWorkspaceStore }; }); vi.mock('../../../../src/renderer/src/store/notes', async () => { const { useSyncExternalStore } = await import('react'); const store = stores.notes; const useNotesStore = ( selector: (s: { selectedId: string | null; select: (id: string | null) => void }) => T, ): T => useSyncExternalStore(store.subscribe, () => selector(store.get())); useNotesStore.getState = () => store.get(); return { useNotesStore }; }); vi.mock('../../../../src/renderer/src/store/literature', async () => { const { useSyncExternalStore } = await import('react'); const store = stores.literature; const useLiteratureStore = ( selector: (s: { items: { id: string; absPath: string }[]; selectedId: string | null; select: (id: string | null) => void; }) => T, ): T => useSyncExternalStore(store.subscribe, () => selector(store.get())); useLiteratureStore.getState = () => store.get(); return { useLiteratureStore }; }); // 阅读器替身:复刻真 PDFReader 的根节点形态 —— // 根: flex-1 flex flex-col (高度靠 flex-1 撑) // 内层: relative flex-1 min-h-0 overflow-hidden (iframe 的定位上下文) vi.mock('../../../../src/renderer/src/components/reader/Reader', () => ({ Reader: () => (