animetix-web / frontend /src /features /manga-reader /offline /offlineLibrary.test.ts
MissawB's picture
Upload folder using huggingface_hub (part 4)
df2e14d verified
Raw
History Blame Contribute Delete
3.89 kB
import 'fake-indexeddb/auto';
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import {
downloadChapter,
isChapterDownloaded,
getChapterPageBlobs,
getDownloadedChapter,
deleteChapter,
listDownloads,
OfflineQuotaError,
} from './offlineLibrary';
const META = { mediaId: 'm1', mediaTitle: 'Test Manga', chapterNumber: 3, chapterTitle: 'Ch 3' };
const PAGES = [
{ number: 0, image_url: 'https://host/p0.jpg' },
{ number: 1, image_url: 'https://host/p1.jpg' },
];
function mockFetchOk() {
vi.stubGlobal(
'fetch',
vi.fn(async (url: string) => ({
ok: true,
blob: async () => new Blob([url], { type: 'image/jpeg' }),
})),
);
}
describe('offlineLibrary', () => {
beforeEach(async () => {
// Clear DB between tests by deleting any known chapter
await deleteChapter('m1', 3).catch(() => {});
});
afterEach(() => vi.unstubAllGlobals());
it('downloads, stores blobs + meta, and reports progress', async () => {
mockFetchOk();
const progress: number[] = [];
const result = await downloadChapter(META, PAGES, (r) => progress.push(r));
expect(result.pageCount).toBe(2);
expect(result.pageKeys).toHaveLength(2);
expect(await isChapterDownloaded('m1', 3)).toBe(true);
expect(progress[progress.length - 1]).toBe(1);
const blobs = await getChapterPageBlobs('m1', 3);
expect(blobs).toHaveLength(2);
expect(await blobs[0].text()).toBe('https://host/p0.jpg'); // ordered by page number
});
it('is idempotent: re-downloading overwrites cleanly', async () => {
mockFetchOk();
await downloadChapter(META, PAGES);
await downloadChapter(META, PAGES);
expect((await getChapterPageBlobs('m1', 3))).toHaveLength(2);
expect(await listDownloads()).toHaveLength(1);
});
it('deleteChapter removes blobs, meta, and index entry', async () => {
mockFetchOk();
await downloadChapter(META, PAGES);
await deleteChapter('m1', 3);
expect(await isChapterDownloaded('m1', 3)).toBe(false);
expect(await getDownloadedChapter('m1', 3)).toBeNull();
expect(await getChapterPageBlobs('m1', 3)).toHaveLength(0);
expect(await listDownloads()).toHaveLength(0);
});
it('rolls back and rejects when a page fetch fails (nothing left downloaded)', async () => {
vi.stubGlobal(
'fetch',
vi.fn(async (url: string) => {
if (url.endsWith('p1.jpg')) return { ok: false, status: 500, blob: async () => new Blob() };
return { ok: true, blob: async () => new Blob([url]) };
}),
);
await expect(downloadChapter(META, PAGES)).rejects.toThrow();
expect(await isChapterDownloaded('m1', 3)).toBe(false);
expect(await getChapterPageBlobs('m1', 3)).toHaveLength(0);
});
it('wraps QuotaExceededError as OfflineQuotaError', async () => {
mockFetchOk();
// Force the second blob store to throw a quota error by spying via a sabotaged fetch blob
vi.stubGlobal(
'fetch',
vi.fn(async () => ({
ok: true,
blob: async () => {
const e = new Error('quota');
e.name = 'QuotaExceededError';
throw e;
},
})),
);
await expect(downloadChapter(META, PAGES)).rejects.toBeInstanceOf(OfflineQuotaError);
expect(await isChapterDownloaded('m1', 3)).toBe(false);
});
it('listDownloads parses mediaIds that contain a colon', async () => {
mockFetchOk();
const colonMeta = { mediaId: 'source:42', mediaTitle: 'Colon Manga', chapterNumber: 7, chapterTitle: 'Ch 7' };
await downloadChapter(colonMeta, PAGES);
try {
const downloads = await listDownloads();
const found = downloads.find((d) => d.mediaId === 'source:42' && d.chapterNumber === 7);
expect(found).toBeDefined();
expect(found?.mediaTitle).toBe('Colon Manga');
} finally {
await deleteChapter('source:42', 7);
}
});
});