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

This commit is contained in:
zyh
2025-04-11 13:44:22 +00:00
parent c0a623bf23
commit 6d53da5880
7 changed files with 262 additions and 104 deletions

View File

@@ -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;
};

View File

@@ -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)
// 应该大于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: 表格数据加载

View File

@@ -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: '',

View File

@@ -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 = () => {