import { describe, it, expect, beforeAll } from 'vitest'; import { indexedDB, IDBKeyRange } from 'fake-indexeddb'; // Implementation placeholder import; will fail until implemented per tasks T016, T017 // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error - module not implemented yet import { openDb } from '../../src/db'; // Attach fake IndexedDB globals so the implementation (when added) can use global indexedDB // and our test can also open the DB by name to inspect stores/indexes // @ts-ignore if (!(globalThis as any).indexedDB) { // @ts-ignore (globalThis as any).indexedDB = indexedDB; // @ts-ignore (globalThis as any).IDBKeyRange = IDBKeyRange; } const expected = { name: 'glowtrack', version: 1, stores: { settings: { keyPath: undefined, key: 'singleton', indexes: [] }, habits: { keyPath: 'id', indexes: ['by_type'] }, days: { keyPath: 'date', indexes: [] }, entries: { keyPath: 'id', indexes: ['by_date', 'by_habit'] } } } as const; async function getDbMeta(dbName: string) { // Open the DB directly to inspect metadata when implementation exists return await new Promise((resolve, reject) => { const req = indexedDB.open(dbName); req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); }); } describe('Contract: IndexedDB storage schema (T010)', () => { beforeAll(async () => { // Ensure call occurs to create DB/migrations once impl exists try { await openDb(); } catch { // Expected to fail or throw until implemented } }); it('should define object stores and indexes per storage.schema.md', async () => { // Open by expected name; impl should use same name const name = expected.name; let db: IDBDatabase | null = null; try { db = await getDbMeta(name); } catch (e) { // If DB doesn't exist yet, that's fine; we still run expectations to intentionally fail } // If implementation not present, construct a minimal snapshot that will fail below const snapshot = db ? { name: db.name, version: db.version, stores: Object.fromEntries( (Array.from(((db as any).objectStoreNames as unknown as string[]))).map((storeName: string) => { const tx = db!.transaction(storeName, 'readonly'); const store = tx.objectStore(storeName); const indexes = Array.from(store.indexNames); return [ storeName, { keyPath: store.keyPath as string | string[] | null, indexes } ]; }) ) } : { name: null, version: null, stores: {} }; // Assertions — structured to produce helpful diffs expect(snapshot.name).toBe(expected.name); expect(snapshot.version).toBe(expected.version); // Required stores const storeNames = ['settings', 'habits', 'days', 'entries'] as const; for (const s of storeNames) { expect(Object.prototype.hasOwnProperty.call(snapshot.stores, s)).toBe(true); } // Keys and indexes if (db) { // settings store: no keyPath, manual key 'singleton' { const tx = db.transaction('settings', 'readonly'); const store = tx.objectStore('settings'); // In v1 we accept keyPath null/undefined; key is provided at put time expect(store.keyPath === null || store.keyPath === undefined).toBe(true); expect(Array.from(store.indexNames)).toEqual([]); } // habits { const tx = db.transaction('habits', 'readonly'); const store = tx.objectStore('habits'); expect(store.keyPath).toBe('id'); expect(Array.from(store.indexNames)).toContain('by_type'); } // days { const tx = db.transaction('days', 'readonly'); const store = tx.objectStore('days'); expect(store.keyPath).toBe('date'); expect(Array.from(store.indexNames)).toEqual([]); } // entries { const tx = db.transaction('entries', 'readonly'); const store = tx.objectStore('entries'); expect(store.keyPath).toBe('id'); const idx = Array.from(store.indexNames); expect(idx).toContain('by_date'); expect(idx).toContain('by_habit'); } } else { // Force failure with descriptive message until DB is created by implementation expect({ exists: false, reason: 'DB not created yet' }).toEqual({ exists: true, reason: '' }); } }); });