Files
edentext/tests/unit/image-store.test.ts
Steffen Becker 554f0f6674 feat: each tab edits its own document
Every document-scoped key now goes through docKey(), which hangs it off the
tab's document id; the first document keeps the empty id, so existing browser
copies read back unchanged. A fresh tab takes up the most recently used
document no live tab holds, else starts a new one — one signed timestamp per
document (edentext-live@id) says which, written only by the tab holding it.
Snapshots and embedded fonts get a database per document. The picture store
stays shared and content-addressed, so its sweep now drops only keys no stored
document names any more, instead of the open document's own.
2026-09-07 23:50:11 +02:00

63 lines
2.7 KiB
TypeScript

import { describe, it, expect, vi } from 'vitest';
import { stashImages, putImages, IDB_SRC } from '../../src/lib/storage/imageStore';
import { fakeIndexedDb } from '../fakeIdb';
const big = (fill: string) => `data:image/png;base64,${fill.repeat(5000)}`;
const doc = (...content: any[]) => ({ type: 'doc', content });
const img = (src: string) => ({ type: 'image', attrs: { src, width: 100 } });
const para = (...content: any[]) => ({ type: 'paragraph', content });
describe('stashImages', () => {
it('replaces a large picture and keeps its other attrs', () => {
const src = big('A');
const json: any = doc(para(img(src)));
const { json: slim, blobs } = stashImages(json);
const out = (slim as any).content[0].content[0];
expect(out.attrs.src.startsWith(IDB_SRC)).toBe(true);
expect(out.attrs.width).toBe(100);
expect([...blobs.values()]).toEqual([src]);
// The caller's JSON is what the editor is still holding.
expect(json.content[0].content[0].attrs.src).toBe(src);
});
it('leaves an icon and a non-data src inline', () => {
const { json: slim, blobs } = stashImages(doc(para(img('data:image/png;base64,AAAA'), img('https://x/y.png'))));
expect(blobs.size).toBe(0);
expect((slim as any).content[0].content[0].attrs.src).toBe('data:image/png;base64,AAAA');
expect((slim as any).content[0].content[1].attrs.src).toBe('https://x/y.png');
});
it('reaches a picture at any depth and dedupes by content', () => {
const src = big('B');
const cell = (...c: any[]) => ({ type: 'tableCell', content: c });
const { json: slim, blobs } = stashImages(doc(
{ type: 'table', content: [{ type: 'tableRow', content: [cell(para(img(src)))] }] },
{ type: 'textBox', content: [para(img(src))] },
));
expect(blobs.size).toBe(1);
const keys = JSON.stringify(slim).match(new RegExp(`${IDB_SRC}[a-z0-9]+`, 'g'));
expect(keys?.length).toBe(2);
expect(new Set(keys).size).toBe(1);
});
it('gives two different pictures two keys', () => {
const { blobs } = stashImages(doc(para(img(big('C')), img(big('D')))));
expect(blobs.size).toBe(2);
});
});
describe('putImages', () => {
it('sweeps what no document names any more, and nothing else', async () => {
const idb = fakeIndexedDb();
vi.stubGlobal('indexedDB', { open: idb.open });
localStorage.clear();
// A second document, stored by another tab and not open here.
localStorage.setItem('edentext-doc@d2', JSON.stringify({ attrs: { src: `${IDB_SRC}theirs` } }));
idb.data.set('theirs', big('B'));
idb.data.set('orphan', big('C'));
await putImages(new Map([['mine', big('A')]]));
expect([...idb.data.keys()].sort()).toEqual(['mine', 'theirs']);
});
});