Add targeted Vitest coverage
This commit is contained in:
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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'] });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { describe, expect, test, vi, beforeEach } from 'vitest';
|
||||||
|
|
||||||
|
describe('main entry point', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.resetModules();
|
||||||
|
vi.clearAllMocks();
|
||||||
|
document.body.innerHTML = '<div id="root"></div>';
|
||||||
|
});
|
||||||
|
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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));
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { vi } from 'vitest';
|
||||||
|
|
||||||
|
export type TableMock<T = any> = {
|
||||||
|
toArray: ReturnType<typeof vi.fn>;
|
||||||
|
add: ReturnType<typeof vi.fn>;
|
||||||
|
update: ReturnType<typeof vi.fn>;
|
||||||
|
where: ReturnType<typeof vi.fn>;
|
||||||
|
filter: ReturnType<typeof vi.fn>;
|
||||||
|
count: ReturnType<typeof vi.fn>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createWhere = () => ({
|
||||||
|
equals: vi.fn().mockReturnValue({ first: vi.fn().mockResolvedValue(undefined) }),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const makeTableMock = <T = any>(items: T[] = []): TableMock<T> => ({
|
||||||
|
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<Record<string, any>> = {}) => {
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user