/** * Page Metadata Tests * * Tests the metadata collector for: * - Resolving contributions into deduplicated metadata * - HTML rendering with proper escaping * - Safe JSON-LD serialization * - HTML attribute escaping */ import { describe, it, expect } from "vitest"; import { resolvePageMetadata, renderPageMetadata, safeJsonLdSerialize, escapeHtmlAttr, } from "../../../src/page/metadata.js"; import type { PageMetadataContribution } from "../../../src/plugins/types.js"; describe("resolvePageMetadata", () => { it("resolves meta tags correctly", () => { const contributions: PageMetadataContribution[] = [ { kind: "meta", name: "description", content: "A test page" }, { kind: "meta", name: "robots", content: "index, follow" }, ]; const result = resolvePageMetadata(contributions); expect(result.meta).toEqual([ { name: "description", content: "A test page" }, { name: "robots", content: "index, follow" }, ]); }); it("resolves property tags correctly", () => { const contributions: PageMetadataContribution[] = [ { kind: "property", property: "og:title", content: "My Page" }, { kind: "property", property: "og:type", content: "article" }, ]; const result = resolvePageMetadata(contributions); expect(result.properties).toEqual([ { property: "og:title", content: "My Page" }, { property: "og:type", content: "article" }, ]); }); it("resolves canonical link", () => { const contributions: PageMetadataContribution[] = [ { kind: "link", rel: "canonical", href: "https://example.com/page" }, ]; const result = resolvePageMetadata(contributions); expect(result.links).toEqual([{ rel: "canonical", href: "https://example.com/page" }]); }); it("resolves alternate links with hreflang", () => { const contributions: PageMetadataContribution[] = [ { kind: "link", rel: "alternate", href: "https://example.com/en/page", hreflang: "en" }, { kind: "link", rel: "alternate", href: "https://example.com/fr/page", hreflang: "fr" }, ]; const result = resolvePageMetadata(contributions); expect(result.links).toEqual([ { rel: "alternate", href: "https://example.com/en/page", hreflang: "en" }, { rel: "alternate", href: "https://example.com/fr/page", hreflang: "fr" }, ]); }); it("resolves nlweb link for agent discovery", () => { const contributions: PageMetadataContribution[] = [ { kind: "link", rel: "nlweb", href: "https://example.com/nlweb" }, ]; const result = resolvePageMetadata(contributions); expect(result.links).toEqual([{ rel: "nlweb", href: "https://example.com/nlweb" }]); }); it("resolves JSON-LD", () => { const graph = { "@type": "Article", name: "Test" }; const contributions: PageMetadataContribution[] = [{ kind: "jsonld", id: "article", graph }]; const result = resolvePageMetadata(contributions); expect(result.jsonld).toHaveLength(1); expect(result.jsonld[0]!.id).toBe("article"); expect(JSON.parse(result.jsonld[0]!.json)).toEqual(graph); }); it("first-wins dedupe for meta by name", () => { const contributions: PageMetadataContribution[] = [ { kind: "meta", name: "description", content: "First" }, { kind: "meta", name: "description", content: "Second" }, ]; const result = resolvePageMetadata(contributions); expect(result.meta).toHaveLength(1); expect(result.meta[0]!.content).toBe("First"); }); it("first-wins dedupe for meta by explicit key", () => { const contributions: PageMetadataContribution[] = [ { kind: "meta", name: "description", content: "First", key: "seo-desc" }, { kind: "meta", name: "og-description", content: "Second", key: "seo-desc" }, ]; const result = resolvePageMetadata(contributions); expect(result.meta).toHaveLength(1); expect(result.meta[0]!.content).toBe("First"); }); it("first-wins dedupe for property", () => { const contributions: PageMetadataContribution[] = [ { kind: "property", property: "og:title", content: "First" }, { kind: "property", property: "og:title", content: "Second" }, ]; const result = resolvePageMetadata(contributions); expect(result.properties).toHaveLength(1); expect(result.properties[0]!.content).toBe("First"); }); it("canonical is singleton (second canonical ignored)", () => { const contributions: PageMetadataContribution[] = [ { kind: "link", rel: "canonical", href: "https://example.com/first" }, { kind: "link", rel: "canonical", href: "https://example.com/second" }, ]; const result = resolvePageMetadata(contributions); expect(result.links).toHaveLength(1); expect(result.links[0]!.href).toBe("https://example.com/first"); }); it("alternate links deduped by hreflang", () => { const contributions: PageMetadataContribution[] = [ { kind: "link", rel: "alternate", href: "https://example.com/en/v1", hreflang: "en" }, { kind: "link", rel: "alternate", href: "https://example.com/en/v2", hreflang: "en" }, ]; const result = resolvePageMetadata(contributions); expect(result.links).toHaveLength(1); expect(result.links[0]!.href).toBe("https://example.com/en/v1"); }); it("JSON-LD deduped by id", () => { const contributions: PageMetadataContribution[] = [ { kind: "jsonld", id: "article", graph: { "@type": "Article", name: "First" } }, { kind: "jsonld", id: "article", graph: { "@type": "Article", name: "Second" } }, ]; const result = resolvePageMetadata(contributions); expect(result.jsonld).toHaveLength(1); expect(JSON.parse(result.jsonld[0]!.json)).toEqual({ "@type": "Article", name: "First", }); }); it("JSON-LD without id is always appended", () => { const contributions: PageMetadataContribution[] = [ { kind: "jsonld", graph: { "@type": "Article", name: "First" } }, { kind: "jsonld", graph: { "@type": "BreadcrumbList", name: "Second" } }, ]; const result = resolvePageMetadata(contributions); expect(result.jsonld).toHaveLength(2); }); it("rejects non-HTTP link href (javascript:, data:, blob:)", () => { const contributions: PageMetadataContribution[] = [ { kind: "link", rel: "canonical", href: "javascript:alert(1)" }, { kind: "link", rel: "alternate", href: "data:text/html,