const test = require('node:test'); const assert = require('node:assert'); const mangaEpub = require('../library/manga-epub'); const zip = require('../zip'); function img(byte, ext) { return { ext: ext || 'jpg', data: Buffer.from([byte, byte, byte]) }; } test('createMangaEpub: 产出的 zip 含标准 EPUB 骨架与章节旁路索引', () => { const { bytes, chapterCount, pageCount } = mangaEpub.createMangaEpub({ title: '测试漫画', mangaId: 'manga-1', originalLanguage: 'ja', chapterId: 'ch-1', label: '第 1 话', volume: '1', chapter: '1', translatedLanguage: 'zh', group: '汉化组', images: [img(1), img(2), img(3)] }); assert.strictEqual(chapterCount, 1); assert.strictEqual(pageCount, 3); const entries = zip.readZip(bytes); assert.ok(entries.has('mimetype')); assert.strictEqual(entries.get('mimetype').toString('utf8'), 'application/epub+zip'); assert.ok(entries.has('META-INF/container.xml')); assert.ok(entries.has('content.opf')); assert.ok(entries.has('nav.xhtml')); assert.ok(entries.has('images/ch-1/0001.jpg')); assert.ok(entries.has('chapters/ch-1/p0001.xhtml')); const meta = JSON.parse(entries.get('peoplelib/chapters.json').toString('utf8')); assert.strictEqual(meta.mangaId, 'manga-1'); assert.strictEqual(meta.chapters.length, 1); assert.strictEqual(meta.chapters[0].chapterId, 'ch-1'); assert.strictEqual(meta.chapters[0].pageCount, 3); }); test('appendMangaChapter: 合并新章节,保留旧章节图片字节不变,按卷/话排序', () => { const first = mangaEpub.createMangaEpub({ title: '测试漫画', mangaId: 'manga-1', originalLanguage: 'ja', chapterId: 'ch-2', label: '第 2 话', volume: '1', chapter: '2', images: [img(10)] }); const { bytes } = mangaEpub.appendMangaChapter(first.bytes, { title: '测试漫画', mangaId: 'manga-1', originalLanguage: 'ja', chapterId: 'ch-1', label: '第 1 话', volume: '1', chapter: '1', images: [img(20), img(21)] }); const entries = zip.readZip(bytes); assert.ok(entries.has('images/ch-1/0001.jpg')); assert.ok(entries.has('images/ch-2/0001.jpg')); // 旧章节图片字节原样保留,未被重新编码 assert.deepStrictEqual([...entries.get('images/ch-2/0001.jpg')], [10, 10, 10]); const list = mangaEpub.listChapters(bytes); assert.strictEqual(list.length, 2); // 按 volume/chapter 排序:话 1 应排在话 2 前面,尽管是后追加的 assert.strictEqual(list[0].chapterId, 'ch-1'); assert.strictEqual(list[1].chapterId, 'ch-2'); }); test('appendMangaChapter: 追加同一章节 ID 时抛出可读错误', () => { const first = mangaEpub.createMangaEpub({ title: '测试漫画', mangaId: 'manga-1', originalLanguage: 'ja', chapterId: 'ch-1', label: '第 1 话', images: [img(1)] }); assert.throws( () => mangaEpub.appendMangaChapter(first.bytes, { title: '测试漫画', mangaId: 'manga-1', originalLanguage: 'ja', chapterId: 'ch-1', label: '重复章节', images: [img(2)] }), /该章节已存在/ ); }); test('appendMangaChapter: 追加到不同漫画的 EPUB 会被拒绝', () => { const first = mangaEpub.createMangaEpub({ title: '测试漫画', mangaId: 'manga-1', originalLanguage: 'ja', chapterId: 'ch-1', label: '第 1 话', images: [img(1)] }); assert.throws( () => mangaEpub.appendMangaChapter(first.bytes, { title: '另一部漫画', mangaId: 'manga-2', originalLanguage: 'ja', chapterId: 'ch-9', label: '第 9 话', images: [img(2)] }), /不是同一部漫画/ ); }); test('appendMangaChapter: 目标不是漫画 EPUB(缺少旁路索引)时给出可操作提示', () => { const plain = zip.writeZip([{ name: 'mimetype', data: Buffer.from('application/epub+zip') }]); assert.throws( () => mangaEpub.appendMangaChapter(plain, { title: '测试漫画', mangaId: 'manga-1', originalLanguage: 'ja', chapterId: 'ch-1', label: '第 1 话', images: [img(1)] }), /请改为新建条目/ ); }); test('hasChapter: 命中已收录章节,未收录或非法数据都返回 false', () => { const { bytes } = mangaEpub.createMangaEpub({ title: '测试漫画', mangaId: 'manga-1', originalLanguage: 'ja', chapterId: 'ch-1', label: '第 1 话', images: [img(1)] }); assert.strictEqual(mangaEpub.hasChapter(bytes, 'ch-1'), true); assert.strictEqual(mangaEpub.hasChapter(bytes, 'ch-missing'), false); assert.strictEqual(mangaEpub.hasChapter(Buffer.from('not a zip'), 'ch-1'), false); }); test('compareChapters: 缺省卷号/话号排到最后', () => { const withNum = { volume: '1', chapter: '1' }; const withoutNum = { volume: '', chapter: '' }; assert.ok(mangaEpub.compareChapters(withNum, withoutNum) < 0); assert.ok(mangaEpub.compareChapters(withoutNum, withNum) > 0); }); test('漫画 EPUB 用 sourceId 隔离不同站点的同名 ID', () => { const created = mangaEpub.createMangaEpub({ title: '中文漫画', sourceId: 'copymanga', mangaId: 'same-id', originalLanguage: 'zh', chapterId: 'chapter-1', label: '第 1 话', images: [img(1)] }); const parsed = mangaEpub.parseMangaEpub(created.bytes); assert.strictEqual(parsed.sourceId, 'copymanga'); assert.strictEqual(mangaEpub.isMangaEpub(created.bytes, 'same-id', 'copymanga'), true); assert.strictEqual(mangaEpub.isMangaEpub(created.bytes, 'same-id', 'mangadex'), false); assert.throws(() => mangaEpub.appendMangaChapter(parsed, { title: '另一来源', sourceId: 'mangadex', mangaId: 'same-id', chapterId: 'chapter-2', label: '第 2 话', images: [img(2)] }), /不是同一部漫画/); });