/** @vitest-environment jsdom */ import 'fake-indexeddb/auto'; import { describe, it, expect, beforeEach, vi } from 'vitest'; import { IDBFactory } from 'fake-indexeddb'; import type { TryOnHistoryItem } from '../types'; type Store = typeof import('./tryOnStore'); // Every test gets a fresh database AND a fresh module, because the module // memoizes its open connection. Re-importing is also how the reload test // gets an honest answer: a new module instance against the same storage is // exactly what a page reload produces. async function freshModule(): Promise { vi.resetModules(); return import('./tryOnStore'); } function tryOn(overrides: Partial = {}): Omit { return { resultImage: 'data:image/png;base64,RESULT', clothingImage: 'data:image/png;base64,GARMENT', productTitle: 'Linen Shirt', productId: '222', productUrl: 'https://shop.example/product/linen-shirt', createdAt: '2026-07-30T10:00:00.000Z', ...overrides, }; } beforeEach(() => { // fake-indexeddb keeps its data on the factory instance, so swapping the // factory is a full wipe between tests. globalThis.indexedDB = new IDBFactory(); localStorage.clear(); }); describe('saving and listing try-ons', () => { it('returns an empty gallery before anything is saved', async () => { const store = await freshModule(); expect(await store.listTryOns()).toEqual([]); }); it('gives every saved try-on an id and keeps all of its fields', async () => { const store = await freshModule(); const saved = await store.saveTryOn(tryOn()); expect(saved?.id).toBeTruthy(); const [item] = await store.listTryOns(); expect(item).toMatchObject({ resultImage: 'data:image/png;base64,RESULT', clothingImage: 'data:image/png;base64,GARMENT', productTitle: 'Linen Shirt', productId: '222', productUrl: 'https://shop.example/product/linen-shirt', }); }); it('lists the newest try-on first', async () => { const store = await freshModule(); await store.saveTryOn(tryOn({ productTitle: 'Older', createdAt: '2026-07-01T10:00:00.000Z' })); await store.saveTryOn(tryOn({ productTitle: 'Newer', createdAt: '2026-07-29T10:00:00.000Z' })); const titles = (await store.listTryOns()).map((i) => i.productTitle); expect(titles).toEqual(['Newer', 'Older']); }); // The whole point of IndexedDB over React state: the gallery is still there // on the next visit. it('survives a reload', async () => { const first = await freshModule(); await first.saveTryOn(tryOn({ productTitle: 'Wool Coat' })); const afterReload = await freshModule(); const items = await afterReload.listTryOns(); expect(items).toHaveLength(1); expect(items[0].productTitle).toBe('Wool Coat'); expect(items[0].resultImage).toBe('data:image/png;base64,RESULT'); }); it('counts what it holds', async () => { const store = await freshModule(); await store.saveTryOn(tryOn()); await store.saveTryOn(tryOn({ createdAt: '2026-07-30T11:00:00.000Z' })); expect(await store.countTryOns()).toBe(2); }); }); describe('pruning', () => { it('drops the oldest try-ons once the cap is passed', async () => { const store = await freshModule(); for (let i = 0; i < store.MAX_TRY_ONS + 3; i++) { await store.saveTryOn( tryOn({ productTitle: `Item ${i}`, // Zero-padded so the createdAt index orders them the way they were // saved rather than lexicographically by a bare number. createdAt: `2026-07-${String(i + 1).padStart(2, '0')}T10:00:00.000Z`, }) ); } const items = await store.listTryOns(); expect(items).toHaveLength(store.MAX_TRY_ONS); expect(items[0].productTitle).toBe(`Item ${store.MAX_TRY_ONS + 2}`); expect(items.map((i) => i.productTitle)).not.toContain('Item 0'); }); }); describe('removing try-ons', () => { it('deletes a single try-on by id', async () => { const store = await freshModule(); const keep = await store.saveTryOn(tryOn({ productTitle: 'Keep' })); const drop = await store.saveTryOn( tryOn({ productTitle: 'Drop', createdAt: '2026-07-30T11:00:00.000Z' }) ); await store.deleteTryOn(drop!.id); const items = await store.listTryOns(); expect(items).toHaveLength(1); expect(items[0].id).toBe(keep!.id); }); it('clears the whole gallery', async () => { const store = await freshModule(); await store.saveTryOn(tryOn()); await store.clearTryOns(); expect(await store.listTryOns()).toEqual([]); }); }); describe('when IndexedDB is unavailable', () => { // Safari private mode and locked-down browsers. A storefront widget must // degrade to "no gallery", never to a crash. it('reports no gallery and answers every call quietly', async () => { const realIndexedDB = globalThis.indexedDB; // @ts-expect-error deliberately removing the API for this test delete globalThis.indexedDB; try { const store = await freshModule(); expect(store.isGalleryAvailable()).toBe(false); expect(await store.saveTryOn(tryOn())).toBeNull(); expect(await store.listTryOns()).toEqual([]); expect(await store.countTryOns()).toBe(0); await expect(store.deleteTryOn('nope')).resolves.toBeUndefined(); await expect(store.clearTryOns()).resolves.toBeUndefined(); } finally { globalThis.indexedDB = realIndexedDB; } }); });