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.
This commit is contained in:
Steffen Becker committed 2026-09-06 19:07:19 +02:00
1 parent ab133f7966
commit 7e2eb53ddc
4 files changed
+92 -1

No files matched your search

+4 -1
View File
@@ -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' });
+41
View File
@@ -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);
});
});
+10
View File
@@ -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('<w:type w:val="oddPage"');
+37
View File
@@ -0,0 +1,37 @@
// A run underlining differently from the style it sits under: both importers suppress a
// run that only repeats its style, and the shape and colour ride along with it.
import { describe, it, expect } from 'vitest';
import { buildOdt } from '../../src/lib/export/odt';
import { importOdt } from '../../src/lib/import/odt';
import { buildDocx } from '../../src/lib/export/docx';
import { importDocx } from '../../src/lib/import/docx';
import { builtinStyleSheet } from '../../src/lib/styles/styleSheet';
const sheet = builtinStyleSheet();
sheet.paragraph['Unterstrichen'] = {
name: 'Unterstrichen', parent: 'Standard', next: 'Standard', para: {}, text: { underline: true },
};
const doc = {
type: 'doc',
content: [{
type: 'paragraph',
attrs: { styleName: 'Unterstrichen' },
content: [
{ type: 'text', text: 'rot', marks: [{ type: 'underline', attrs: { lineColor: '#FF0000' } }] },
{ type: 'text', text: 'wellig', marks: [{ type: 'underline', attrs: { lineStyle: 'wavy' } }] },
{ type: 'text', text: 'wie der Stil', marks: [{ type: 'underline' }] },
],
}],
} as any;
const runs = (n: any) => 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]);
});
});