Skip to content

Commit 6dfbfa7

Browse files
committed
test(notes): 抽出阅读列视图测试的共享夹具
1 parent 521a848 commit 6dfbfa7

2 files changed

Lines changed: 103 additions & 72 deletions

File tree

‎app/src/components/notes/NotesReadingColumn.views.test.tsx‎

Lines changed: 3 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,10 @@
3232
* 副作用:只挂 React 树 + 读写 jsdom 的 `localStorage`;不写盘、不发请求。
3333
*/
3434
import { act, cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
35-
import { useRef } from "react";
3635
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
37-
import type { Note } from "../../types";
38-
import type { NoteEditHandle } from "../NoteEditView";
39-
import type { NoteViewSlot, ViewSpec } from "../../views/registry";
4036
import { viewsFor } from "../../views/registry";
41-
import { columnSpec } from "../../shell/columnRegistry";
42-
import { useColumnLayout } from "../../hooks/useColumnLayout";
4337
import { Toast } from "../../ui/primitives";
44-
import NotesReadingColumn from "./NotesReadingColumn";
38+
import { HarnessHost, MEMORY_KEY, handleOf, noop, note, type HarnessProps } from "./readingColumnTestKit";
4539

4640
const { invokeMock } = vi.hoisted(() => ({ invokeMock: vi.fn() }));
4741
vi.mock("@tauri-apps/api/core", () => ({
@@ -50,71 +44,8 @@ vi.mock("@tauri-apps/api/core", () => ({
5044
}));
5145
vi.mock("../RichEditorView", () => ({ default: () => <div data-testid="editor-stub" /> }));
5246

53-
const note: Note = {
54-
id: 1,
55-
title: "标题",
56-
content: "# 正文\n内容段落",
57-
source: "session",
58-
session_id: 42,
59-
rule_version: null,
60-
purify_stats: null,
61-
tags: "[]",
62-
properties: null,
63-
pin: 0,
64-
group_id: null,
65-
created_at: 1,
66-
updated_at: 2,
67-
};
68-
69-
const noop = vi.fn();
70-
const MEMORY_KEY = "view:default:note";
71-
72-
/** 注入的编辑器出口(`NoteEditHandle` 契约一字未改 —— 这行是 C4「不改接口」的编译期锚) */
73-
function handleOf(flushSave: () => Promise<void>): NoteEditHandle {
74-
return { flushSave, getContent: () => note.content };
75-
}
76-
77-
interface HarnessProps {
78-
readonly views?: readonly ViewSpec<NoteViewSlot>[];
79-
readonly editing?: boolean;
80-
readonly selected?: Note | null;
81-
readonly handle?: NoteEditHandle | null;
82-
/** 批 7 T17:卡片流那条链的 ms 出口(F11 用;缺省 = 今天的形态) */
83-
readonly onOpenSession?: (sessionId: number) => void;
84-
readonly onOpenSessionAt?: (sessionId: number, ms: number) => void;
85-
}
86-
87-
/** 页面形态的宿主:真 `useColumnLayout` + 真注册表清单(与 `NotesPage` 的那一行同源) */
88-
function Harness({ views = viewsFor("note"), editing = false, selected = note, handle = null, onOpenSession, onOpenSessionAt }: HarnessProps) {
89-
const outlineCol = useColumnLayout("notes-outline", columnSpec("notes-outline"));
90-
const editorRef = useRef<NoteEditHandle | null>(handle);
91-
return (
92-
<NotesReadingColumn
93-
selected={selected}
94-
editing={editing}
95-
setEditing={noop}
96-
readerSearch={null}
97-
noteColors={{}}
98-
groups={[]}
99-
editorRef={editorRef}
100-
outlineCol={outlineCol}
101-
onChanged={noop}
102-
onError={noop}
103-
onOpenAi={noop}
104-
onOpenModelCard={noop}
105-
onSelectionAction={noop}
106-
onPinToggle={noop}
107-
onDelete={noop}
108-
onTaskToggle={noop}
109-
onTagClick={noop}
110-
onOpenSession={onOpenSession}
111-
onOpenSessionAt={onOpenSessionAt}
112-
onImageOpen={noop}
113-
onCleanNotice={noop}
114-
views={views}
115-
/>
116-
);
117-
}
47+
/** 页面形态的宿主:夹具与挂载工具已搬到 `./readingColumnTestKit` —— 本行只补「真注册表清单」这个默认值(夹具件不得 import 注册表,理由见该件文件头)。 */
48+
const Harness = ({ views = viewsFor("note"), ...rest }: Partial<HarnessProps>) => <HarnessHost views={views} {...rest} />;
11849

11950
/** 当前 `aria-pressed` 为真的段文本(= 切换器的受控值;`ViewSwitcher` 用 `aria-pressed` 承载选中态) */
12051
const pressedLabel = (): string => {
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/**
2+
* readingColumnTestKit — `NotesReadingColumn.views.test.tsx` 的**共享夹具与宿主挂载工具**(批 8 T7 拆件)。
3+
*
4+
* @ai-context 自 `NotesReadingColumn.views.test.tsx` **纯搬迁**抽出(行为零变化 · 用例零变化):
5+
* 夹具(`note` / `noop` / `MEMORY_KEY` / `handleOf`)、props 面(`HarnessProps`)与
6+
* **宿主挂载工具**(页面形态的 `HarnessHost`)都是「怎么把被测宿主挂起来」的事实,
7+
* 与被测的 F 系列判据无关 ⇒ 物理分离到本件,让判据件腾出行数头寸(原 300 / 余 0)。
8+
* @ai-context 🔴 本件**不得** import `views/registry`(故 `HarnessHost` 的 `views` 是**必填**、
9+
* 类型由 `NotesReadingColumn` 的 props 面派生):`views/architecture.guard.test.ts` 的
10+
* A2①(运行时导入者恰 2 个页面)与 A2④(触碰注册的非 views 文件恰 5 个)都是 `toEqual`
11+
* 精确集合 ⇒ 夹具件会成为第 3 / 第 6 个而红。默认值 `viewsFor("note")` 由判据件那一侧给。
12+
* @ai-context 为什么 `HarnessHost` 必须走**真 hook**:`useColumnLayout("notes-outline", …)` 与
13+
* `columnSpec("notes-outline")` 是页面侧的真实接线(C1① 的注入面),换成假值就测不到
14+
* 「注入清单真的进到切换器」(F0)。
15+
* @ai-context `vi.mock("@tauri-apps/api/core")` 与 `vi.mock("../RichEditorView")` 留在**判据件**里
16+
* (`vi.mock` 是文件级注册,搬到本件会改变模块图的作用面);本件只消费它们的效果。
17+
* 副作用:`noop` 是 `vi.fn()`(每次 import 一份);渲染时只挂 React 树,不写盘、不发请求。
18+
* 边界:本件在六棘轮的 PROD 域内(它不是 `*.test.*`)⇒ 零颜色 / 零字号 / 零边框字面量。
19+
*/
20+
import { useRef } from "react";
21+
import type { ComponentProps } from "react";
22+
import { vi } from "vitest";
23+
import type { Note } from "../../types";
24+
import type { NoteEditHandle } from "../NoteEditView";
25+
import { columnSpec } from "../../shell/columnRegistry";
26+
import { useColumnLayout } from "../../hooks/useColumnLayout";
27+
import NotesReadingColumn from "./NotesReadingColumn";
28+
29+
/**
30+
* 视图清单的 props 面 —— **从宿主组件派生**(本件不 import 注册表,理由见文件头)。
31+
* 调用侧传 `viewsFor("note")`(真注册表清单)时天然结构匹配。
32+
*/
33+
export type ColumnViews = NonNullable<ComponentProps<typeof NotesReadingColumn>["views"]>;
34+
35+
export const note: Note = {
36+
id: 1,
37+
title: "标题",
38+
content: "# 正文\n内容段落",
39+
source: "session",
40+
session_id: 42,
41+
rule_version: null,
42+
purify_stats: null,
43+
tags: "[]",
44+
properties: null,
45+
pin: 0,
46+
group_id: null,
47+
created_at: 1,
48+
updated_at: 2,
49+
};
50+
51+
export const noop = vi.fn();
52+
export const MEMORY_KEY = "view:default:note";
53+
54+
/** 注入的编辑器出口(`NoteEditHandle` 契约一字未改 —— 这行是 C4「不改接口」的编译期锚) */
55+
export function handleOf(flushSave: () => Promise<void>): NoteEditHandle {
56+
return { flushSave, getContent: () => note.content };
57+
}
58+
59+
export interface HarnessProps {
60+
/** 视图清单(**必填**:本件不得 import 注册表 —— 见文件头;判据件那一侧给 `viewsFor("note")`) */
61+
readonly views: ColumnViews;
62+
readonly editing?: boolean;
63+
readonly selected?: Note | null;
64+
readonly handle?: NoteEditHandle | null;
65+
/** 批 7 T17:卡片流那条链的 ms 出口(F11 用;缺省 = 今天的形态) */
66+
readonly onOpenSession?: (sessionId: number) => void;
67+
readonly onOpenSessionAt?: (sessionId: number, ms: number) => void;
68+
}
69+
70+
/** 页面形态的宿主:真 `useColumnLayout` + 调用侧注入的真注册表清单(与 `NotesPage` 的那一行同源) */
71+
export function HarnessHost({ views, editing = false, selected = note, handle = null, onOpenSession, onOpenSessionAt }: HarnessProps) {
72+
const outlineCol = useColumnLayout("notes-outline", columnSpec("notes-outline"));
73+
const editorRef = useRef<NoteEditHandle | null>(handle);
74+
return (
75+
<NotesReadingColumn
76+
selected={selected}
77+
editing={editing}
78+
setEditing={noop}
79+
readerSearch={null}
80+
noteColors={{}}
81+
groups={[]}
82+
editorRef={editorRef}
83+
outlineCol={outlineCol}
84+
onChanged={noop}
85+
onError={noop}
86+
onOpenAi={noop}
87+
onOpenModelCard={noop}
88+
onSelectionAction={noop}
89+
onPinToggle={noop}
90+
onDelete={noop}
91+
onTaskToggle={noop}
92+
onTagClick={noop}
93+
onOpenSession={onOpenSession}
94+
onOpenSessionAt={onOpenSessionAt}
95+
onImageOpen={noop}
96+
onCleanNotice={noop}
97+
views={views}
98+
/>
99+
);
100+
}

0 commit comments

Comments
 (0)