T010
This commit is contained in:
@@ -14,6 +14,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.5.4",
|
||||
"vitest": "^2.1.1"
|
||||
"vitest": "^2.1.1",
|
||||
"fake-indexeddb": "^6.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
130
packages/storage/tests/contract/schema.spec.ts
Normal file
130
packages/storage/tests/contract/schema.spec.ts
Normal file
@@ -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<IDBDatabase>((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: '' });
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user