flnxi-Wu/components/ui/__tests__/RestrictedToggle.test.tsx
2026-05-25 14:52:24 +08:00

255 lines
9.0 KiB
TypeScript

/**
* RestrictedToggle 组件单元测试
*
* 测试 RestrictedToggle 组件的点击拦截和禁用状态
*
* **Feature: free-version-ui-restrictions**
* **Property 1: 免费版点击拦截**
* **Property 6: 禁用开关视觉状态**
* **Validates: Requirements 1.1, 5.1, 5.2, 5.4**
*
* @jest-environment jsdom
*/
import React from 'react'
import { render, screen, fireEvent } from '@testing-library/react'
import '@testing-library/jest-dom'
import RestrictedToggle from '../RestrictedToggle'
import type { FeatureKey } from '@/lib/license/types'
// Mock useTheme hook
jest.mock('@/lib/ui-styles', () => ({
useTheme: jest.fn(() => 'light'),
cn: (...classes: (string | boolean | undefined)[]) =>
classes.filter(Boolean).join(' '),
}))
// Mock useThemeStyles for UpgradeModal
jest.mock('@/lib/ui-styles', () => ({
useTheme: jest.fn(() => 'light'),
useThemeStyles: jest.fn(() => ({
buttonStyles: {
secondary: 'mock-secondary-button',
},
modalStyles: {
overlay: 'mock-overlay',
container: 'mock-container',
content: 'mock-content',
header: 'mock-header',
title: 'mock-title',
closeButton: 'mock-close-button',
body: 'mock-body',
footer: 'mock-footer',
sizes: {
small: 'mock-small',
},
},
})),
cn: (...classes: (string | boolean | undefined)[]) =>
classes.filter(Boolean).join(' '),
}))
// Mock createPortal
jest.mock('react-dom', () => ({
...jest.requireActual('react-dom'),
createPortal: (node: React.ReactNode) => node,
}))
describe('RestrictedToggle 组件', () => {
const mockOnChange = jest.fn()
const defaultProps = {
label: '测试功能',
featureKey: 'customTemplates' as FeatureKey,
value: false,
onChange: mockOnChange,
isRestricted: false,
}
beforeEach(() => {
mockOnChange.mockClear()
})
describe('基础渲染', () => {
it('应该正确渲染标签', () => {
render(<RestrictedToggle {...defaultProps} />)
expect(screen.getByText('测试功能')).toBeInTheDocument()
})
it('应该正确渲染描述', () => {
render(<RestrictedToggle {...defaultProps} description="这是功能描述" />)
expect(screen.getByText('这是功能描述')).toBeInTheDocument()
})
it('应该有正确的 data-testid', () => {
render(<RestrictedToggle {...defaultProps} />)
expect(screen.getByTestId('restricted-toggle')).toBeInTheDocument()
})
})
/**
* Property 1: 免费版点击拦截
* *For any* 受限功能开关和 FREE 许可证类型,点击开关必须显示升级弹窗且不改变开关值
* **Feature: free-version-ui-restrictions, Property 1: Click Interception for FREE License**
* **Validates: Requirements 1.1, 5.2, 5.4**
*/
describe('Property 1: 免费版点击拦截', () => {
it('受限时点击应该显示升级弹窗', () => {
render(<RestrictedToggle {...defaultProps} isRestricted={true} />)
const toggleButton = screen.getByTestId('toggle-button')
fireEvent.click(toggleButton)
// 应该显示升级弹窗
expect(screen.getByTestId('upgrade-modal')).toBeInTheDocument()
})
it('受限时点击不应该调用 onChange', () => {
render(<RestrictedToggle {...defaultProps} isRestricted={true} />)
const toggleButton = screen.getByTestId('toggle-button')
fireEvent.click(toggleButton)
// 不应该调用 onChange
expect(mockOnChange).not.toHaveBeenCalled()
})
it('受限时开关值不应该改变', () => {
const { rerender } = render(
<RestrictedToggle {...defaultProps} isRestricted={true} value={false} />
)
const toggleButton = screen.getByTestId('toggle-button')
fireEvent.click(toggleButton)
// 值应该保持不变
expect(mockOnChange).not.toHaveBeenCalled()
// 重新渲染确认值没变
rerender(<RestrictedToggle {...defaultProps} isRestricted={true} value={false} />)
expect(toggleButton).toHaveAttribute('aria-checked', 'false')
})
it('不受限时点击应该调用 onChange', () => {
render(<RestrictedToggle {...defaultProps} isRestricted={false} />)
const toggleButton = screen.getByTestId('toggle-button')
fireEvent.click(toggleButton)
// 应该调用 onChange
expect(mockOnChange).toHaveBeenCalledWith(true)
})
it('不受限时点击不应该显示升级弹窗', () => {
render(<RestrictedToggle {...defaultProps} isRestricted={false} />)
const toggleButton = screen.getByTestId('toggle-button')
fireEvent.click(toggleButton)
// 不应该显示升级弹窗
expect(screen.queryByTestId('upgrade-modal')).not.toBeInTheDocument()
})
})
/**
* Property 6: 禁用开关视觉状态
* *For any* 受限功能和 FREE 许可证,开关必须以禁用视觉状态显示,具有降低的不透明度或灰色外观
* **Feature: free-version-ui-restrictions, Property 6: Disabled Toggle Visual State**
* **Validates: Requirements 5.1**
*/
describe('Property 6: 禁用开关视觉状态', () => {
it('受限时应该显示 PRO 徽章', () => {
render(<RestrictedToggle {...defaultProps} isRestricted={true} />)
expect(screen.getByTestId('pro-badge')).toBeInTheDocument()
})
it('不受限时不应该显示 PRO 徽章', () => {
render(<RestrictedToggle {...defaultProps} isRestricted={false} />)
expect(screen.queryByTestId('pro-badge')).not.toBeInTheDocument()
})
it('受限时开关应该有禁用样式', () => {
render(<RestrictedToggle {...defaultProps} isRestricted={true} />)
const toggleButton = screen.getByTestId('toggle-button')
expect(toggleButton.className).toContain('opacity-50')
expect(toggleButton.className).toContain('cursor-not-allowed')
})
it('不受限时开关应该有正常样式', () => {
render(<RestrictedToggle {...defaultProps} isRestricted={false} />)
const toggleButton = screen.getByTestId('toggle-button')
expect(toggleButton.className).toContain('cursor-pointer')
expect(toggleButton.className).not.toContain('opacity-50')
})
it('受限时应该显示升级提示文字', () => {
render(<RestrictedToggle {...defaultProps} isRestricted={true} />)
expect(screen.getByTestId('restricted-hint')).toBeInTheDocument()
expect(screen.getByText('升级到付费版解锁此功能')).toBeInTheDocument()
})
it('不受限时不应该显示升级提示文字', () => {
render(<RestrictedToggle {...defaultProps} isRestricted={false} />)
expect(screen.queryByTestId('restricted-hint')).not.toBeInTheDocument()
})
it('受限时标签应该有降低的不透明度', () => {
render(<RestrictedToggle {...defaultProps} isRestricted={true} />)
const label = screen.getByText('测试功能')
expect(label.className).toContain('opacity-60')
})
it('受限时 data-restricted 属性应该为 true', () => {
render(<RestrictedToggle {...defaultProps} isRestricted={true} />)
const toggle = screen.getByTestId('restricted-toggle')
expect(toggle).toHaveAttribute('data-restricted', 'true')
})
it('不受限时 data-restricted 属性应该为 false', () => {
render(<RestrictedToggle {...defaultProps} isRestricted={false} />)
const toggle = screen.getByTestId('restricted-toggle')
expect(toggle).toHaveAttribute('data-restricted', 'false')
})
})
describe('开关状态', () => {
it('value 为 true 时开关应该在右侧', () => {
render(<RestrictedToggle {...defaultProps} value={true} />)
const toggleButton = screen.getByTestId('toggle-button')
expect(toggleButton).toHaveAttribute('aria-checked', 'true')
})
it('value 为 false 时开关应该在左侧', () => {
render(<RestrictedToggle {...defaultProps} value={false} />)
const toggleButton = screen.getByTestId('toggle-button')
expect(toggleButton).toHaveAttribute('aria-checked', 'false')
})
it('受限时 aria-disabled 应该为 true', () => {
render(<RestrictedToggle {...defaultProps} isRestricted={true} />)
const toggleButton = screen.getByTestId('toggle-button')
expect(toggleButton).toHaveAttribute('aria-disabled', 'true')
})
it('不受限时 aria-disabled 应该为 false', () => {
render(<RestrictedToggle {...defaultProps} isRestricted={false} />)
const toggleButton = screen.getByTestId('toggle-button')
expect(toggleButton).toHaveAttribute('aria-disabled', 'false')
})
})
describe('升级弹窗交互', () => {
it('关闭升级弹窗后应该隐藏弹窗', () => {
render(<RestrictedToggle {...defaultProps} isRestricted={true} />)
// 打开弹窗
const toggleButton = screen.getByTestId('toggle-button')
fireEvent.click(toggleButton)
expect(screen.getByTestId('upgrade-modal')).toBeInTheDocument()
// 关闭弹窗
fireEvent.click(screen.getByText('关闭'))
expect(screen.queryByTestId('upgrade-modal')).not.toBeInTheDocument()
})
})
})