diff --git a/packages/storage/package.json b/packages/storage/package.json index f85d90b..758d26d 100644 --- a/packages/storage/package.json +++ b/packages/storage/package.json @@ -14,6 +14,7 @@ }, "devDependencies": { "typescript": "^5.5.4", - "vitest": "^2.1.1" + "vitest": "^2.1.1", + "fake-indexeddb": "^6.0.0" } } diff --git a/packages/storage/tests/contract/schema.spec.ts b/packages/storage/tests/contract/schema.spec.ts new file mode 100644 index 0000000..efb7e05 --- /dev/null +++ b/packages/storage/tests/contract/schema.spec.ts @@ -0,0 +1,130 @@ +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: '' }); + } + }); +}); diff --git a/specs/001-glowtrack-a-mood/tasks.md b/specs/001-glowtrack-a-mood/tasks.md index 7ef3ce9..4979aed 100644 --- a/specs/001-glowtrack-a-mood/tasks.md +++ b/specs/001-glowtrack-a-mood/tasks.md @@ -74,7 +74,7 @@ Contract files from /home/jawz/Development/Projects/GlowTrack/specs/001-glowtrac - Expect failure until export service implemented - Dependencies: T007 -- [ ] T010 [P] Contract test: IndexedDB storage schema +- [X] T010 [P] Contract test: IndexedDB storage schema - Create /home/jawz/Development/Projects/GlowTrack/packages/storage/tests/contract/schema.spec.ts - Open DB via openDb() and assert stores/indexes per /home/jawz/Development/Projects/GlowTrack/specs/001-glowtrack-a-mood/contracts/storage.schema.md - Expect failure until DB module/migrations implemented