diff --git a/client/admin/components_protected_route.tsx b/client/admin/components_protected_route.tsx new file mode 100644 index 0000000..18b09f4 --- /dev/null +++ b/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 ( +
+
+
+ ); + } + + // 如果未认证且不再加载中,不显示任何内容(等待重定向) + if (!isAuthenticated) { + return null; + } + + return children; +}; \ No newline at end of file diff --git a/client/admin/pages_know_info.test.tsx b/client/admin/pages_know_info.test.tsx index a7cd5e8..6288420 100644 --- a/client/admin/pages_know_info.test.tsx +++ b/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(``, { 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: ( + + + + ) + }, + ]); + return +}; + // 使用异步测试处理组件渲染 Deno.test({ name: '知识库管理页面测试', @@ -105,6 +170,8 @@ Deno.test({ globalThis.setInterval = originalSetInterval; }; + + try { // 渲染组件 const { @@ -113,79 +180,116 @@ Deno.test({ } = render( - + ); // 测试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) - - // 应该大于2行 - // assert(rows.length > 2, '表格没有数据'); // 1是表头行 2是数据行 - - if (rows.length <= 2) { - throw new Error('表格没有数据'); - } - }, { - timeout: 1000 * 10, - }); - - // 测试2: 搜索表单功能 - await t.step('搜索表单应正常工作', async () => { - const searchInput = await findByPlaceholderText(/请输入文章标题/i) as HTMLInputElement; - const searchButton = await findByText(/搜 索/i); - - // 输入搜索内容 - fireEvent.change(searchInput, { target: { value: '数据分析' } }); - assertEquals(searchInput.value, '数据分析', '搜索输入框值未更新'); - - // 提交搜索 - fireEvent.click(searchButton); - - // // 验证是否触发了搜索 - // await waitFor(() => { - // const loading = queryByText(/正在加载数据/i); - // assertNotEquals(loading, null, '搜索未触发加载状态'); - // }); - - // 等待搜索结果并验证 + await t.step('初始加载表格数据', async () => { await waitFor(async () => { const table = await findByRole('table'); const rows = await within(table).findAllByRole('row'); - debug(rows) - - console.log('rows', rows.length); + // 应该大于2行 + assert(rows.length > 2, '表格没有数据'); // 1是表头行 2是数据行 - // 检查至少有一行包含"数据分析" - const hasMatch = rows.some(async row => { - const cells = await within(row).findAllByRole('cell'); - return cells.some(cell => cell.textContent?.includes('数据分析')); - }); - - console.log('hasMatch', hasMatch); - - assert(hasMatch, '搜索结果中没有找到包含"数据分析"的文章'); }, { - timeout: 5000, + 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, '未找到搜索按钮'); + + // 输入搜索内容 + try { + await user.type(searchInput, '数据分析') + } catch (error: unknown) { + // console.error('输入搜索内容失败', error) + } + assertEquals(searchInput.value, '数据分析', '搜索输入框值未更新'); + + console.log('searchInput', searchInput.value) + + debug(searchInput) + debug(searchButton) + + // 提交搜索 + try { + await user.click(searchButton); + } catch (error: unknown) { + // console.error('点击搜索按钮失败', error) + } + + + let rows: HTMLElement[] = []; + + + const table = await findByRole('table'); + assertExists(table, '未找到数据表格'); + + // 等待表格刷新并验证 + await waitFor(async () => { + rows = await within(table).findAllByRole('row'); + console.log('等待表格刷新并验证', rows.length) + assert(rows.length === 2, '表格未刷新'); + }, { + timeout: 1000 * 5, + onTimeout: () => new Error('等待表格刷新超时') + }); + + // 等待搜索结果并验证 + await waitFor(async () => { + rows = await within(table).findAllByRole('row'); + console.log('等待搜索结果并验证', rows.length) + assert(rows.length > 2, '表格没有数据'); + }, { + 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: 表格数据加载 diff --git a/client/admin/pages_know_info.tsx b/client/admin/pages_know_info.tsx index ba37fbf..7839478 100644 --- a/client/admin/pages_know_info.tsx +++ b/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 = () => { -