pages_know_info.test.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. import { JSDOM } from 'jsdom'
  2. import React from 'react'
  3. import {render, fireEvent, within, screen, waitFor, configure} from '@testing-library/react'
  4. import {userEvent} from '@testing-library/user-event'
  5. import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
  6. import { createBrowserRouter, RouterProvider, Navigate } from 'react-router'
  7. import {
  8. assertEquals,
  9. assertExists,
  10. assertNotEquals,
  11. assertRejects,
  12. assert,
  13. } from "https://deno.land/std@0.217.0/assert/mod.ts";
  14. import axios from 'axios';
  15. import { KnowInfoPage } from "./pages_know_info.tsx"
  16. import { AuthProvider } from './hooks_sys.tsx'
  17. import { ProtectedRoute } from './components_protected_route.tsx'
  18. // 拦截React DOM中的attachEvent和detachEvent错误
  19. const originalError = console.error;
  20. console.error = (...args) => {
  21. // 过滤掉attachEvent和detachEvent相关的错误
  22. if (args[0] instanceof Error) {
  23. if (args[0].message?.includes('attachEvent is not a function') ||
  24. args[0].message?.includes('detachEvent is not a function')) {
  25. return; // 不输出这些错误
  26. }
  27. } else if (typeof args[0] === 'string') {
  28. if (args[0].includes('attachEvent is not a function') ||
  29. args[0].includes('detachEvent is not a function')) {
  30. return; // 不输出这些错误
  31. }
  32. }
  33. originalError(...args);
  34. };
  35. // // 配置Testing Library的eventWrapper来处理这个问题
  36. // configure({
  37. // eventWrapper: (cb) => {
  38. // try {
  39. // return cb();
  40. // } catch (error) {
  41. // console.log('eventWrapper', cb)
  42. // // 忽略attachEvent和detachEvent相关的错误
  43. // if (error instanceof Error &&
  44. // (error.message?.includes('attachEvent is not a function') ||
  45. // error.message?.includes('detachEvent is not a function'))) {
  46. // // 忽略这个错误并返回一个默认值
  47. // return undefined;
  48. // }
  49. // // 其他错误正常抛出
  50. // throw error;
  51. // }
  52. // }
  53. // });
  54. const queryClient = new QueryClient()
  55. const dom = new JSDOM(`<body></body>`, {
  56. runScripts: "dangerously",
  57. pretendToBeVisual: true,
  58. url: "http://localhost",
  59. });
  60. // 模拟浏览器环境
  61. globalThis.window = dom.window;
  62. globalThis.document = dom.window.document;
  63. // 添加必要的 DOM 配置
  64. globalThis.Node = dom.window.Node;
  65. globalThis.Document = dom.window.Document;
  66. globalThis.HTMLInputElement = dom.window.HTMLInputElement;
  67. globalThis.HTMLButtonElement = dom.window.HTMLButtonElement;
  68. // 定义浏览器环境所需的类
  69. globalThis.Element = dom.window.Element;
  70. globalThis.HTMLElement = dom.window.HTMLElement;
  71. globalThis.ShadowRoot = dom.window.ShadowRoot;
  72. globalThis.SVGElement = dom.window.SVGElement;
  73. // 模拟 getComputedStyle
  74. globalThis.getComputedStyle = (elt) => {
  75. const style = new dom.window.CSSStyleDeclaration();
  76. style.getPropertyValue = () => '';
  77. return style;
  78. };
  79. // 模拟matchMedia函数
  80. globalThis.matchMedia = (query) => ({
  81. matches: query.includes('max-width'),
  82. media: query,
  83. onchange: null,
  84. addListener: () => {},
  85. removeListener: () => {},
  86. addEventListener: () => {},
  87. removeEventListener: () => {},
  88. dispatchEvent: () => false,
  89. });
  90. // 模拟动画相关API
  91. globalThis.AnimationEvent = globalThis.AnimationEvent || dom.window.Event;
  92. globalThis.TransitionEvent = globalThis.TransitionEvent || dom.window.Event;
  93. // 模拟requestAnimationFrame
  94. globalThis.requestAnimationFrame = globalThis.requestAnimationFrame || ((cb) => setTimeout(cb, 0));
  95. globalThis.cancelAnimationFrame = globalThis.cancelAnimationFrame || clearTimeout;
  96. // 设置浏览器尺寸相关方法
  97. window.resizeTo = (width, height) => {
  98. window.innerWidth = width || window.innerWidth;
  99. window.innerHeight = height || window.innerHeight;
  100. window.dispatchEvent(new Event('resize'));
  101. };
  102. window.scrollTo = () => {};
  103. localStorage.setItem('token', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwidXNlcm5hbWUiOiJhZG1pbiIsInNlc3Npb25JZCI6Ijk4T2lzTW5SMm0zQ0dtNmo4SVZrNyIsInJvbGVJbmZvIjpudWxsLCJpYXQiOjE3NDQzNjIzNTUsImV4cCI6MTc0NDQ0ODc1NX0.k1Ld7qWAZmdzsbjmrl_0ec1FqF_GimaOuQIic4znRtc');
  104. axios.defaults.baseURL = 'https://23957.dev.d8dcloud.com'
  105. const customScreen = within(document.body);
  106. // 应用入口组件
  107. const App = () => {
  108. // 路由配置
  109. const router = createBrowserRouter([
  110. {
  111. path: '/',
  112. element: (
  113. <ProtectedRoute>
  114. <KnowInfoPage />
  115. </ProtectedRoute>
  116. )
  117. },
  118. ]);
  119. return <RouterProvider router={router} />
  120. };
  121. // 使用异步测试处理组件渲染
  122. Deno.test({
  123. name: '知识库管理页面测试',
  124. fn: async (t) => {
  125. // 存储所有需要清理的定时器
  126. const timers: number[] = [];
  127. const originalSetTimeout = globalThis.setTimeout;
  128. const originalSetInterval = globalThis.setInterval;
  129. // 重写定时器方法以跟踪所有创建的定时器
  130. globalThis.setTimeout = ((callback, delay, ...args) => {
  131. const id = originalSetTimeout(callback, delay, ...args);
  132. timers.push(id);
  133. return id;
  134. }) as typeof setTimeout;
  135. globalThis.setInterval = ((callback, delay, ...args) => {
  136. const id = originalSetInterval(callback, delay, ...args);
  137. timers.push(id);
  138. return id;
  139. }) as typeof setInterval;
  140. // 清理函数
  141. const cleanup = () => {
  142. for (const id of timers) {
  143. clearTimeout(id);
  144. clearInterval(id);
  145. }
  146. // 恢复原始定时器方法
  147. globalThis.setTimeout = originalSetTimeout;
  148. globalThis.setInterval = originalSetInterval;
  149. };
  150. try {
  151. // 渲染组件
  152. const {
  153. findByText, findByPlaceholderText, queryByText,
  154. findByRole, findAllByRole, findByLabelText, findAllByText, debug
  155. } = render(
  156. <QueryClientProvider client={queryClient}>
  157. <AuthProvider>
  158. <App />
  159. </AuthProvider>
  160. </QueryClientProvider>
  161. );
  162. // 测试1: 基本渲染
  163. await t.step('应正确渲染页面元素', async () => {
  164. await waitFor(async () => {
  165. const title = await findByText(/知识库管理/i);
  166. assertExists(title, '未找到知识库管理标题');
  167. }, {
  168. timeout: 1000 * 5,
  169. });
  170. });
  171. // 初始加载表格数据
  172. await t.step('初始加载表格数据', async () => {
  173. await waitFor(async () => {
  174. const table = await findByRole('table');
  175. const rows = await within(table).findAllByRole('row');
  176. // 应该大于2行
  177. assert(rows.length > 2, '表格没有数据'); // 1是表头行 2是数据行
  178. }, {
  179. timeout: 1000 * 5,
  180. });
  181. });
  182. // 测试2: 搜索表单功能
  183. await t.step('搜索表单应正常工作', async () => {
  184. // 确保在正确的测试环境中设置 userEvent
  185. const user = userEvent.setup({
  186. document: dom.window.document,
  187. delay: 0
  188. });
  189. const searchInput = await findByPlaceholderText(/请输入文章标题/i) as HTMLInputElement;
  190. const searchButton = await findByText(/搜 索/i);
  191. assertExists(searchInput, '未找到搜索输入框');
  192. assertExists(searchButton, '未找到搜索按钮');
  193. // 输入搜索内容
  194. try {
  195. await user.type(searchInput, '数据分析')
  196. } catch (error: unknown) {
  197. // console.error('输入搜索内容失败', error)
  198. }
  199. assertEquals(searchInput.value, '数据分析', '搜索输入框值未更新');
  200. console.log('searchInput', searchInput.value)
  201. debug(searchInput)
  202. debug(searchButton)
  203. // 提交搜索
  204. try {
  205. await user.click(searchButton);
  206. } catch (error: unknown) {
  207. // console.error('点击搜索按钮失败', error)
  208. }
  209. let rows: HTMLElement[] = [];
  210. const table = await findByRole('table');
  211. assertExists(table, '未找到数据表格');
  212. // 等待表格刷新并验证
  213. await waitFor(async () => {
  214. rows = await within(table).findAllByRole('row');
  215. console.log('等待表格刷新并验证', rows.length)
  216. assert(rows.length === 2, '表格未刷新');
  217. }, {
  218. timeout: 1000 * 5,
  219. onTimeout: () => new Error('等待表格刷新超时')
  220. });
  221. // 等待搜索结果并验证
  222. await waitFor(async () => {
  223. rows = await within(table).findAllByRole('row');
  224. console.log('等待搜索结果并验证', rows.length)
  225. assert(rows.length > 2, '表格没有数据');
  226. }, {
  227. timeout: 1000 * 5,
  228. onTimeout: () => new Error('等待搜索结果超时')
  229. });
  230. // 检查至少有一行包含"数据分析"
  231. const matchResults = await Promise.all(rows.map(async row => {
  232. try{
  233. const cells = await within(row).findAllByRole('cell');
  234. return cells.some(cell => {
  235. return cell.textContent?.includes('数据分析')
  236. });
  237. } catch (error: unknown) {
  238. // console.error('搜索结果获取失败', error)
  239. return false
  240. }
  241. }))
  242. // console.log('matchResults', matchResults)
  243. const hasMatch = matchResults.some(result => result);
  244. console.log('hasMatch', hasMatch)
  245. assert(hasMatch, '搜索结果中没有找到包含"数据分析"的文章');
  246. });
  247. // 测试3: 表格数据加载
  248. await t.step('表格应加载并显示数据', async () => {
  249. // 等待数据加载完成或表格出现,最多等待5秒
  250. await waitFor(async () => {
  251. // 检查加载状态是否消失
  252. const loading = queryByText(/正在加载数据/i);
  253. if (loading) {
  254. throw new Error('数据仍在加载中');
  255. }
  256. // 检查表格是否出现
  257. const table = await findByRole('table');
  258. assertExists(table, '未找到数据表格');
  259. // 检查表格是否有数据行
  260. const rows = await within(table).findAllByRole('row');
  261. assertNotEquals(rows.length, 1, '表格没有数据行'); // 1是表头行
  262. }, {
  263. timeout: 5000, // 5秒超时
  264. onTimeout: (error) => {
  265. return new Error(`数据加载超时: ${error.message}`);
  266. }
  267. });
  268. });
  269. // 测试4: 添加文章功能
  270. await t.step('应能打开添加文章模态框', async () => {
  271. const addButton = await findByText(/添加文章/i);
  272. fireEvent.click(addButton);
  273. const modalTitle = await findByText(/添加知识库文章/i);
  274. assertExists(modalTitle, '未找到添加文章模态框');
  275. // 验证表单字段
  276. const titleInput = await findByLabelText(/文章标题/i);
  277. assertExists(titleInput, '未找到标题输入框');
  278. });
  279. // 测试5: 完整添加文章流程
  280. await t.step('应能完整添加一篇文章', async () => {
  281. // 打开添加模态框
  282. const addButton = await findByText(/添加文章/i);
  283. fireEvent.click(addButton);
  284. // 填写表单
  285. const titleInput = await findByLabelText(/文章标题/i) as HTMLInputElement;
  286. const contentInput = await findByLabelText(/文章内容/i) as HTMLTextAreaElement;
  287. const submitButton = await findByText(/确 定/i);
  288. fireEvent.change(titleInput, { target: { value: '测试文章标题' } });
  289. fireEvent.change(contentInput, { target: { value: '这是测试文章内容' } });
  290. // 验证表单字段
  291. assertEquals(titleInput.value, '测试文章标题', '标题输入框值未更新');
  292. assertEquals(contentInput.value, '这是测试文章内容', '内容输入框值未更新');
  293. // 提交表单
  294. fireEvent.click(submitButton);
  295. // // 验证提交后状态
  296. // await waitFor(() => {
  297. // const successMessage = queryByText(/添加成功/i);
  298. // assertExists(successMessage, '未显示添加成功提示');
  299. // });
  300. // // 验证模态框已关闭
  301. // await waitFor(() => {
  302. // const modalTitle = queryByText(/添加知识库文章/i);
  303. // assertEquals(modalTitle, null, '添加模态框未关闭');
  304. // });
  305. // 验证表格中是否出现新添加的文章
  306. await waitFor(async () => {
  307. const table = await findByRole('table');
  308. const rows = await within(table).findAllByRole('row');
  309. const hasNewArticle = rows.some(async row => {
  310. // 使用更通用的选择器来查找包含文本的单元格
  311. const cells = await within(row).findAllByRole('cell')
  312. return cells.some(cell => cell.textContent?.includes('测试文章标题'));
  313. });
  314. assert(hasNewArticle, '新添加的文章未出现在表格中');
  315. },
  316. // {
  317. // timeout: 5000,
  318. // onTimeout: () => new Error('等待新文章出现在表格中超时')
  319. // }
  320. );
  321. });
  322. // // 测试5: 分页功能
  323. // await t.step('应显示分页控件', async () => {
  324. // const pagination = await findByRole('navigation');
  325. // assertExists(pagination, '未找到分页控件');
  326. // const pageItems = await findAllByRole('button', { name: /1|2|3|下一页|上一页/i });
  327. // assertNotEquals(pageItems.length, 0, '未找到分页按钮');
  328. // });
  329. // // 测试6: 操作按钮
  330. // await t.step('应显示操作按钮', async () => {
  331. // const editButtons = await findAllByText(/编辑/i);
  332. // assertNotEquals(editButtons.length, 0, '未找到编辑按钮');
  333. // const deleteButtons = await findAllByText(/删除/i);
  334. // assertNotEquals(deleteButtons.length, 0, '未找到删除按钮');
  335. // });
  336. } finally {
  337. // 确保清理所有定时器
  338. cleanup();
  339. }
  340. },
  341. sanitizeOps: false, // 禁用操作清理检查
  342. sanitizeResources: false, // 禁用资源清理检查
  343. });