/** * @jest-environment node * * OdtStyling.js tests: page-size patching and header/footer injection into a * Pandoc-style ODT package. Fixtures are minimal zips built with PizZip, * mirroring the structure Pandoc's ODT writer emits (styles.xml with a "pm1" * page layout and a "Standard" master page). */ const fs = require('fs'); const os = require('os'); const path = require('path'); const PizZip = require('pizzip'); const { setOdtPageSize, addHeaderFooterToOdt, parseDimensionsMm, escapeOdtText, } = require('../../src/main/OdtStyling'); // Minimal Pandoc-like styles.xml: page layout pm1 + Standard master page const STYLES_XML = ` `; /** * Write a minimal .odt fixture containing just styles.xml. * @param {string} filePath destination * @param {string} [stylesXml] styles.xml content (defaults to STYLES_XML) */ function writeOdtFixture(filePath, stylesXml = STYLES_XML) { const zip = new PizZip(); zip.file('mimetype', 'application/vnd.oasis.opendocument.text'); zip.file('styles.xml', stylesXml); zip.file('content.xml', ''); fs.writeFileSync(filePath, zip.generate({ type: 'nodebuffer' })); } /** Read styles.xml back out of a written fixture. */ function readStylesXml(filePath) { return new PizZip(fs.readFileSync(filePath)).file('styles.xml').asText(); } describe('OdtStyling', () => { let tmpDir, odtPath; beforeEach(() => { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'odtstyle_')); odtPath = path.join(tmpDir, 'doc.odt'); writeOdtFixture(odtPath); }); afterEach(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); }); describe('parseDimensionsMm', () => { it('parses "210×297mm" (U+00D7 separator) into numbers', () => { expect(parseDimensionsMm('210×297mm')).toEqual({ widthMm: 210, heightMm: 297 }); }); it('returns null for unparseable input', () => { expect(parseDimensionsMm('8.5in')).toBeNull(); expect(parseDimensionsMm(undefined)).toBeNull(); }); }); describe('escapeOdtText', () => { it('escapes XML-significant characters', () => { expect(escapeOdtText('a&"c"')).toBe('a<b>&"c"'); }); }); describe('setOdtPageSize', () => { it('rewrites width/height/orientation on the existing page layout (A4 portrait)', async () => { await setOdtPageSize(odtPath, { size: 'a4', orientation: 'portrait', pageSizes: { a4: { dimensions: '210×297mm' } }, }); const xml = readStylesXml(odtPath); expect(xml).toContain('fo:page-width="210mm"'); expect(xml).toContain('fo:page-height="297mm"'); expect(xml).toContain('style:print-orientation="portrait"'); // Old inch-based attributes must be gone (no duplicate size attrs) expect(xml).not.toContain('fo:page-width="8.267in"'); }); it('swaps dimensions for landscape', async () => { await setOdtPageSize(odtPath, { size: 'a4', orientation: 'landscape', pageSizes: { a4: { dimensions: '210×297mm' } }, }); const xml = readStylesXml(odtPath); expect(xml).toContain('fo:page-width="297mm"'); expect(xml).toContain('fo:page-height="210mm"'); expect(xml).toContain('style:print-orientation="landscape"'); }); it('falls back to custom mm dimensions and A4 default', async () => { await setOdtPageSize(odtPath, { customWidth: '148', customHeight: '210' }); expect(readStylesXml(odtPath)).toContain('fo:page-width="148mm"'); writeOdtFixture(odtPath); await setOdtPageSize(odtPath, {}); expect(readStylesXml(odtPath)).toContain('fo:page-width="210mm"'); }); }); describe('addHeaderFooterToOdt', () => { it('is a no-op when disabled', async () => { await addHeaderFooterToOdt(odtPath, { enabled: false, header: { left: 'X' } }); expect(readStylesXml(odtPath)).not.toContain(' { await addHeaderFooterToOdt(odtPath, { enabled: true, header: { left: 'My Doc', right: 'Draft ' }, footer: { center: 'Page $PAGE$ of $TOTAL$' }, }); const xml = readStylesXml(odtPath); expect(xml).toContain(''); expect(xml).toContain(''); expect(xml).toContain(''); // The self-closing master page must have been expanded to hold children expect(xml).toMatch(/]*>[\s\S]*<\/style:master-page>/); }); it('does nothing when all regions are empty', async () => { await addHeaderFooterToOdt(odtPath, { enabled: true, header: { left: '', center: '', right: '' }, footer: {}, }); expect(readStylesXml(odtPath)).toBe(STYLES_XML); }); it('creates a master page when the package has none', async () => { const bareXml = STYLES_XML.replace( / [\s\S]*<\/office:master-styles>/, '' ); writeOdtFixture(odtPath, bareXml); await addHeaderFooterToOdt(odtPath, { enabled: true, footer: { center: 'hi' }, }); const xml = readStylesXml(odtPath); expect(xml).toContain(''); expect(xml).toContain('style:name="Standard"'); expect(xml).toContain('hi'); }); it('replaces existing header/footer content on re-export', async () => { await addHeaderFooterToOdt(odtPath, { enabled: true, header: { left: 'first' } }); await addHeaderFooterToOdt(odtPath, { enabled: true, header: { left: 'second' } }); const xml = readStylesXml(odtPath); expect(xml).toContain('second'); expect(xml).not.toContain('first'); // Exactly one header element, not nested duplicates expect(xml.match(//g)).toHaveLength(1); }); }); });