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 { elementText } from "../../../src/domain/desktop/viewPointer.js";
import type { JsonValue } from "../../../src/domain/jsonValue.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',
"",
'&"'>',
'
${before}${"".repeat(65)}${dropped}${"".repeat(65)}${after}
`, ), }); const files = [ paragraph("at 3", "WRONG", ":30 PM"), paragraph("alpha", "zzz", "beta"), ]; await expectSameFileViews(files); // The kept runs join into text neither file has. const kept = await Promise.all( files.map(async (file) => { const { view } = await typescriptFileView(file); return elementText((view as { root: JsonValue }).root); }), ); expect(kept).toEqual(["at 3:30 PM", "alphabeta"]); const results = await expectSameCheckResults( deep, [ { id: "time", kind: "text_contains", file: deep, pointer: "/root", value: "at 3:30 PM", }, { id: "word", kind: "text_contains", file: deep, pointer: "/root", value: "alphabeta", }, { id: "id", kind: "text_equals", file: deep, pointer: "/root/attributes/id", value: "t", }, ], files, ); expect(results).toHaveLength(files.length); for (const outcomes of results) { expect( outcomes.map(({ id, passed, undecided }) => [id, passed, undecided]), ).toEqual([ ["time", false, true], ["word", false, true], ["id", true, undefined], ]); for (const { description } of outcomes) expect(description).toContain(deep); } }); });