返回 CodeWhale
faq-schema.test.ts
根目录 / web / lib / faq-schema.test.ts
1 import { createElement } from "react";
2 import { readFileSync } from "node:fs";
3 import { describe, expect, it } from "vitest";
4 import { buildFaqPageJsonLd } from "./faq-schema";
5 import { serializeJsonLd } from "./json-ld";
6 import { extractText, flattenExtractedText } from "./react-text";
7 import { SITE_URL } from "./page-meta";
8
9 const faqPage = readFileSync(new URL("../app/[locale]/faq/page.tsx", import.meta.url), "utf8");
10
11 describe("FAQPage structured data", () => {
12 it("maps every question to an acceptedAnswer with flattened text", () => {
13 const items = [
14 {
15 q: "What is Codewhale?",
16 a: createElement(
17 "p",
18 null,
19 "A terminal-native agent. Run ",
20 createElement("code", null, "codewhale"),
21 ".",
22 ),
23 },
24 {
25 q: "How do I install?",
26 a: ["npm install -g codewhale", createElement("span", null, " or Cargo.")],
27 },
28 ];
29 const schema = buildFaqPageJsonLd({
30 items,
31 url: `${SITE_URL}/en/faq`,
32 inLanguage: "en",
33 });
34
35 expect(schema).toEqual({
36 "@context": "https://schema.org",
37 "@type": "FAQPage",
38 url: "https://codewhale.net/en/faq",
39 inLanguage: "en",
40 mainEntity: [
41 {
42 "@type": "Question",
43 name: "What is Codewhale?",
44 acceptedAnswer: {
45 "@type": "Answer",
46 text: "A terminal-native agent. Run codewhale .",
47 },
48 },
49 {
50 "@type": "Question",
51 name: "How do I install?",
52 acceptedAnswer: {
53 "@type": "Answer",
54 text: "npm install -g codewhale or Cargo.",
55 },
56 },
57 ],
58 });
59 });
60
61 it("covers all 40 curated pairs from the FAQ page arrays", () => {
62 const questions = [...faqPage.matchAll(/^\s+q: "(.+)"/gm)].map((match) => match[1]);
63 expect(questions).toHaveLength(40);
64 expect(new Set(questions).size).toBe(40);
65 expect(faqPage).toContain("const faqEn");
66 expect(faqPage).toContain("const faqZh");
67 expect(faqPage).toContain("buildFaqPageJsonLd({");
68 expect(faqPage).toContain("items,");
69 expect(faqPage).toContain('canonicalLocaleForPath("/faq", locale)');
70 expect(faqPage).toContain('type="application/ld+json"');
71 expect(faqPage).toContain("serializeJsonLd(jsonLd)");
72 });
73 });
74
75 describe("extractText", () => {
76 it("flattens nested elements and ignores booleans", () => {
77 const node = createElement(
78 "div",
79 null,
80 "Hello ",
81 createElement("code", null, "codewhale"),
82 false,
83 createElement("span", null, [" ", 2]),
84 );
85
86 expect(extractText(node)).toContain("codewhale");
87 expect(flattenExtractedText(node)).toBe("Hello codewhale 2");
88 });
89
90 it("escapes angle brackets so JSON-LD cannot close the script tag", () => {
91 expect(serializeJsonLd({ text: "</script>" })).toBe('{"text":"\\u003c/script>"}');
92 });
93 });
94
94 lines TYPESCRIPT