test: add Stage 3-5 component and layout test coverage

Add tests for:
- PageLayout components: ContentCard, FilterCard, TableCard, TreeCard, PageLayout
- AuthLayout layout component
- LoginLogDetailDrawer and OperationLogDetailDrawer page components

All 518 tests pass across 82 test files.
This commit is contained in:
2026-04-18 07:46:42 +08:00
parent 40d146b6aa
commit 8b8c05bb60
8 changed files with 574 additions and 0 deletions

View File

@@ -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
}) => (
<div data-testid="card" data-class={className} style={style}>
{title && <div data-testid="card-title">{title}</div>}
{children}
</div>
),
}))
describe('ContentCard', () => {
it('renders children content', () => {
render(
<ContentCard>
<div>card content</div>
</ContentCard>,
)
expect(screen.getByText('card content')).toBeInTheDocument()
})
it('applies custom className', () => {
render(
<ContentCard className="custom-class">
<div>content</div>
</ContentCard>,
)
expect(screen.getByTestId('card')).toHaveAttribute('data-class', expect.stringContaining('custom-class'))
})
it('applies custom style', () => {
const customStyle = { marginTop: '20px' }
render(
<ContentCard style={customStyle}>
<div>content</div>
</ContentCard>,
)
expect(screen.getByTestId('card')).toHaveStyle({ marginTop: '20px' })
})
it('renders with title', () => {
render(
<ContentCard title="Card Title">
<div>content</div>
</ContentCard>,
)
expect(screen.getByTestId('card-title')).toHaveTextContent('Card Title')
})
})

View File

@@ -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
}) => (
<div data-testid="card" data-class={className}>
{children}
</div>
),
}))
describe('FilterCard', () => {
it('renders children content', () => {
render(
<FilterCard>
<div>filter content</div>
</FilterCard>,
)
expect(screen.getByText('filter content')).toBeInTheDocument()
})
it('applies custom className', () => {
render(
<FilterCard className="custom-filter-class">
<div>content</div>
</FilterCard>,
)
expect(screen.getByTestId('card')).toHaveAttribute('data-class', expect.stringContaining('custom-filter-class'))
})
})

View File

@@ -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(
<PageLayout>
<div>page content</div>
</PageLayout>,
)
expect(screen.getByText('page content')).toBeInTheDocument()
})
it('applies custom className', () => {
render(
<PageLayout className="custom-page-layout">
<div>content</div>
</PageLayout>,
)
const element = screen.getByText('content')
expect(element.parentElement).toHaveClass('custom-page-layout')
})
})

View File

@@ -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
}) => (
<div data-testid="card" data-class={className}>
{children}
</div>
),
}))
describe('TableCard', () => {
it('renders children content', () => {
render(
<TableCard>
<div>table content</div>
</TableCard>,
)
expect(screen.getByText('table content')).toBeInTheDocument()
})
it('applies custom className', () => {
render(
<TableCard className="custom-table-class">
<div>content</div>
</TableCard>,
)
expect(screen.getByTestId('card')).toHaveAttribute('data-class', expect.stringContaining('custom-table-class'))
})
})

View File

@@ -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
}) => (
<div data-testid="card" data-class={className}>
{children}
</div>
),
}))
describe('TreeCard', () => {
it('renders children content', () => {
render(
<TreeCard>
<div>tree content</div>
</TreeCard>,
)
expect(screen.getByText('tree content')).toBeInTheDocument()
})
it('applies custom className', () => {
render(
<TreeCard className="custom-tree-class">
<div>content</div>
</TreeCard>,
)
expect(screen.getByTestId('card')).toHaveAttribute('data-class', expect.stringContaining('custom-tree-class'))
})
})

View File

@@ -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(
<AuthLayout>
<div>login form</div>
</AuthLayout>,
)
expect(screen.getByText('login form')).toBeInTheDocument()
})
it('displays the brand title', () => {
render(
<AuthLayout>
<div>content</div>
</AuthLayout>,
)
expect(screen.getByText('用户管理系统')).toBeInTheDocument()
})
it('displays brand description', () => {
render(
<AuthLayout>
<div>content</div>
</AuthLayout>,
)
expect(screen.getByText('企业级用户管理解决方案')).toBeInTheDocument()
})
it('displays feature list', () => {
render(
<AuthLayout>
<div>content</div>
</AuthLayout>,
)
expect(screen.getByText('支持多种登录方式')).toBeInTheDocument()
expect(screen.getByText('基于角色的权限控制')).toBeInTheDocument()
expect(screen.getByText('完整的审计日志')).toBeInTheDocument()
expect(screen.getByText('安全的双因素认证')).toBeInTheDocument()
})
})

View File

@@ -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
}) => <div>{children}</div>
return {
Drawer: ({
children,
title,
open,
onClose,
}: {
children?: ReactNode
title?: string
open?: boolean
onClose?: () => void
}) => (
<div data-testid="drawer" data-open={open}>
<div data-testid="drawer-title">{title}</div>
<button onClick={onClose}>close</button>
{children}
</div>
),
Descriptions: Object.assign(Descriptions, {
Item: ({
label,
children,
}: {
label?: ReactNode
children?: ReactNode
}) => (
<div>
<span>{label}</span>
<span>{children}</span>
</div>
),
}),
Tag: ({ children, color }: { children?: ReactNode; color?: string }) => (
<span data-testid="tag" data-color={color}>
{children}
</span>
),
}
})
vi.mock('dayjs', () => ({
default: () => ({
format: () => '2024-01-15 10:30:00',
}),
}))
describe('LoginLogDetailDrawer', () => {
it('renders nothing when log is null', () => {
render(<LoginLogDetailDrawer open={true} log={null} onClose={vi.fn()} />)
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(<LoginLogDetailDrawer open={true} log={mockLog} onClose={vi.fn()} />)
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(<LoginLogDetailDrawer open={true} log={mockLog} onClose={vi.fn()} />)
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(<LoginLogDetailDrawer open={true} log={mockLog} onClose={vi.fn()} />)
expect(screen.getByTestId('drawer')).toHaveAttribute('data-open', 'true')
})
})

View File

@@ -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
}) => <div>{children}</div>
return {
Drawer: ({
children,
title,
open,
onClose,
}: {
children?: ReactNode
title?: string
open?: boolean
onClose?: () => void
}) => (
<div data-testid="drawer" data-open={open}>
<div data-testid="drawer-title">{title}</div>
<button onClick={onClose}>close</button>
{children}
</div>
),
Descriptions: Object.assign(Descriptions, {
Item: ({
label,
children,
}: {
label?: ReactNode
children?: ReactNode
}) => (
<div>
<span>{label}</span>
<span>{children}</span>
</div>
),
}),
Tag: ({ children, color }: { children?: ReactNode; color?: string }) => (
<span data-testid="tag" data-color={color}>
{children}
</span>
),
Typography: {
Paragraph: ({ children }: { children?: ReactNode }) => <div>{children}</div>,
Text: ({ children }: { children?: ReactNode }) => <span>{children}</span>,
},
}
})
vi.mock('dayjs', () => ({
default: () => ({
format: () => '2024-01-15 10:30:00',
}),
}))
describe('OperationLogDetailDrawer', () => {
it('renders nothing when log is null', () => {
render(<OperationLogDetailDrawer open={true} log={null} onClose={vi.fn()} />)
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(<OperationLogDetailDrawer open={true} log={mockLog} onClose={vi.fn()} />)
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(<OperationLogDetailDrawer open={true} log={mockLog} onClose={vi.fn()} />)
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(<OperationLogDetailDrawer open={true} log={mockLog} onClose={vi.fn()} />)
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(<OperationLogDetailDrawer open={true} log={mockLog} onClose={vi.fn()} />)
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: '<script>alert("xss")</script>',
response_status: 200,
ip: '192.168.1.1',
created_at: '2024-01-15T10:30:00Z',
}
render(<OperationLogDetailDrawer open={true} log={mockLog} onClose={vi.fn()} />)
// HTML tags are stripped to prevent XSS, so <script> should not be present
expect(screen.queryByText('<script>')).not.toBeInTheDocument()
// But the content inside tags becomes plain text after stripping
expect(screen.getByText('alert("xss")')).toBeInTheDocument()
})
it('handles null user_id gracefully', () => {
const mockLog: OperationLog = {
id: 1,
user_id: null,
request_method: 'GET',
request_path: '/api/test',
response_status: 200,
ip: '192.168.1.1',
created_at: '2024-01-15T10:30:00Z',
}
render(<OperationLogDetailDrawer open={true} log={mockLog} onClose={vi.fn()} />)
expect(screen.getByTestId('drawer')).toHaveAttribute('data-open', 'true')
})
})