import { describe, it, expect } from "vitest"; import { escapeHtml, truncateText, parseEventForDisplay, dungeonTemplate, } from "../../src/dungeonTemplate.js"; describe("escapeHtml", () => { it("returns empty string for empty input", () => { expect(escapeHtml("")).toBe(""); }); it("returns empty string for null/undefined-like", () => { expect(escapeHtml(null)).toBe(""); expect(escapeHtml(undefined)).toBe(""); }); it("escapes & < > \" '", () => { expect(escapeHtml("&")).toBe("&"); expect(escapeHtml("<")).toBe("<"); expect(escapeHtml(">")).toBe(">"); expect(escapeHtml('"')).toBe("""); expect(escapeHtml("'")).toBe("'"); expect(escapeHtml('')).toBe( "<script>&"'</script>" ); }); it("leaves normal text unchanged", () => { expect(escapeHtml("Hello World")).toBe("Hello World"); }); }); describe("truncateText", () => { it("returns empty for empty input", () => { expect(truncateText("", 1, 100)).toBe(""); }); it("returns text when within sentence and char limits", () => { const one = "One sentence."; expect(truncateText(one, 1, 100)).toBe(one); }); it("truncates to maxSentences", () => { const three = "First. Second. Third."; expect(truncateText(three, 1, 500)).toBe("First."); expect(truncateText(three, 2, 500)).toContain("First."); expect(truncateText(three, 2, 500)).toContain("Second."); }); it("truncates by maxChars and ends at sentence boundary when possible", () => { const long = "A short bit. Then a much longer sentence that goes past the limit we set."; const out = truncateText(long, 99, 30); expect(out.length).toBeLessThanOrEqual(33); expect(out === "A short bit." || out.endsWith("...")).toBe(true); }); it("appends ... when no sentence boundary near end", () => { const noPeriod = "No period here and more text"; expect(truncateText(noPeriod, 1, 15)).toMatch(/\.\.\.$/); }); }); describe("parseEventForDisplay", () => { it("returns object name and description when given object", () => { const event = { name: "Event A", description: "Something happened." }; const got = parseEventForDisplay(event, 0); expect(got.name).toBe("Event A"); expect(got.description).toContain("Something"); }); it('parses "Name: Description" string', () => { const got = parseEventForDisplay("Fire: The room catches fire.", 0); expect(got.name).toBe("Fire"); expect(got.description).toContain("catches fire"); }); it("splits string without colon into first two words as name, rest as description", () => { const got = parseEventForDisplay("One Two Three Four", 0); expect(got.name).toBe("One Two"); expect(got.description).toBe("Three Four"); }); it("uses fallback Event N and full string for short string", () => { const got = parseEventForDisplay("Hi", 2); expect(got.name).toBe("Event 3"); expect(got.description).toBe("Hi"); }); it("handles non-string non-object with index", () => { const got = parseEventForDisplay(null, 1); expect(got.name).toBe("Event 2"); expect(got.description).toBe(""); }); }); describe("dungeonTemplate", () => { it("produces HTML with title and main sections for minimal data", () => { const data = { title: "Test Dungeon", flavor: "A dark place.", hooksRumors: ["Hook one.", "Hook two."], rooms: [{ name: "Room 1", description: "A room." }], encounters: [{ name: "Encounter 1", details: "Hall: Something happens." }], npcs: [{ name: "NPC 1", trait: "A guard." }], treasure: [{ name: "Gold", description: "Shiny." }], randomEvents: [{ name: "Event 1", description: "Something." }], plotResolutions: ["Resolution one."], }; const html = dungeonTemplate(data); expect(html).toContain("Test Dungeon"); expect(html).toContain("A dark place."); expect(html).toContain("Room 1"); expect(html).toContain("Encounter 1"); expect(html).toContain("NPC 1"); expect(html).toContain("Gold"); expect(html).toContain("Event 1"); expect(html).toContain("Resolution one."); expect(html).toContain(""); }); it("includes map page when data.map is data URL", () => { const data = { title: "With Map", flavor: "Flavor.", map: "data:image/png;base64,abc123", hooksRumors: ["H1"], rooms: [], encounters: [], npcs: [], treasure: [], randomEvents: [], plotResolutions: [], }; const html = dungeonTemplate(data); expect(html).toContain("map-page"); expect(html).toContain("data:image/png;base64,abc123"); }); it("omits flavor paragraph when flavor is empty", () => { const data = { title: "No Flavor", flavor: "", hooksRumors: ["H1"], rooms: [], encounters: [], npcs: [], treasure: [], randomEvents: [], plotResolutions: [], }; const html = dungeonTemplate(data); expect(html).toContain("No Flavor"); expect(html).not.toMatch(/
/); }); it("renders treasure as string Name — Desc", () => { const data = { title: "T", flavor: "F", hooksRumors: [], rooms: [], encounters: [], npcs: [], treasure: ["Gold — Shiny coins."], randomEvents: [], plotResolutions: [], }; const html = dungeonTemplate(data); expect(html).toContain("Gold"); expect(html).toContain("Shiny coins"); }); it("renders NPC as string Name: Trait", () => { const data = { title: "T", flavor: "F", hooksRumors: [], rooms: [], encounters: [], npcs: ["Guard: A stern guard."], treasure: [], randomEvents: [], plotResolutions: [], }; const html = dungeonTemplate(data); expect(html).toContain("Guard"); expect(html).toContain("stern guard"); }); it("strips location prefix from encounter details when it looks like a location name", () => { const data = { title: "T", flavor: "F", hooksRumors: [], rooms: [{ name: "Grand Hall", description: "Big." }], encounters: [ { name: "E1", details: "Grand Hall: The fight happens here in the hall." }, ], npcs: [], treasure: [], randomEvents: [], plotResolutions: [], }; const html = dungeonTemplate(data); expect(html).toContain("The fight happens here"); }); it("renders encounter details without name when details start with encounter name", () => { const data = { title: "T", flavor: "F", hooksRumors: [], rooms: [], encounters: [ { name: "Goblin Attack", details: "Goblin Attack: They strike." }, ], npcs: [], treasure: [], randomEvents: [], plotResolutions: [], }; const html = dungeonTemplate(data); expect(html).toContain("They strike"); }); });