Files
edentext/tests/fuzz-roundtrip.test.ts
Steffen Becker d156482a3f test: LibreOffice, the layout lint and a monkey read the editor's work back
lo-fuzz.test.ts hands every fuzz seed to LibreOffice as .odt and .docx and
compares what comes back, listing the differences LibreOffice makes by design.
test:layout opens the corpus, the showcase and fuzz seeds in the browser, holds
the page count against LibreOffice and checks the lines for overlap, margin
escapes and stranded headings. test:monkey types at random on corpus documents
and asserts the schema, undo to the start, redo, and that both saved formats
read back. browser.mjs now carries the dev server and the settle helper.
2026-09-06 19:07:16 +02:00

75 lines
3.9 KiB
TypeScript

// Property leg: seeded random documents under random export options (fuzzDoc.ts,
// fuzzOptions.ts) -> buildOdt/buildDocx -> import come back unchanged (normalized), and every
// export validates against the schemas. A failure names the seed: genCase(mulberry32(seed)).
import { describe, it, expect } from 'vitest';
import { unzipSync } from 'fflate';
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 { normalize, firstDiff, stripFontHoist } from './normalize';
import { mulberry32 } from './fuzzDoc';
import { genCase, exportArgs, expectedOptions, importedOptions, diffLoose, omit, DOCX_LOSSY, docWideOddEven } from './fuzzOptions';
import { hasXmllint, validateOdt, validateDocx } from './schemaValidate';
const SEEDS = Number(process.env.FUZZ_SEEDS ?? 50); // FUZZ_SEEDS=500 for a wide sweep
// The schema leg pays ~0.7s of xmllint per seed, so it takes fewer by default.
const SCHEMA_SEEDS = Number(process.env.SCHEMA_SEEDS ?? Math.min(SEEDS, 20));
// What ODT has no place for: nothing so far (DOCX_LOSSY lists Word's).
const ODT_LOSSY: string[] = [];
describe('fuzz round-trip: editor → buildOdt → importOdt', () => {
it(`${SEEDS} seeded random documents come back identical`, async () => {
for (let seed = 1; seed <= SEEDS; seed++) {
const { doc, opts } = genCase(mulberry32(seed));
const res = importOdt(await buildOdt(doc, ...exportArgs(opts)));
expect.soft(res.warnings, `seed ${seed}: warnings`).toEqual([]);
const diff = firstDiff(stripFontHoist(normalize(doc)), stripFontHoist(normalize(res.content)));
expect.soft(diff, `seed ${seed}: content`).toBeNull();
const optDiff = diffLoose(omit(expectedOptions(opts), ODT_LOSSY), omit(importedOptions(res, opts), ODT_LOSSY));
expect.soft(optDiff, `seed ${seed}: options`).toBeNull();
}
}, 600_000);
// ODT leaves default column widths implicit where DOCX always writes w:tcW; ignore
// them in the cross-format comparison (each format's own leg still covers widths).
function stripColwidth(node: any): any {
if (node.attrs?.colwidth) {
delete node.attrs.colwidth;
if (!Object.keys(node.attrs).length) delete node.attrs;
}
for (const c of node.content ?? []) stripColwidth(c);
return node;
}
// The DOCX leg: its options against what went out, and its content against the ODT
// reading — catches one exporter silently losing what the other keeps.
it(`${SEEDS} seeded documents survive DOCX and agree with the ODT round-trip`, async () => {
for (let seed = 1; seed <= SEEDS; seed++) {
const { doc, opts } = genCase(mulberry32(seed));
const viaOdt = importOdt(await buildOdt(doc, ...exportArgs(opts))).content;
const res = importDocx(await buildDocx(doc, ...exportArgs(opts)));
const diff = firstDiff(stripColwidth(stripFontHoist(normalize(viaOdt))),
stripColwidth(stripFontHoist(normalize(res.content))));
expect.soft(diff, `seed ${seed}: ODT vs DOCX import`).toBeNull();
const optDiff = diffLoose(docWideOddEven(omit(expectedOptions(opts), DOCX_LOSSY)),
omit(importedOptions(res, opts), DOCX_LOSSY));
expect.soft(optDiff, `seed ${seed}: DOCX options`).toBeNull();
}
}, 600_000);
});
describe.skipIf(!hasXmllint)('fuzz: every export validates against the format schemas', () => {
it(`${SCHEMA_SEEDS} seeded documents are schema-valid as ODT and as DOCX`, async () => {
for (let seed = 1; seed <= SCHEMA_SEEDS; seed++) {
const { doc, opts } = genCase(mulberry32(seed));
const failures = [
...validateOdt(unzipSync(await buildOdt(doc, ...exportArgs(opts)))),
...validateDocx(unzipSync(await buildDocx(doc, ...exportArgs(opts)))),
];
expect.soft(failures.join('\n\n'), `seed ${seed}`).toBe('');
}
}, 1_800_000);
});