import { describe, expect, it } from "vitest"; import { extractRevision, parseLegalParagraph, sectionAnchors, splitSentences, tokenizeInline } from "./legal-blocks"; describe("parseLegalParagraph", () => { it("keeps ordinary prose as a text block", () => { expect(parseLegalParagraph("Cookies sind kleine Textdateien. Sie speichern Einstellungen.")).toEqual({ kind: "text", text: "Cookies sind kleine Textdateien. Sie speichern Einstellungen.", }); }); it("turns a paragraph whose lines all start with a dash into a list", () => { expect(parseLegalParagraph("- Auskunft\n- Berichtigung\n• Löschung")).toEqual({ kind: "list", items: ["Auskunft", "Berichtigung", "Löschung"], }); }); it("does not treat a single dashed line mixed with prose as a list", () => { expect(parseLegalParagraph("Einleitung\n- Punkt").kind).toBe("text"); }); it("recognises a storage entry and pulls labelled facts out of it", () => { const block = parseLegalParagraph( "wabenhain_session – Hält dich angemeldet. Inhalt: signierte Kennung, keine Passwörter. Speicherdauer: 30 Tage; wird beim Abmelden gelöscht." ); expect(block).toEqual({ kind: "entry", name: "wabenhain_session", note: null, text: "Hält dich angemeldet.", facts: [ { label: "Inhalt", value: "signierte Kennung, keine Passwörter." }, { label: "Speicherdauer", value: "30 Tage; wird beim Abmelden gelöscht." }, ], }); }); it("keeps abbreviations such as Art. and Abs. inside one fact", () => { const block = parseLegalParagraph( "theme – Speichert die Darstellung. Rechtsgrundlage: § 25 Abs. 2 Nr. 2 TDDDG und Art. 6 Abs. 1 lit. f DSGVO. Wird sonst gelöscht." ); expect(block.kind === "entry" && block.facts).toEqual([ { label: "Rechtsgrundlage", value: "§ 25 Abs. 2 Nr. 2 TDDDG und Art. 6 Abs. 1 lit. f DSGVO." }, ]); expect(block.kind === "entry" && block.text).toBe("Speichert die Darstellung. Wird sonst gelöscht."); }); it("recognises a provider entry written as 'Name (Zweck): Text'", () => { const block = parseLegalParagraph("Stripe (Zahlungsabwicklung): Lädt das Formular. Anbieter: Stripe Payments Europe, Ltd., Dublin."); expect(block).toMatchObject({ kind: "entry", name: "Stripe", note: "Zahlungsabwicklung", text: "Lädt das Formular." }); expect(block.kind === "entry" && block.facts).toEqual([{ label: "Anbieter", value: "Stripe Payments Europe, Ltd., Dublin." }]); }); it("recognises English fact labels", () => { const block = parseLegalParagraph("locale – Stores your language. Storage period: 1 year."); expect(block.kind === "entry" && block.facts).toEqual([{ label: "Storage period", value: "1 year." }]); }); it("does not mistake a sentence with a spaced dash for an entry", () => { expect(parseLegalParagraph("Wir versenden schnell – meist in zwei Tagen.").kind).toBe("text"); }); }); describe("splitSentences", () => { it("does not split after z. B. or Art.", () => { expect(splitSentences("Etwa z. B. Chrome. Nach Art. 6 DSGVO. Ende.")).toEqual(["Etwa z. B. Chrome.", "Nach Art. 6 DSGVO.", "Ende."]); }); }); describe("extractRevision", () => { it("moves a trailing 'Stand:' line out of the sections", () => { const { sections, revision } = extractRevision([ { heading: "A", paragraphs: ["Text"] }, { heading: "B", paragraphs: ["Mehr", "Stand: September 2026"] }, ]); expect(revision).toBe("September 2026"); expect(sections).toEqual([ { heading: "A", paragraphs: ["Text"] }, { heading: "B", paragraphs: ["Mehr"] }, ]); }); it("drops a section that only held the revision line", () => { const { sections, revision } = extractRevision([ { heading: "A", paragraphs: ["Text"] }, { heading: "Stand", paragraphs: ["Last updated: May 2026"] }, ]); expect(revision).toBe("May 2026"); expect(sections).toHaveLength(1); }); it("returns null when no revision is given", () => { expect(extractRevision([{ heading: "A", paragraphs: ["Text"] }]).revision).toBeNull(); }); }); describe("sectionAnchors", () => { it("builds readable, unique anchors from headings", () => { expect(sectionAnchors(["1. Verantwortlicher", "Deine Rechte", "Deine Rechte", "Über uns & mehr"])).toEqual([ "verantwortlicher", "deine-rechte", "deine-rechte-2", "ueber-uns-mehr", ]); }); it("falls back to a numbered anchor for headings without letters", () => { expect(sectionAnchors(["§§", ""])).toEqual(["abschnitt-1", "abschnitt-2"]); }); }); describe("tokenizeInline", () => { it("links web addresses with a path and e-mail addresses", () => { expect(tokenizeInline("Siehe stripe.com/de/privacy oder schreib an info@wabenhain.com.")).toEqual([ { kind: "text", value: "Siehe " }, { kind: "link", value: "stripe.com/de/privacy", href: "https://stripe.com/de/privacy" }, { kind: "text", value: " oder schreib an " }, { kind: "link", value: "info@wabenhain.com", href: "mailto:info@wabenhain.com" }, { kind: "text", value: "." }, ]); }); it("keeps explicit https links and never links other schemes", () => { expect(tokenizeInline("https://example.org/a javascript:alert(1)")).toEqual([ { kind: "link", value: "https://example.org/a", href: "https://example.org/a" }, { kind: "text", value: " javascript:alert(1)" }, ]); }); it("leaves a bare domain without path as text", () => { expect(tokenizeInline("wabenhain.com")).toEqual([{ kind: "text", value: "wabenhain.com" }]); }); });