From bbeba42749be8691f0ef44365f7f79c0658d703a Mon Sep 17 00:00:00 2001 From: spring-raining Date: Sat, 20 Jan 2024 15:36:40 +0900 Subject: [PATCH] test: Add test for real-world EPUB generation --- package.json | 2 + tests/__snapshots__/epub.test.ts.snap | 49 +++++++++++ tests/epub.test.ts | 112 +++++++++++++++++++++++++- yarn.lock | 12 +++ 4 files changed, 174 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 686a7638..f8f63e53 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "devDependencies": { "@hyrious/esbuild-plugin-commonjs": "^0.2.2", "@release-it/conventional-changelog": "^5.1.1", + "@types/adm-zip": "^0.5.5", "@types/archiver": "^5.3.2", "@types/command-exists": "1.2.0", "@types/debug": "^4.1.7", @@ -85,6 +86,7 @@ "@types/w3c-xmlserializer": "^2.0.2", "@types/whatwg-mimetype": "^3.0.0", "@vitest/coverage-v8": "^0.33.0", + "adm-zip": "^0.5.10", "esbuild": "^0.18.11", "file-type": "^16.5.3", "fs-extra": "^11.1.1", diff --git a/tests/__snapshots__/epub.test.ts.snap b/tests/__snapshots__/epub.test.ts.snap index 1025e973..6b2c750c 100644 --- a/tests/__snapshots__/epub.test.ts.snap +++ b/tests/__snapshots__/epub.test.ts.snap @@ -281,3 +281,52 @@ exports[`generate EPUB from single HTML with pub manifest > tree 1`] = ` │ └─ publication.json └─ output.epub" `; + +exports[`generate EPUB from single Markdown input > tree 1`] = ` +"/ +├─ tmp/ +│ ├─ 1/ +│ │ ├─ index.html +│ │ └─ publication.json +│ └─ 2/ +│ ├─ EPUB/ +│ │ ├─ content.opf +│ │ ├─ index.xhtml +│ │ └─ toc.ncx +│ └─ META-INF/ +│ └─ container.xml +└─ work/ + ├─ input/ + │ ├─ .vs-0.index.html + │ ├─ .vs-0.publication.json + │ └─ index.md + └─ output.epub" +`; + +exports[`generate EPUB from vivliostyle.config.js > tree 1`] = ` +"/ +├─ tmp/ +│ ├─ 1/ +│ │ ├─ index.html +│ │ ├─ manuscript.html +│ │ ├─ my-theme.css +│ │ └─ publication.json +│ └─ 2/ +│ ├─ EPUB/ +│ │ ├─ content.opf +│ │ ├─ index.xhtml +│ │ ├─ manuscript.xhtml +│ │ ├─ my-theme.css +│ │ └─ toc.ncx +│ └─ META-INF/ +│ └─ container.xml +└─ work/ + └─ input/ + ├─ index.html + ├─ manuscript.html + ├─ manuscript.md + ├─ my-theme.css + ├─ output.epub + ├─ publication.json + └─ vivliostyle.config.json" +`; diff --git a/tests/epub.test.ts b/tests/epub.test.ts index cf041a04..cf0b3038 100644 --- a/tests/epub.test.ts +++ b/tests/epub.test.ts @@ -1,10 +1,18 @@ +import AdmZip from 'adm-zip'; import { fs as memfs, vol } from 'memfs'; import { format } from 'prettier'; import tmp from 'tmp'; import { afterEach, expect, it, vi } from 'vitest'; import { exportEpub } from '../src/output/epub.js'; +import { buildWebPublication } from '../src/output/webbook.js'; +import { + compile, + copyAssets, + prepareThemeDirectory, +} from '../src/processor/compile.js'; import { PublicationManifest } from '../src/schema/publication.schema.js'; -import { toTree } from './commandUtil.js'; +import { upath } from '../vendors/index.js'; +import { getMergedConfig, toTree } from './commandUtil.js'; vi.mock('node:fs', () => ({ ...memfs, default: memfs })); @@ -24,11 +32,42 @@ vi.mock('tmp', () => { return { default: mod }; }); +vi.mock('archiver', async () => { + const { default: archiver } = await vi.importActual<{ + default: typeof import('archiver'); + }>('archiver'); + return { + default: (...args: Parameters) => { + const archive = archiver(...args); + archive.directory = (dirpath, destpath) => { + for (const p in vol.toJSON(dirpath, undefined, true)) { + archive.append(vol.readFileSync(upath.join(dirpath, p)), { + name: upath.join(destpath, p), + }); + } + return archive; + }; + return archive; + }, + }; +}); + afterEach(() => { vol.reset(); (tmp as any).__count = 0; }); +function checkValidEpubZip(epub: Buffer) { + // Check epub file contains uncompressed mimetype file + expect(epub.readUInt32BE(0)).toBe(0x504b0304); + expect(epub.readUInt16LE(8)).toBe(0); + expect(epub.slice(30, 38).toString()).toBe('mimetype'); + expect(epub.slice(38, 58).toString()).toBe('application/epub+zip'); + // Check the remaining files are compressed + expect(epub.readUInt32BE(58)).toBe(0x504b0304); + expect(epub.readUInt16LE(66)).not.toBe(0); +} + it('generate EPUB from single HTML with pub manifest', async () => { const manifest: PublicationManifest = { '@context': ['https://schema.org', 'https://www.w3.org/ns/pub-context'], @@ -194,3 +233,74 @@ it('generate EPUB from series of HTML files', async () => { 'src/index.xhtml', ); }); + +it('generate EPUB from single Markdown input', async () => { + vol.fromJSON({ + '/work/input/index.md': '# Hello', + }); + const config = await getMergedConfig([ + '/work/input/index.md', + '--output', + '/work/output.epub', + ]); + await compile(config); + await copyAssets(config); + await buildWebPublication({ + ...config, + target: config.outputs[0], + }); + + expect(toTree(vol).replace(/\.vs-[^.]+/g, '.vs-0')).toMatchSnapshot('tree'); + + const epub = vol.readFileSync('/work/output.epub') as Buffer; + checkValidEpubZip(epub); + const zipFiles = new AdmZip(epub).getEntries(); + expect( + zipFiles.reduce((acc, z) => { + acc[z.entryName] = z.getData().toString(); + return acc; + }, {}), + ).toEqual({ + mimetype: 'application/epub+zip', + ...vol.toJSON('/tmp/2', undefined, true), + }); +}); + +it('generate EPUB from vivliostyle.config.js', async () => { + vol.fromJSON({ + '/work/input/vivliostyle.config.json': JSON.stringify({ + entry: 'manuscript.md', + output: './output.epub', + theme: './my-theme.css', + toc: true, + }), + '/work/input/manuscript.md': '# Hello', + '/work/input/my-theme.css': '/* theme CSS */', + }); + const config = await getMergedConfig([ + '-c', + '/work/input/vivliostyle.config.json', + ]); + await prepareThemeDirectory(config); + await compile(config); + await copyAssets(config); + await buildWebPublication({ + ...config, + target: config.outputs[0], + }); + + expect(toTree(vol)).toMatchSnapshot('tree'); + + const epub = vol.readFileSync('/work/input/output.epub') as Buffer; + checkValidEpubZip(epub); + const zipFiles = new AdmZip(epub).getEntries(); + expect( + zipFiles.reduce((acc, z) => { + acc[z.entryName] = z.getData().toString(); + return acc; + }, {}), + ).toEqual({ + mimetype: 'application/epub+zip', + ...vol.toJSON('/tmp/2', undefined, true), + }); +}); diff --git a/yarn.lock b/yarn.lock index 8265d936..b6d798b7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -677,6 +677,13 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== +"@types/adm-zip@^0.5.5": + version "0.5.5" + resolved "https://registry.yarnpkg.com/@types/adm-zip/-/adm-zip-0.5.5.tgz#4588042726aa5f351d7ea88232e4a952f60e7c1a" + integrity sha512-YCGstVMjc4LTY5uK9/obvxBya93axZOVOyf2GSUulADzmLhYE45u2nAssCs/fWBs1Ifq5Vat75JTPwd5XZoPJw== + dependencies: + "@types/node" "*" + "@types/archiver@^5.3.2": version "5.3.2" resolved "https://registry.yarnpkg.com/@types/archiver/-/archiver-5.3.2.tgz#a9f0bcb0f0b991400e7766d35f6e19d163bdadcc" @@ -1178,6 +1185,11 @@ add-stream@^1.0.0: resolved "https://registry.yarnpkg.com/add-stream/-/add-stream-1.0.0.tgz#6a7990437ca736d5e1288db92bd3266d5f5cb2aa" integrity sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ== +adm-zip@^0.5.10: + version "0.5.10" + resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.5.10.tgz#4a51d5ab544b1f5ce51e1b9043139b639afff45b" + integrity sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ== + agent-base@6, agent-base@^6.0.0, agent-base@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"