import type { StepperProps } from './index' import type { Step, StepperStepProps } from './step' import { render, screen } from '@testing-library/react' import { Stepper } from './index' import { StepperStep } from './step' // Test data factory for creating steps const createStep = (overrides: Partial = {}): Step => ({ name: 'Test Step', ...overrides, }) const createSteps = (count: number, namePrefix = 'Step'): Step[] => Array.from({ length: count }, (_, i) => createStep({ name: `${namePrefix} ${i + 1}` })) // Helper to render Stepper with default props const renderStepper = (props: Partial = {}) => { const defaultProps: StepperProps = { steps: createSteps(3), activeIndex: 0, ...props, } return render() } // Helper to render StepperStep with default props const renderStepperStep = (props: Partial = {}) => { const defaultProps: StepperStepProps = { name: 'Test Step', index: 0, activeIndex: 0, ...props, } return render() } // ============================================================================ // Stepper Component Tests // ============================================================================ describe('Stepper', () => { beforeEach(() => { vi.clearAllMocks() }) // -------------------------------------------------------------------------- // Rendering Tests - Verify component renders properly with various inputs // -------------------------------------------------------------------------- describe('Rendering', () => { it('should render without crashing', () => { // Arrange & Act renderStepper() // Assert expect(screen.getByText('Step 1')).toBeInTheDocument() }) it('should render all step names', () => { // Arrange const steps = createSteps(3, 'Custom Step') // Act renderStepper({ steps }) // Assert expect(screen.getByText('Custom Step 1')).toBeInTheDocument() expect(screen.getByText('Custom Step 2')).toBeInTheDocument() expect(screen.getByText('Custom Step 3')).toBeInTheDocument() }) it('should render dividers between steps', () => { // Arrange const steps = createSteps(3) // Act const { container } = renderStepper({ steps }) // Assert - Should have 2 dividers for 3 steps const dividers = container.querySelectorAll('.bg-divider-deep') expect(dividers.length).toBe(2) }) it('should not render divider after last step', () => { // Arrange const steps = createSteps(2) // Act const { container } = renderStepper({ steps }) // Assert - Should have 1 divider for 2 steps const dividers = container.querySelectorAll('.bg-divider-deep') expect(dividers.length).toBe(1) }) it('should render with flex container layout', () => { // Arrange & Act const { container } = renderStepper() // Assert const wrapper = container.firstChild as HTMLElement expect(wrapper).toHaveClass('flex', 'items-center', 'gap-3') }) }) // -------------------------------------------------------------------------- // Props Testing - Test all prop variations and combinations // -------------------------------------------------------------------------- describe('Props', () => { describe('steps prop', () => { it('should render correct number of steps', () => { // Arrange const steps = createSteps(5) // Act renderStepper({ steps }) // Assert expect(screen.getByText('Step 1')).toBeInTheDocument() expect(screen.getByText('Step 2')).toBeInTheDocument() expect(screen.getByText('Step 3')).toBeInTheDocument() expect(screen.getByText('Step 4')).toBeInTheDocument() expect(screen.getByText('Step 5')).toBeInTheDocument() }) it('should handle single step correctly', () => { // Arrange const steps = [createStep({ name: 'Only Step' })] // Act const { container } = renderStepper({ steps, activeIndex: 0 }) // Assert expect(screen.getByText('Only Step')).toBeInTheDocument() // No dividers for single step const dividers = container.querySelectorAll('.bg-divider-deep') expect(dividers.length).toBe(0) }) it('should handle steps with long names', () => { // Arrange const longName = 'This is a very long step name that might overflow' const steps = [createStep({ name: longName })] // Act renderStepper({ steps, activeIndex: 0 }) // Assert expect(screen.getByText(longName)).toBeInTheDocument() }) it('should handle steps with special characters', () => { // Arrange const steps = [ createStep({ name: 'Step & Configuration' }), createStep({ name: 'Step ' }), createStep({ name: 'Step "Complete"' }), ] // Act renderStepper({ steps, activeIndex: 0 }) // Assert expect(screen.getByText('Step & Configuration')).toBeInTheDocument() expect(screen.getByText('Step ')).toBeInTheDocument() expect(screen.getByText('Step "Complete"')).toBeInTheDocument() }) }) describe('activeIndex prop', () => { it('should highlight first step when activeIndex is 0', () => { // Arrange & Act renderStepper({ activeIndex: 0 }) // Assert - First step should show "STEP 1" label expect(screen.getByText('STEP 1')).toBeInTheDocument() }) it('should highlight second step when activeIndex is 1', () => { // Arrange & Act renderStepper({ activeIndex: 1 }) // Assert - Second step should show "STEP 2" label expect(screen.getByText('STEP 2')).toBeInTheDocument() }) it('should highlight last step when activeIndex equals steps length - 1', () => { // Arrange const steps = createSteps(3) // Act renderStepper({ steps, activeIndex: 2 }) // Assert - Third step should show "STEP 3" label expect(screen.getByText('STEP 3')).toBeInTheDocument() }) it('should show completed steps with number only (no STEP prefix)', () => { // Arrange const steps = createSteps(3) // Act renderStepper({ steps, activeIndex: 2 }) // Assert - Completed steps show just the number expect(screen.getByText('1')).toBeInTheDocument() expect(screen.getByText('2')).toBeInTheDocument() expect(screen.getByText('STEP 3')).toBeInTheDocument() }) it('should show disabled steps with number only (no STEP prefix)', () => { // Arrange const steps = createSteps(3) // Act renderStepper({ steps, activeIndex: 0 }) // Assert - Disabled steps show just the number expect(screen.getByText('STEP 1')).toBeInTheDocument() expect(screen.getByText('2')).toBeInTheDocument() expect(screen.getByText('3')).toBeInTheDocument() }) }) }) // -------------------------------------------------------------------------- // Edge Cases - Test boundary conditions and unexpected inputs // -------------------------------------------------------------------------- describe('Edge Cases', () => { it('should handle empty steps array', () => { // Arrange & Act const { container } = renderStepper({ steps: [] }) // Assert - Container should render but be empty expect(container.firstChild).toBeInTheDocument() expect(container.firstChild?.childNodes.length).toBe(0) }) it('should handle activeIndex greater than steps length', () => { // Arrange const steps = createSteps(2) // Act - activeIndex 5 is beyond array bounds renderStepper({ steps, activeIndex: 5 }) // Assert - All steps should render as completed (since activeIndex > all indices) expect(screen.getByText('1')).toBeInTheDocument() expect(screen.getByText('2')).toBeInTheDocument() }) it('should handle negative activeIndex', () => { // Arrange const steps = createSteps(2) // Act - negative activeIndex renderStepper({ steps, activeIndex: -1 }) // Assert - All steps should render as disabled (since activeIndex < all indices) expect(screen.getByText('1')).toBeInTheDocument() expect(screen.getByText('2')).toBeInTheDocument() }) it('should handle large number of steps', () => { // Arrange const steps = createSteps(10) // Act const { container } = renderStepper({ steps, activeIndex: 5 }) // Assert expect(screen.getByText('STEP 6')).toBeInTheDocument() // Should have 9 dividers for 10 steps const dividers = container.querySelectorAll('.bg-divider-deep') expect(dividers.length).toBe(9) }) it('should handle steps with empty name', () => { // Arrange const steps = [createStep({ name: '' })] // Act const { container } = renderStepper({ steps, activeIndex: 0 }) // Assert - Should still render the step structure expect(screen.getByText('STEP 1')).toBeInTheDocument() expect(container.firstChild).toBeInTheDocument() }) }) // -------------------------------------------------------------------------- // Integration - Test step state combinations // -------------------------------------------------------------------------- describe('Step States', () => { it('should render mixed states: completed, active, disabled', () => { // Arrange const steps = createSteps(5) // Act renderStepper({ steps, activeIndex: 2 }) // Assert // Steps 1-2 are completed (show number only) expect(screen.getByText('1')).toBeInTheDocument() expect(screen.getByText('2')).toBeInTheDocument() // Step 3 is active (shows STEP prefix) expect(screen.getByText('STEP 3')).toBeInTheDocument() // Steps 4-5 are disabled (show number only) expect(screen.getByText('4')).toBeInTheDocument() expect(screen.getByText('5')).toBeInTheDocument() }) it('should transition through all states correctly', () => { // Arrange const steps = createSteps(3) // Act & Assert - Step 1 active const { rerender } = render() expect(screen.getByText('STEP 1')).toBeInTheDocument() // Step 2 active rerender() expect(screen.getByText('1')).toBeInTheDocument() expect(screen.getByText('STEP 2')).toBeInTheDocument() // Step 3 active rerender() expect(screen.getByText('1')).toBeInTheDocument() expect(screen.getByText('2')).toBeInTheDocument() expect(screen.getByText('STEP 3')).toBeInTheDocument() }) }) }) // ============================================================================ // StepperStep Component Tests // ============================================================================ describe('StepperStep', () => { beforeEach(() => { vi.clearAllMocks() }) // -------------------------------------------------------------------------- // Rendering Tests // -------------------------------------------------------------------------- describe('Rendering', () => { it('should render without crashing', () => { // Arrange & Act renderStepperStep() // Assert expect(screen.getByText('Test Step')).toBeInTheDocument() }) it('should render step name', () => { // Arrange & Act renderStepperStep({ name: 'Configure Dataset' }) // Assert expect(screen.getByText('Configure Dataset')).toBeInTheDocument() }) it('should render with flex container layout', () => { // Arrange & Act const { container } = renderStepperStep() // Assert const wrapper = container.firstChild as HTMLElement expect(wrapper).toHaveClass('flex', 'items-center', 'gap-2') }) }) // -------------------------------------------------------------------------- // Active State Tests // -------------------------------------------------------------------------- describe('Active State', () => { it('should show STEP prefix when active', () => { // Arrange & Act renderStepperStep({ index: 0, activeIndex: 0 }) // Assert expect(screen.getByText('STEP 1')).toBeInTheDocument() }) it('should apply active styles to label container', () => { // Arrange & Act const { container } = renderStepperStep({ index: 0, activeIndex: 0 }) // Assert const labelContainer = container.querySelector('.bg-state-accent-solid') expect(labelContainer).toBeInTheDocument() expect(labelContainer).toHaveClass('px-2') }) it('should apply active text color to label', () => { // Arrange & Act const { container } = renderStepperStep({ index: 0, activeIndex: 0 }) // Assert const label = container.querySelector('.text-text-primary-on-surface') expect(label).toBeInTheDocument() }) it('should apply accent text color to name when active', () => { // Arrange & Act const { container } = renderStepperStep({ index: 0, activeIndex: 0 }) // Assert const nameElement = container.querySelector('.text-text-accent') expect(nameElement).toBeInTheDocument() expect(nameElement).toHaveClass('system-xs-semibold-uppercase') }) it('should calculate active correctly for different indices', () => { // Test index 1 with activeIndex 1 const { rerender } = render( , ) expect(screen.getByText('STEP 2')).toBeInTheDocument() // Test index 5 with activeIndex 5 rerender() expect(screen.getByText('STEP 6')).toBeInTheDocument() }) }) // -------------------------------------------------------------------------- // Completed State Tests (index < activeIndex) // -------------------------------------------------------------------------- describe('Completed State', () => { it('should show number only when completed (not active)', () => { // Arrange & Act renderStepperStep({ index: 0, activeIndex: 1 }) // Assert expect(screen.getByText('1')).toBeInTheDocument() expect(screen.queryByText('STEP 1')).not.toBeInTheDocument() }) it('should apply completed styles to label container', () => { // Arrange & Act const { container } = renderStepperStep({ index: 0, activeIndex: 1 }) // Assert const labelContainer = container.querySelector('.border-text-quaternary') expect(labelContainer).toBeInTheDocument() expect(labelContainer).toHaveClass('w-5') }) it('should apply tertiary text color to label when completed', () => { // Arrange & Act const { container } = renderStepperStep({ index: 0, activeIndex: 1 }) // Assert const label = container.querySelector('.text-text-tertiary') expect(label).toBeInTheDocument() }) it('should apply tertiary text color to name when completed', () => { // Arrange & Act const { container } = renderStepperStep({ index: 0, activeIndex: 2 }) // Assert const nameElements = container.querySelectorAll('.text-text-tertiary') expect(nameElements.length).toBeGreaterThan(0) }) }) // -------------------------------------------------------------------------- // Disabled State Tests (index > activeIndex) // -------------------------------------------------------------------------- describe('Disabled State', () => { it('should show number only when disabled', () => { // Arrange & Act renderStepperStep({ index: 2, activeIndex: 0 }) // Assert expect(screen.getByText('3')).toBeInTheDocument() expect(screen.queryByText('STEP 3')).not.toBeInTheDocument() }) it('should apply disabled styles to label container', () => { // Arrange & Act const { container } = renderStepperStep({ index: 2, activeIndex: 0 }) // Assert const labelContainer = container.querySelector('.border-divider-deep') expect(labelContainer).toBeInTheDocument() expect(labelContainer).toHaveClass('w-5') }) it('should apply quaternary text color to label when disabled', () => { // Arrange & Act const { container } = renderStepperStep({ index: 2, activeIndex: 0 }) // Assert const label = container.querySelector('.text-text-quaternary') expect(label).toBeInTheDocument() }) it('should apply quaternary text color to name when disabled', () => { // Arrange & Act const { container } = renderStepperStep({ index: 2, activeIndex: 0 }) // Assert const nameElements = container.querySelectorAll('.text-text-quaternary') expect(nameElements.length).toBeGreaterThan(0) }) }) // -------------------------------------------------------------------------- // Props Testing // -------------------------------------------------------------------------- describe('Props', () => { describe('name prop', () => { it('should render provided name', () => { // Arrange & Act renderStepperStep({ name: 'Custom Name' }) // Assert expect(screen.getByText('Custom Name')).toBeInTheDocument() }) it('should handle empty name', () => { // Arrange & Act const { container } = renderStepperStep({ name: '' }) // Assert - Label should still render expect(screen.getByText('STEP 1')).toBeInTheDocument() expect(container.firstChild).toBeInTheDocument() }) it('should handle name with whitespace', () => { // Arrange & Act renderStepperStep({ name: ' Padded Name ' }) // Assert expect(screen.getByText('Padded Name')).toBeInTheDocument() }) }) describe('index prop', () => { it('should display correct 1-based number for index 0', () => { // Arrange & Act renderStepperStep({ index: 0, activeIndex: 0 }) // Assert expect(screen.getByText('STEP 1')).toBeInTheDocument() }) it('should display correct 1-based number for index 9', () => { // Arrange & Act renderStepperStep({ index: 9, activeIndex: 9 }) // Assert expect(screen.getByText('STEP 10')).toBeInTheDocument() }) it('should handle large index values', () => { // Arrange & Act renderStepperStep({ index: 99, activeIndex: 99 }) // Assert expect(screen.getByText('STEP 100')).toBeInTheDocument() }) }) describe('activeIndex prop', () => { it('should determine state based on activeIndex comparison', () => { // Active: index === activeIndex const { rerender } = render( , ) expect(screen.getByText('STEP 2')).toBeInTheDocument() // Completed: index < activeIndex rerender() expect(screen.getByText('2')).toBeInTheDocument() // Disabled: index > activeIndex rerender() expect(screen.getByText('2')).toBeInTheDocument() }) }) }) // -------------------------------------------------------------------------- // Edge Cases // -------------------------------------------------------------------------- describe('Edge Cases', () => { it('should handle zero index correctly', () => { // Arrange & Act renderStepperStep({ index: 0, activeIndex: 0 }) // Assert expect(screen.getByText('STEP 1')).toBeInTheDocument() }) it('should handle negative activeIndex', () => { // Arrange & Act renderStepperStep({ index: 0, activeIndex: -1 }) // Assert - Step should be disabled (index > activeIndex) expect(screen.getByText('1')).toBeInTheDocument() }) it('should handle equal boundary (index equals activeIndex)', () => { // Arrange & Act renderStepperStep({ index: 5, activeIndex: 5 }) // Assert - Should be active expect(screen.getByText('STEP 6')).toBeInTheDocument() }) it('should handle name with HTML-like content safely', () => { // Arrange & Act renderStepperStep({ name: '' }) // Assert - Should render as text, not execute expect(screen.getByText('')).toBeInTheDocument() }) it('should handle name with unicode characters', () => { // Arrange & Act renderStepperStep({ name: 'Step 数据 🚀' }) // Assert expect(screen.getByText('Step 数据 🚀')).toBeInTheDocument() }) }) // -------------------------------------------------------------------------- // Style Classes Verification // -------------------------------------------------------------------------- describe('Style Classes', () => { it('should apply correct typography classes to label', () => { // Arrange & Act const { container } = renderStepperStep() // Assert const label = container.querySelector('.system-2xs-semibold-uppercase') expect(label).toBeInTheDocument() }) it('should apply correct typography classes to name', () => { // Arrange & Act const { container } = renderStepperStep() // Assert const name = container.querySelector('.system-xs-medium-uppercase') expect(name).toBeInTheDocument() }) it('should have rounded pill shape for label container', () => { // Arrange & Act const { container } = renderStepperStep() // Assert const labelContainer = container.querySelector('.rounded-3xl') expect(labelContainer).toBeInTheDocument() }) it('should apply h-5 height to label container', () => { // Arrange & Act const { container } = renderStepperStep() // Assert const labelContainer = container.querySelector('.h-5') expect(labelContainer).toBeInTheDocument() }) }) }) // ============================================================================ // Integration Tests - Stepper and StepperStep working together // ============================================================================ describe('Stepper Integration', () => { beforeEach(() => { vi.clearAllMocks() }) it('should pass correct props to each StepperStep', () => { // Arrange const steps = [ createStep({ name: 'First' }), createStep({ name: 'Second' }), createStep({ name: 'Third' }), ] // Act renderStepper({ steps, activeIndex: 1 }) // Assert - Each step receives correct index and displays correctly expect(screen.getByText('1')).toBeInTheDocument() // Completed expect(screen.getByText('First')).toBeInTheDocument() expect(screen.getByText('STEP 2')).toBeInTheDocument() // Active expect(screen.getByText('Second')).toBeInTheDocument() expect(screen.getByText('3')).toBeInTheDocument() // Disabled expect(screen.getByText('Third')).toBeInTheDocument() }) it('should maintain correct visual hierarchy across steps', () => { // Arrange const steps = createSteps(4) // Act const { container } = renderStepper({ steps, activeIndex: 2 }) // Assert - Check visual hierarchy // Completed steps (0, 1) have border-text-quaternary const completedLabels = container.querySelectorAll('.border-text-quaternary') expect(completedLabels.length).toBe(2) // Active step has bg-state-accent-solid const activeLabel = container.querySelector('.bg-state-accent-solid') expect(activeLabel).toBeInTheDocument() // Disabled step (3) has border-divider-deep const disabledLabels = container.querySelectorAll('.border-divider-deep') expect(disabledLabels.length).toBe(1) }) it('should render correctly with dynamic step updates', () => { // Arrange const initialSteps = createSteps(2) // Act const { rerender } = render() expect(screen.getByText('Step 1')).toBeInTheDocument() expect(screen.getByText('Step 2')).toBeInTheDocument() // Update with more steps const updatedSteps = createSteps(4) rerender() // Assert expect(screen.getByText('STEP 3')).toBeInTheDocument() expect(screen.getByText('Step 4')).toBeInTheDocument() }) })