228 lines
6.9 KiB
JavaScript
228 lines
6.9 KiB
JavaScript
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('<script>&"\'</script>')).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("<!DOCTYPE html>");
|
|
});
|
|
|
|
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(/<p class="flavor">/);
|
|
});
|
|
|
|
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");
|
|
});
|
|
});
|