/** * 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() }) }) })