From 4163124c6a7c89c4d66fdb753595b338f589c6b7 Mon Sep 17 00:00:00 2001 From: Nicolas Varrot Date: Sun, 22 Feb 2026 09:03:17 +0000 Subject: [PATCH] test: add unit tests for useSwipeSidebar and useLocale hooks, exclude tests from build tsconfig --- src/hooks/__tests__/useLocale.test.ts | 57 +++++++++ src/hooks/__tests__/useSwipeSidebar.test.ts | 130 ++++++++++++++++++++ tsconfig.app.json | 3 +- 3 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 src/hooks/__tests__/useLocale.test.ts create mode 100644 src/hooks/__tests__/useSwipeSidebar.test.ts diff --git a/src/hooks/__tests__/useLocale.test.ts b/src/hooks/__tests__/useLocale.test.ts new file mode 100644 index 0000000..2d26788 --- /dev/null +++ b/src/hooks/__tests__/useLocale.test.ts @@ -0,0 +1,57 @@ +/** + * @vitest-environment jsdom + */ +import { describe, it, expect, beforeEach } from 'vitest'; +import { renderHook, act } from '@testing-library/react'; +import { useLocale, useT } from '../useLocale'; +import { setLocale } from '../../lib/i18n'; +import type { TranslationKey } from '../../lib/i18n'; + +describe('useLocale', () => { + beforeEach(() => { + setLocale('en'); + }); + + it('returns current locale', () => { + const { result } = renderHook(() => useLocale()); + expect(result.current).toBe('en'); + }); + + it('updates when locale changes', () => { + const { result } = renderHook(() => useLocale()); + expect(result.current).toBe('en'); + + act(() => { setLocale('fr'); }); + expect(result.current).toBe('fr'); + }); +}); + +describe('useT', () => { + beforeEach(() => { + setLocale('en'); + }); + + it('returns a translation function', () => { + const { result } = renderHook(() => useT()); + expect(typeof result.current).toBe('function'); + }); + + it('translates keys for current locale', () => { + const { result } = renderHook(() => useT()); + // 'send' is a common key that should exist + const translated = result.current('chat.send' as TranslationKey); + expect(typeof translated).toBe('string'); + expect(translated.length).toBeGreaterThan(0); + }); + + it('re-renders with new translations when locale changes', () => { + const { result } = renderHook(() => useT()); + const enText = result.current('chat.send' as TranslationKey); + + act(() => { setLocale('fr'); }); + const frText = result.current('chat.send' as TranslationKey); + + // EN='Send', FR='Envoyer' + expect(enText).not.toBe(frText); + }); +}); diff --git a/src/hooks/__tests__/useSwipeSidebar.test.ts b/src/hooks/__tests__/useSwipeSidebar.test.ts new file mode 100644 index 0000000..d365f74 --- /dev/null +++ b/src/hooks/__tests__/useSwipeSidebar.test.ts @@ -0,0 +1,130 @@ +/** + * @vitest-environment jsdom + */ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { renderHook } from '@testing-library/react'; +import { useSwipeSidebar } from '../useSwipeSidebar'; + +function touch(x: number, y: number) { + return { clientX: x, clientY: y } as Touch; +} + +function fireTouchStart(x: number, y: number) { + const ev = new TouchEvent('touchstart', { + touches: [touch(x, y)], + bubbles: true, + }); + document.dispatchEvent(ev); +} + +function fireTouchMove(x: number, y: number) { + const ev = new TouchEvent('touchmove', { + touches: [touch(x, y)], + bubbles: true, + }); + document.dispatchEvent(ev); +} + +function fireTouchEnd(x: number, y: number) { + const ev = new TouchEvent('touchend', { + changedTouches: [touch(x, y)], + bubbles: true, + }); + document.dispatchEvent(ev); +} + +describe('useSwipeSidebar', () => { + let onOpen: ReturnType; + let onClose: ReturnType; + + beforeEach(() => { + onOpen = vi.fn(); + onClose = vi.fn(); + vi.spyOn(Date, 'now').mockReturnValue(1000); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('opens sidebar on right swipe from left edge when closed', () => { + renderHook(() => useSwipeSidebar(false, onOpen, onClose)); + + fireTouchStart(10, 200); + fireTouchMove(70, 205); + vi.spyOn(Date, 'now').mockReturnValue(1200); + fireTouchEnd(100, 205); + + expect(onOpen).toHaveBeenCalledTimes(1); + expect(onClose).not.toHaveBeenCalled(); + }); + + it('does not open when swipe starts outside edge zone', () => { + renderHook(() => useSwipeSidebar(false, onOpen, onClose)); + + fireTouchStart(50, 200); + fireTouchMove(120, 205); + vi.spyOn(Date, 'now').mockReturnValue(1200); + fireTouchEnd(150, 205); + + expect(onOpen).not.toHaveBeenCalled(); + }); + + it('closes sidebar on left swipe when open', () => { + renderHook(() => useSwipeSidebar(true, onOpen, onClose)); + + fireTouchStart(200, 200); + fireTouchMove(130, 205); + vi.spyOn(Date, 'now').mockReturnValue(1200); + fireTouchEnd(100, 205); + + expect(onClose).toHaveBeenCalledTimes(1); + expect(onOpen).not.toHaveBeenCalled(); + }); + + it('ignores swipe with too much vertical drift', () => { + renderHook(() => useSwipeSidebar(false, onOpen, onClose)); + + fireTouchStart(10, 200); + fireTouchMove(70, 300); // 100px vertical drift > MAX_Y_DRIFT (80) + vi.spyOn(Date, 'now').mockReturnValue(1200); + fireTouchEnd(100, 300); + + expect(onOpen).not.toHaveBeenCalled(); + expect(onClose).not.toHaveBeenCalled(); + }); + + it('ignores swipe that is too slow (>500ms)', () => { + renderHook(() => useSwipeSidebar(false, onOpen, onClose)); + + fireTouchStart(10, 200); + fireTouchMove(70, 205); + vi.spyOn(Date, 'now').mockReturnValue(1600); // 600ms elapsed + fireTouchEnd(100, 205); + + expect(onOpen).not.toHaveBeenCalled(); + }); + + it('ignores swipe that is too short (<50px)', () => { + renderHook(() => useSwipeSidebar(false, onOpen, onClose)); + + fireTouchStart(10, 200); + fireTouchMove(30, 205); + vi.spyOn(Date, 'now').mockReturnValue(1200); + fireTouchEnd(40, 205); + + expect(onOpen).not.toHaveBeenCalled(); + }); + + it('cleans up event listeners on unmount', () => { + const removeSpy = vi.spyOn(document, 'removeEventListener'); + const { unmount } = renderHook(() => useSwipeSidebar(false, onOpen, onClose)); + + unmount(); + + const removedEvents = removeSpy.mock.calls.map(c => c[0]); + expect(removedEvents).toContain('touchstart'); + expect(removedEvents).toContain('touchmove'); + expect(removedEvents).toContain('touchend'); + }); +}); diff --git a/tsconfig.app.json b/tsconfig.app.json index a9b5a59..d428b38 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -24,5 +24,6 @@ "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true }, - "include": ["src"] + "include": ["src"], + "exclude": ["src/**/__tests__/**", "src/**/*.test.ts", "src/**/*.test.tsx"] }