/** * 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() expect(screen.getByText('测试功能')).toBeInTheDocument() }) it('应该正确渲染描述', () => { render() expect(screen.getByText('这是功能描述')).toBeInTheDocument() }) it('应该有正确的 data-testid', () => { render() 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() const toggleButton = screen.getByTestId('toggle-button') fireEvent.click(toggleButton) // 应该显示升级弹窗 expect(screen.getByTestId('upgrade-modal')).toBeInTheDocument() }) it('受限时点击不应该调用 onChange', () => { render() const toggleButton = screen.getByTestId('toggle-button') fireEvent.click(toggleButton) // 不应该调用 onChange expect(mockOnChange).not.toHaveBeenCalled() }) it('受限时开关值不应该改变', () => { const { rerender } = render( ) const toggleButton = screen.getByTestId('toggle-button') fireEvent.click(toggleButton) // 值应该保持不变 expect(mockOnChange).not.toHaveBeenCalled() // 重新渲染确认值没变 rerender() expect(toggleButton).toHaveAttribute('aria-checked', 'false') }) it('不受限时点击应该调用 onChange', () => { render() const toggleButton = screen.getByTestId('toggle-button') fireEvent.click(toggleButton) // 应该调用 onChange expect(mockOnChange).toHaveBeenCalledWith(true) }) it('不受限时点击不应该显示升级弹窗', () => { render() 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() expect(screen.getByTestId('pro-badge')).toBeInTheDocument() }) it('不受限时不应该显示 PRO 徽章', () => { render() expect(screen.queryByTestId('pro-badge')).not.toBeInTheDocument() }) it('受限时开关应该有禁用样式', () => { render() const toggleButton = screen.getByTestId('toggle-button') expect(toggleButton.className).toContain('opacity-50') expect(toggleButton.className).toContain('cursor-not-allowed') }) it('不受限时开关应该有正常样式', () => { render() const toggleButton = screen.getByTestId('toggle-button') expect(toggleButton.className).toContain('cursor-pointer') expect(toggleButton.className).not.toContain('opacity-50') }) it('受限时应该显示授权提示文字', () => { render() expect(screen.getByTestId('restricted-hint')).toBeInTheDocument() expect(screen.getByText('当前许可证未包含此功能')).toBeInTheDocument() }) it('不受限时不应该显示授权提示文字', () => { render() expect(screen.queryByTestId('restricted-hint')).not.toBeInTheDocument() }) it('受限时标签应该有降低的不透明度', () => { render() const label = screen.getByText('测试功能') expect(label.className).toContain('opacity-60') }) it('受限时 data-restricted 属性应该为 true', () => { render() const toggle = screen.getByTestId('restricted-toggle') expect(toggle).toHaveAttribute('data-restricted', 'true') }) it('不受限时 data-restricted 属性应该为 false', () => { render() const toggle = screen.getByTestId('restricted-toggle') expect(toggle).toHaveAttribute('data-restricted', 'false') }) }) describe('开关状态', () => { it('value 为 true 时开关应该在右侧', () => { render() const toggleButton = screen.getByTestId('toggle-button') expect(toggleButton).toHaveAttribute('aria-checked', 'true') }) it('value 为 false 时开关应该在左侧', () => { render() const toggleButton = screen.getByTestId('toggle-button') expect(toggleButton).toHaveAttribute('aria-checked', 'false') }) it('受限时 aria-disabled 应该为 true', () => { render() const toggleButton = screen.getByTestId('toggle-button') expect(toggleButton).toHaveAttribute('aria-disabled', 'true') }) it('不受限时 aria-disabled 应该为 false', () => { render() const toggleButton = screen.getByTestId('toggle-button') expect(toggleButton).toHaveAttribute('aria-disabled', 'false') }) }) describe('升级弹窗交互', () => { it('关闭升级弹窗后应该隐藏弹窗', () => { render() // 打开弹窗 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() }) }) })