pages_know_info.test.tsx 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  1. import { JSDOM } from 'jsdom'
  2. import React from 'react'
  3. import {render, waitFor, within, fireEvent} from '@testing-library/react'
  4. import {userEvent} from '@testing-library/user-event'
  5. import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
  6. import { createBrowserRouter, RouterProvider } 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. // 应用入口组件
  36. const App = () => {
  37. // 路由配置
  38. const router = createBrowserRouter([
  39. {
  40. path: '/',
  41. element: (
  42. <ProtectedRoute>
  43. <KnowInfoPage />
  44. </ProtectedRoute>
  45. )
  46. },
  47. ]);
  48. return <RouterProvider router={router} />
  49. };
  50. // setup function
  51. function setup() {
  52. const dom = new JSDOM(`<body></body>`, {
  53. runScripts: "dangerously",
  54. pretendToBeVisual: true,
  55. url: "http://localhost",
  56. });
  57. // 模拟浏览器环境
  58. globalThis.window = dom.window;
  59. globalThis.document = dom.window.document;
  60. // 添加必要的 DOM 配置
  61. globalThis.Node = dom.window.Node;
  62. globalThis.Document = dom.window.Document;
  63. globalThis.HTMLInputElement = dom.window.HTMLInputElement;
  64. globalThis.HTMLButtonElement = dom.window.HTMLButtonElement;
  65. // 定义浏览器环境所需的类
  66. globalThis.Element = dom.window.Element;
  67. globalThis.HTMLElement = dom.window.HTMLElement;
  68. globalThis.ShadowRoot = dom.window.ShadowRoot;
  69. globalThis.SVGElement = dom.window.SVGElement;
  70. // 模拟 getComputedStyle
  71. globalThis.getComputedStyle = (elt) => {
  72. const style = new dom.window.CSSStyleDeclaration();
  73. style.getPropertyValue = () => '';
  74. return style;
  75. };
  76. // 模拟matchMedia函数
  77. globalThis.matchMedia = (query) => ({
  78. matches: query.includes('max-width'),
  79. media: query,
  80. onchange: null,
  81. addListener: () => {},
  82. removeListener: () => {},
  83. addEventListener: () => {},
  84. removeEventListener: () => {},
  85. dispatchEvent: () => false,
  86. });
  87. // 模拟动画相关API
  88. globalThis.AnimationEvent = globalThis.AnimationEvent || dom.window.Event;
  89. globalThis.TransitionEvent = globalThis.TransitionEvent || dom.window.Event;
  90. // 模拟requestAnimationFrame
  91. globalThis.requestAnimationFrame = globalThis.requestAnimationFrame || ((cb) => setTimeout(cb, 0));
  92. globalThis.cancelAnimationFrame = globalThis.cancelAnimationFrame || clearTimeout;
  93. // 设置浏览器尺寸相关方法
  94. window.resizeTo = (width, height) => {
  95. window.innerWidth = width || window.innerWidth;
  96. window.innerHeight = height || window.innerHeight;
  97. window.dispatchEvent(new Event('resize'));
  98. };
  99. window.scrollTo = () => {};
  100. const customScreen = within(document.body);
  101. const user = userEvent.setup({
  102. document: dom.window.document,
  103. delay: 10,
  104. skipAutoClose: true,
  105. });
  106. localStorage.setItem('token', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwidXNlcm5hbWUiOiJhZG1pbiIsInNlc3Npb25JZCI6Ijk4T2lzTW5SMm0zQ0dtNmo4SVZrNyIsInJvbGVJbmZvIjpudWxsLCJpYXQiOjE3NDQzNjIzNTUsImV4cCI6MTc0NDQ0ODc1NX0.k1Ld7qWAZmdzsbjmrl_0ec1FqF_GimaOuQIic4znRtc');
  107. axios.defaults.baseURL = 'https://23957.dev.d8dcloud.com'
  108. const queryClient = new QueryClient()
  109. return {
  110. user,
  111. // Import `render` from the framework library of your choice.
  112. // See https://testing-library.com/docs/dom-testing-library/install#wrappers
  113. ...render(
  114. <QueryClientProvider client={queryClient}>
  115. <AuthProvider>
  116. <App />
  117. </AuthProvider>
  118. </QueryClientProvider>
  119. ),
  120. }
  121. }
  122. // // 使用异步测试处理组件渲染
  123. // Deno.test({
  124. // name: '知识库管理页面基础测试',
  125. // fn: async (t) => {
  126. // // 存储所有需要清理的定时器
  127. // const timers: number[] = [];
  128. // const originalSetTimeout = globalThis.setTimeout;
  129. // const originalSetInterval = globalThis.setInterval;
  130. // // 重写定时器方法以跟踪所有创建的定时器
  131. // globalThis.setTimeout = ((callback, delay, ...args) => {
  132. // const id = originalSetTimeout(callback, delay, ...args);
  133. // timers.push(id);
  134. // return id;
  135. // }) as typeof setTimeout;
  136. // globalThis.setInterval = ((callback, delay, ...args) => {
  137. // const id = originalSetInterval(callback, delay, ...args);
  138. // timers.push(id);
  139. // return id;
  140. // }) as typeof setInterval;
  141. // // 清理函数
  142. // const cleanup = () => {
  143. // for (const id of timers) {
  144. // clearTimeout(id);
  145. // clearInterval(id);
  146. // }
  147. // // 恢复原始定时器方法
  148. // globalThis.setTimeout = originalSetTimeout;
  149. // globalThis.setInterval = originalSetInterval;
  150. // };
  151. // try {
  152. // // // 渲染组件
  153. // // const {
  154. // // findByText, findByPlaceholderText, queryByText,
  155. // // findByRole, findAllByRole, findByLabelText, findAllByText, debug,
  156. // // queryByRole
  157. // // } = render(
  158. // // <QueryClientProvider client={queryClient}>
  159. // // <AuthProvider>
  160. // // <App />
  161. // // </AuthProvider>
  162. // // </QueryClientProvider>
  163. // // );
  164. // // 测试1: 基本渲染
  165. // await t.step('应正确渲染页面元素', async () => {
  166. // const { findByText } = setup()
  167. // await waitFor(async () => {
  168. // const title = await findByText(/知识库管理/i);
  169. // assertExists(title, '未找到知识库管理标题');
  170. // }, {
  171. // timeout: 1000 * 5,
  172. // });
  173. // });
  174. // // 初始加载表格数据
  175. // await t.step('初始加载表格数据', async () => {
  176. // const { findByRole } = setup()
  177. // await waitFor(async () => {
  178. // const table = await findByRole('table');
  179. // const rows = await within(table).findAllByRole('row');
  180. // // 应该大于2行
  181. // assert(rows.length > 2, '表格没有数据'); // 1是表头行 2是数据行
  182. // }, {
  183. // timeout: 1000 * 5,
  184. // });
  185. // });
  186. // // 测试2: 搜索表单功能
  187. // await t.step('搜索表单应正常工作', async () => {
  188. // const {findByPlaceholderText, findByText, findByRole, user} = setup()
  189. // // 等待知识库管理标题出现
  190. // await waitFor(async () => {
  191. // const title = await findByText(/知识库管理/i);
  192. // assertExists(title, '未找到知识库管理标题');
  193. // }, {
  194. // timeout: 1000 * 5,
  195. // });
  196. // // 直接查找标题搜索输入框和搜索按钮
  197. // const searchInput = await findByPlaceholderText(/请输入文章标题/i) as HTMLInputElement;
  198. // const searchButton = await findByText(/搜 索/i);
  199. // assertExists(searchInput, '未找到搜索输入框');
  200. // assertExists(searchButton, '未找到搜索按钮');
  201. // // 输入搜索内容
  202. // await user.type(searchInput, '数据分析')
  203. // assertEquals(searchInput.value, '数据分析', '搜索输入框值未更新');
  204. // // 提交搜索
  205. // await user.click(searchButton);
  206. // let rows: HTMLElement[] = [];
  207. // const table = await findByRole('table');
  208. // assertExists(table, '未找到数据表格');
  209. // // 等待表格刷新并验证
  210. // await waitFor(async () => {
  211. // rows = await within(table).findAllByRole('row');
  212. // assert(rows.length === 2, '表格未刷新');
  213. // }, {
  214. // timeout: 1000 * 5,
  215. // onTimeout: () => new Error('等待表格刷新超时')
  216. // });
  217. // // 等待搜索结果并验证
  218. // await waitFor(async () => {
  219. // rows = await within(table).findAllByRole('row');
  220. // assert(rows.length > 2, '表格没有数据');
  221. // }, {
  222. // timeout: 1000 * 5,
  223. // onTimeout: () => new Error('等待搜索结果超时')
  224. // });
  225. // // 检查至少有一行包含"数据分析"
  226. // const matchResults = await Promise.all(rows.map(async row => {
  227. // try{
  228. // const cells = await within(row).findAllByRole('cell');
  229. // return cells.some(cell => {
  230. // return cell.textContent?.includes('数据分析')
  231. // });
  232. // } catch (error: unknown) {
  233. // // console.error('搜索结果获取失败', error)
  234. // return false
  235. // }
  236. // }))
  237. // // console.log('matchResults', matchResults)
  238. // const hasMatch = matchResults.some(result => result);
  239. // assert(hasMatch, '搜索结果中没有找到包含"数据分析"的文章');
  240. // });
  241. // // 测试3: 表格数据加载
  242. // await t.step('表格应加载并显示数据', async () => {
  243. // const {findByRole, queryByText} = setup()
  244. // // 等待数据加载完成或表格出现,最多等待5秒
  245. // await waitFor(async () => {
  246. // // 检查加载状态是否消失
  247. // const loading = queryByText(/正在加载数据/i);
  248. // if (loading) {
  249. // throw new Error('数据仍在加载中');
  250. // }
  251. // // 检查表格是否出现
  252. // const table = await findByRole('table');
  253. // assertExists(table, '未找到数据表格');
  254. // // 检查表格是否有数据行
  255. // const rows = await within(table).findAllByRole('row');
  256. // assertNotEquals(rows.length, 1, '表格没有数据行'); // 1是表头行
  257. // }, {
  258. // timeout: 5000, // 5秒超时
  259. // onTimeout: (error) => {
  260. // return new Error(`数据加载超时: ${error.message}`);
  261. // }
  262. // });
  263. // });
  264. // // 测试4: 添加文章功能
  265. // await t.step('应能打开添加文章模态框', async () => {
  266. // const {findByText, findByRole, user} = setup()
  267. // // 等待知识库管理标题出现
  268. // await waitFor(async () => {
  269. // const title = await findByText(/知识库管理/i);
  270. // assertExists(title, '未找到知识库管理标题');
  271. // }, {
  272. // timeout: 1000 * 5,
  273. // });
  274. // const addButton = await findByText(/添加文章/i);
  275. // assertExists(addButton, '未找到添加文章按钮');
  276. // await user.click(addButton);
  277. // // 找到模态框
  278. // const modal = await findByRole('dialog');
  279. // assertExists(modal, '未找到模态框');
  280. // const modalTitle = await within(modal).findByText(/添加知识库文章/i);
  281. // assertExists(modalTitle, '未找到添加文章模态框');
  282. // // 验证表单字段
  283. // const titleInput = await within(modal).findByPlaceholderText(/请输入文章标题/i);
  284. // assertExists(titleInput, '未找到标题输入框');
  285. // const contentInput = await within(modal).findByPlaceholderText(/请输入文章内容/i);
  286. // assertExists(contentInput, '未找到文章内容输入框');
  287. // // 取消
  288. // const cancelButton = await within(modal).findByText(/取 消/i);
  289. // assertExists(cancelButton, '未找到取消按钮');
  290. // await user.click(cancelButton);
  291. // // 验证模态框是否关闭
  292. // await waitFor(async () => {
  293. // const modalTitle = await within(modal).findByText(/添加知识库文章/i);
  294. // assertExists(!modalTitle, '模态框未关闭');
  295. // }, {
  296. // timeout: 1000 * 5,
  297. // onTimeout: () => new Error('等待模态框关闭超时')
  298. // });
  299. // });
  300. // } finally {
  301. // // 确保清理所有定时器
  302. // cleanup();
  303. // }
  304. // },
  305. // sanitizeOps: false, // 禁用操作清理检查
  306. // sanitizeResources: false, // 禁用资源清理检查
  307. // });
  308. Deno.test({
  309. name: '知识库管理页面新增测试',
  310. fn: async (t) => {
  311. // 存储所有需要清理的定时器
  312. const timers: number[] = [];
  313. const originalSetTimeout = globalThis.setTimeout;
  314. const originalSetInterval = globalThis.setInterval;
  315. // 重写定时器方法以跟踪所有创建的定时器
  316. globalThis.setTimeout = ((callback, delay, ...args) => {
  317. const id = originalSetTimeout(callback, delay, ...args);
  318. timers.push(id);
  319. return id;
  320. }) as typeof setTimeout;
  321. globalThis.setInterval = ((callback, delay, ...args) => {
  322. const id = originalSetInterval(callback, delay, ...args);
  323. timers.push(id);
  324. return id;
  325. }) as typeof setInterval;
  326. // 清理函数
  327. const cleanup = () => {
  328. for (const id of timers) {
  329. clearTimeout(id);
  330. clearInterval(id);
  331. }
  332. // 恢复原始定时器方法
  333. globalThis.setTimeout = originalSetTimeout;
  334. globalThis.setInterval = originalSetInterval;
  335. };
  336. try {
  337. // // 渲染组件
  338. // const {
  339. // findByText, findByPlaceholderText, queryByText,
  340. // findByRole, findAllByRole, findByLabelText, findAllByText, debug,
  341. // queryByRole
  342. // } = render(
  343. // <QueryClientProvider client={queryClient}>
  344. // <AuthProvider>
  345. // <App />
  346. // </AuthProvider>
  347. // </QueryClientProvider>
  348. // );
  349. // 测试5: 完整添加文章流程
  350. await t.step('应能完整添加一篇文章', async () => {
  351. const {findByText, findByRole, debug, user} = setup()
  352. // 等待知识库管理标题出现
  353. await waitFor(async () => {
  354. const title = await findByText(/知识库管理/i);
  355. assertExists(title, '未找到知识库管理标题');
  356. }, {
  357. timeout: 1000 * 5,
  358. });
  359. // 打开添加模态框
  360. const addButton = await findByText(/添加文章/i);
  361. assertExists(addButton, '未找到添加文章按钮');
  362. await user.click(addButton);
  363. // 找到模态框
  364. const modal = await findByRole('dialog');
  365. assertExists(modal, '未找到模态框');
  366. const modalTitle = await within(modal).findByText(/添加知识库文章/i);
  367. assertExists(modalTitle, '未找到添加文章模态框');
  368. // 确保上一个测试的输入框值不会影响这个测试
  369. // 在模态框内查找标题和内容输入框
  370. const titleInput = await within(modal).findByPlaceholderText(/请输入文章标题/i) as HTMLInputElement;
  371. const contentInput = await within(modal).findByPlaceholderText(/请输入文章内容/i) as HTMLTextAreaElement;
  372. const submitButton = await within(modal).findByText(/确 定/i);
  373. assertExists(titleInput, '未找到模态框中的标题输入框');
  374. assertExists(contentInput, '未找到文章内容输入框');
  375. assertExists(submitButton, '未找到提交按钮');
  376. // 先清空输入框,以防止之前的测试影响
  377. // await user.clear(titleInput);
  378. // await user.clear(contentInput);
  379. // 输入新值
  380. await user.type(titleInput, '测试文章标题');
  381. await user.type(contentInput, '这是测试文章内容');
  382. debug(titleInput);
  383. debug(contentInput);
  384. debug(submitButton);
  385. // 提交表单
  386. await user.click(submitButton);
  387. // 验证表单字段
  388. assertEquals(titleInput.value, '测试文章标题', '模态框中标题输入框值未更新');
  389. assertEquals(contentInput.value, '这是测试文章内容', '内容输入框值未更新');
  390. let rows: HTMLElement[] = [];
  391. const table = await findByRole('table');
  392. assertExists(table, '未找到数据表格');
  393. // 等待表格刷新并验证
  394. await waitFor(async () => {
  395. rows = await within(table).findAllByRole('row');
  396. assert(rows.length === 2, '表格未刷新');
  397. }, {
  398. timeout: 1000 * 5,
  399. onTimeout: () => new Error('等待表格刷新超时')
  400. });
  401. // 等待搜索结果并验证
  402. await waitFor(async () => {
  403. rows = await within(table).findAllByRole('row');
  404. assert(rows.length > 2, '表格没有数据');
  405. }, {
  406. timeout: 1000 * 5,
  407. onTimeout: () => new Error('等待搜索结果超时')
  408. });
  409. // 检查至少有一行包含"测试文章标题"
  410. const matchResults = await Promise.all(rows.map(async row => {
  411. try{
  412. const cells = await within(row).findAllByRole('cell');
  413. return cells.some(cell => {
  414. return cell.textContent?.includes('测试文章标题')
  415. });
  416. } catch (error: unknown) {
  417. // console.error('搜索结果获取失败', error)
  418. return false
  419. }
  420. }))
  421. // console.log('matchResults', matchResults)
  422. const hasMatch = matchResults.some(result => result);
  423. assert(hasMatch, '搜索结果中没有找到包含"测试文章标题"的文章');
  424. });
  425. // // 测试5: 分页功能
  426. // await t.step('应显示分页控件', async () => {
  427. // const pagination = await findByRole('navigation');
  428. // assertExists(pagination, '未找到分页控件');
  429. // const pageItems = await findAllByRole('button', { name: /1|2|3|下一页|上一页/i });
  430. // assertNotEquals(pageItems.length, 0, '未找到分页按钮');
  431. // });
  432. // // 测试6: 操作按钮
  433. // await t.step('应显示操作按钮', async () => {
  434. // const editButtons = await findAllByText(/编辑/i);
  435. // assertNotEquals(editButtons.length, 0, '未找到编辑按钮');
  436. // const deleteButtons = await findAllByText(/删除/i);
  437. // assertNotEquals(deleteButtons.length, 0, '未找到删除按钮');
  438. // });
  439. } finally {
  440. // 确保清理所有定时器
  441. cleanup();
  442. }
  443. },
  444. sanitizeOps: false, // 禁用操作清理检查
  445. sanitizeResources: false, // 禁用资源清理检查
  446. });