Browse Source

新增受保护路由组件以增强认证逻辑,更新知识库管理页面的测试用例以集成用户事件,提升测试的准确性和稳定性。同时,优化相关依赖项配置,确保与最新库版本兼容,提升代码可维护性和用户体验。

zyh 8 months ago
parent
commit
6d53da5880

+ 36 - 0
client/admin/components_protected_route.tsx

@@ -0,0 +1,36 @@
+import React, { useEffect } from 'react';
+import { 
+  useNavigate,
+} from 'react-router';
+
+
+import { useAuth } from './hooks_sys.tsx';
+
+
+export const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
+  const { isAuthenticated, isLoading } = useAuth();
+  const navigate = useNavigate();
+  
+  useEffect(() => {
+    // 只有在加载完成且未认证时才重定向
+    if (!isLoading && !isAuthenticated) {
+      navigate('/admin/login', { replace: true });
+    }
+  }, [isAuthenticated, isLoading, navigate]);
+  
+  // 显示加载状态,直到认证检查完成
+  if (isLoading) {
+    return (
+      <div className="flex justify-center items-center h-screen">
+        <div className="loader ease-linear rounded-full border-4 border-t-4 border-gray-200 h-12 w-12"></div>
+      </div>
+    );
+  }
+  
+  // 如果未认证且不再加载中,不显示任何内容(等待重定向)
+  if (!isAuthenticated) {
+    return null;
+  }
+  
+  return children;
+};

+ 150 - 46
client/admin/pages_know_info.test.tsx

@@ -1,7 +1,9 @@
 import { JSDOM } from 'jsdom'
 import React from 'react'
-import {render, fireEvent, within, screen, waitFor} from '@testing-library/react'
+import {render, fireEvent, within, screen, waitFor, configure} from '@testing-library/react'
+import {userEvent} from '@testing-library/user-event'
 import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import { createBrowserRouter, RouterProvider, Navigate } from 'react-router'
 import {
   assertEquals,
   assertExists,
@@ -12,25 +14,72 @@ import {
 import axios from 'axios';
 import { KnowInfoPage } from "./pages_know_info.tsx"
 import { AuthProvider } from './hooks_sys.tsx'
+import { ProtectedRoute } from './components_protected_route.tsx'
+
+// 拦截React DOM中的attachEvent和detachEvent错误
+const originalError = console.error;
+console.error = (...args) => {
+  // 过滤掉attachEvent和detachEvent相关的错误
+  if (args[0] instanceof Error) {
+    if (args[0].message?.includes('attachEvent is not a function') || 
+        args[0].message?.includes('detachEvent is not a function')) {
+      return; // 不输出这些错误
+    }
+  } else if (typeof args[0] === 'string') {
+    if (args[0].includes('attachEvent is not a function') || 
+        args[0].includes('detachEvent is not a function')) {
+      return; // 不输出这些错误
+    }
+  }
+  originalError(...args);
+};
+
+// // 配置Testing Library的eventWrapper来处理这个问题
+// configure({
+//   eventWrapper: (cb) => {
+//     try {
+//       return cb();
+//     } catch (error) {
+//     console.log('eventWrapper', cb)
+//       // 忽略attachEvent和detachEvent相关的错误
+//       if (error instanceof Error && 
+//           (error.message?.includes('attachEvent is not a function') || 
+//            error.message?.includes('detachEvent is not a function'))) {
+//         // 忽略这个错误并返回一个默认值
+//         return undefined;
+//       }
+//       // 其他错误正常抛出
+//       throw error;
+//     }
+//   }
+// });
 
 const queryClient = new QueryClient()
 
 const dom = new JSDOM(`<body></body>`, { 
   runScripts: "dangerously",
   pretendToBeVisual: true,
-  url: "http://localhost"
+  url: "http://localhost",
 });
 
 // 模拟浏览器环境
 globalThis.window = dom.window;
 globalThis.document = dom.window.document;
 
+// 添加必要的 DOM 配置
+globalThis.Node = dom.window.Node;
+globalThis.Document = dom.window.Document;
+globalThis.HTMLInputElement = dom.window.HTMLInputElement;
+globalThis.HTMLButtonElement = dom.window.HTMLButtonElement;
+
 // 定义浏览器环境所需的类
 globalThis.Element = dom.window.Element;
 globalThis.HTMLElement = dom.window.HTMLElement;
 globalThis.ShadowRoot = dom.window.ShadowRoot;
 globalThis.SVGElement = dom.window.SVGElement;
 
+
+
 // 模拟 getComputedStyle
 globalThis.getComputedStyle = (elt) => {
   const style = new dom.window.CSSStyleDeclaration();
@@ -72,6 +121,22 @@ axios.defaults.baseURL = 'https://23957.dev.d8dcloud.com'
 
 const customScreen = within(document.body);
 
+// 应用入口组件
+const App = () => {
+  // 路由配置
+  const router = createBrowserRouter([
+    {
+      path: '/',
+      element: (
+        <ProtectedRoute>
+          <KnowInfoPage />
+        </ProtectedRoute>
+      )
+    },
+  ]);
+  return <RouterProvider router={router} />
+};
+
 // 使用异步测试处理组件渲染
 Deno.test({
   name: '知识库管理页面测试',
@@ -105,6 +170,8 @@ Deno.test({
       globalThis.setInterval = originalSetInterval;
     };
 
+
+
     try {
       // 渲染组件
       const {
@@ -113,79 +180,116 @@ Deno.test({
       } = render(
         <QueryClientProvider client={queryClient}>
           <AuthProvider>
-            <KnowInfoPage />
+            <App />
           </AuthProvider>
         </QueryClientProvider>
       );
 
       // 测试1: 基本渲染
       await t.step('应正确渲染页面元素', async () => {
-        const title = await findByText(/知识库管理/i);
-        assertExists(title, '未找到知识库管理标题');
+        await waitFor(async () => {
+          const title = await findByText(/知识库管理/i);
+          assertExists(title, '未找到知识库管理标题');
+        }, {
+          timeout: 1000 * 5,
+        });
       });
 
-      let i = 0
-
       // 初始加载表格数据
-      await waitFor(async () => {
-        const table = await findByRole('table');
-        const rows = await within(table).findAllByRole('row');
-
-        // debug(rows[1])
-        i++
-        console.log('i', i)
-        console.log('rows', rows.length)
+      await t.step('初始加载表格数据', async () => {
+        await waitFor(async () => {
+          const table = await findByRole('table');
+          const rows = await within(table).findAllByRole('row');
 
-        // 应该大于2行
-        // assert(rows.length > 2, '表格没有数据'); // 1是表头行 2是数据行
-        
-        if (rows.length <= 2) {
-          throw new Error('表格没有数据');
-        }
-      }, {
-        timeout: 1000 * 10,
+          // 应该大于2行
+          assert(rows.length > 2, '表格没有数据'); // 1是表头行 2是数据行
+          
+        }, {
+          timeout: 1000 * 5,
+        });
       });
 
       // 测试2: 搜索表单功能
       await t.step('搜索表单应正常工作', async () => {
+        // 确保在正确的测试环境中设置 userEvent
+        const user = userEvent.setup({
+          document: dom.window.document,
+          delay: 0
+        });
+        
         const searchInput = await findByPlaceholderText(/请输入文章标题/i) as HTMLInputElement;
         const searchButton = await findByText(/搜 索/i);
+
+        assertExists(searchInput, '未找到搜索输入框');
+        assertExists(searchButton, '未找到搜索按钮');
         
         // 输入搜索内容
-        fireEvent.change(searchInput, { target: { value: '数据分析' } });
+        try {
+          await user.type(searchInput, '数据分析')
+        } catch (error: unknown) {
+          // console.error('输入搜索内容失败', error)
+        }
         assertEquals(searchInput.value, '数据分析', '搜索输入框值未更新');
+
+        console.log('searchInput', searchInput.value)
+
+        debug(searchInput)
+        debug(searchButton)
         
         // 提交搜索
-        fireEvent.click(searchButton);
+        try {
+          await user.click(searchButton);
+        } catch (error: unknown) {
+          // console.error('点击搜索按钮失败', error)
+        }
         
-        // // 验证是否触发了搜索
-        // await waitFor(() => {
-        //   const loading = queryByText(/正在加载数据/i);
-        //   assertNotEquals(loading, null, '搜索未触发加载状态');
-        // });
 
-        // 等待搜索结果并验证
-        await waitFor(async () => {
-          const table = await findByRole('table');
-          const rows = await within(table).findAllByRole('row');
+        let rows: HTMLElement[] = [];
 
-          debug(rows)
+        
+        const table = await findByRole('table');
+        assertExists(table, '未找到数据表格');
 
-          console.log('rows', rows.length);
-          
-          // 检查至少有一行包含"数据分析"
-          const hasMatch = rows.some(async row => {
-            const cells = await within(row).findAllByRole('cell');
-            return cells.some(cell => cell.textContent?.includes('数据分析'));
-          });
+        // 等待表格刷新并验证
+        await waitFor(async () => {
+          rows = await within(table).findAllByRole('row');
+          console.log('等待表格刷新并验证', rows.length)
+          assert(rows.length === 2, '表格未刷新');
+        }, {
+          timeout: 1000 * 5,
+          onTimeout: () => new Error('等待表格刷新超时')
+        });
 
-          console.log('hasMatch', hasMatch);
-          
-          assert(hasMatch, '搜索结果中没有找到包含"数据分析"的文章');
+        // 等待搜索结果并验证
+        await waitFor(async () => {
+          rows = await within(table).findAllByRole('row');
+          console.log('等待搜索结果并验证', rows.length)
+          assert(rows.length > 2, '表格没有数据');
         }, {
-          timeout: 5000,
+          timeout: 1000 * 5,
           onTimeout: () => new Error('等待搜索结果超时')
         });
+
+
+        
+        // 检查至少有一行包含"数据分析"
+        const matchResults = await Promise.all(rows.map(async row => {
+          try{
+            const cells = await within(row).findAllByRole('cell');
+            return cells.some(cell => {
+              return cell.textContent?.includes('数据分析')
+            });
+          } catch (error: unknown) {
+            // console.error('搜索结果获取失败', error)
+            return false
+          }
+        }))
+        // console.log('matchResults', matchResults)
+        const hasMatch = matchResults.some(result => result);
+
+        console.log('hasMatch', hasMatch)
+
+        assert(hasMatch, '搜索结果中没有找到包含"数据分析"的文章');
       });
 
       // 测试3: 表格数据加载

+ 2 - 1
client/admin/pages_know_info.tsx

@@ -148,6 +148,7 @@ export const KnowInfoPage = () => {
   // 处理搜索
   const handleSearch = async (values: any) => {
     try {
+      console.log('handleSearch', values)
       queryClient.removeQueries({ queryKey: ['knowInfos'] });
       setSearchParams({
         title: values.title || '',
@@ -286,7 +287,7 @@ export const KnowInfoPage = () => {
               <Button type="primary" htmlType="submit">
                 搜索
               </Button>
-              <Button onClick={() => {
+              <Button htmlType="reset" onClick={() => {
                 setSearchParams({
                   title: '',
                   category: '',

+ 3 - 45
client/admin/web_app.tsx

@@ -1,4 +1,4 @@
-import React, { useState, useEffect, createContext, useContext } from 'react';
+import React, { useState, useEffect} from 'react';
 import { createRoot } from 'react-dom/client';
 import { 
   createBrowserRouter,
@@ -18,7 +18,6 @@ import {
   Switch, Badge, Image, Upload, Divider, Descriptions,
   Popconfirm, Tag, Statistic, DatePicker, Radio, Progress, Tabs, List, Alert, Collapse, Empty, Drawer
 } from 'antd';
-import zhCN from "antd/locale/zh_CN";
 import {
   MenuFoldOutlined,
   MenuUnfoldOutlined,
@@ -31,8 +30,6 @@ import {
   BookOutlined,
   FileOutlined,
   PieChartOutlined,
-  UploadOutlined,
-  GlobalOutlined,
   VerticalAlignTopOutlined,
   CloseOutlined,
   SearchOutlined
@@ -40,27 +37,15 @@ import {
 import { 
   QueryClient,
   QueryClientProvider,
-  useQuery,
-  useMutation,
-  useQueryClient
 } from '@tanstack/react-query';
-import axios from 'axios';
 import dayjs from 'dayjs';
 import weekday from 'dayjs/plugin/weekday';
 import localeData from 'dayjs/plugin/localeData';
-import { uploadMinIOWithPolicy } from '@d8d-appcontainer/api';
-import type { MinioUploadPolicy } from '@d8d-appcontainer/types';
-import { Line, Pie, Column } from "@ant-design/plots";
 import 'dayjs/locale/zh-cn';
 import type { 
   GlobalConfig
 } from '../share/types.ts';
 
-import {
-  EnableStatus, DeleteStatus, ThemeMode, FontSize, CompactMode
-} from '../share/types.ts';
-
-import { getEnumOptions } from './utils.ts';
 
 import {
   AuthProvider,
@@ -81,7 +66,7 @@ import {ThemeSettingsPage} from './pages_theme_settings.tsx'
 import { ChartDashboardPage } from './pages_chart.tsx';
 import { LoginMapPage } from './pages_map.tsx';
 import { LoginPage } from './pages_login_reg.tsx';
-
+import { ProtectedRoute } from './components_protected_route.tsx';
 
 // 配置 dayjs 插件
 dayjs.extend(weekday);
@@ -440,34 +425,7 @@ const MainLayout = () => {
   );
 };
 
-// 受保护的路由组件
-const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
-  const { isAuthenticated, isLoading } = useAuth();
-  const navigate = useNavigate();
-  
-  useEffect(() => {
-    // 只有在加载完成且未认证时才重定向
-    if (!isLoading && !isAuthenticated) {
-      navigate('/admin/login', { replace: true });
-    }
-  }, [isAuthenticated, isLoading, navigate]);
-  
-  // 显示加载状态,直到认证检查完成
-  if (isLoading) {
-    return (
-      <div className="flex justify-center items-center h-screen">
-        <div className="loader ease-linear rounded-full border-4 border-t-4 border-gray-200 h-12 w-12"></div>
-      </div>
-    );
-  }
-  
-  // 如果未认证且不再加载中,不显示任何内容(等待重定向)
-  if (!isAuthenticated) {
-    return null;
-  }
-  
-  return children;
-};
+
 
 // 错误页面组件
 const ErrorPage = () => {

+ 1 - 0
deno.json

@@ -30,6 +30,7 @@
     "@heroicons/react/24/outline": "https://esm.d8d.fun/@heroicons/react@2.1.1/24/outline?dev&deps=react@19.0.0,react-dom@19.0.0",
     "@heroicons/react/24/solid": "https://esm.d8d.fun/@heroicons/react@2.1.1/24/solid?dev&deps=react@19.0.0,react-dom@19.0.0",
     "@testing-library/react": "https://esm.d8d.fun/@testing-library/react@16.3.0?dev&deps=react@19.0.0,react-dom@19.0.0",
+    "@testing-library/user-event":"npm:@testing-library/user-event@14.6.1",
     "jsdom":"npm:jsdom@26.0.0"
   },
   "compilerOptions": {

+ 13 - 0
deno.lock

@@ -6,6 +6,7 @@
     "npm:@testing-library/react@*": "16.3.0_@testing-library+dom@10.4.0_react@19.1.0_react-dom@19.1.0__react@19.1.0",
     "npm:@testing-library/react@16.3.0": "16.3.0_@testing-library+dom@10.4.0_react@19.1.0_react-dom@19.1.0__react@19.1.0",
     "npm:@testing-library/user-event@*": "14.6.1_@testing-library+dom@10.4.0",
+    "npm:@testing-library/user-event@14.6.1": "14.6.1_@testing-library+dom@10.4.0",
     "npm:@types/node@*": "22.12.0",
     "npm:antd@*": "5.24.6_react@19.1.0_react-dom@19.1.0__react@19.1.0_dayjs@1.11.13",
     "npm:global-jsdom@*": "26.0.0_jsdom@26.0.0",
@@ -1377,6 +1378,7 @@
     "https://esm.d8d.fun/@deno/shim-deno@~0.18.0?target=denonext": "https://esm.d8d.fun/@deno/shim-deno@0.18.2?target=denonext",
     "https://esm.d8d.fun/@socket.io/component-emitter@~3.1.0?target=denonext": "https://esm.d8d.fun/@socket.io/component-emitter@3.1.2?target=denonext",
     "https://esm.d8d.fun/@testing-library/dom": "https://esm.d8d.fun/@testing-library/dom@10.4.0",
+    "https://esm.d8d.fun/@testing-library/dom@%3E=7.21.4?target=denonext": "https://esm.d8d.fun/@testing-library/dom@10.4.0?target=denonext",
     "https://esm.d8d.fun/@testing-library/dom@^10.0.0?target=denonext": "https://esm.d8d.fun/@testing-library/dom@10.4.0?target=denonext",
     "https://esm.d8d.fun/@testing-library/dom@^10.0.0?target=denonext&dev": "https://esm.d8d.fun/@testing-library/dom@10.4.0?target=denonext&dev",
     "https://esm.d8d.fun/@testing-library/react?deps=react@19.0.0,react-dom@19.0.0": "https://esm.d8d.fun/@testing-library/react@16.3.0?deps=react@19.0.0,react-dom@19.0.0",
@@ -1427,6 +1429,7 @@
     "https://esm.d8d.fun/@types/react-dom@~19.0.4/client.d.ts": "https://esm.d8d.fun/@types/react-dom@19.0.6/client.d.ts",
     "https://esm.d8d.fun/@types/react-dom@~19.0.4/index.d.ts": "https://esm.d8d.fun/@types/react-dom@19.0.6/index.d.ts",
     "https://esm.d8d.fun/@types/react-dom@~19.0.6/client.d.ts": "https://esm.d8d.fun/@types/react-dom@19.0.6/client.d.ts",
+    "https://esm.d8d.fun/@types/react-dom@~19.0.6/index.d.ts": "https://esm.d8d.fun/@types/react-dom@19.0.6/index.d.ts",
     "https://esm.d8d.fun/@types/react-dom@~19.1.2/client.d.ts": "https://esm.d8d.fun/@types/react-dom@19.1.2/client.d.ts",
     "https://esm.d8d.fun/@types/react-dom@~19.1.2/index.d.ts": "https://esm.d8d.fun/@types/react-dom@19.1.2/index.d.ts",
     "https://esm.d8d.fun/@types/react-dom@~19.1.2/test-utils/index.d.ts": "https://esm.d8d.fun/@types/react-dom@19.1.2/test-utils/index.d.ts",
@@ -2777,8 +2780,12 @@
     "https://esm.d8d.fun/@deno/shim-deno@0.18.2/denonext/shim-deno.mjs": "819d8ac34fdaf60658cf03d137f14adaff3f13a279ffd79cd8797d84a6ac46ab",
     "https://esm.d8d.fun/@deno/shim-deno@0.18.2?target=denonext": "ffa3ca347bb6b6530720158f307a2e31b16728fbb52e6432254a07d52fcbc404",
     "https://esm.d8d.fun/@heroicons/react@2.1.1/24/outline?deps=react@19.0.0,react-dom@19.0.0": "5e99f4d40ce60c55b5cf421c3cf3f13df1707cf53152e447b2332570412cd77a",
+    "https://esm.d8d.fun/@heroicons/react@2.1.1/24/outline?dev&deps=react@19.0.0,react-dom@19.0.0": "6b98b262ef98bf59857e12d089c9506e0a4f9982fb335ce22ff1f9f76ac4245a",
     "https://esm.d8d.fun/@heroicons/react@2.1.1/24/solid?deps=react@19.0.0,react-dom@19.0.0": "e3940182b574da537337b1e90a1b7f380e17050457423e13d5ac8c7bc88a3cc0",
+    "https://esm.d8d.fun/@heroicons/react@2.1.1/24/solid?dev&deps=react@19.0.0,react-dom@19.0.0": "6c56d2e8d159ed0238518cd0dea1f1dfd3c05866a732e98f636bafa1b171153d",
+    "https://esm.d8d.fun/@heroicons/react@2.1.1/X-ZHJlYWN0LWRvbUAxOS4wLjAscmVhY3RAMTkuMC4w/denonext/24/outline.development.mjs": "1045f8e5320626992f4a069bd24ae6c1f341d386d768bbf4904608bad876b0f3",
     "https://esm.d8d.fun/@heroicons/react@2.1.1/X-ZHJlYWN0LWRvbUAxOS4wLjAscmVhY3RAMTkuMC4w/denonext/24/outline.mjs": "640f934a0c987f682032049e5d4a455567db676de47bca0d44e76b72023661f7",
+    "https://esm.d8d.fun/@heroicons/react@2.1.1/X-ZHJlYWN0LWRvbUAxOS4wLjAscmVhY3RAMTkuMC4w/denonext/24/solid.development.mjs": "00ca0e838f7d4df154ab86d24e36adbf88708606adf3ea2cc7cc56e59026540d",
     "https://esm.d8d.fun/@heroicons/react@2.1.1/X-ZHJlYWN0LWRvbUAxOS4wLjAscmVhY3RAMTkuMC4w/denonext/24/solid.mjs": "dcbd0c377d92857b6eb23c7dbb2ee6e650b12aa6ae1ef7fcc10dc1964df8ba47",
     "https://esm.d8d.fun/@socket.io/component-emitter@3.1.2/denonext/component-emitter.mjs": "3c6c5f2d64d4933b577a7117df1d8855c51ff01ab3dea8f42af1adcb1a5989e7",
     "https://esm.d8d.fun/@socket.io/component-emitter@3.1.2?target=denonext": "f6ff0f94ae3c9850a2c3a925cc2b236ec03a80fc2298d0ca48c2a90b10487db3",
@@ -2803,6 +2810,8 @@
     "https://esm.d8d.fun/@testing-library/react@16.3.0?dev&deps=react@19.0.0,react-dom@19.0.0": "45f68d1334937328f1f41c9785e1beb85d912fff54fbb32eed597a4f262475a4",
     "https://esm.d8d.fun/@testing-library/react@16.3.0?dev&deps=react@19.0.0,react-dom@19.0.0&export=": "45f68d1334937328f1f41c9785e1beb85d912fff54fbb32eed597a4f262475a4",
     "https://esm.d8d.fun/@testing-library/react@16.3.0?dev&deps=react@19.0.0,react-dom@19.0.0&export=document": "45f68d1334937328f1f41c9785e1beb85d912fff54fbb32eed597a4f262475a4",
+    "https://esm.d8d.fun/@testing-library/user-event@14.6.1": "3f7d71e48391de42ba52305286ecb9e6d3764e34867baec0a3575d24ed699dda",
+    "https://esm.d8d.fun/@testing-library/user-event@14.6.1/denonext/user-event.mjs": "5c0a61f970c02d481dff4294bd64d739f049ea44e9b57f9c14450933a47252cb",
     "https://esm.d8d.fun/agent-base@7.1.3/denonext/agent-base.development.mjs": "0f740785213e5c410760457aa209a0f224da6d5cc65124626b7a1a344c0504f4",
     "https://esm.d8d.fun/agent-base@7.1.3/denonext/agent-base.mjs": "65d873bec0946614e2e33c0c3c1c958ee346695facfdeb51e2dda348e700bd57",
     "https://esm.d8d.fun/agent-base@7.1.3?target=denonext": "c76a77281d852899060a3ecf8bed33525c5f252f53dfac18e2f0ae5957b22910",
@@ -3262,6 +3271,7 @@
     "https://esm.d8d.fun/react-dom@19.0.0/denonext/client.mjs": "af662fd134eea98f37fdcea6142accd0f8a7d2e13c1c3c9e98dc37a8c7aad46b",
     "https://esm.d8d.fun/react-dom@19.0.0/denonext/react-dom.development.mjs": "617b31a0b5be498d4ae231b7deee56d3efde81cd6a4cbec2da66291b08cac51c",
     "https://esm.d8d.fun/react-dom@19.0.0/denonext/react-dom.mjs": "a2f7bc344e1d5b7ca47e68665291e206ae4db17ee84f234f3d3e2533b9119f63",
+    "https://esm.d8d.fun/react-dom@19.0.0?dev": "a80f467a0d1c4e36b15ff59a33c55646a33d35e761ea218b8354ea26980f34d2",
     "https://esm.d8d.fun/react-dom@19.1.0/client?target=denonext": "a84db2e3e86b71bf712d55f37603df8be4a624d7e93b63795524ec0554b1a6af",
     "https://esm.d8d.fun/react-dom@19.1.0/client?target=denonext&dev": "ac56773a1d6cde780e97c8337f3a66e5200d623fb90b93076f0fe6675f945306",
     "https://esm.d8d.fun/react-dom@19.1.0/denonext/client.development.mjs": "ae802423d440da19a3d9dec9e500e8c80f20f085f7ce6e8f06a26cf9e616ee3c",
@@ -3274,8 +3284,10 @@
     "https://esm.d8d.fun/react-dom@19.1.0/test-utils?target=denonext&dev": "10082781244c8cce4b151b990b6475c52d5b83e422a09b66936da87cbdd8e35e",
     "https://esm.d8d.fun/react-dom@19.1.0?target=denonext": "0af10bc13bc7ef60cd74313dc427d357184e28c86cc878340add5cde7ba00120",
     "https://esm.d8d.fun/react-dom@19.1.0?target=denonext&dev": "cd750cefba42a1c5575f80e70fba6a2f1fdf3dac780cf380dbe5ffc005b04bd2",
+    "https://esm.d8d.fun/react-hook-form@7.55.0/X-ZHJlYWN0LWRvbUAxOS4wLjAscmVhY3RAMTkuMC4w/denonext/react-hook-form.development.mjs": "b58c0d230f984a8dc8302b5dfddd173f03308faa92b432cd21edaa823fe43d8c",
     "https://esm.d8d.fun/react-hook-form@7.55.0/X-ZHJlYWN0LWRvbUAxOS4wLjAscmVhY3RAMTkuMC4w/denonext/react-hook-form.mjs": "788ec1a54e10051f539ba435aa513802c823bad03e11e2534b1b17df99189a87",
     "https://esm.d8d.fun/react-hook-form@7.55.0?deps=react@19.0.0,react-dom@19.0.0": "8ed376b3af6e11be43538b15e654692d5995232523a6dc16ce7f81263b1a3614",
+    "https://esm.d8d.fun/react-hook-form@7.55.0?dev&deps=react@19.0.0,react-dom@19.0.0": "f4fc9cd4c769d32b703007cf3353ab2e0ddcd3de90a5536fc03fdcf7396fbcf4",
     "https://esm.d8d.fun/react-is@17.0.2/denonext/react-is.development.mjs": "4b640b0b829848a8150fcae01bc5c44894f71490a58e2de7b346b44e35c29af6",
     "https://esm.d8d.fun/react-is@17.0.2/denonext/react-is.mjs": "6b144222bce64ee266c22c6c06d7620f948997404e9bf285c48ee265446193d5",
     "https://esm.d8d.fun/react-is@17.0.2?target=denonext": "d8b7baf6cea6e132411aa2e4d69366fe0b7c911c7481d31f790b8f159311c7a3",
@@ -3443,6 +3455,7 @@
   },
   "workspace": {
     "dependencies": [
+      "npm:@testing-library/user-event@14.6.1",
       "npm:jsdom@26.0.0"
     ]
   }

+ 45 - 0
test.log

@@ -0,0 +1,45 @@
+running 1 test from ./client/admin/pages_know_info.test.tsx
+知识库管理页面测试 ...
+  应正确渲染页面元素 ... ok (1s)
+  初始加载表格数据 ... ok (940ms)
+  搜索表单应正常工作 ...
+------- output -------
+searchInput 数据分析
+<input
+  class="ant-input css-dev-only-do-not-override-1a3rktk ant-input-outlined ant-input-status-success"
+  id="title"
+  placeholder="请输入文章标题"
+  type="text"
+  value="数据分析"
+/>
+<span>
+  搜 索
+</span>
+handleSearch { title: "数据分析", category: undefined }
+等待表格刷新并验证 2
+等待搜索结果并验证 2
+等待搜索结果并验证 2
+等待搜索结果并验证 2
+等待搜索结果并验证 2
+等待搜索结果并验证 2
+等待搜索结果并验证 2
+等待搜索结果并验证 2
+等待搜索结果并验证 2
+等待搜索结果并验证 2
+等待搜索结果并验证 2
+等待搜索结果并验证 2
+等待搜索结果并验证 2
+等待搜索结果并验证 2
+等待搜索结果并验证 2
+等待搜索结果并验证 2
+等待搜索结果并验证 3
+hasMatch true
+----- output end -----
+  搜索表单应正常工作 ... ok (2s)
+  表格应加载并显示数据 ... ok (28ms)
+  应能打开添加文章模态框 ... ok (241ms)
+  应能完整添加一篇文章 ... ok (153ms)
+知识库管理页面测试 ... ok (4s)
+
+ok | 1 passed (6 steps) | 0 failed (4s)
+