From 7e2eb53ddc06412b39cf580bb1a1d2cfa034d29e Mon Sep 17 00:00:00 2001 From: Steffen Becker Date: Sat, 5 Sep 2026 17:15:08 +0200 Subject: [PATCH] test: a decorated section, a run under an underlining style, a wider sweep Three regression tests for what a 5000-seed sweep turned up, each checked to fail without its fix. A run repeating its style's own plain underline is lossy by design, so the generator no longer puts one under a named style. --- tests/fuzzDoc.ts | 5 +++- tests/unit/decor-sections.test.ts | 41 +++++++++++++++++++++++++++ tests/unit/section-start-side.test.ts | 10 +++++++ tests/unit/style-underline.test.ts | 37 ++++++++++++++++++++++++ 4 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 tests/unit/decor-sections.test.ts create mode 100644 tests/unit/style-underline.test.ts diff --git a/tests/fuzzDoc.ts b/tests/fuzzDoc.ts index 16d4905..740471c 100644 --- a/tests/fuzzDoc.ts +++ b/tests/fuzzDoc.ts @@ -41,7 +41,10 @@ function marks(r: Rng, heading = false): N[] | undefined { const attrs: N = {}; if (maybe(r, 0.3)) attrs.lineStyle = pick(r, ['dotted', 'double', 'dashed']); if (maybe(r, 0.2)) attrs.lineColor = '#FF0000'; - out.push({ type: 'underline', ...(Object.keys(attrs).length ? { attrs } : {}) }); + // A user style may underline plainly, and a run repeating exactly that is suppressed + // on import — under a named style a run underlines only in its own shape or colour. + const line = Object.keys(attrs).length ? { attrs } : null; + if (line || !heading) out.push({ type: 'underline', ...(line ?? {}) }); } if (maybe(r, 0.1)) { out.push(maybe(r, 0.25) ? { type: 'strike', attrs: { lineStyle: 'double' } } : { type: 'strike' }); diff --git a/tests/unit/decor-sections.test.ts b/tests/unit/decor-sections.test.ts new file mode 100644 index 0000000..9447d22 --- /dev/null +++ b/tests/unit/decor-sections.test.ts @@ -0,0 +1,41 @@ +// A document whose first master page carries no header at all: the watermark and the fold +// marks mint one, and a section break puts a second master page after it. +import { describe, it, expect } from 'vitest'; +import { buildOdt } from '../../src/lib/export/odt'; +import { importOdt } from '../../src/lib/import/odt'; +import { EMPTY_PAGE_DECOR, DEFAULT_WATERMARK } from '../../src/lib/storage/pageDecor'; + +const doc = { + type: 'doc', + content: [ + { type: 'paragraph', content: [{ type: 'text', text: 'one' }] }, + { type: 'paragraph', attrs: { sectionBreak: true, breakBefore: 'page' }, content: [{ type: 'text', text: 'two' }] }, + ], +} as any; + +// The first section has no zone of its own and the second does, so the first master page +// is written self-closing while the second carries a header. +const hf = { + header: null, footer: null, pageCount: 2, + sections: [{ header: null, footer: null }, + { header: { type: 'paragraph', content: [{ type: 'text', text: 'zwei' }] }, footer: null }], +} as never; + +const build = (decor: unknown, foldMarks = false) => buildOdt(doc, undefined, undefined, hf, undefined, + undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, + decor as never, undefined, undefined, foldMarks); + +describe('page decor across a section break', () => { + const watermark = { ...DEFAULT_WATERMARK, text: 'DRAFT', angle: 135 }; + + it('reads the watermark back off the first page', async () => { + const res = importOdt(await build({ ...EMPTY_PAGE_DECOR, watermark })); + expect(res.decor.watermark).toEqual(watermark); + }); + + it('gives every master page the shape', async () => { + const res = importOdt(await build({ ...EMPTY_PAGE_DECOR, watermark }, true)); + expect(res.decor.watermark?.text).toBe('DRAFT'); + expect(res.foldMarks).toBe(true); + }); +}); diff --git a/tests/unit/section-start-side.test.ts b/tests/unit/section-start-side.test.ts index d1c33b5..34a6709 100644 --- a/tests/unit/section-start-side.test.ts +++ b/tests/unit/section-start-side.test.ts @@ -33,6 +33,16 @@ describe('the side a section opens on', () => { expect(sides(importOdt(bytes))).toEqual([null, 'odd']); }); + // The page layout takes the header band off the margin for the whole section, so a + // master of it that writes no running zone puts its body up into the reserved band. + it('keeps the band it reserved on every master of the section', async () => { + const second = { ...EMPTY_HF_SET, headerFirst: zone, differentFirstPage: true, + startsOn: 'odd' as const, margins: { top: 1.5, bottom: 2, left: 2, right: 2 } }; + const bytes = await buildOdt(doc, margins, 'portrait', + { ...hf, sections: [{ ...EMPTY_HF_SET, header: zone }, second] } as never); + expect((importOdt(bytes).hfSections ?? [])[1]?.margins).toEqual(second.margins); + }); + it('round-trips through DOCX as w:type', async () => { const bytes = await buildDocx(doc, margins, 'portrait', hf as never); expect(strFromU8(unzipSync(bytes)['word/document.xml'])).toContain(' n.content[0].content.map((c: any) => c.marks?.find((m: any) => m.type === 'underline')?.attrs ?? null); + +describe.each([ + ['odt', async () => importOdt(await buildOdt(doc, undefined, undefined, undefined, undefined, undefined, sheet)).content], + ['docx', async () => importDocx(await buildDocx(doc, undefined, undefined, undefined, undefined, undefined, sheet)).content], +] as const)('a run under an underlining style (%s)', (_fmt, read) => { + it('keeps its own colour and shape, and drops what the style already draws', async () => { + expect(runs(await read())).toEqual([{ lineColor: '#FF0000' }, { lineStyle: 'wavy' }, null]); + }); +});