/**
* ActivationModal 组件单元测试
*
* 测试激活码输入弹窗的功能和交互行为
*
* @jest-environment jsdom
*/
import React from 'react'
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import '@testing-library/jest-dom'
import ActivationModal from '../ActivationModal'
// Mock cn function
jest.mock('@/lib/ui-styles', () => ({
cn: (...classes: (string | boolean | undefined)[]) =>
classes.filter(Boolean).join(' '),
}))
// Mock fetch
const mockFetch = jest.fn()
global.fetch = mockFetch
// Mock window.open
const mockOpen = jest.fn()
Object.defineProperty(window, 'open', {
value: mockOpen,
writable: true,
})
describe('ActivationModal 组件', () => {
const mockOnClose = jest.fn()
const mockOnSuccess = jest.fn()
beforeEach(() => {
mockOnClose.mockClear()
mockOnSuccess.mockClear()
mockFetch.mockClear()
mockOpen.mockClear()
})
describe('基础渲染', () => {
it('当 isOpen 为 true 时应该渲染弹窗', () => {
render(
)
expect(screen.getByText('激活许可证')).toBeInTheDocument()
})
it('当 isOpen 为 false 时不应该渲染弹窗', () => {
render(
)
expect(screen.queryByText('激活许可证')).not.toBeInTheDocument()
})
it('应该显示激活码输入框', () => {
render(
)
expect(screen.getByPlaceholderText('FLNXI-PRO-XXXX-XXXX-XXXX-XXXX')).toBeInTheDocument()
})
it('应该显示格式提示', () => {
render(
)
expect(screen.getByText('激活码格式:FLNXI-PRO-XXXX-XXXX-XXXX-XXXX')).toBeInTheDocument()
})
it('应该显示立即激活按钮', () => {
render(
)
expect(screen.getByText('立即激活')).toBeInTheDocument()
})
it('应该显示私有化许可证提示', () => {
render(
)
expect(screen.getByText('如需许可证,请联系系统交付人员或管理员。')).toBeInTheDocument()
expect(screen.queryByText('前往购买 →')).not.toBeInTheDocument()
})
})
describe('输入交互', () => {
it('输入应该自动转换为大写', () => {
render(
)
const input = screen.getByPlaceholderText('FLNXI-PRO-XXXX-XXXX-XXXX-XXXX') as HTMLInputElement
fireEvent.change(input, { target: { value: 'flnxi-pro-abcd-1234-efgh-5678' } })
expect(input.value).toBe('FLNXI-PRO-ABCD-1234-EFGH-5678')
})
})
describe('表单验证', () => {
it('空激活码应该显示错误', async () => {
render(
)
fireEvent.click(screen.getByText('立即激活'))
expect(await screen.findByText('请输入激活码')).toBeInTheDocument()
})
it('格式不正确的激活码应该显示错误', async () => {
render(
)
const input = screen.getByPlaceholderText('FLNXI-PRO-XXXX-XXXX-XXXX-XXXX')
fireEvent.change(input, { target: { value: 'INVALID-CODE' } })
fireEvent.click(screen.getByText('立即激活'))
expect(await screen.findByText('激活码格式不正确')).toBeInTheDocument()
})
})
describe('API 调用', () => {
it('有效激活码应该调用 API', async () => {
mockFetch.mockResolvedValueOnce({
json: () => Promise.resolve({ success: true }),
})
render(
)
const input = screen.getByPlaceholderText('FLNXI-PRO-XXXX-XXXX-XXXX-XXXX')
fireEvent.change(input, { target: { value: 'FLNXI-PRO-ABCD-1234-EFGH-5678' } })
fireEvent.click(screen.getByText('立即激活'))
await waitFor(() => {
expect(mockFetch).toHaveBeenCalledWith('/api/license/activate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ activationCode: 'FLNXI-PRO-ABCD-1234-EFGH-5678' }),
})
})
})
it('激活成功应该调用 onSuccess 和 onClose', async () => {
mockFetch.mockResolvedValueOnce({
json: () => Promise.resolve({ success: true }),
})
render(
)
const input = screen.getByPlaceholderText('FLNXI-PRO-XXXX-XXXX-XXXX-XXXX')
fireEvent.change(input, { target: { value: 'FLNXI-PRO-ABCD-1234-EFGH-5678' } })
fireEvent.click(screen.getByText('立即激活'))
await waitFor(() => {
expect(mockOnSuccess).toHaveBeenCalled()
expect(mockOnClose).toHaveBeenCalled()
})
})
it('激活失败应该显示错误信息', async () => {
mockFetch.mockResolvedValueOnce({
json: () => Promise.resolve({ success: false, error: '激活码无效' }),
})
render(
)
const input = screen.getByPlaceholderText('FLNXI-PRO-XXXX-XXXX-XXXX-XXXX')
fireEvent.change(input, { target: { value: 'FLNXI-PRO-ABCD-1234-EFGH-5678' } })
fireEvent.click(screen.getByText('立即激活'))
expect(await screen.findByText('激活码无效')).toBeInTheDocument()
})
it('网络错误应该显示错误信息', async () => {
mockFetch.mockRejectedValueOnce(new Error('Network error'))
render(
)
const input = screen.getByPlaceholderText('FLNXI-PRO-XXXX-XXXX-XXXX-XXXX')
fireEvent.change(input, { target: { value: 'FLNXI-PRO-ABCD-1234-EFGH-5678' } })
fireEvent.click(screen.getByText('立即激活'))
expect(await screen.findByText('网络错误,请稍后重试')).toBeInTheDocument()
})
})
describe('加载状态', () => {
it('提交时应该显示加载状态', async () => {
mockFetch.mockImplementation(() => new Promise(() => {})) // 永不 resolve
render(
)
const input = screen.getByPlaceholderText('FLNXI-PRO-XXXX-XXXX-XXXX-XXXX')
fireEvent.change(input, { target: { value: 'FLNXI-PRO-ABCD-1234-EFGH-5678' } })
fireEvent.click(screen.getByText('立即激活'))
expect(await screen.findByText('激活中...')).toBeInTheDocument()
})
})
describe('关闭交互', () => {
it('点击关闭按钮应该调用 onClose', () => {
render(
)
// 找到关闭按钮(SVG 的父元素)
const closeButton = screen.getByText('激活许可证').parentElement?.querySelector('button')
if (closeButton) {
fireEvent.click(closeButton)
expect(mockOnClose).toHaveBeenCalled()
}
})
it('点击背景遮罩应该调用 onClose', () => {
render(
)
// 点击背景遮罩
const overlay = document.querySelector('.backdrop-blur-sm')
if (overlay) {
fireEvent.click(overlay)
expect(mockOnClose).toHaveBeenCalled()
}
})
})
describe('私有化许可证口径', () => {
it('不应该提供公网购买跳转', () => {
render(
)
expect(screen.queryByText('前往购买 →')).not.toBeInTheDocument()
expect(mockOpen).not.toHaveBeenCalled()
})
})
})