From 5bdf20a82992df18c7e7be169fd19e6e61f45a48 Mon Sep 17 00:00:00 2001 From: beatz174-bit Date: Tue, 2 Dec 2025 18:04:43 +1000 Subject: [PATCH] Add targeted Vitest coverage --- tests/db.index.spec.ts | 66 ++++++++++++++++++++++++++++++++ tests/db.migrations.spec.ts | 61 +++++++++++++++++++++++++++++ tests/main.spec.tsx | 25 ++++++++++++ tests/useBarcodeScanner.spec.tsx | 57 +++++++++++++++++++++++++++ tests/useServiceWorker.spec.tsx | 41 ++++++++++++++++++++ tests/utils/mockDb.ts | 53 +++++++++++++++++++++++++ 6 files changed, 303 insertions(+) create mode 100644 tests/db.index.spec.ts create mode 100644 tests/db.migrations.spec.ts create mode 100644 tests/main.spec.tsx create mode 100644 tests/useBarcodeScanner.spec.tsx create mode 100644 tests/useServiceWorker.spec.tsx create mode 100644 tests/utils/mockDb.ts diff --git a/tests/db.index.spec.ts b/tests/db.index.spec.ts new file mode 100644 index 0000000..094e4d5 --- /dev/null +++ b/tests/db.index.spec.ts @@ -0,0 +1,66 @@ +import { describe, expect, test, vi, beforeEach } from 'vitest'; + +const makeDexieMock = () => { + const version = vi.fn().mockReturnThis(); + const stores = vi.fn().mockReturnThis(); + const upgrade = vi.fn().mockReturnThis(); + const table = vi.fn().mockReturnValue({ + count: vi.fn().mockResolvedValue(0), + toArray: vi.fn().mockResolvedValue([]), + bulkAdd: vi.fn(), + }); + + class MockDexie { + version = version; + stores = stores; + upgrade = upgrade; + table = table; + constructor() {} + } + + return { MockDexie, version }; +}; + +describe('db/index initializeDatabase', () => { + beforeEach(() => { + vi.resetModules(); + vi.clearAllMocks(); + }); + + test('initializeDatabase calls applyMigrations with db', async () => { + const { MockDexie } = makeDexieMock(); + const applyMigrations = vi.fn().mockResolvedValue(undefined); + + vi.mock('dexie', () => ({ default: MockDexie, Dexie: MockDexie, Table: class {} })); + vi.mock('../src/db/migrations', () => ({ applyMigrations })); + + const { initializeDatabase, db } = await import('../src/db'); + await initializeDatabase(); + + expect(applyMigrations).toHaveBeenCalledWith(db); + }); + + test('initializeDatabase rejects when applyMigrations fails', async () => { + const { MockDexie } = makeDexieMock(); + const applyMigrations = vi.fn().mockRejectedValue(new Error('fail')); + + vi.mock('dexie', () => ({ default: MockDexie, Dexie: MockDexie, Table: class {} })); + vi.mock('../src/db/migrations', () => ({ applyMigrations })); + + const { initializeDatabase } = await import('../src/db'); + + await expect(initializeDatabase()).rejects.toThrow('fail'); + }); + + test('StockFillDB sets up versions on construction', async () => { + const { MockDexie, version } = makeDexieMock(); + vi.mock('dexie', () => ({ default: MockDexie, Dexie: MockDexie, Table: class {} })); + vi.mock('../src/db/migrations', () => ({ applyMigrations: vi.fn() })); + + const { db } = await import('../src/db'); + + expect(version).toHaveBeenCalled(); + expect((version as any).mock.calls.length).toBeGreaterThanOrEqual(1); + expect(db).toBeDefined(); + }); +}); diff --git a/tests/db.migrations.spec.ts b/tests/db.migrations.spec.ts new file mode 100644 index 0000000..d7d935f --- /dev/null +++ b/tests/db.migrations.spec.ts @@ -0,0 +1,61 @@ +import { describe, expect, test, vi, beforeEach } from 'vitest'; +import { makeMockDb } from './utils/mockDb'; + +vi.mock('uuid', () => ({ v4: vi.fn(() => 'mock-uuid') })); + +describe('applyMigrations', () => { + beforeEach(() => { + vi.resetModules(); + vi.clearAllMocks(); + }); + + test('creates category for product name and updates product to use new id', async () => { + const db = makeMockDb(); + (db.categories.toArray as any) + .mockResolvedValueOnce([]) + .mockResolvedValueOnce([{ id: 'mock-uuid', name: 'Beverages', created_at: 1, updated_at: 1 }]); + (db.products.toArray as any).mockResolvedValueOnce([ + { id: 'p1', category: 'Beverages', name: 'Cola' }, + ]); + + const { applyMigrations } = await import('../src/db/migrations'); + await applyMigrations(db as any); + + expect(db.categories.add).toHaveBeenCalledWith( + expect.objectContaining({ id: 'mock-uuid', name: 'Beverages' }), + ); + expect(db.products.update).toHaveBeenCalledWith('p1', { category: 'mock-uuid' }); + }); + + test('skips update when product category already matches existing id', async () => { + const db = makeMockDb(); + (db.categories.toArray as any) + .mockResolvedValueOnce([{ id: 'cat-1', name: 'Snacks' }]) + .mockResolvedValueOnce([{ id: 'cat-1', name: 'Snacks' }]); + (db.products.toArray as any).mockResolvedValueOnce([ + { id: 'p2', category: 'cat-1', name: 'Chips' }, + ]); + + const { applyMigrations } = await import('../src/db/migrations'); + await applyMigrations(db as any); + + expect(db.categories.add).not.toHaveBeenCalled(); + expect(db.products.update).not.toHaveBeenCalled(); + }); + + test('normalizes pickList categories names to ids', async () => { + const db = makeMockDb(); + (db.categories.toArray as any) + .mockResolvedValueOnce([{ id: 'cat-fruit', name: 'Fruit' }]) + .mockResolvedValueOnce([{ id: 'cat-fruit', name: 'Fruit' }]); + (db.products.toArray as any).mockResolvedValueOnce([]); + (db.pickLists.toArray as any).mockResolvedValueOnce([ + { id: 'pl-1', categories: ['Fruit'] }, + ]); + + const { applyMigrations } = await import('../src/db/migrations'); + await applyMigrations(db as any); + + expect(db.pickLists.update).toHaveBeenCalledWith('pl-1', { categories: ['cat-fruit'] }); + }); +}); diff --git a/tests/main.spec.tsx b/tests/main.spec.tsx new file mode 100644 index 0000000..568bcf5 --- /dev/null +++ b/tests/main.spec.tsx @@ -0,0 +1,25 @@ +import { describe, expect, test, vi, beforeEach } from 'vitest'; + +describe('main entry point', () => { + beforeEach(() => { + vi.resetModules(); + vi.clearAllMocks(); + document.body.innerHTML = '
'; + }); + + test('renders app when root element exists', async () => { + const render = vi.fn(); + vi.mock('react-dom/client', () => ({ createRoot: () => ({ render }) })); + + await import('../src/main'); + + expect(render).toHaveBeenCalled(); + }); + + test('throws when root element is missing', async () => { + document.body.innerHTML = ''; + vi.mock('react-dom/client', () => ({ createRoot: () => ({ render: vi.fn() }) })); + + await expect(import('../src/main')).rejects.toThrow('Root element not found'); + }); +}); diff --git a/tests/useBarcodeScanner.spec.tsx b/tests/useBarcodeScanner.spec.tsx new file mode 100644 index 0000000..f55b3d7 --- /dev/null +++ b/tests/useBarcodeScanner.spec.tsx @@ -0,0 +1,57 @@ +import { renderHook, waitFor } from '@testing-library/react'; +import { describe, expect, test, vi, beforeEach, afterEach } from 'vitest'; + +vi.mock('@zxing/browser', () => { + class MockReader { + decodeFromVideoDevice = vi.fn(async (_device: any, _video: any, callback: any) => { + callback({ getText: () => 'decoded-fallback' } as any); + return { stop: vi.fn() } as any; + }); + } + return { BrowserMultiFormatReader: MockReader }; +}); + +describe('useBarcodeScanner', () => { + beforeEach(() => { + vi.resetModules(); + vi.clearAllMocks(); + (globalThis as any).BarcodeDetector = undefined; + }); + + afterEach(() => { + delete (globalThis as any).BarcodeDetector; + }); + + test('falls back to ZXing reader when BarcodeDetector is unavailable', async () => { + const { useBarcodeScanner } = await import('../src/hooks/useBarcodeScanner'); + const { result } = renderHook(() => useBarcodeScanner()); + result.current.videoRef.current = document.createElement('video'); + + await waitFor(() => expect(result.current.result.code).toBe('decoded-fallback')); + }); + + test('uses BarcodeDetector when available', async () => { + const detect = vi.fn().mockResolvedValue([{ rawValue: 'detected-code' }]); + class MockBarcodeDetector { + detect = detect; + } + (globalThis as any).BarcodeDetector = MockBarcodeDetector as any; + + const play = vi.fn().mockResolvedValue(undefined); + const getVideoTracks = vi.fn().mockReturnValue([{ stop: vi.fn() }]); + const stream = { getVideoTracks } as any; + navigator.mediaDevices = { + getUserMedia: vi.fn().mockResolvedValue(stream), + } as any; + (globalThis as any).createImageBitmap = vi.fn().mockResolvedValue({}); + + const { useBarcodeScanner } = await import('../src/hooks/useBarcodeScanner'); + const { result } = renderHook(() => useBarcodeScanner()); + const video = document.createElement('video'); + Object.defineProperty(video, 'play', { value: play }); + result.current.videoRef.current = video; + + await waitFor(() => expect(result.current.result.code).toBe('detected-code')); + expect(detect).toHaveBeenCalled(); + }); +}); diff --git a/tests/useServiceWorker.spec.tsx b/tests/useServiceWorker.spec.tsx new file mode 100644 index 0000000..92dbb4d --- /dev/null +++ b/tests/useServiceWorker.spec.tsx @@ -0,0 +1,41 @@ +import { renderHook, waitFor } from '@testing-library/react'; +import { describe, expect, test, vi, beforeEach } from 'vitest'; + +const setupHook = async () => { + const module = await import('../src/hooks/useServiceWorker'); + return module.useServiceWorker; +}; + +describe('useServiceWorker', () => { + beforeEach(() => { + vi.resetModules(); + vi.clearAllMocks(); + }); + + test('registers service worker successfully', async () => { + const register = vi.fn().mockResolvedValue({}); + Object.defineProperty(navigator, 'serviceWorker', { + value: { register }, + configurable: true, + }); + + const useServiceWorker = await setupHook(); + const { result } = renderHook(() => useServiceWorker()); + + await waitFor(() => expect(result.current).toBe(true)); + expect(register).toHaveBeenCalledWith('/service-worker.js'); + }); + + test('handles registration failure', async () => { + const register = vi.fn().mockRejectedValue(new Error('fail')); + Object.defineProperty(navigator, 'serviceWorker', { + value: { register }, + configurable: true, + }); + + const useServiceWorker = await setupHook(); + const { result } = renderHook(() => useServiceWorker()); + + await waitFor(() => expect(result.current).toBe(false)); + }); +}); diff --git a/tests/utils/mockDb.ts b/tests/utils/mockDb.ts new file mode 100644 index 0000000..b83b172 --- /dev/null +++ b/tests/utils/mockDb.ts @@ -0,0 +1,53 @@ +import { vi } from 'vitest'; + +export type TableMock = { + toArray: ReturnType; + add: ReturnType; + update: ReturnType; + where: ReturnType; + filter: ReturnType; + count: ReturnType; +}; + +const createWhere = () => ({ + equals: vi.fn().mockReturnValue({ first: vi.fn().mockResolvedValue(undefined) }), +}); + +export const makeTableMock = (items: T[] = []): TableMock => ({ + toArray: vi.fn().mockResolvedValue([...items]), + add: vi.fn().mockResolvedValue(undefined), + update: vi.fn().mockResolvedValue(undefined), + where: vi.fn().mockImplementation(() => createWhere()), + filter: vi.fn().mockReturnValue({ first: vi.fn().mockResolvedValue(undefined) }), + count: vi.fn().mockResolvedValue(0), +}); + +export const makeMockDb = (overrides: Partial> = {}) => { + const products = makeTableMock(overrides.products ?? []); + const categories = makeTableMock(overrides.categories ?? []); + const pickLists = makeTableMock(overrides.pickLists ?? []); + const pickItems = makeTableMock(overrides.pickItems ?? []); + const importExportLogs = makeTableMock(overrides.importExportLogs ?? []); + + const transaction = vi + .fn() + .mockImplementation(async (_mode: any, ...args: any[]) => { + const cb = args[args.length - 1]; + if (typeof cb === 'function') { + return cb(); + } + return undefined; + }); + + const open = vi.fn().mockResolvedValue(undefined); + + return { + products, + categories, + pickLists, + pickItems, + importExportLogs, + transaction, + open, + }; +};