diff --git a/frontend/admin/src/components/layout/PageLayout/ContentCard.test.tsx b/frontend/admin/src/components/layout/PageLayout/ContentCard.test.tsx new file mode 100644 index 0000000..c6c24f8 --- /dev/null +++ b/frontend/admin/src/components/layout/PageLayout/ContentCard.test.tsx @@ -0,0 +1,66 @@ +import { render, screen } from '@testing-library/react' +import { describe, expect, it } from 'vitest' + +import { ContentCard } from './ContentCard' + +vi.mock('antd', () => ({ + Card: ({ + children, + className, + style, + title, + }: { + children?: React.ReactNode + className?: string + style?: React.CSSProperties + title?: React.ReactNode + }) => ( +
+ {title &&
{title}
} + {children} +
+ ), +})) + +describe('ContentCard', () => { + it('renders children content', () => { + render( + +
card content
+
, + ) + + expect(screen.getByText('card content')).toBeInTheDocument() + }) + + it('applies custom className', () => { + render( + +
content
+
, + ) + + expect(screen.getByTestId('card')).toHaveAttribute('data-class', expect.stringContaining('custom-class')) + }) + + it('applies custom style', () => { + const customStyle = { marginTop: '20px' } + render( + +
content
+
, + ) + + expect(screen.getByTestId('card')).toHaveStyle({ marginTop: '20px' }) + }) + + it('renders with title', () => { + render( + +
content
+
, + ) + + expect(screen.getByTestId('card-title')).toHaveTextContent('Card Title') + }) +}) diff --git a/frontend/admin/src/components/layout/PageLayout/FilterCard.test.tsx b/frontend/admin/src/components/layout/PageLayout/FilterCard.test.tsx new file mode 100644 index 0000000..4664114 --- /dev/null +++ b/frontend/admin/src/components/layout/PageLayout/FilterCard.test.tsx @@ -0,0 +1,40 @@ +import { render, screen } from '@testing-library/react' +import { describe, expect, it } from 'vitest' + +import { FilterCard } from './FilterCard' + +vi.mock('antd', () => ({ + Card: ({ + children, + className, + }: { + children?: React.ReactNode + className?: string + }) => ( +
+ {children} +
+ ), +})) + +describe('FilterCard', () => { + it('renders children content', () => { + render( + +
filter content
+
, + ) + + expect(screen.getByText('filter content')).toBeInTheDocument() + }) + + it('applies custom className', () => { + render( + +
content
+
, + ) + + expect(screen.getByTestId('card')).toHaveAttribute('data-class', expect.stringContaining('custom-filter-class')) + }) +}) diff --git a/frontend/admin/src/components/layout/PageLayout/PageLayout.test.tsx b/frontend/admin/src/components/layout/PageLayout/PageLayout.test.tsx new file mode 100644 index 0000000..62ffb01 --- /dev/null +++ b/frontend/admin/src/components/layout/PageLayout/PageLayout.test.tsx @@ -0,0 +1,27 @@ +import { render, screen } from '@testing-library/react' +import { describe, expect, it } from 'vitest' + +import { PageLayout } from './PageLayout' + +describe('PageLayout', () => { + it('renders children content', () => { + render( + +
page content
+
, + ) + + expect(screen.getByText('page content')).toBeInTheDocument() + }) + + it('applies custom className', () => { + render( + +
content
+
, + ) + + const element = screen.getByText('content') + expect(element.parentElement).toHaveClass('custom-page-layout') + }) +}) diff --git a/frontend/admin/src/components/layout/PageLayout/TableCard.test.tsx b/frontend/admin/src/components/layout/PageLayout/TableCard.test.tsx new file mode 100644 index 0000000..9eaf61e --- /dev/null +++ b/frontend/admin/src/components/layout/PageLayout/TableCard.test.tsx @@ -0,0 +1,40 @@ +import { render, screen } from '@testing-library/react' +import { describe, expect, it } from 'vitest' + +import { TableCard } from './TableCard' + +vi.mock('antd', () => ({ + Card: ({ + children, + className, + }: { + children?: React.ReactNode + className?: string + }) => ( +
+ {children} +
+ ), +})) + +describe('TableCard', () => { + it('renders children content', () => { + render( + +
table content
+
, + ) + + expect(screen.getByText('table content')).toBeInTheDocument() + }) + + it('applies custom className', () => { + render( + +
content
+
, + ) + + expect(screen.getByTestId('card')).toHaveAttribute('data-class', expect.stringContaining('custom-table-class')) + }) +}) diff --git a/frontend/admin/src/components/layout/PageLayout/TreeCard.test.tsx b/frontend/admin/src/components/layout/PageLayout/TreeCard.test.tsx new file mode 100644 index 0000000..4722b08 --- /dev/null +++ b/frontend/admin/src/components/layout/PageLayout/TreeCard.test.tsx @@ -0,0 +1,40 @@ +import { render, screen } from '@testing-library/react' +import { describe, expect, it } from 'vitest' + +import { TreeCard } from './TreeCard' + +vi.mock('antd', () => ({ + Card: ({ + children, + className, + }: { + children?: React.ReactNode + className?: string + }) => ( +
+ {children} +
+ ), +})) + +describe('TreeCard', () => { + it('renders children content', () => { + render( + +
tree content
+
, + ) + + expect(screen.getByText('tree content')).toBeInTheDocument() + }) + + it('applies custom className', () => { + render( + +
content
+
, + ) + + expect(screen.getByTestId('card')).toHaveAttribute('data-class', expect.stringContaining('custom-tree-class')) + }) +}) diff --git a/frontend/admin/src/layouts/AuthLayout/AuthLayout.test.tsx b/frontend/admin/src/layouts/AuthLayout/AuthLayout.test.tsx new file mode 100644 index 0000000..d38b1b6 --- /dev/null +++ b/frontend/admin/src/layouts/AuthLayout/AuthLayout.test.tsx @@ -0,0 +1,49 @@ +import { render, screen } from '@testing-library/react' +import { describe, expect, it } from 'vitest' + +import { AuthLayout } from './AuthLayout' + +describe('AuthLayout', () => { + it('renders children in the form area', () => { + render( + +
login form
+
, + ) + + expect(screen.getByText('login form')).toBeInTheDocument() + }) + + it('displays the brand title', () => { + render( + +
content
+
, + ) + + expect(screen.getByText('用户管理系统')).toBeInTheDocument() + }) + + it('displays brand description', () => { + render( + +
content
+
, + ) + + expect(screen.getByText('企业级用户管理解决方案')).toBeInTheDocument() + }) + + it('displays feature list', () => { + render( + +
content
+
, + ) + + expect(screen.getByText('支持多种登录方式')).toBeInTheDocument() + expect(screen.getByText('基于角色的权限控制')).toBeInTheDocument() + expect(screen.getByText('完整的审计日志')).toBeInTheDocument() + expect(screen.getByText('安全的双因素认证')).toBeInTheDocument() + }) +}) diff --git a/frontend/admin/src/pages/admin/LoginLogsPage/LoginLogDetailDrawer.test.tsx b/frontend/admin/src/pages/admin/LoginLogsPage/LoginLogDetailDrawer.test.tsx new file mode 100644 index 0000000..ae27eda --- /dev/null +++ b/frontend/admin/src/pages/admin/LoginLogsPage/LoginLogDetailDrawer.test.tsx @@ -0,0 +1,123 @@ +import type { ReactNode } from 'react' +import { render, screen } from '@testing-library/react' +import { describe, expect, it, vi } from 'vitest' + +import { LoginLogDetailDrawer } from './LoginLogDetailDrawer' +import type { LoginLog } from '@/types/login-log' + +vi.mock('antd', () => { + const Descriptions = ({ + children, + }: { + children?: ReactNode + }) =>
{children}
+ + return { + Drawer: ({ + children, + title, + open, + onClose, + }: { + children?: ReactNode + title?: string + open?: boolean + onClose?: () => void + }) => ( +
+
{title}
+ + {children} +
+ ), + Descriptions: Object.assign(Descriptions, { + Item: ({ + label, + children, + }: { + label?: ReactNode + children?: ReactNode + }) => ( +
+ {label} + {children} +
+ ), + }), + Tag: ({ children, color }: { children?: ReactNode; color?: string }) => ( + + {children} + + ), + } +}) + +vi.mock('dayjs', () => ({ + default: () => ({ + format: () => '2024-01-15 10:30:00', + }), +})) + +describe('LoginLogDetailDrawer', () => { + it('renders nothing when log is null', () => { + render() + + expect(screen.queryByTestId('drawer')).not.toBeInTheDocument() + }) + + it('renders drawer when log is provided and open is true', () => { + const mockLog: LoginLog = { + id: 1, + user_id: 10, + login_type: 1, + status: 1, + ip: '192.168.1.1', + device_id: 'device-123', + location: 'Beijing, China', + created_at: '2024-01-15T10:30:00Z', + } + + render() + + expect(screen.getByTestId('drawer')).toHaveAttribute('data-open', 'true') + expect(screen.getByTestId('drawer-title')).toHaveTextContent('登录日志详情') + }) + + it('renders log details correctly', () => { + const mockLog: LoginLog = { + id: 42, + user_id: 15, + login_type: 2, + status: 0, + ip: '10.0.0.1', + device_id: 'device-456', + location: 'Shanghai, China', + fail_reason: 'Invalid password', + created_at: '2024-01-15T10:30:00Z', + } + + render() + + expect(screen.getByText('42')).toBeInTheDocument() + expect(screen.getByText('15')).toBeInTheDocument() + expect(screen.getByText('10.0.0.1')).toBeInTheDocument() + expect(screen.getByText('device-456')).toBeInTheDocument() + expect(screen.getByText('Shanghai, China')).toBeInTheDocument() + expect(screen.getByText('Invalid password')).toBeInTheDocument() + }) + + it('handles null user_id gracefully', () => { + const mockLog: LoginLog = { + id: 1, + user_id: null, + login_type: 1, + status: 1, + ip: '192.168.1.1', + created_at: '2024-01-15T10:30:00Z', + } + + render() + + expect(screen.getByTestId('drawer')).toHaveAttribute('data-open', 'true') + }) +}) diff --git a/frontend/admin/src/pages/admin/OperationLogsPage/OperationLogDetailDrawer.test.tsx b/frontend/admin/src/pages/admin/OperationLogsPage/OperationLogDetailDrawer.test.tsx new file mode 100644 index 0000000..7a1abc0 --- /dev/null +++ b/frontend/admin/src/pages/admin/OperationLogsPage/OperationLogDetailDrawer.test.tsx @@ -0,0 +1,189 @@ +import type { ReactNode } from 'react' +import { render, screen } from '@testing-library/react' +import { describe, expect, it, vi } from 'vitest' + +import { OperationLogDetailDrawer } from './OperationLogDetailDrawer' +import type { OperationLog } from '@/types/operation-log' + +vi.mock('antd', () => { + const Descriptions = ({ + children, + }: { + children?: ReactNode + }) =>
{children}
+ + return { + Drawer: ({ + children, + title, + open, + onClose, + }: { + children?: ReactNode + title?: string + open?: boolean + onClose?: () => void + }) => ( +
+
{title}
+ + {children} +
+ ), + Descriptions: Object.assign(Descriptions, { + Item: ({ + label, + children, + }: { + label?: ReactNode + children?: ReactNode + }) => ( +
+ {label} + {children} +
+ ), + }), + Tag: ({ children, color }: { children?: ReactNode; color?: string }) => ( + + {children} + + ), + Typography: { + Paragraph: ({ children }: { children?: ReactNode }) =>
{children}
, + Text: ({ children }: { children?: ReactNode }) => {children}, + }, + } +}) + +vi.mock('dayjs', () => ({ + default: () => ({ + format: () => '2024-01-15 10:30:00', + }), +})) + +describe('OperationLogDetailDrawer', () => { + it('renders nothing when log is null', () => { + render() + + expect(screen.queryByTestId('drawer')).not.toBeInTheDocument() + }) + + it('renders drawer when log is provided and open is true', () => { + const mockLog: OperationLog = { + id: 1, + user_id: 10, + operation_type: 'user', + operation_name: 'update_user', + request_method: 'PUT', + request_path: '/api/users/1', + request_params: '{}', + response_status: 200, + ip: '192.168.1.1', + user_agent: 'Mozilla/5.0', + created_at: '2024-01-15T10:30:00Z', + } + + render() + + expect(screen.getByTestId('drawer')).toHaveAttribute('data-open', 'true') + expect(screen.getByTestId('drawer-title')).toHaveTextContent('操作日志详情') + }) + + it('renders log details correctly', () => { + const mockLog: OperationLog = { + id: 42, + user_id: 15, + operation_type: 'role', + operation_name: 'create_role', + request_method: 'POST', + request_path: '/api/roles', + request_params: '{"name":"admin"}', + response_status: 201, + ip: '10.0.0.1', + user_agent: 'Chrome/120.0', + created_at: '2024-01-15T10:30:00Z', + } + + render() + + expect(screen.getByText('42')).toBeInTheDocument() + expect(screen.getByText('15')).toBeInTheDocument() + expect(screen.getByText('role')).toBeInTheDocument() + expect(screen.getByText('create_role')).toBeInTheDocument() + expect(screen.getByText('POST')).toBeInTheDocument() + expect(screen.getByText('201')).toBeInTheDocument() + }) + + it('shows success tag for 2xx response status', () => { + const mockLog: OperationLog = { + id: 1, + user_id: 10, + request_method: 'GET', + request_path: '/api/test', + response_status: 200, + ip: '192.168.1.1', + created_at: '2024-01-15T10:30:00Z', + } + + render() + + const tags = screen.getAllByTestId('tag') + const statusTag = tags.find(tag => tag.getAttribute('data-color') === 'success') + expect(statusTag).toBeDefined() + }) + + it('shows error tag for non-2xx response status', () => { + const mockLog: OperationLog = { + id: 1, + user_id: 10, + request_method: 'POST', + request_path: '/api/test', + response_status: 500, + ip: '192.168.1.1', + created_at: '2024-01-15T10:30:00Z', + } + + render() + + const tags = screen.getAllByTestId('tag') + const statusTag = tags.find(tag => tag.getAttribute('data-color') === 'error') + expect(statusTag).toBeDefined() + }) + + it('strips HTML tags from request_params to prevent XSS', () => { + const mockLog: OperationLog = { + id: 1, + user_id: 10, + request_method: 'POST', + request_path: '/api/test', + request_params: '', + response_status: 200, + ip: '192.168.1.1', + created_at: '2024-01-15T10:30:00Z', + } + + render() + + // HTML tags are stripped to prevent XSS, so