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.
75 lines
3.9 KiB
TypeScript
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);
|
|
});
|