Files
brevyscribe/src/transforms/integration.test.js
T
luxick 444d26fd47
CI / check (push) Has been cancelled
Improve Keyword rendering
2026-07-25 16:08:21 +02:00

112 lines
3.9 KiB
JavaScript

// The transforms have to leave a document the roster parser still understands,
// and the whole point is that what comes out the far end reads like the official
// card. These tests go through the real parser rather than inspecting the DOM.
import { readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
import { Create40kRoster11th } from "../roster40k-11th.js";
import { applyTransforms } from "./index.js";
const FIXTURES = join(dirname(fileURLToPath(import.meta.url)), "__fixtures__");
const GAME_TYPE = "Warhammer 40,000 11th Edition";
/** Parse a fixture, optionally transforming it, and build the roster. */
function build(name, { transform }) {
const xml = readFileSync(join(FIXTURES, `${name}.ros`), "utf8");
const doc = new DOMParser().parseFromString(xml, "text/xml");
if (transform) applyTransforms(doc);
return Create40kRoster11th(doc, GAME_TYPE);
}
const units = (roster) => roster.forces.flatMap((force) => force.units);
const unitNamed = (roster, name) =>
units(roster).find((unit) => unit.name.includes(name));
describe("the transformed roster still parses", () => {
it.each(["bellisarius-cawl", "datasmith", "rangers", "skatros"])(
"builds a roster from %s",
(example) => {
const roster = build(example, { transform: true });
expect(roster).toBeTruthy();
expect(roster.forces.length).toBeGreaterThan(0);
expect(units(roster).length).toBeGreaterThan(0);
},
);
});
describe("merging duplicates", () => {
it("turns two Skatros cards into one carrying both weapons", () => {
const before = build("skatros", { transform: false });
const after = build("skatros", { transform: true });
const skatrosBefore = units(before).filter((unit) =>
unit.name.includes("Skatros"),
);
const skatrosAfter = units(after).filter((unit) =>
unit.name.includes("Skatros"),
);
expect(skatrosBefore.length).toBe(2);
expect(skatrosAfter.length).toBe(1);
// The single card now lists the weapon each copy was taken for.
const weapons = [
...skatrosAfter[0].rangedWeapons,
...skatrosAfter[0].meleeWeapons,
]
.map((weapon) => weapon.name)
.join(" | ");
expect(weapons).toMatch(/jezzail/i);
expect(weapons).toMatch(/arquebus/i);
});
it("brings an option's own ability along with the model that carries it", () => {
const after = build("rangers", { transform: true });
const rangers = unitNamed(after, "Ranger");
const abilityNames = Object.values(rangers.abilities).flatMap((group) =>
Array.from(group.keys()),
);
expect(abilityNames.join(" | ")).toMatch(/data-tether/i);
});
});
describe("stripping Leader/Support", () => {
// The Datasmith is a Support character, not a Leader; the transform strips
// both, and this is the example that has one.
it("leaves no attachment ability on the card", () => {
const before = build("datasmith", { transform: false });
const after = build("datasmith", { transform: true });
const abilitiesOf = (roster) =>
units(roster)
.flatMap((unit) => Object.values(unit.abilities))
.flatMap((group) => Array.from(group.keys()))
.join(" | ");
expect(abilitiesOf(before)).toMatch(/^Support \|/);
expect(abilitiesOf(after)).not.toMatch(/Leader|Support/);
});
});
describe("splitting choice abilities", () => {
it("gives each Canticle its own row under one titled group", () => {
const after = build("bellisarius-cawl", { transform: true });
const cawl = unitNamed(after, "Cawl");
const group = cawl.abilities["CANTICLES OF THE OMNISSIAH"];
expect(group).toBeTruthy();
expect(group.size).toBeGreaterThan(1);
// The option text keeps GW's ^^keyword^^ / **emphasis** markers for the
// renderer to format; only the row titles are plain text, since those
// render as-is.
expect([...group.values()].join("\n")).toMatch(/\^\^/);
for (const name of group.keys()) {
expect(name).not.toMatch(/\^\^|\*\*/);
}
});
});