新增受保护路由组件以增强认证逻辑,更新知识库管理页面的测试用例以集成用户事件,提升测试的准确性和稳定性。同时,优化相关依赖项配置,确保与最新库版本兼容,提升代码可维护性和用户体验。
This commit is contained in:
36
client/admin/components_protected_route.tsx
Normal file
36
client/admin/components_protected_route.tsx
Normal 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;
|
||||
};
|
||||
@@ -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: 表格数据加载
|
||||
|
||||
@@ -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: '',
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
Reference in New Issue
Block a user