diff --git a/src/components/ui/__snapshots__/aspect-ratio.test.tsx.snap b/src/components/ui/__snapshots__/aspect-ratio.test.tsx.snap new file mode 100644 index 0000000..7619ceb --- /dev/null +++ b/src/components/ui/__snapshots__/aspect-ratio.test.tsx.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AspectRatio applies ratio prop 1`] = ` +
No export
} ` const component = transformReactCode(code) diff --git a/src/lib/snippets/index.test.ts b/src/lib/snippets/index.test.ts new file mode 100644 index 0000000..5053ccc --- /dev/null +++ b/src/lib/snippets/index.test.ts @@ -0,0 +1,35 @@ +import { + atomsCodeSnippets, + moleculesCodeSnippets, + organismsCodeSnippets, + templatesCodeSnippets, +} from './index' + +describe('snippets barrel export', () => { + test('exports atomsCodeSnippets', () => { + expect(atomsCodeSnippets).toBeDefined() + expect(typeof atomsCodeSnippets).toBe('object') + }) + + test('exports moleculesCodeSnippets', () => { + expect(moleculesCodeSnippets).toBeDefined() + expect(typeof moleculesCodeSnippets).toBe('object') + }) + + test('exports organismsCodeSnippets', () => { + expect(organismsCodeSnippets).toBeDefined() + expect(typeof organismsCodeSnippets).toBe('object') + }) + + test('exports templatesCodeSnippets', () => { + expect(templatesCodeSnippets).toBeDefined() + expect(typeof templatesCodeSnippets).toBe('object') + }) + + test('all exports are objects with content', () => { + expect(Object.keys(atomsCodeSnippets).length).toBeGreaterThan(0) + expect(Object.keys(moleculesCodeSnippets).length).toBeGreaterThan(0) + expect(Object.keys(organismsCodeSnippets).length).toBeGreaterThan(0) + expect(Object.keys(templatesCodeSnippets).length).toBeGreaterThan(0) + }) +}) diff --git a/src/lib/utils.test.ts b/src/lib/utils.test.ts new file mode 100644 index 0000000..81a89e2 --- /dev/null +++ b/src/lib/utils.test.ts @@ -0,0 +1,267 @@ +import { cn, formatBytes, debounce, sleep } from './utils' + +describe('utils', () => { + describe('cn', () => { + test('combines single class', () => { + expect(cn('bg-blue-500')).toBe('bg-blue-500') + }) + + test('combines multiple classes', () => { + expect(cn('bg-blue-500', 'text-white', 'p-4')).toBe('bg-blue-500 text-white p-4') + }) + + test('filters out undefined values', () => { + expect(cn('bg-blue-500', undefined, 'text-white')).toBe('bg-blue-500 text-white') + }) + + test('filters out null values', () => { + expect(cn('bg-blue-500', null, 'text-white')).toBe('bg-blue-500 text-white') + }) + + test('filters out false values', () => { + expect(cn('bg-blue-500', false, 'text-white')).toBe('bg-blue-500 text-white') + }) + + test('handles all falsy values', () => { + expect(cn('active', undefined, null, false, '', 'inactive')).toBe('active inactive') + }) + + test('handles empty input', () => { + expect(cn()).toBe('') + }) + + test('handles all falsy input', () => { + expect(cn(undefined, null, false)).toBe('') + }) + + test('preserves order of classes', () => { + expect(cn('z-10', 'absolute', 'top-0')).toBe('z-10 absolute top-0') + }) + + test('handles conditional classes', () => { + const isActive = true + expect(cn(isActive && 'active', 'base')).toBe('active base') + }) + + test('handles conditional classes false case', () => { + const isActive = false + expect(cn(isActive && 'active', 'base')).toBe('base') + }) + }) + + describe('formatBytes', () => { + test('formats zero bytes', () => { + expect(formatBytes(0)).toBe('0 Bytes') + }) + + test('formats bytes', () => { + expect(formatBytes(100)).toBe('100 Bytes') + }) + + test('formats kilobytes', () => { + const result = formatBytes(1024) + expect(result).toMatch(/^1 KB$/) + }) + + test('formats megabytes', () => { + const result = formatBytes(1024 * 1024) + expect(result).toMatch(/^1 MB$/) + }) + + test('formats gigabytes', () => { + const result = formatBytes(1024 * 1024 * 1024) + expect(result).toMatch(/^1 GB$/) + }) + + test('rounds decimal places', () => { + const result = formatBytes(1536) // 1.5 KB + expect(result).toMatch(/1.5 KB/) + }) + + test('handles fractional bytes', () => { + const result = formatBytes(512) + expect(result).toBe('512 Bytes') + }) + + test('formats small files correctly', () => { + expect(formatBytes(100)).toMatch(/100 Bytes/) + }) + + test('formats medium files correctly', () => { + const result = formatBytes(2048 * 1024) // ~2 MB + expect(result).toMatch(/2 MB/) + }) + + test('handles large numbers', () => { + const result = formatBytes(Math.pow(1024, 3) * 5) // 5 GB + expect(result).toMatch(/5 GB/) + }) + }) + + describe('debounce', () => { + beforeEach(() => { + jest.useFakeTimers() + }) + + afterEach(() => { + jest.runOnlyPendingTimers() + jest.useRealTimers() + }) + + test('debounces function calls', () => { + const mockFn = jest.fn() + const debouncedFn = debounce(mockFn, 100) + + debouncedFn('arg1') + debouncedFn('arg2') + debouncedFn('arg3') + + expect(mockFn).not.toHaveBeenCalled() + + jest.advanceTimersByTime(100) + + expect(mockFn).toHaveBeenCalledTimes(1) + expect(mockFn).toHaveBeenCalledWith('arg3') + }) + + test('calls function with latest arguments', () => { + const mockFn = jest.fn() + const debouncedFn = debounce(mockFn, 100) + + debouncedFn('first') + jest.advanceTimersByTime(50) + debouncedFn('second') + jest.advanceTimersByTime(50) + debouncedFn('third') + + expect(mockFn).not.toHaveBeenCalled() + + jest.advanceTimersByTime(100) + expect(mockFn).toHaveBeenCalledWith('third') + }) + + test('resets timer on each call', () => { + const mockFn = jest.fn() + const debouncedFn = debounce(mockFn, 100) + + debouncedFn('arg') + jest.advanceTimersByTime(50) + expect(mockFn).not.toHaveBeenCalled() + + debouncedFn('arg') + jest.advanceTimersByTime(50) + expect(mockFn).not.toHaveBeenCalled() + + jest.advanceTimersByTime(100) + expect(mockFn).toHaveBeenCalledTimes(1) + }) + + test('handles multiple arguments', () => { + const mockFn = jest.fn() + const debouncedFn = debounce(mockFn, 100) + + debouncedFn('arg1', 'arg2', 'arg3') + + jest.advanceTimersByTime(100) + + expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2', 'arg3') + }) + + test('handles no arguments', () => { + const mockFn = jest.fn() + const debouncedFn = debounce(mockFn, 100) + + debouncedFn() + + jest.advanceTimersByTime(100) + + expect(mockFn).toHaveBeenCalledWith() + }) + + test('uses provided wait time', () => { + const mockFn = jest.fn() + const debouncedFn = debounce(mockFn, 250) + + debouncedFn('arg') + + jest.advanceTimersByTime(200) + expect(mockFn).not.toHaveBeenCalled() + + jest.advanceTimersByTime(50) + expect(mockFn).toHaveBeenCalledTimes(1) + }) + + test('clears timeout when cancelled', () => { + const mockFn = jest.fn() + const debouncedFn = debounce(mockFn, 100) + + debouncedFn('arg1') + jest.advanceTimersByTime(50) + debouncedFn('arg2') + + // At this point, timer should be reset + jest.advanceTimersByTime(100) + expect(mockFn).toHaveBeenCalledTimes(1) + }) + }) + + describe('sleep', () => { + beforeEach(() => { + jest.useFakeTimers() + }) + + afterEach(() => { + jest.useRealTimers() + }) + + test('resolves after specified time', async () => { + const promise = sleep(100) + + jest.advanceTimersByTime(100) + await promise + + expect(true).toBe(true) // Just verify it resolves + }) + + test('returns a promise', () => { + const result = sleep(100) + expect(result).toBeInstanceOf(Promise) + }) + + test('handles zero milliseconds', async () => { + const promise = sleep(0) + jest.advanceTimersByTime(0) + await promise + + expect(true).toBe(true) + }) + + test('resolves correctly with different times', async () => { + const promise1 = sleep(50) + const promise2 = sleep(100) + + jest.advanceTimersByTime(50) + await promise1 + + jest.advanceTimersByTime(50) + await promise2 + + expect(true).toBe(true) + }) + + test('can be used in async/await', async () => { + let executed = false + + const asyncFn = async () => { + await sleep(100) + executed = true + } + + const promise = asyncFn() + jest.advanceTimersByTime(100) + await promise + + expect(executed).toBe(true) + }) + }) +}) diff --git a/src/store/hooks/usePersistenceConfig.test.ts b/src/store/hooks/usePersistenceConfig.test.ts new file mode 100644 index 0000000..a8a479b --- /dev/null +++ b/src/store/hooks/usePersistenceConfig.test.ts @@ -0,0 +1,194 @@ +import { renderHook, act } from '@testing-library/react' +import { usePersistenceConfig } from './usePersistenceConfig' +import * as middleware from '../middleware' + +jest.mock('../middleware') + +describe('usePersistenceConfig', () => { + beforeEach(() => { + jest.clearAllMocks() + + ;(middleware.getPersistenceConfig as jest.Mock).mockReturnValue({ + enabled: true, + logging: false, + debounceDelay: 500, + }) + }) + + test('initializes with current config', () => { + const { result } = renderHook(() => usePersistenceConfig()) + + expect(result.current.config).toEqual({ + enabled: true, + logging: false, + debounceDelay: 500, + }) + }) + + test('provides updateConfig function', () => { + const { result } = renderHook(() => usePersistenceConfig()) + + act(() => { + result.current.updateConfig({ logging: true }) + }) + + expect(middleware.updatePersistenceConfig).toHaveBeenCalledWith({ logging: true }) + }) + + test('provides togglePersistence function that disables when enabled', () => { + const { result } = renderHook(() => usePersistenceConfig()) + + act(() => { + result.current.togglePersistence() + }) + + expect(middleware.disablePersistence).toHaveBeenCalled() + }) + + test('provides togglePersistence function that enables when disabled', () => { + (middleware.getPersistenceConfig as jest.Mock).mockReturnValue({ + enabled: false, + logging: false, + debounceDelay: 500, + }) + + const { result } = renderHook(() => usePersistenceConfig()) + + act(() => { + result.current.togglePersistence() + }) + + expect(middleware.enablePersistence).toHaveBeenCalled() + }) + + test('provides toggleLogging function that disables when enabled', () => { + (middleware.getPersistenceConfig as jest.Mock).mockReturnValue({ + enabled: true, + logging: true, + debounceDelay: 500, + }) + + const { result } = renderHook(() => usePersistenceConfig()) + + act(() => { + result.current.toggleLogging() + }) + + expect(middleware.disableLogging).toHaveBeenCalled() + }) + + test('provides toggleLogging function that enables when disabled', () => { + const { result } = renderHook(() => usePersistenceConfig()) + + act(() => { + result.current.toggleLogging() + }) + + expect(middleware.enableLogging).toHaveBeenCalled() + }) + + test('provides updateDebounceDelay function', () => { + const { result } = renderHook(() => usePersistenceConfig()) + + act(() => { + result.current.updateDebounceDelay(1000) + }) + + expect(middleware.setDebounceDelay).toHaveBeenCalledWith(1000) + }) + + test('provides refreshConfig function', () => { + const { result } = renderHook(() => usePersistenceConfig()) + + act(() => { + result.current.refreshConfig() + }) + + expect(middleware.getPersistenceConfig).toHaveBeenCalled() + }) + + test('refreshConfig updates state', () => { + const { result, rerender } = renderHook(() => usePersistenceConfig()) + + ;(middleware.getPersistenceConfig as jest.Mock).mockReturnValue({ + enabled: false, + logging: true, + debounceDelay: 1000, + }) + + act(() => { + result.current.refreshConfig() + }) + + rerender() + + expect(result.current.config).toEqual({ + enabled: false, + logging: true, + debounceDelay: 1000, + }) + }) + + test('updateConfig calls refreshConfig', () => { + const { result } = renderHook(() => usePersistenceConfig()) + + act(() => { + result.current.updateConfig({ debounceDelay: 2000 }) + }) + + expect(middleware.updatePersistenceConfig).toHaveBeenCalled() + }) + + test('togglePersistence calls refreshConfig', () => { + const { result } = renderHook(() => usePersistenceConfig()) + const initialCallCount = (middleware.getPersistenceConfig as jest.Mock).mock.calls.length + + act(() => { + result.current.togglePersistence() + }) + + expect((middleware.getPersistenceConfig as jest.Mock).mock.calls.length).toBeGreaterThan( + initialCallCount + ) + }) + + test('toggleLogging calls refreshConfig', () => { + const { result } = renderHook(() => usePersistenceConfig()) + const initialCallCount = (middleware.getPersistenceConfig as jest.Mock).mock.calls.length + + act(() => { + result.current.toggleLogging() + }) + + expect((middleware.getPersistenceConfig as jest.Mock).mock.calls.length).toBeGreaterThan( + initialCallCount + ) + }) + + test('updateDebounceDelay calls refreshConfig', () => { + const { result } = renderHook(() => usePersistenceConfig()) + const initialCallCount = (middleware.getPersistenceConfig as jest.Mock).mock.calls.length + + act(() => { + result.current.updateDebounceDelay(750) + }) + + expect((middleware.getPersistenceConfig as jest.Mock).mock.calls.length).toBeGreaterThan( + initialCallCount + ) + }) + + test('handles multiple updates', () => { + const { result } = renderHook(() => usePersistenceConfig()) + + act(() => { + result.current.togglePersistence() + result.current.toggleLogging() + result.current.updateDebounceDelay(2000) + }) + + expect(middleware.disablePersistence).toHaveBeenCalled() + expect(middleware.enableLogging).toHaveBeenCalled() + expect(middleware.setDebounceDelay).toHaveBeenCalledWith(2000) + }) +})