11// @vitest -environment jsdom
22/**
3- * NoteLinkToSystem.test.tsx — 笔记「挂到体系」选择器(v0.13.7 触点②;v0.14 C3 增强)。
3+ * NoteLinkToSystem.test.tsx — 笔记「挂到体系」选择器(v0.13.7 触点②;v0.14 C3;
4+ * REQ-286 v0.19.7 搜索列表 + 三类内联轻建)。
45 *
5- * @ai -context: 覆盖 C3 新增——挂接目标三选一(问题/概念/模型)、已挂态切换目标
6- * (先撤旧链再建新链)、反查命令 list_links_by_target、旧版本仅 nodeId
7- * 兼容显示。修复手工输 id 断点——用户零 id 知识:选体系→选目标→确认。
6+ * @ai -context: 覆盖——选体系→点行选中→link 零 id 契约;未选禁用/切体系重置;
7+ * 三实体挂接 conceptId/modelId;已挂态切换先撤旧链、取消挂接;
8+ * 旧数据兼容显示;REQ-286 行内轻建(问题挂根/挂选中节点下、
9+ * 概念、模型)即建即选→确认挂接。invoke 全 mock(AAA)。
810 */
911import { afterEach , beforeEach , describe , expect , it , vi } from "vitest" ;
1012import { cleanup , fireEvent , render , screen , waitFor } from "@testing-library/react" ;
@@ -49,12 +51,18 @@ function baseMock() {
4951beforeEach ( ( ) => { invokeMock . mockReset ( ) ; baseMock ( ) ; } ) ;
5052afterEach ( ( ) => cleanup ( ) ) ;
5153
52- describe ( "NoteLinkToSystem v0.13.7 基础" , ( ) => {
53- it ( "未挂接时显示「挂到体系」按钮;点开选体系选问题节点确认后调用 link(零 id)" , async ( ) => {
54- render ( < NoteLinkToSystem noteId = { 7 } onChanged = { vi . fn ( ) } /> ) ;
55- fireEvent . click ( await screen . findByTestId ( "note-link-open" ) ) ;
56- fireEvent . change ( await screen . findByTestId ( "note-link-system" ) , { target : { value : "2" } } ) ;
57- fireEvent . change ( await screen . findByTestId ( "note-link-entity" ) , { target : { value : "11" } } ) ;
54+ /** 打开浮层并选体系 2(返回 confirm 按钮供后续断言) */
55+ async function openWithSystem ( ) {
56+ render ( < NoteLinkToSystem noteId = { 7 } onChanged = { vi . fn ( ) } /> ) ;
57+ fireEvent . click ( await screen . findByTestId ( "note-link-open" ) ) ;
58+ fireEvent . change ( await screen . findByTestId ( "note-link-system" ) , { target : { value : "2" } } ) ;
59+ }
60+
61+ describe ( "NoteLinkToSystem v0.13.7 基础(REQ-286 列表选中语义)" , ( ) => {
62+ it ( "点行选中问题节点确认 → link 携带 nodeId(零 id 知识)" , async ( ) => {
63+ await openWithSystem ( ) ;
64+ fireEvent . click ( await screen . findByTestId ( "note-link-row-11" ) ) ;
65+ expect ( ( screen . getByTestId ( "note-link-confirm" ) as HTMLButtonElement ) . disabled ) . toBe ( false ) ;
5866 fireEvent . click ( screen . getByTestId ( "note-link-confirm" ) ) ;
5967 await waitFor ( ( ) => {
6068 expect ( invokeMock ) . toHaveBeenCalledWith ( "link_knowledge_target" , {
@@ -64,24 +72,20 @@ describe("NoteLinkToSystem v0.13.7 基础", () => {
6472 } ) ;
6573
6674 it ( "未选目标时确认按钮禁用(不挂体系根)" , async ( ) => {
67- render ( < NoteLinkToSystem noteId = { 7 } onChanged = { vi . fn ( ) } /> ) ;
68- fireEvent . click ( await screen . findByTestId ( "note-link-open" ) ) ;
69- fireEvent . change ( await screen . findByTestId ( "note-link-system" ) , { target : { value : "2" } } ) ;
70- const entitySelect = ( await screen . findByTestId ( "note-link-entity" ) ) as HTMLSelectElement ;
71- expect ( entitySelect . value ) . toBe ( "" ) ;
75+ await openWithSystem ( ) ;
7276 expect ( ( screen . getByTestId ( "note-link-confirm" ) as HTMLButtonElement ) . disabled ) . toBe ( true ) ;
73- fireEvent . change ( entitySelect , { target : { value : "11" } } ) ;
74- expect ( ( screen . getByTestId ( "note-link-confirm" ) as HTMLButtonElement ) . disabled ) . toBe ( false ) ;
7577 } ) ;
7678
77- it ( "切换体系时重置目标选择(防实体残留串体系——后端报「引用实体不属于该体系」 )" , async ( ) => {
78- render ( < NoteLinkToSystem noteId = { 7 } onChanged = { vi . fn ( ) } /> ) ;
79- fireEvent . click ( await screen . findByTestId ( "note-link-open " ) ) ;
80- fireEvent . change ( await screen . findByTestId ( "note-link-system" ) , { target : { value : "2" } } ) ;
81- fireEvent . change ( await screen . findByTestId ( "note-link-entity" ) , { target : { value : "11" } } ) ;
79+ it ( "切换体系时重置目标选择(防实体残留串体系)" , async ( ) => {
80+ await openWithSystem ( ) ;
81+ fireEvent . click ( await screen . findByTestId ( "note-link-row-11 " ) ) ;
82+ expect ( ( screen . getByTestId ( "note-link-confirm" ) as HTMLButtonElement ) . disabled ) . toBe ( false ) ;
83+ // 切到体系 1(无实体)——目标重置、原行消失、确认禁用
8284 fireEvent . change ( await screen . findByTestId ( "note-link-system" ) , { target : { value : "1" } } ) ;
83- expect ( ( ( await screen . findByTestId ( "note-link-entity" ) ) as HTMLSelectElement ) . value ) . toBe ( "" ) ;
84- expect ( ( screen . getByTestId ( "note-link-confirm" ) as HTMLButtonElement ) . disabled ) . toBe ( true ) ;
85+ await waitFor ( ( ) => {
86+ expect ( screen . queryByTestId ( "note-link-row-11" ) ) . toBeNull ( ) ;
87+ expect ( ( screen . getByTestId ( "note-link-confirm" ) as HTMLButtonElement ) . disabled ) . toBe ( true ) ;
88+ } ) ;
8589 } ) ;
8690
8791 it ( "无体系时提示先创建体系(不显示确认按钮)" , async ( ) => {
@@ -106,11 +110,9 @@ describe("NoteLinkToSystem v0.14 C3 三实体挂接", () => {
106110 } ) ;
107111
108112 it ( "挂接概念:切「概念」tab 选概念 → link 带 conceptId" , async ( ) => {
109- render ( < NoteLinkToSystem noteId = { 7 } onChanged = { vi . fn ( ) } /> ) ;
110- fireEvent . click ( await screen . findByTestId ( "note-link-open" ) ) ;
111- fireEvent . change ( await screen . findByTestId ( "note-link-system" ) , { target : { value : "2" } } ) ;
113+ await openWithSystem ( ) ;
112114 fireEvent . click ( screen . getByTestId ( "note-link-tab-concept" ) ) ;
113- fireEvent . change ( await screen . findByTestId ( "note-link-entity" ) , { target : { value : " 21" } } ) ;
115+ fireEvent . click ( await screen . findByTestId ( "note-link-row- 21" ) ) ;
114116 fireEvent . click ( screen . getByTestId ( "note-link-confirm" ) ) ;
115117 await waitFor ( ( ) => {
116118 expect ( invokeMock ) . toHaveBeenCalledWith ( "link_knowledge_target" , {
@@ -120,11 +122,9 @@ describe("NoteLinkToSystem v0.14 C3 三实体挂接", () => {
120122 } ) ;
121123
122124 it ( "挂接模型:切「模型」tab 选模型 → link 带 modelId" , async ( ) => {
123- render ( < NoteLinkToSystem noteId = { 7 } onChanged = { vi . fn ( ) } /> ) ;
124- fireEvent . click ( await screen . findByTestId ( "note-link-open" ) ) ;
125- fireEvent . change ( await screen . findByTestId ( "note-link-system" ) , { target : { value : "2" } } ) ;
125+ await openWithSystem ( ) ;
126126 fireEvent . click ( screen . getByTestId ( "note-link-tab-model" ) ) ;
127- fireEvent . change ( await screen . findByTestId ( "note-link-entity" ) , { target : { value : " 31" } } ) ;
127+ fireEvent . click ( await screen . findByTestId ( "note-link-row- 31" ) ) ;
128128 fireEvent . click ( screen . getByTestId ( "note-link-confirm" ) ) ;
129129 await waitFor ( ( ) => {
130130 expect ( invokeMock ) . toHaveBeenCalledWith ( "link_knowledge_target" , {
@@ -148,7 +148,6 @@ describe("NoteLinkToSystem v0.14 C3 已挂态与切换", () => {
148148 render ( < NoteLinkToSystem noteId = { 7 } onChanged = { vi . fn ( ) } /> ) ;
149149 const btn = await screen . findByTestId ( "note-link-open" ) ;
150150 expect ( btn . textContent ) . toContain ( "已挂 · 摄影" ) ;
151- // 实体名异步加载(linked 到达后才按体系拉列表)——waitFor 等名称映射完成
152151 await waitFor ( ( ) => {
153152 expect ( screen . getByTestId ( "note-link-linked-label" ) . textContent ) . toContain ( "概念 · 白平衡" ) ;
154153 } ) ;
@@ -166,7 +165,6 @@ describe("NoteLinkToSystem v0.14 C3 已挂态与切换", () => {
166165 }
167166 } ) ;
168167 render ( < NoteLinkToSystem noteId = { 7 } onChanged = { vi . fn ( ) } /> ) ;
169- // 实体名异步加载(linked 到达后才按体系拉列表)——waitFor 等名称映射完成
170168 await waitFor ( ( ) => {
171169 expect ( screen . getByTestId ( "note-link-linked-label" ) . textContent ) . toContain ( "问题 · 照片为什么发灰?" ) ;
172170 } ) ;
@@ -187,10 +185,8 @@ describe("NoteLinkToSystem v0.14 C3 已挂态与切换", () => {
187185 } ) ;
188186 render ( < NoteLinkToSystem noteId = { 7 } onChanged = { vi . fn ( ) } /> ) ;
189187 fireEvent . click ( await screen . findByTestId ( "note-link-open" ) ) ;
190- // 已挂概念 → 切到模型目标
191188 fireEvent . click ( screen . getByTestId ( "note-link-tab-model" ) ) ;
192- fireEvent . change ( await screen . findByTestId ( "note-link-entity" ) , { target : { value : "31" } } ) ;
193- // 已挂态确认按钮为「切换目标」(面板关闭前断言)
189+ fireEvent . click ( await screen . findByTestId ( "note-link-row-31" ) ) ;
194190 expect ( ( screen . getByTestId ( "note-link-confirm" ) as HTMLButtonElement ) . textContent ) . toContain ( "切换目标" ) ;
195191 fireEvent . click ( screen . getByTestId ( "note-link-confirm" ) ) ;
196192 await waitFor ( ( ) => {
@@ -218,3 +214,92 @@ describe("NoteLinkToSystem v0.14 C3 已挂态与切换", () => {
218214 await waitFor ( ( ) => expect ( invokeMock ) . toHaveBeenCalledWith ( "delete_knowledge_link" , { id : 50 } ) ) ;
219215 } ) ;
220216} ) ;
217+
218+ describe ( "NoteLinkToSystem REQ-286 行内轻建(即建即选)" , ( ) => {
219+ function withCreateMock ( ) {
220+ invokeMock . mockImplementation ( async ( cmd : string , args ?: Record < string , unknown > ) => {
221+ switch ( cmd ) {
222+ case "list_knowledge_systems" : return systems ;
223+ case "list_knowledge_nodes" : {
224+ const created = args ?. __created ? [ { id : 88 , systemId : 2 , parentId : null , type : "question" , text : "为什么天空是蓝的?" , orderIdx : 0 , status : "active" , createdAt : 0 } ] : [ ] ;
225+ return args ?. systemId === 2 ? [ ...nodes , ...created ] : [ ] ;
226+ }
227+ case "list_knowledge_concepts" : return args ?. systemId === 2 ? concepts : [ ] ;
228+ case "list_knowledge_models" : return args ?. systemId === 2 ? models : [ ] ;
229+ case "list_links_by_target" : return [ ] ;
230+ case "add_knowledge_node" : {
231+ // 记录创建以便列表重载可见(简化:直接置标)
232+ return { id : 88 , systemId : 2 , parentId : ( args ?. parentId as number | null ) ?? null , type : "question" , text : args ?. text , orderIdx : 0 , status : "active" , createdAt : 0 } ;
233+ }
234+ case "link_knowledge_target" : return { id : 99 } ;
235+ case "delete_knowledge_link" : return true ;
236+ default : throw new Error ( `unexpected: ${ cmd } ` ) ;
237+ }
238+ } ) ;
239+ }
240+
241+ it ( "问题轻建挂体系根:输入名回车 → 新建即选 → 确认挂接 nodeId=新节点" , async ( ) => {
242+ withCreateMock ( ) ;
243+ await openWithSystem ( ) ;
244+ const search = await screen . findByTestId ( "note-link-search" ) ;
245+ fireEvent . change ( search , { target : { value : "为什么天空是蓝的?" } } ) ;
246+ const createBtn = await screen . findByTestId ( "note-link-create" ) ;
247+ fireEvent . click ( createBtn ) ;
248+ await waitFor ( ( ) => {
249+ expect ( invokeMock ) . toHaveBeenCalledWith ( "add_knowledge_node" , {
250+ systemId : 2 , parentId : null , nodeType : "question" , text : "为什么天空是蓝的?" ,
251+ } ) ;
252+ } ) ;
253+ // 即建即选:确认按钮可用,link 携带新节点 id
254+ await waitFor ( ( ) => {
255+ expect ( ( screen . getByTestId ( "note-link-confirm" ) as HTMLButtonElement ) . disabled ) . toBe ( false ) ;
256+ } ) ;
257+ fireEvent . click ( screen . getByTestId ( "note-link-confirm" ) ) ;
258+ await waitFor ( ( ) => {
259+ expect ( invokeMock ) . toHaveBeenCalledWith ( "link_knowledge_target" , {
260+ systemId : 2 , nodeId : 88 , targetType : "note" , targetId : 7 ,
261+ } ) ;
262+ } ) ;
263+ } ) ;
264+
265+ it ( "问题轻建挂选中节点下(父锚点=行点击)" , async ( ) => {
266+ withCreateMock ( ) ;
267+ await openWithSystem ( ) ;
268+ fireEvent . click ( await screen . findByTestId ( "note-link-row-11" ) ) ; // 父锚点
269+ const search = await screen . findByTestId ( "note-link-search" ) ;
270+ fireEvent . change ( search , { target : { value : "子问题:怎么测白平衡?" } } ) ;
271+ fireEvent . click ( await screen . findByTestId ( "note-link-create" ) ) ;
272+ await waitFor ( ( ) => {
273+ expect ( invokeMock ) . toHaveBeenCalledWith ( "add_knowledge_node" , expect . objectContaining ( {
274+ systemId : 2 , parentId : 11 , nodeType : "question" ,
275+ } ) ) ;
276+ } ) ;
277+ } ) ;
278+
279+ it ( "概念轻建:切概念 tab 输入名回车 → add_knowledge_concept 名称级落库" , async ( ) => {
280+ invokeMock . mockImplementation ( async ( cmd : string , args ?: Record < string , unknown > ) => {
281+ switch ( cmd ) {
282+ case "list_knowledge_systems" : return systems ;
283+ case "list_knowledge_nodes" : return args ?. systemId === 2 ? nodes : [ ] ;
284+ case "list_knowledge_concepts" : return args ?. systemId === 2 ? concepts : [ ] ;
285+ case "list_knowledge_models" : return args ?. systemId === 2 ? models : [ ] ;
286+ case "list_links_by_target" : return [ ] ;
287+ case "add_knowledge_concept" :
288+ return { id : 22 , systemId : 2 , name : args ?. name , essence : null , boundary : null , relation : null , status : "core" , lastAppliedAt : null , createdAt : 0 , updatedAt : 0 } ;
289+ default : throw new Error ( `unexpected: ${ cmd } ` ) ;
290+ }
291+ } ) ;
292+ await openWithSystem ( ) ;
293+ fireEvent . click ( screen . getByTestId ( "note-link-tab-concept" ) ) ;
294+ const search = await screen . findByTestId ( "note-link-search" ) ;
295+ fireEvent . keyDown ( search , { key : "Enter" } ) ; // 空输入不建
296+ expect ( invokeMock ) . not . toHaveBeenCalledWith ( "add_knowledge_concept" , expect . anything ( ) ) ;
297+ fireEvent . change ( search , { target : { value : "色温" } } ) ;
298+ fireEvent . keyDown ( search , { key : "Enter" } ) ;
299+ await waitFor ( ( ) => {
300+ expect ( invokeMock ) . toHaveBeenCalledWith ( "add_knowledge_concept" , {
301+ systemId : 2 , name : "色温" , essence : null , boundary : null , relation : null ,
302+ } ) ;
303+ } ) ;
304+ } ) ;
305+ } ) ;
0 commit comments