memos/web/tests/markdown-roundtrip.test.ts

51 lines
2.2 KiB
TypeScript

import { readdirSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { parseMarkdown, roundTripMarkdown } from "@/components/MemoEditor/Editor/markdownCodec";
const CORPUS_DIR = join(__dirname, "fixtures", "markdown-corpus");
function fixtures(group: string): Array<[name: string, source: string]> {
const dir = join(CORPUS_DIR, group);
return readdirSync(dir)
.filter((f) => f.endsWith(".md"))
.sort()
.map((f) => [f, readFileSync(join(dir, f), "utf8")]);
}
// Fidelity contract part 1: supported constructs round-trip semantically.
// "Semantic" = the parsed document tree is identical before and after a
// serialize cycle; marker style (e.g. `*` vs `-` bullets) may normalize.
describe("markdown round-trip corpus: supported syntax (semantic equality)", () => {
for (const [name, source] of fixtures("supported")) {
it(`${name}: parse → serialize → parse is identity`, () => {
const serialized = roundTripMarkdown(source);
expect(parseMarkdown(serialized)).toEqual(parseMarkdown(source));
});
it(`${name}: serialization is idempotent`, () => {
const once = roundTripMarkdown(source);
expect(roundTripMarkdown(once)).toBe(once);
});
}
});
// Fidelity contract part 2: unmodeled constructs round-trip byte-for-byte.
// Fixtures in preserved/ contain only preserved constructs + inert prose,
// so the entire file must survive a round trip unchanged (modulo outer trim).
// One known whitespace normalization: a block construct separated from the
// next block by a single \n gains a blank line (\n\n) on the first round trip
// — the Document serializer joins sibling blocks with \n\n. It is a one-time
// normalization; the idempotence tests below pin the output as stable.
describe("markdown round-trip corpus: preserved syntax (byte equality)", () => {
for (const [name, source] of fixtures("preserved")) {
it(`${name}: round-trips byte-for-byte`, () => {
expect(roundTripMarkdown(source).trim()).toBe(source.trim());
});
it(`${name}: serialization is idempotent`, () => {
const once = roundTripMarkdown(source);
expect(roundTripMarkdown(once)).toBe(once);
});
}
});