import { describe, it, expect, beforeEach, vi } from 'vitest'; import { instanceToPlain } from 'class-transformer'; import { KGProject } from '../KGProject'; // --- Mock idb for reading old IndexedDB projects --- const mockProjects: Array<{ name: string; data: Record; lastModified: number }> = []; vi.mock('idb', () => { const stores: Record> = {}; const getStore = (name: string) => { if (!stores[name]) stores[name] = new Map(); return stores[name]; }; return { openDB: vi.fn(() => { const db = { getAll: vi.fn((storeName: string) => { if (storeName === 'projects') return Promise.resolve([...mockProjects]); return Promise.resolve([...getStore(storeName).values()]); }), get: vi.fn((storeName: string, key: string) => getStore(storeName).get(key)), put: vi.fn((storeName: string, value: { name: string }) => { getStore(storeName).set(value.name, value); }), close: vi.fn(), objectStoreNames: { contains: () => false }, }; return Promise.resolve(db); }), __stores: stores, __reset: () => Object.keys(stores).forEach((k) => delete stores[k]), }; }); // --- Mock OPFS for KGProjectStorage --- class MockWritable { data = ''; async write(content: string) { this.data = content; } async close() {} } class MockFileHandle { kind = 'file' as const; private _content = ''; constructor(public name: string) {} async getFile() { return { text: () => Promise.resolve(this._content) }; } async createWritable() { const w = new MockWritable(); const origClose = w.close.bind(w); w.close = async () => { this._content = w.data; await origClose(); }; return w; } } class MockDirHandle { kind = 'directory' as const; entries = new Map(); constructor(public name: string) {} async getDirectoryHandle(name: string, opts?: { create?: boolean }) { let e = this.entries.get(name); if (!e || e.kind !== 'directory') { if (opts?.create) { e = new MockDirHandle(name); this.entries.set(name, e); } else { throw new DOMException('Not found', 'NotFoundError'); } } return e as MockDirHandle; } async getFileHandle(name: string, opts?: { create?: boolean }) { let e = this.entries.get(name); if (!e || e.kind !== 'file') { if (opts?.create) { e = new MockFileHandle(name); this.entries.set(name, e); } else { throw new DOMException('Not found', 'NotFoundError'); } } return e as MockFileHandle; } async removeEntry(name: string) { this.entries.delete(name); } async *values() { for (const e of this.entries.values()) yield e; } } const mockRoot = new MockDirHandle('root'); vi.stubGlobal('navigator', { ...navigator, storage: { getDirectory: vi.fn(() => Promise.resolve(mockRoot)), persist: vi.fn(() => Promise.resolve(true)), }, }); // Now import the module under test import { upgradeConfigToV1 } from './upgradeConfigToV1'; import { KGProjectStorage } from '../io/KGProjectStorage'; describe('upgradeConfigToV1', () => { beforeEach(async () => { mockProjects.length = 0; mockRoot.entries.clear(); // Reset KGProjectStorage singleton ;(KGProjectStorage as unknown as { _instance: undefined })._instance = undefined; const storage = KGProjectStorage.getInstance(); await storage.initialize(); }); it('migrates projects from IndexedDB to OPFS', async () => { // Set up a mock IndexedDB project const project = new KGProject('My Old Song', 32, 0, 130); mockProjects.push({ name: 'My Old Song', data: instanceToPlain(project) as Record, lastModified: 1700000000000, }); await upgradeConfigToV1(); const storage = KGProjectStorage.getInstance(); const names = await storage.list(); expect(names).toContain('My Old Song'); const loaded = await storage.load('My Old Song'); expect(loaded).not.toBeNull(); expect(loaded!.getBpm()).toBe(130); }); it('sanitizes project names with invalid characters', async () => { const project = new KGProject('My:Song/Here', 32, 0, 120); mockProjects.push({ name: 'My:Song/Here', data: instanceToPlain(project) as Record, lastModified: Date.now(), }); await upgradeConfigToV1(); const storage = KGProjectStorage.getInstance(); const names = await storage.list(); // Should be sanitized — no colons or slashes expect(names).toContain('My_Song_Here'); }); it('skips projects that already exist in OPFS', async () => { // Pre-create a project in OPFS const storage = KGProjectStorage.getInstance(); const existing = new KGProject('Existing', 32, 0, 100); await storage.save('Existing', existing); // Add same project to IndexedDB mock mockProjects.push({ name: 'Existing', data: instanceToPlain(existing) as Record, lastModified: Date.now(), }); // Should not throw or overwrite await upgradeConfigToV1(); const loaded = await storage.load('Existing'); expect(loaded!.getBpm()).toBe(100); // Original BPM preserved }); it('handles empty IndexedDB gracefully', async () => { // No projects in IndexedDB await expect(upgradeConfigToV1()).resolves.not.toThrow(); }); });