mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 10:12:34 +00:00
Chart theming: custom named themes + Theme Builder
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* IndexedDB wrapper — store-layout verification and self-healing (the
|
||||
* interrupted-upgrade recovery documented on `openDB`). Runs against
|
||||
* fake-indexeddb; each test gets a pristine database.
|
||||
*/
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { IDBFactory } from 'fake-indexeddb';
|
||||
import {
|
||||
DATASETS_STORE,
|
||||
SNIPPETS_STORE,
|
||||
THEMES_STORE,
|
||||
_resetDbForTests,
|
||||
getAll,
|
||||
openDB,
|
||||
put,
|
||||
} from './db';
|
||||
|
||||
const ALL_STORES = [SNIPPETS_STORE, DATASETS_STORE, THEMES_STORE];
|
||||
|
||||
/** Open the raw database at `version` with a custom (or absent) upgrade body. */
|
||||
function rawOpen(version: number, upgrade?: (db: IDBDatabase) => void): Promise<IDBDatabase> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = indexedDB.open('astrolabe', version);
|
||||
req.onupgradeneeded = () => upgrade?.(req.result);
|
||||
req.onsuccess = () => resolve(req.result);
|
||||
req.onerror = () => reject(req.error ?? new Error('open failed'));
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
// A fresh factory per test — no databases survive between tests.
|
||||
globalThis.indexedDB = new IDBFactory();
|
||||
_resetDbForTests();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
_resetDbForTests();
|
||||
});
|
||||
|
||||
describe('openDB', () => {
|
||||
it('creates every expected store on first run', async () => {
|
||||
const db = await openDB();
|
||||
for (const store of ALL_STORES) expect(db.objectStoreNames.contains(store)).toBe(true);
|
||||
});
|
||||
|
||||
it('upgrades a v1 database (snippets + datasets) to include the themes store', async () => {
|
||||
const v1 = await rawOpen(1, (db) => {
|
||||
db.createObjectStore(SNIPPETS_STORE, { keyPath: 'id' });
|
||||
db.createObjectStore(DATASETS_STORE, { keyPath: 'id' });
|
||||
});
|
||||
v1.close();
|
||||
|
||||
const db = await openDB();
|
||||
expect(db.objectStoreNames.contains(THEMES_STORE)).toBe(true);
|
||||
});
|
||||
|
||||
it('self-heals a database stamped at the current version with stores missing', async () => {
|
||||
// The interrupted-upgrade state: version already 2, but the themes store
|
||||
// was never created (e.g. a hot reload opened v2 before the create-store
|
||||
// code existed). onupgradeneeded will never fire again for v2.
|
||||
const broken = await rawOpen(2, (db) => {
|
||||
db.createObjectStore(SNIPPETS_STORE, { keyPath: 'id' });
|
||||
db.createObjectStore(DATASETS_STORE, { keyPath: 'id' });
|
||||
});
|
||||
broken.close();
|
||||
|
||||
const db = await openDB();
|
||||
expect(db.objectStoreNames.contains(THEMES_STORE)).toBe(true);
|
||||
expect(db.version).toBe(3); // healed by a forced extra upgrade pass
|
||||
// Transactions on every store now work.
|
||||
await put(THEMES_STORE, { id: 1, name: 'ok' });
|
||||
await expect(getAll(THEMES_STORE)).resolves.toEqual([{ id: 1, name: 'ok' }]);
|
||||
});
|
||||
|
||||
it('opens a database whose version is already past DB_VERSION', async () => {
|
||||
// A prior self-heal bump leaves the version above the code's constant; a
|
||||
// versioned open would throw VersionError. All stores already exist here.
|
||||
const ahead = await rawOpen(7, (db) => {
|
||||
for (const store of ALL_STORES) db.createObjectStore(store, { keyPath: 'id' });
|
||||
});
|
||||
ahead.close();
|
||||
|
||||
const db = await openDB();
|
||||
expect(db.version).toBe(7);
|
||||
for (const store of ALL_STORES) expect(db.objectStoreNames.contains(store)).toBe(true);
|
||||
});
|
||||
|
||||
it('self-heals an ahead-of-code database with stores missing', async () => {
|
||||
const ahead = await rawOpen(5, (db) => {
|
||||
db.createObjectStore(SNIPPETS_STORE, { keyPath: 'id' });
|
||||
});
|
||||
ahead.close();
|
||||
|
||||
const db = await openDB();
|
||||
expect(db.version).toBe(6);
|
||||
for (const store of ALL_STORES) expect(db.objectStoreNames.contains(store)).toBe(true);
|
||||
});
|
||||
|
||||
it('preserves existing records across the self-heal upgrade', async () => {
|
||||
const broken = await rawOpen(2, (db) => {
|
||||
db.createObjectStore(SNIPPETS_STORE, { keyPath: 'id' });
|
||||
db.createObjectStore(DATASETS_STORE, { keyPath: 'id' });
|
||||
});
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const t = broken.transaction(SNIPPETS_STORE, 'readwrite');
|
||||
t.objectStore(SNIPPETS_STORE).put({ id: 'a', name: 'kept' });
|
||||
t.oncomplete = () => resolve();
|
||||
t.onerror = () => reject(t.error ?? new Error('tx failed'));
|
||||
});
|
||||
broken.close();
|
||||
|
||||
await openDB();
|
||||
await expect(getAll(SNIPPETS_STORE)).resolves.toEqual([{ id: 'a', name: 'kept' }]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user