fix/status-review-sync-20260409 #1
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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'))
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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'))
|
||||
})
|
||||
})
|
||||
@@ -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'))
|
||||
})
|
||||
})
|
||||
49
frontend/admin/src/layouts/AuthLayout/AuthLayout.test.tsx
Normal file
49
frontend/admin/src/layouts/AuthLayout/AuthLayout.test.tsx
Normal 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()
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user