import { MemoryRouter } from 'react-router-dom' import { fireEvent, render, screen, waitFor } from '@testing-library/react' import { message } from 'antd' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { AuthCapabilities, RegisterResponse } from '@/types' import { RegisterPage } from './RegisterPage' const TEXT = { usernamePlaceholder: '用户名', nicknamePlaceholder: '昵称(选填)', emailPlaceholder: '邮箱地址(选填)', phonePlaceholder: '手机号(选填)', passwordPlaceholder: '密码', confirmPasswordPlaceholder: '确认密码', createAccount: '创建账号', returnLogin: '返回登录', forgotPassword: '忘记密码?', sendCode: '获取验证码', resendActivation: '重新发送激活邮件', adminBootstrapTitle: '当前仍需管理员初始化', adminBootstrapAction: '初始化管理员', phoneCodeValidation: '请输入 6 位短信验证码', activeSummary: '账号已创建,现在可以返回登录页使用新账号登录。', inactiveSummaryNoEmail: '账号已创建,请按页面提示完成激活后再登录。', smsDisabledDescription: '支持用户名和邮箱注册;当前环境未启用短信能力,因此手机号注册暂不可用。', } const getAuthCapabilitiesMock = vi.fn<() => Promise>() const registerMock = vi.fn<(payload: unknown) => Promise>() const sendSmsCodeMock = vi.fn<(payload: unknown) => Promise>() const defaultCapabilities: AuthCapabilities = { password: true, email_activation: false, email_code: false, sms_code: false, password_reset: false, admin_bootstrap_required: false, oauth_providers: [], } const activeRegisterResponse: RegisterResponse = { id: 2, username: 'new-user', email: 'new-user@example.com', phone: '', nickname: 'New User', avatar: '', status: 1, } vi.mock('@/services/auth', () => ({ getAuthCapabilities: () => getAuthCapabilitiesMock(), register: (payload: unknown) => registerMock(payload), sendSmsCode: (payload: unknown) => sendSmsCodeMock(payload), })) function renderRegisterPage() { return render( , ) } function fillBaseRegistrationFields(values?: { username?: string nickname?: string email?: string password?: string confirmPassword?: string }) { fireEvent.change(screen.getByPlaceholderText(TEXT.usernamePlaceholder), { target: { value: values?.username ?? 'new-user' }, }) if (values?.nickname !== undefined || screen.queryByPlaceholderText(TEXT.nicknamePlaceholder)) { fireEvent.change(screen.getByPlaceholderText(TEXT.nicknamePlaceholder), { target: { value: values?.nickname ?? 'New User' }, }) } if (values?.email !== undefined || screen.queryByPlaceholderText(TEXT.emailPlaceholder)) { fireEvent.change(screen.getByPlaceholderText(TEXT.emailPlaceholder), { target: { value: values?.email ?? 'new-user@example.com' }, }) } fireEvent.change(screen.getByPlaceholderText(TEXT.passwordPlaceholder), { target: { value: values?.password ?? 'SecurePass123!' }, }) fireEvent.change(screen.getByPlaceholderText(TEXT.confirmPasswordPlaceholder), { target: { value: values?.confirmPassword ?? values?.password ?? 'SecurePass123!' }, }) } describe('RegisterPage', () => { beforeEach(() => { getAuthCapabilitiesMock.mockReset() registerMock.mockReset() sendSmsCodeMock.mockReset() getAuthCapabilitiesMock.mockResolvedValue(defaultCapabilities) registerMock.mockResolvedValue(activeRegisterResponse) sendSmsCodeMock.mockResolvedValue(undefined) vi.spyOn(message, 'success').mockImplementation(() => undefined as never) vi.spyOn(message, 'error').mockImplementation(() => undefined as never) }) afterEach(() => { vi.restoreAllMocks() }) it('renders username registration fields and hides phone registration when sms is disabled', async () => { renderRegisterPage() await waitFor(() => expect(getAuthCapabilitiesMock).toHaveBeenCalledTimes(1)) expect(screen.getByPlaceholderText(TEXT.usernamePlaceholder)).toBeInTheDocument() expect(screen.getByPlaceholderText(TEXT.nicknamePlaceholder)).toBeInTheDocument() expect(screen.getByPlaceholderText(TEXT.emailPlaceholder)).toBeInTheDocument() expect(screen.queryByPlaceholderText(TEXT.phonePlaceholder)).not.toBeInTheDocument() expect(screen.getByRole('link', { name: /返回登录/ })).toBeInTheDocument() }) it('falls back to default capabilities when loading capabilities fails', async () => { getAuthCapabilitiesMock.mockRejectedValue(new Error('capabilities unavailable')) renderRegisterPage() await waitFor(() => expect(screen.getByPlaceholderText(TEXT.usernamePlaceholder)).toBeInTheDocument()) expect(screen.getByText(TEXT.smsDisabledDescription)).toBeInTheDocument() expect(screen.queryByPlaceholderText(TEXT.phonePlaceholder)).not.toBeInTheDocument() expect(screen.queryByRole('link', { name: TEXT.forgotPassword })).not.toBeInTheDocument() }) it('shows the forgot-password entry when the capability is enabled', async () => { getAuthCapabilitiesMock.mockResolvedValue({ ...defaultCapabilities, password_reset: true, }) renderRegisterPage() await waitFor(() => expect(screen.getByRole('link', { name: TEXT.forgotPassword })).toBeInTheDocument()) }) it('sends register sms codes with the normalized purpose payload and starts the countdown', async () => { getAuthCapabilitiesMock.mockResolvedValue({ ...defaultCapabilities, sms_code: true, }) renderRegisterPage() await waitFor(() => expect(screen.getByPlaceholderText(TEXT.phonePlaceholder)).toBeInTheDocument()) fireEvent.change(screen.getByPlaceholderText(TEXT.phonePlaceholder), { target: { value: '13812345678' }, }) fireEvent.click(screen.getByRole('button', { name: TEXT.sendCode })) await waitFor(() => expect(sendSmsCodeMock).toHaveBeenCalledWith({ phone: '13812345678', purpose: 'register', }), ) expect(message.success).toHaveBeenCalledTimes(1) expect(screen.getByRole('button', { name: '60s' })).toBeInTheDocument() }) it('ignores form validation errors when the phone format is invalid', async () => { getAuthCapabilitiesMock.mockResolvedValue({ ...defaultCapabilities, sms_code: true, }) renderRegisterPage() await waitFor(() => expect(screen.getByPlaceholderText(TEXT.phonePlaceholder)).toBeInTheDocument()) fireEvent.change(screen.getByPlaceholderText(TEXT.phonePlaceholder), { target: { value: '123' }, }) fireEvent.click(screen.getByRole('button', { name: TEXT.sendCode })) await waitFor(() => expect(sendSmsCodeMock).not.toHaveBeenCalled()) expect(message.error).not.toHaveBeenCalled() expect(screen.queryByRole('button', { name: '60s' })).not.toBeInTheDocument() }) it('surfaces sms code send failures from the backend', async () => { getAuthCapabilitiesMock.mockResolvedValue({ ...defaultCapabilities, sms_code: true, }) sendSmsCodeMock.mockRejectedValue(new Error('sms send failed')) renderRegisterPage() await waitFor(() => expect(screen.getByPlaceholderText(TEXT.phonePlaceholder)).toBeInTheDocument()) fireEvent.change(screen.getByPlaceholderText(TEXT.phonePlaceholder), { target: { value: '13812345678' }, }) fireEvent.click(screen.getByRole('button', { name: TEXT.sendCode })) await waitFor(() => expect(message.error).toHaveBeenCalledWith('sms send failed')) expect(screen.queryByRole('button', { name: '60s' })).not.toBeInTheDocument() }) it('blocks sms-backed registration when the phone code is missing', async () => { getAuthCapabilitiesMock.mockResolvedValue({ ...defaultCapabilities, sms_code: true, }) renderRegisterPage() await waitFor(() => expect(screen.getByPlaceholderText(TEXT.phonePlaceholder)).toBeInTheDocument()) fillBaseRegistrationFields() fireEvent.change(screen.getByPlaceholderText(TEXT.phonePlaceholder), { target: { value: '13812345678' }, }) fireEvent.click(screen.getByRole('button', { name: TEXT.createAccount })) await waitFor(() => expect(registerMock).not.toHaveBeenCalled()) expect(message.error).not.toHaveBeenCalled() expect(screen.getByRole('button', { name: TEXT.createAccount })).toBeInTheDocument() }) it('submits the self-service registration payload and shows the success state', async () => { renderRegisterPage() await waitFor(() => expect(screen.getByPlaceholderText(TEXT.usernamePlaceholder)).toBeInTheDocument()) fillBaseRegistrationFields() fireEvent.click(screen.getByRole('button', { name: TEXT.createAccount })) await waitFor(() => expect(registerMock).toHaveBeenCalledWith({ username: 'new-user', password: 'SecurePass123!', nickname: 'New User', email: 'new-user@example.com', phone: undefined, phone_code: undefined, }), ) expect(await screen.findByRole('button', { name: TEXT.returnLogin })).toBeInTheDocument() expect(screen.getByText(TEXT.activeSummary)).toBeInTheDocument() }) it('submits sms-backed registration payload with trimmed username, nickname, and phone code', async () => { getAuthCapabilitiesMock.mockResolvedValue({ ...defaultCapabilities, sms_code: true, }) renderRegisterPage() await waitFor(() => expect(screen.getByPlaceholderText(TEXT.phonePlaceholder)).toBeInTheDocument()) fillBaseRegistrationFields({ username: ' new-user ', nickname: ' New User ', email: 'new-user@example.com', }) fireEvent.change(screen.getByPlaceholderText(TEXT.phonePlaceholder), { target: { value: '13812345678' }, }) fireEvent.change(screen.getAllByRole('textbox')[4], { target: { value: ' 654321 ' }, }) fireEvent.click(screen.getByRole('button', { name: TEXT.createAccount })) await waitFor(() => expect(registerMock).toHaveBeenCalledWith({ username: 'new-user', password: 'SecurePass123!', nickname: 'New User', email: 'new-user@example.com', phone: '13812345678', phone_code: '654321', }), ) }) it('surfaces self-service registration failures from the backend', async () => { registerMock.mockRejectedValue(new Error('register failed')) renderRegisterPage() await waitFor(() => expect(screen.getByPlaceholderText(TEXT.usernamePlaceholder)).toBeInTheDocument()) fillBaseRegistrationFields() fireEvent.click(screen.getByRole('button', { name: TEXT.createAccount })) await waitFor(() => expect(message.error).toHaveBeenCalledWith('register failed')) expect(screen.getByRole('button', { name: TEXT.createAccount })).toBeInTheDocument() }) it('shows a resend-activation entry for newly created inactive email accounts', async () => { getAuthCapabilitiesMock.mockResolvedValue({ ...defaultCapabilities, email_activation: true, }) registerMock.mockResolvedValue({ id: 3, username: 'inactive-user', email: 'inactive-user@example.com', phone: '', nickname: 'Inactive User', avatar: '', status: 0, }) renderRegisterPage() await waitFor(() => expect(screen.getByPlaceholderText(TEXT.usernamePlaceholder)).toBeInTheDocument()) fillBaseRegistrationFields({ username: 'inactive-user', nickname: '', email: 'inactive-user@example.com', }) fireEvent.click(screen.getByRole('button', { name: TEXT.createAccount })) const resendLink = await screen.findByRole('link', { name: TEXT.resendActivation }) expect(resendLink).toHaveAttribute('href', '/activate-account?email=inactive-user%40example.com') }) it('shows the generic activation summary when the new inactive account has no email address', async () => { registerMock.mockResolvedValue({ id: 4, username: 'inactive-without-email', email: '', phone: '', nickname: '', avatar: '', status: 0, }) renderRegisterPage() await waitFor(() => expect(screen.getByPlaceholderText(TEXT.usernamePlaceholder)).toBeInTheDocument()) fillBaseRegistrationFields({ username: 'inactive-without-email', nickname: '', email: '', }) fireEvent.click(screen.getByRole('button', { name: TEXT.createAccount })) expect(await screen.findByText(TEXT.inactiveSummaryNoEmail)).toBeInTheDocument() expect(screen.queryByRole('link', { name: TEXT.resendActivation })).not.toBeInTheDocument() }) it('shows an admin bootstrap entry when the system still has no active admin', async () => { getAuthCapabilitiesMock.mockResolvedValue({ ...defaultCapabilities, admin_bootstrap_required: true, }) renderRegisterPage() expect(await screen.findByText(TEXT.adminBootstrapTitle)).toBeInTheDocument() expect(screen.getByRole('button', { name: TEXT.adminBootstrapAction }).closest('a')).toHaveAttribute('href', '/bootstrap-admin') }) })