296 lines
8.3 KiB
TypeScript
296 lines
8.3 KiB
TypeScript
/**
|
||
* 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(
|
||
<ActivationModal
|
||
isOpen={true}
|
||
onClose={mockOnClose}
|
||
/>
|
||
)
|
||
expect(screen.getByText('激活许可证')).toBeInTheDocument()
|
||
})
|
||
|
||
it('当 isOpen 为 false 时不应该渲染弹窗', () => {
|
||
render(
|
||
<ActivationModal
|
||
isOpen={false}
|
||
onClose={mockOnClose}
|
||
/>
|
||
)
|
||
expect(screen.queryByText('激活许可证')).not.toBeInTheDocument()
|
||
})
|
||
|
||
it('应该显示激活码输入框', () => {
|
||
render(
|
||
<ActivationModal
|
||
isOpen={true}
|
||
onClose={mockOnClose}
|
||
/>
|
||
)
|
||
expect(screen.getByPlaceholderText('FLNXI-PRO-XXXX-XXXX-XXXX-XXXX')).toBeInTheDocument()
|
||
})
|
||
|
||
it('应该显示格式提示', () => {
|
||
render(
|
||
<ActivationModal
|
||
isOpen={true}
|
||
onClose={mockOnClose}
|
||
/>
|
||
)
|
||
expect(screen.getByText('激活码格式:FLNXI-PRO-XXXX-XXXX-XXXX-XXXX')).toBeInTheDocument()
|
||
})
|
||
|
||
it('应该显示立即激活按钮', () => {
|
||
render(
|
||
<ActivationModal
|
||
isOpen={true}
|
||
onClose={mockOnClose}
|
||
/>
|
||
)
|
||
expect(screen.getByText('立即激活')).toBeInTheDocument()
|
||
})
|
||
|
||
it('应该显示购买链接', () => {
|
||
render(
|
||
<ActivationModal
|
||
isOpen={true}
|
||
onClose={mockOnClose}
|
||
/>
|
||
)
|
||
expect(screen.getByText('还没有激活码?')).toBeInTheDocument()
|
||
expect(screen.getByText('前往购买 →')).toBeInTheDocument()
|
||
})
|
||
})
|
||
|
||
describe('输入交互', () => {
|
||
it('输入应该自动转换为大写', () => {
|
||
render(
|
||
<ActivationModal
|
||
isOpen={true}
|
||
onClose={mockOnClose}
|
||
/>
|
||
)
|
||
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(
|
||
<ActivationModal
|
||
isOpen={true}
|
||
onClose={mockOnClose}
|
||
/>
|
||
)
|
||
fireEvent.click(screen.getByText('立即激活'))
|
||
expect(await screen.findByText('请输入激活码')).toBeInTheDocument()
|
||
})
|
||
|
||
it('格式不正确的激活码应该显示错误', async () => {
|
||
render(
|
||
<ActivationModal
|
||
isOpen={true}
|
||
onClose={mockOnClose}
|
||
/>
|
||
)
|
||
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(
|
||
<ActivationModal
|
||
isOpen={true}
|
||
onClose={mockOnClose}
|
||
onSuccess={mockOnSuccess}
|
||
/>
|
||
)
|
||
|
||
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(
|
||
<ActivationModal
|
||
isOpen={true}
|
||
onClose={mockOnClose}
|
||
onSuccess={mockOnSuccess}
|
||
/>
|
||
)
|
||
|
||
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(
|
||
<ActivationModal
|
||
isOpen={true}
|
||
onClose={mockOnClose}
|
||
/>
|
||
)
|
||
|
||
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(
|
||
<ActivationModal
|
||
isOpen={true}
|
||
onClose={mockOnClose}
|
||
/>
|
||
)
|
||
|
||
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(
|
||
<ActivationModal
|
||
isOpen={true}
|
||
onClose={mockOnClose}
|
||
/>
|
||
)
|
||
|
||
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(
|
||
<ActivationModal
|
||
isOpen={true}
|
||
onClose={mockOnClose}
|
||
/>
|
||
)
|
||
// 找到关闭按钮(SVG 的父元素)
|
||
const closeButton = screen.getByText('激活许可证').parentElement?.querySelector('button')
|
||
if (closeButton) {
|
||
fireEvent.click(closeButton)
|
||
expect(mockOnClose).toHaveBeenCalled()
|
||
}
|
||
})
|
||
|
||
it('点击背景遮罩应该调用 onClose', () => {
|
||
render(
|
||
<ActivationModal
|
||
isOpen={true}
|
||
onClose={mockOnClose}
|
||
/>
|
||
)
|
||
// 点击背景遮罩
|
||
const overlay = document.querySelector('.backdrop-blur-sm')
|
||
if (overlay) {
|
||
fireEvent.click(overlay)
|
||
expect(mockOnClose).toHaveBeenCalled()
|
||
}
|
||
})
|
||
})
|
||
|
||
describe('购买链接', () => {
|
||
it('点击购买链接应该打开新窗口', () => {
|
||
render(
|
||
<ActivationModal
|
||
isOpen={true}
|
||
onClose={mockOnClose}
|
||
/>
|
||
)
|
||
fireEvent.click(screen.getByText('前往购买 →'))
|
||
expect(mockOpen).toHaveBeenCalledWith('https://www.flnxi.com/pricing', '_blank')
|
||
})
|
||
})
|
||
})
|