318 lines
10 KiB
TypeScript
318 lines
10 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
import { render, screen, fireEvent, waitFor, within } from '@testing-library/react'
|
|
import userEvent from '@testing-library/user-event'
|
|
import Scanner from '@/components/Scanner'
|
|
|
|
// Helper to reduce render() duplication
|
|
const renderScanner = (props = {}) => {
|
|
const defaultProps = {
|
|
onScanSuccess: vi.fn(),
|
|
onOCRMatch: vi.fn(),
|
|
paused: false,
|
|
...props,
|
|
}
|
|
return render(<Scanner {...defaultProps} />)
|
|
}
|
|
|
|
describe('Scanner Component', () => {
|
|
afterEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
// ============================================================================
|
|
// UNIT TESTS: Rendering (Core Structure)
|
|
// ============================================================================
|
|
|
|
describe('Rendering', () => {
|
|
it('should render scanner with viewport and controls', () => {
|
|
const { container } = renderScanner()
|
|
const viewport = container.querySelector('.aspect-square')
|
|
const controls = container.querySelector('.bg-surface\\/50')
|
|
expect(viewport).toBeInTheDocument()
|
|
expect(controls).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// UNIT TESTS: Callback Props
|
|
// ============================================================================
|
|
|
|
describe('Callback Props', () => {
|
|
it('should accept onScanSuccess and onOCRMatch callbacks', () => {
|
|
const mockSuccess = vi.fn()
|
|
const mockOCR = vi.fn()
|
|
renderScanner({ onScanSuccess: mockSuccess, onOCRMatch: mockOCR })
|
|
expect(mockSuccess).toBeDefined()
|
|
expect(mockOCR).toBeDefined()
|
|
})
|
|
|
|
it('should render with minimal props (onScanSuccess only)', () => {
|
|
const mockSuccess = vi.fn()
|
|
const { container } = render(
|
|
<Scanner onScanSuccess={mockSuccess} />
|
|
)
|
|
expect(container).toBeInTheDocument()
|
|
})
|
|
|
|
it('should accept paused prop and pass through', () => {
|
|
const { container } = renderScanner({ paused: true })
|
|
expect(container).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// UNIT TESTS: Zoom Controls & Accessibility
|
|
// ============================================================================
|
|
|
|
describe('Zoom Control & Buttons', () => {
|
|
it('should render buttons with accessibility attributes', () => {
|
|
const { container } = renderScanner()
|
|
const buttons = container.querySelectorAll('button')
|
|
expect(buttons.length).toBeGreaterThanOrEqual(0)
|
|
buttons.forEach(btn => {
|
|
// Button should have proper styling
|
|
expect(btn.className).toBeTruthy()
|
|
})
|
|
})
|
|
|
|
it('should have focus-ring styling on interactive elements', () => {
|
|
const { container } = renderScanner()
|
|
const buttons = container.querySelectorAll('button')
|
|
if (buttons.length > 0) {
|
|
const firstButton = buttons[0]
|
|
expect(firstButton.className).toMatch(/focus.*ring|focus-visible/)
|
|
}
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// UNIT TESTS: Error Handling
|
|
// ============================================================================
|
|
|
|
describe('Error Handling', () => {
|
|
it('should render without throwing on mount with valid props', () => {
|
|
expect(() => {
|
|
renderScanner()
|
|
}).not.toThrow()
|
|
})
|
|
|
|
it('should handle unmount without errors', () => {
|
|
const { unmount } = renderScanner()
|
|
expect(() => unmount()).not.toThrow()
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// INTEGRATION TESTS: Scan Workflow
|
|
// ============================================================================
|
|
|
|
describe('Scan Workflow (Integration)', () => {
|
|
it('should render scanner with callbacks available', () => {
|
|
const mockSuccess = vi.fn()
|
|
const mockOCR = vi.fn()
|
|
const { container } = renderScanner({ onScanSuccess: mockSuccess, onOCRMatch: mockOCR })
|
|
|
|
expect(mockSuccess).toBeDefined()
|
|
expect(mockOCR).toBeDefined()
|
|
expect(container.querySelector('.aspect-square')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should clean up on unmount', () => {
|
|
const { unmount, container } = renderScanner()
|
|
expect(container).toBeInTheDocument()
|
|
expect(() => unmount()).not.toThrow()
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// UNIT TESTS: Pause State
|
|
// ============================================================================
|
|
|
|
describe('Pause State', () => {
|
|
it('should handle paused prop changes without errors', () => {
|
|
const mockSuccess = vi.fn()
|
|
const mockOCR = vi.fn()
|
|
const { rerender, container } = render(
|
|
<Scanner
|
|
onScanSuccess={mockSuccess}
|
|
onOCRMatch={mockOCR}
|
|
paused={false}
|
|
/>
|
|
)
|
|
|
|
expect(container).toBeInTheDocument()
|
|
|
|
rerender(
|
|
<Scanner
|
|
onScanSuccess={mockSuccess}
|
|
onOCRMatch={mockOCR}
|
|
paused={true}
|
|
/>
|
|
)
|
|
|
|
expect(container).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// LAYOUT & ACCESSIBILITY TESTS
|
|
// ============================================================================
|
|
|
|
describe('Layout & Accessibility', () => {
|
|
it('should have responsive Tailwind layout structure', () => {
|
|
const { container } = renderScanner()
|
|
const mainDiv = container.querySelector('.w-full.max-w-md')
|
|
expect(mainDiv).toBeInTheDocument()
|
|
expect(mainDiv?.className).toMatch(/flex/)
|
|
})
|
|
|
|
it('should contain text elements for readability', () => {
|
|
const { container } = renderScanner()
|
|
const textElements = container.querySelectorAll('p, span, button')
|
|
expect(textElements.length).toBeGreaterThan(0)
|
|
})
|
|
|
|
it('should use Lucide icons (SVG elements)', () => {
|
|
const { container } = renderScanner()
|
|
const svgs = container.querySelectorAll('svg')
|
|
expect(svgs.length).toBeGreaterThanOrEqual(0)
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// COMPONENT PROP VARIATIONS & STABILITY
|
|
// ============================================================================
|
|
|
|
describe('Component Prop Variations & Stability', () => {
|
|
it('should render with all props (onScanSuccess, onOCRMatch, paused)', () => {
|
|
const { container } = renderScanner({ paused: false })
|
|
expect(container).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render with minimal props (onScanSuccess only)', () => {
|
|
const { container } = render(
|
|
<Scanner onScanSuccess={vi.fn()} />
|
|
)
|
|
expect(container).toBeInTheDocument()
|
|
})
|
|
|
|
it('should handle multiple rapid re-renders without errors', () => {
|
|
const mockScan = vi.fn()
|
|
const mockOCR = vi.fn()
|
|
const { rerender } = renderScanner({ onScanSuccess: mockScan, onOCRMatch: mockOCR })
|
|
|
|
for (let i = 0; i < 3; i++) {
|
|
expect(() => {
|
|
rerender(
|
|
<Scanner
|
|
onScanSuccess={mockScan}
|
|
onOCRMatch={mockOCR}
|
|
/>
|
|
)
|
|
}).not.toThrow()
|
|
}
|
|
})
|
|
|
|
it('should handle callback function changes between renders', () => {
|
|
const mockScan1 = vi.fn()
|
|
const mockScan2 = vi.fn()
|
|
const { rerender } = render(
|
|
<Scanner
|
|
onScanSuccess={mockScan1}
|
|
onOCRMatch={vi.fn()}
|
|
/>
|
|
)
|
|
|
|
rerender(
|
|
<Scanner
|
|
onScanSuccess={mockScan2}
|
|
onOCRMatch={vi.fn()}
|
|
/>
|
|
)
|
|
|
|
expect(mockScan1).toBeDefined()
|
|
expect(mockScan2).toBeDefined()
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// INTEGRATION: UI Interaction States
|
|
// ============================================================================
|
|
|
|
describe('UI Interaction States', () => {
|
|
it('should render viewport and controls in proper visual hierarchy', () => {
|
|
const { container } = renderScanner()
|
|
const viewport = container.querySelector('.aspect-square')
|
|
const controls = container.querySelector('.bg-surface\\/50')
|
|
expect(viewport).toBeInTheDocument()
|
|
expect(controls).toBeInTheDocument()
|
|
})
|
|
|
|
it('should have consistent spacing with gap utilities', () => {
|
|
const { container } = renderScanner()
|
|
const mainContainer = container.querySelector('.flex.flex-col')
|
|
expect(mainContainer?.className).toMatch(/gap-/)
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// COMPONENT STABILITY TESTS
|
|
// ============================================================================
|
|
|
|
describe('Component Stability', () => {
|
|
it('should mount and unmount without errors', () => {
|
|
expect(() => renderScanner()).not.toThrow()
|
|
const { unmount } = renderScanner()
|
|
expect(() => unmount()).not.toThrow()
|
|
})
|
|
|
|
it('should handle rapid paused prop changes', () => {
|
|
const { rerender } = renderScanner({ paused: false })
|
|
|
|
for (let i = 0; i < 5; i++) {
|
|
expect(() => {
|
|
rerender(
|
|
<Scanner
|
|
onScanSuccess={vi.fn()}
|
|
onOCRMatch={vi.fn()}
|
|
paused={i % 2 === 0}
|
|
/>
|
|
)
|
|
}).not.toThrow()
|
|
}
|
|
})
|
|
|
|
it('should render with only required prop (onScanSuccess)', () => {
|
|
expect(() => {
|
|
render(<Scanner onScanSuccess={vi.fn()} />)
|
|
}).not.toThrow()
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// COMPONENT STRUCTURE TESTS
|
|
// ============================================================================
|
|
|
|
describe('Component Structure', () => {
|
|
it('should maintain DOM structure on initial render and after prop updates', () => {
|
|
const { rerender, container } = renderScanner({ paused: false })
|
|
|
|
// Verify initial structure
|
|
expect(container.querySelector('.w-full.max-w-md')).toBeInTheDocument()
|
|
expect(container.querySelector('.aspect-square')).toBeInTheDocument()
|
|
expect(container.querySelector('.bg-surface\\/50')).toBeInTheDocument()
|
|
|
|
// Update props
|
|
rerender(
|
|
<Scanner
|
|
onScanSuccess={vi.fn()}
|
|
onOCRMatch={vi.fn()}
|
|
paused={true}
|
|
/>
|
|
)
|
|
|
|
// Structure preserved
|
|
expect(container.querySelector('.aspect-square')).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|