import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import { describe, expect, it } from "vitest";
import type { DesktopTaskCheck } from "../../../src/domain/desktop/appTask.js";
import { xmlView } from "../../../src/domain/desktop/stateFormats.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import {
expectSameCheckResults,
expectSameFileViews,
odtOf,
pythonXmlViews,
savedEnds,
typescriptFileView,
} from "../../support/desktopExportPython.js";
/** The meeting note as AbiWord saves it, before and after its time was changed. */
const notes = () => savedEnds("abiword", "meeting.abw");
/** Writer's flyer before and after an edit split its body paragraph into runs, zipped with its mimetype. */
const flyers = () =>
savedEnds("libreoffice-runs", "content.xml", "flyer.odt", odtOf);
const NOTE = "Documents/meeting.abw";
const PARAGRAPH = "/root/children/[name=section]/children/[name=p][@xid=3]";
const MEETING = "The team meeting starts at 3:30 in room 4.";
/** The paragraph's whole text, a time within it, and its first text node, which the edit split. */
const NOTE_CHECKS: DesktopTaskCheck[] = [
{
id: "time",
kind: "text_equals",
file: NOTE,
pointer: PARAGRAPH,
value: MEETING,
},
{
id: "within",
kind: "text_contains",
file: NOTE,
pointer: PARAGRAPH,
value: "at 3:30 in",
},
{
id: "first-node",
kind: "value_equals",
file: NOTE,
pointer: `${PARAGRAPH}/children/0`,
value: MEETING,
},
];
const FLYER = "Documents/flyer.odt";
const BODY =
"/entries/[name=content.xml]/view/root/children/[name=office:body]/children/[name=office:text]/children/[name=text:p][@text:style-name=Standard]";
const CAKES = "Cakes, biscuits and bread from 9:30 in the village green.";
const FLYER_CHECKS: DesktopTaskCheck[] = [
{ id: "body", kind: "text_equals", file: FLYER, pointer: BODY, value: CAKES },
{
id: "time",
kind: "text_contains",
file: FLYER,
pointer: BODY,
value: " 9:30\n",
normalize_whitespace: true,
},
{
id: "reflowed",
kind: "text_equals",
file: FLYER,
pointer: BODY,
value: " Cakes, biscuits and bread\n from 9:30 in the village green. ",
normalize_whitespace: true,
},
{
id: "first-run",
kind: "value_equals",
file: FLYER,
pointer: `${BODY}/children/0`,
value: CAKES,
},
];
const passes = (outcomes: readonly { passed: boolean }[] | undefined) =>
outcomes?.map(({ passed }) => passed);
describe("XML with a DOCTYPE in the exported module", () => {
it("reads an AbiWord document, which names its DTD, as REA does: the element tree and the declaration", async () => {
const files = await notes();
await expectSameFileViews(files);
const [, after] = files;
if (after === undefined) throw new Error("fixture");
const { format, view } = await typescriptFileView(after);
expect(format).toBe("xml");
expect(view).toMatchObject({
doctype: {
name: "abiword",
public_id: "-//ABISOURCE//DTD AWML 1.0 Strict//EN",
system_id: "http://www.abisource.com/awml.dtd",
},
root: { name: "abiword", attributes: { "xml:space": "preserve" } },
truncated: false,
});
});
it("reads a DOCTYPE that only names a DTD alike, and refuses an internal subset or an undeclared entity in both", () => {
const accepted = [
"",
't',
"",
'&"'>',
'',
'',
];
const refused = [
"",
']>',
']>&e;',
']>&e;',
' %p;]>',
'&e;',
'',
'',
'',
"",
"",
];
const texts = [...accepted, ...refused];
const ours = texts.map((text) => xmlView(text) ?? null);
expect(pythonXmlViews(texts)).toEqual(ours);
expect(ours.slice(0, accepted.length)).not.toContain(null);
expect(ours.slice(accepted.length)).toEqual(refused.map(() => null));
expect(ours[2]).toMatchObject({
doctype: { name: "a", public_id: "-//A B//EN", system_id: "s y" },
});
expect(ours[5]).toMatchObject({
doctype: { name: "a", public_id: null, system_id: "x\ny\nz\n" },
});
});
it("never reads the DTD a DOCTYPE names: neither its entities nor its default attributes apply", async () => {
const folder = await createTestTempDirectory("desktop-xml-dtd-");
const dtd = join(folder, "a.dtd");
await writeFile(
dtd,
'',
);
const system = pathToFileURL(dtd).href;
const texts = [
``,
`&e;`,
``,
];
const ours = texts.map((text) => xmlView(text) ?? null);
expect(pythonXmlViews(texts)).toEqual(ours);
expect(ours).toEqual([
{
doctype: { name: "a", public_id: null, system_id: system },
root: { name: "a", attributes: {}, children: [] },
truncated: false,
},
null,
null,
]);
});
});
describe("whole-text checks in both evaluators", () => {
it("checks the meeting time across AbiWord's runs: the whole text fails before and passes after, its first node does not", async () => {
const [before, after] = await expectSameCheckResults(
NOTE,
NOTE_CHECKS,
await notes(),
);
expect(passes(before)).toEqual([false, false, false]);
for (const fact of [
PARAGRAPH,
JSON.stringify("The team meeting starts at 2:00 in room 4."),
])
expect(before?.[0]?.description).toContain(fact);
expect(passes(after)).toEqual([true, true, false]);
// The first text node holds only the text before the first run.
expect(after?.[2]?.description).toContain(
JSON.stringify("The team meeting starts at "),
);
});
it("reads Writer's runs in its archive alike, the space between two runs included", async () => {
const files = await flyers();
await expectSameFileViews(files);
const [before, after] = await expectSameCheckResults(
FLYER,
FLYER_CHECKS,
files,
);
expect(passes(before)).toEqual([false, false, false, false]);
expect(passes(after)).toEqual([true, true, true, false]);
// The value searched for is the collapsed one, " 9:30\n" read as "9:30".
expect(after?.[1]?.description).toContain(JSON.stringify("9:30"));
});
});