import { describe, expect, it } from "vitest"; import { buildStructuredData, serializeJsonLd, type StructuredDataInput, } from "./structured-data"; const input: StructuredDataInput = { siteUrl: "https://example.com", pageUrl: "https://example.com/", siteName: "Jane Doe", language: "en", description: "Freelance web developer.", imageUrl: "https://example.com/jane.png", person: { name: "Jane Doe", jobTitle: "Freelance web developer", profiles: ["https://github.com/jane", "https://linkedin.com/in/jane"], knowsAbout: ["React", "Websites"], }, address: { street: "Main Street 1", postalCode: "12345", locality: "Town", countryCode: "DE", }, catalogName: "Services", services: [ { name: "Web app development", description: "I join your project." }, { name: "Website development", description: "A website, built for you." }, ], faq: [ { question: "Who owns the code?", answer: "You do." }, { question: "How do I reach you?", answer: "By email." }, ], }; const node = (data: ReturnType, type: string) => data["@graph"].find((entry) => entry["@type"] === type); describe("buildStructuredData", () => { it("is a schema.org graph of the website, the person, the business and the questions", () => { const data = buildStructuredData(input); expect(data["@context"]).toBe("https://schema.org"); expect(data["@graph"].map((entry) => entry["@type"])).toEqual([ "WebSite", "Person", "ProfessionalService", "FAQPage", ]); }); it("describes the website and names the person as its publisher", () => { expect(node(buildStructuredData(input), "WebSite")).toEqual({ "@type": "WebSite", "@id": "https://example.com/#website", url: "https://example.com", name: "Jane Doe", description: "Freelance web developer.", inLanguage: "en", publisher: { "@id": "https://example.com/#person" }, }); }); it("describes the person with job title, portrait, topics and profiles", () => { expect(node(buildStructuredData(input), "Person")).toEqual({ "@type": "Person", "@id": "https://example.com/#person", name: "Jane Doe", url: "https://example.com", image: "https://example.com/jane.png", jobTitle: "Freelance web developer", knowsAbout: ["React", "Websites"], sameAs: ["https://github.com/jane", "https://linkedin.com/in/jane"], }); }); it("leaves out the profile list when there are no profiles", () => { const data = buildStructuredData({ ...input, person: { ...input.person, profiles: [] }, }); expect(node(data, "Person")).not.toHaveProperty("sameAs"); }); it("describes the business with its address, founder and one offer per service", () => { expect(node(buildStructuredData(input), "ProfessionalService")).toEqual({ "@type": "ProfessionalService", "@id": "https://example.com/#business", name: "Jane Doe", url: "https://example.com", description: "Freelance web developer.", image: "https://example.com/jane.png", founder: { "@id": "https://example.com/#person" }, address: { "@type": "PostalAddress", streetAddress: "Main Street 1", postalCode: "12345", addressLocality: "Town", addressCountry: "DE", }, hasOfferCatalog: { "@type": "OfferCatalog", name: "Services", itemListElement: [ { "@type": "Offer", itemOffered: { "@type": "Service", name: "Web app development", description: "I join your project.", provider: { "@id": "https://example.com/#business" }, }, }, { "@type": "Offer", itemOffered: { "@type": "Service", name: "Website development", description: "A website, built for you.", provider: { "@id": "https://example.com/#business" }, }, }, ], }, }); }); it("lists every question with its answer", () => { expect(node(buildStructuredData(input), "FAQPage")).toEqual({ "@type": "FAQPage", "@id": "https://example.com/#faq", inLanguage: "en", mainEntity: [ { "@type": "Question", name: "Who owns the code?", acceptedAnswer: { "@type": "Answer", text: "You do." }, }, { "@type": "Question", name: "How do I reach you?", acceptedAnswer: { "@type": "Answer", text: "By email." }, }, ], }); }); it("describes the page in its own language: questions, catalogue and website", () => { const data = buildStructuredData({ ...input, pageUrl: "https://example.com/de", language: "de", catalogName: "Leistungen", }); expect(node(data, "FAQPage")).toMatchObject({ "@id": "https://example.com/de#faq", inLanguage: "de", }); expect(node(data, "ProfessionalService")).toMatchObject({ hasOfferCatalog: { name: "Leistungen" }, }); expect(node(data, "WebSite")).toMatchObject({ inLanguage: "de" }); }); it("names the same person, business and website on the page of every language", () => { const data = buildStructuredData({ ...input, pageUrl: "https://example.com/de", language: "de", }); expect(node(data, "WebSite")).toMatchObject({ "@id": "https://example.com/#website", url: "https://example.com", }); expect(node(data, "Person")).toMatchObject({ "@id": "https://example.com/#person", url: "https://example.com", }); expect(node(data, "ProfessionalService")).toMatchObject({ "@id": "https://example.com/#business", url: "https://example.com", }); }); it("leaves out the questions node when there are no questions", () => { const data = buildStructuredData({ ...input, faq: [] }); expect(data["@graph"].map((entry) => entry["@type"])).toEqual([ "WebSite", "Person", "ProfessionalService", ]); }); }); describe("serializeJsonLd", () => { it("writes JSON that parses back to the same data", () => { const data = buildStructuredData(input); expect(JSON.parse(serializeJsonLd(data))).toEqual(data); }); it("escapes every opening angle bracket so text cannot close the script tag", () => { const data = buildStructuredData({ ...input, description: "", }); const json = serializeJsonLd(data); expect(json).not.toContain("<"); expect(json).toContain("\\u003c/script>\\u003cscript>"); expect(JSON.parse(json)).toEqual(data); }); });