// 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(/\^\^|\*\*/); } }); });