Files
quick-access-hono/tests/epub.test.ts
T
qwsdcvghyu89 363d894c79 Initial commit
2026-06-17 07:19:25 +10:00

144 lines
4.4 KiB
TypeScript

import { describe, test, expect, beforeEach, afterEach, spyOn } from 'bun:test';
import { mkdtempSync, rmSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { zipSync } from 'fflate';
import { Cache } from '../src/services/cache.ts';
import { fetchEpub } from '../src/services/epub.ts';
const enc = new TextEncoder();
function buildEpub(chapters: { title: string; content: string }[]): Uint8Array {
const files: Record<string, Uint8Array> = {};
files['META-INF/container.xml'] = enc.encode(
`<?xml version="1.0"?>
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
<rootfiles>
<rootfile full-path="content.opf" media-type="application/oebps-package+xml"/>
</rootfiles>
</container>`,
);
const manifestItems = chapters
.map((_, i) => `<item id="ch${i}" href="ch${i}.xhtml" media-type="application/xhtml+xml"/>`)
.join('\n ');
const spineItems = chapters.map((_, i) => `<itemref idref="ch${i}"/>`).join('\n ');
files['content.opf'] = enc.encode(
`<?xml version="1.0"?>
<package xmlns="http://www.idpf.org/2007/opf" version="3.0">
<manifest>
${manifestItems}
</manifest>
<spine>
${spineItems}
</spine>
</package>`,
);
for (let i = 0; i < chapters.length; i++) {
const ch = chapters[i]!;
files[`ch${i}.xhtml`] = enc.encode(
`<html><body><div>${ch.content}</div></body></html>`,
);
}
return zipSync(files);
}
let tmpDir: string;
let cache: Cache;
beforeEach(() => {
tmpDir = mkdtempSync(join(tmpdir(), 'qa-epub-test-'));
cache = new Cache(tmpDir);
});
afterEach(() => {
rmSync(tmpDir, { recursive: true, force: true });
});
function mockFetch(data: Uint8Array) {
return spyOn(globalThis, 'fetch').mockResolvedValue(
new Response(data, { status: 200 }),
);
}
describe('epub — parsing', () => {
test('extracts the correct number of chapters', async () => {
const epub = buildEpub([
{ title: 'Ch 1', content: 'First chapter content here.' },
{ title: 'Ch 2', content: 'Second chapter content here.' },
{ title: 'Ch 3', content: 'Third chapter content here.' },
]);
const spy = mockFetch(epub);
let rangeStart = -1, rangeEnd = -1;
const view = await fetchEpub('http://fake/book.epub', 1, 0, 2, cache, (s, e) => {
rangeStart = s; rangeEnd = e;
});
expect(view.chapters).toHaveLength(3);
expect(rangeStart).toBe(0);
expect(rangeEnd).toBe(2);
spy.mockRestore();
});
test('serves from cache on second call without re-fetching', async () => {
const epub = buildEpub([
{ title: 'Ch 1', content: 'Chapter one long enough content.' },
]);
const spy = mockFetch(epub);
await fetchEpub('http://fake/book.epub', 2, 0, 0, cache, () => {});
expect(spy).toHaveBeenCalledTimes(1);
// Second call — should hit cache
await fetchEpub('http://fake/book.epub', 2, 0, 0, cache, () => {});
expect(spy).toHaveBeenCalledTimes(1);
spy.mockRestore();
});
test('skips blank chapters', async () => {
const epub = buildEpub([
{ title: 'Cover', content: ' ' }, // blank — should be skipped
{ title: 'Ch 1', content: 'Real content here for this chapter.' },
{ title: 'Ch 2', content: 'More real content for the next chapter.' },
]);
const spy = mockFetch(epub);
let rangeEnd = -1;
await fetchEpub('http://fake/book.epub', 3, 0, 10, cache, (_, e) => { rangeEnd = e; });
expect(rangeEnd).toBe(1); // only 2 non-blank chapters (indices 0 and 1)
spy.mockRestore();
});
test('throws on HTTP error', async () => {
const spy = spyOn(globalThis, 'fetch').mockResolvedValue(
new Response('Not Found', { status: 404 }),
);
await expect(fetchEpub('http://fake/missing.epub', 4, 0, 0, cache, () => {})).rejects.toThrow(
'HTTP 404',
);
spy.mockRestore();
});
test('slices to the requested range', async () => {
const epub = buildEpub([
{ title: 'Ch 0', content: 'Chapter zero has enough content here.' },
{ title: 'Ch 1', content: 'Chapter one has enough content here.' },
{ title: 'Ch 2', content: 'Chapter two has enough content here.' },
{ title: 'Ch 3', content: 'Chapter three has enough content here.' },
]);
const spy = mockFetch(epub);
const view = await fetchEpub('http://fake/book.epub', 5, 1, 2, cache, () => {});
expect(view.min).toBe(1);
expect(view.max).toBe(2);
expect(view.chapters).toHaveLength(2);
spy.mockRestore();
});
});