Files
zclaw_openfang/admin-v2/tests/pages/Login.test.tsx
iven 8898bb399e
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
docs: audit reports + feature docs + skills + admin-v2 + config sync
Update audit tracker, roadmap, architecture docs,
add admin-v2 Roles page + Billing tests,
sync CLAUDE.md, Cargo.toml, docker-compose.yml,
add deep-research / frontend-design / chart-visualization skills

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 19:25:00 +08:00

220 lines
6.9 KiB
TypeScript

// ============================================================
// Login 页面测试
// ============================================================
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { MemoryRouter } from 'react-router-dom'
import Login from '@/pages/Login'
// ── Mock data ────────────────────────────────────────────────
const mockLoginResponse = {
token: 'jwt-token-123',
refresh_token: 'refresh-token-456',
account: {
id: 'acc-001',
username: 'testadmin',
email: 'admin@zclaw.ai',
display_name: 'Admin',
role: 'super_admin',
status: 'active',
totp_enabled: false,
last_login_at: null,
created_at: '2026-01-01T00:00:00Z',
llm_routing: 'relay',
},
}
const mockAccount = {
id: 'acc-001',
username: 'testadmin',
email: 'admin@zclaw.ai',
display_name: 'Admin',
role: 'super_admin',
status: 'active',
totp_enabled: false,
last_login_at: null,
created_at: '2026-01-01T00:00:00Z',
llm_routing: 'relay',
}
// ── Hoisted mocks ────────────────────────────────────────────
const { mockLogin, mockNavigate, mockAuthServiceLogin } = vi.hoisted(() => ({
mockLogin: vi.fn(),
mockNavigate: vi.fn(),
mockAuthServiceLogin: vi.fn(),
}))
vi.mock('@/stores/authStore', () => ({
useAuthStore: Object.assign(
vi.fn((selector: (s: Record<string, unknown>) => unknown) =>
selector({ login: mockLogin }),
),
{ getState: () => ({ token: null, refreshToken: null, logout: vi.fn() }) },
),
}))
vi.mock('@/services/auth', () => ({
authService: {
login: mockAuthServiceLogin,
},
}))
vi.mock('react-router-dom', async () => {
const actual = await vi.importActual<typeof import('react-router-dom')>('react-router-dom')
return {
...actual,
useNavigate: () => mockNavigate,
}
})
beforeEach(() => {
mockLogin.mockClear()
mockNavigate.mockClear()
mockAuthServiceLogin.mockClear()
})
// ── Helper: render with providers ────────────────────────────
function renderLogin(initialEntries = ['/login']) {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
},
})
return render(
<QueryClientProvider client={queryClient}>
<MemoryRouter initialEntries={initialEntries}>
<Login />
</MemoryRouter>
</QueryClientProvider>,
)
}
/** Click the LoginForm submit button (Ant Design renders "登 录" with a space) */
function getSubmitButton(): HTMLElement {
const btn = document.querySelector<HTMLButtonElement>(
'button.ant-btn-primary[type="button"]',
)
if (!btn) throw new Error('Submit button not found')
return btn
}
// ── Tests ────────────────────────────────────────────────────
describe('Login page', () => {
it('renders the login form with username and password fields', () => {
renderLogin()
expect(screen.getByText('登录到 ZCLAW')).toBeInTheDocument()
expect(screen.getByPlaceholderText('请输入用户名')).toBeInTheDocument()
expect(screen.getByPlaceholderText('请输入密码')).toBeInTheDocument()
const submitButton = getSubmitButton()
expect(submitButton).toBeTruthy()
})
it('shows the ZCLAW brand logo', () => {
renderLogin()
expect(screen.getByText('Z')).toBeInTheDocument()
expect(screen.getByText(/ZCLAW Admin/)).toBeInTheDocument()
})
it('successful login calls authStore.login and navigates to /', async () => {
const user = userEvent.setup()
mockAuthServiceLogin.mockResolvedValue(mockLoginResponse)
renderLogin()
await user.type(screen.getByPlaceholderText('请输入用户名'), 'testadmin')
await user.type(screen.getByPlaceholderText('请输入密码'), 'password123')
await user.click(getSubmitButton())
await waitFor(() => {
expect(mockLogin).toHaveBeenCalledWith(
'jwt-token-123',
'refresh-token-456',
mockAccount,
)
})
expect(mockNavigate).toHaveBeenCalledWith('/', { replace: true })
})
it('navigates to redirect path after login', async () => {
const user = userEvent.setup()
mockAuthServiceLogin.mockResolvedValue(mockLoginResponse)
renderLogin(['/login?from=/settings'])
await user.type(screen.getByPlaceholderText('请输入用户名'), 'testadmin')
await user.type(screen.getByPlaceholderText('请输入密码'), 'password123')
await user.click(getSubmitButton())
await waitFor(() => {
expect(mockNavigate).toHaveBeenCalledWith('/settings', { replace: true })
})
})
it('shows TOTP field when server returns TOTP-related error', async () => {
const user = userEvent.setup()
const error = new Error('请输入两步验证码 (TOTP)')
Object.assign(error, { status: 403 })
mockAuthServiceLogin.mockRejectedValue(error)
renderLogin()
// Initially no TOTP field
expect(screen.queryByPlaceholderText('请输入 6 位验证码')).not.toBeInTheDocument()
await user.type(screen.getByPlaceholderText('请输入用户名'), 'testadmin')
await user.type(screen.getByPlaceholderText('请输入密码'), 'password123')
await user.click(getSubmitButton())
// After TOTP error, TOTP field appears
await waitFor(() => {
expect(screen.getByPlaceholderText('请输入 6 位验证码')).toBeInTheDocument()
})
})
it('shows error message on invalid credentials', async () => {
const user = userEvent.setup()
const error = new Error('用户名或密码错误')
mockAuthServiceLogin.mockRejectedValue(error)
renderLogin()
await user.type(screen.getByPlaceholderText('请输入用户名'), 'wrong')
await user.type(screen.getByPlaceholderText('请输入密码'), 'wrong')
await user.click(getSubmitButton())
await waitFor(() => {
expect(screen.getByText('用户名或密码错误')).toBeInTheDocument()
})
})
it('does not call authStore.login on failed login', async () => {
const user = userEvent.setup()
const error = new Error('用户名或密码错误')
mockAuthServiceLogin.mockRejectedValue(error)
renderLogin()
await user.type(screen.getByPlaceholderText('请输入用户名'), 'wrong')
await user.type(screen.getByPlaceholderText('请输入密码'), 'wrong')
await user.click(getSubmitButton())
await waitFor(() => {
expect(screen.getByText('用户名或密码错误')).toBeInTheDocument()
})
expect(mockLogin).not.toHaveBeenCalled()
expect(mockNavigate).not.toHaveBeenCalled()
})
})