import { describe, expect, it } from "vitest"; import { EXTRA_KEY_RE, SITE_CONFIG_FIELD_DEFS, extraRowError, firstExtraError, parseSiteConfig, serializeSiteConfig, validateFieldValue, } from "./siteConfigForm"; /** docs/09 §3.1 站点配置结构化:parse/serialize/校验 */ describe("siteConfigForm", () => { it("parse:已知键入 fields、未知字符串键入扩展行、theme_params/对象键无损入 preserved", () => { const json = JSON.stringify({ logo: "/cms/upload/logo.png", contact: "电话 0571-88888888", domain: "www.xxx.edu.cn", public_base_url: "https://www.xxx.edu.cn", my_ext: "自定义值", theme_params: { primary: "#b01a2e" }, wechat: { appid: "wx123" }, }); const m = parseSiteConfig(json); expect(m.fields.logo).toBe("/cms/upload/logo.png"); expect(m.fields.contact).toBe("电话 0571-88888888"); expect(m.fields.domain).toBe("www.xxx.edu.cn"); expect(m.fields.public_base_url).toBe("https://www.xxx.edu.cn"); expect(m.extras).toEqual([{ key: "my_ext", value: "自定义值" }]); expect(m.preserved.theme_params).toEqual({ primary: "#b01a2e" }); expect(m.preserved.wechat).toEqual({ appid: "wx123" }); }); it("parse:非法 JSON / 空值按空配置处理,已知键非字符串值无损保留", () => { expect(parseSiteConfig("这不是json{").fields).toEqual({}); expect(parseSiteConfig(null).extras).toEqual([]); expect(parseSiteConfig("").preserved).toEqual({}); const m = parseSiteConfig(JSON.stringify({ logo: 123, other: [1, 2] })); expect(m.fields.logo).toBeUndefined(); expect(m.preserved.logo).toBe(123); expect(m.preserved.other).toEqual([1, 2]); }); it("serialize:空值剔除、扩展行空键/空值剔除、preserved 永不丢失", () => { const m = parseSiteConfig(JSON.stringify({ logo: "/cms/upload/logo.png", theme_params: { primary: "#fff" }, wechat: { x: 1 } })); m.fields.contact = " "; // 空白 → 剔除 m.extras.push({ key: "k1", value: "v1" }); m.extras.push({ key: "", value: "被剔除" }); m.extras.push({ key: "k2", value: " " }); // 空值 → 剔除 const out = JSON.parse(serializeSiteConfig(m)); expect(out.logo).toBe("/cms/upload/logo.png"); expect(out).not.toHaveProperty("contact"); expect(out.k1).toBe("v1"); expect(out).not.toHaveProperty("k2"); expect(out.theme_params).toEqual({ primary: "#fff" }); expect(out.wechat).toEqual({ x: 1 }); }); it("roundtrip:parse→serialize→parse 内容不丢(含扩展行与 preserved)", () => { const json = JSON.stringify({ copyright: "XX学校 · 版权所有 © 2026", seo_description: "校讯", my_ext: "v", theme_params: { bg: "#f7f5f2" }, }); const once = parseSiteConfig(json); once.fields.slogan = "厚德博学"; const twice = parseSiteConfig(serializeSiteConfig(once)); expect(twice.fields.slogan).toBe("厚德博学"); expect(twice.fields.copyright).toBe("XX学校 · 版权所有 © 2026"); expect(twice.extras).toEqual([{ key: "my_ext", value: "v" }]); expect(twice.preserved.theme_params).toEqual({ bg: "#f7f5f2" }); }); it("validateFieldValue:url/host/asset/text 各类型通过与拒绝", () => { const def = (kind: string, maxLength?: number) => SITE_CONFIG_FIELD_DEFS.find(d => d.key === { url: "public_base_url", host: "domain", asset: "logo", text: "copyright" }[kind])!; expect(validateFieldValue(def("url"), "https://www.xxx.edu.cn")).toBe(""); expect(validateFieldValue(def("url"), "www.xxx.edu.cn")).toContain("http"); expect(validateFieldValue(def("host"), "www.xxx.edu.cn")).toBe(""); expect(validateFieldValue(def("host"), "https://www.xxx.edu.cn")).toContain("域名"); expect(validateFieldValue(def("asset"), "/cms/upload/a.png")).toBe(""); expect(validateFieldValue(def("asset"), "https://cdn.example.com/a.png")).toBe(""); expect(validateFieldValue(def("asset"), "javascript:alert(1)")).toContain("路径"); expect(validateFieldValue(def("text"), "正常文字")).toBe(""); expect(validateFieldValue({ key: "t", label: "t", group: "footer", kind: "text", maxLength: 5 }, "超过五个字的文案")).toContain("5"); expect(validateFieldValue(def("url"), "")).toBe(""); // 空值一律放行(非必填) }); it("extraRowError:格式/重名/重复校验;空键行放行", () => { const rows = [{ key: "my_ext", value: "v" }]; expect(extraRowError(rows, 0)).toBe(""); expect(extraRowError([{ key: "", value: "v" }], 0)).toBe(""); expect(extraRowError([{ key: "my-ext", value: "v" }], 0)).toContain("下划线"); expect(extraRowError([{ key: "1bad", value: "v" }], 0)).toContain("下划线"); expect(extraRowError([{ key: "logo", value: "v" }], 0)).toContain("重名"); expect(extraRowError([{ key: "theme_params", value: "v" }], 0)).toContain("重名"); expect(extraRowError([{ key: "a", value: "1" }, { key: "a", value: "2" }], 1)).toContain("重复"); expect(EXTRA_KEY_RE.test("wechat_appid")).toBe(true); expect(EXTRA_KEY_RE.test("_priv")).toBe(true); expect(EXTRA_KEY_RE.test("9bad")).toBe(false); }); it("firstExtraError:返回第一条错误;全部合法返回空串", () => { expect(firstExtraError([{ key: "ok_key", value: "1" }, { key: "bad-key", value: "2" }])).toContain("下划线"); expect(firstExtraError([{ key: "a", value: "1" }, { key: "b", value: "2" }])).toBe(""); expect(firstExtraError([])).toBe(""); }); it("已知键清单与 docs/09 §0 键盘点表一致(防漂移)", () => { expect(SITE_CONFIG_FIELD_DEFS.map(d => d.key)).toEqual([ "logo", "favicon", "slogan", "colleges_label", "news_bg_image", "gallery_title", "gallery_subtitle", "contact", "footer_addresses", "qr_wechat", "qr_weibo", "social_wechat", "social_weibo", "social_douyin", "social_bilibili", "copyright", "beian", "seo_description", "domain", "public_base_url", ]); }); it("docs/12:页脚增强与首页视觉新键 parse/serialize/校验(qr_* asset、social_* url)", () => { const json = JSON.stringify({ footer_addresses: "潭山校区:南湖路 237 号 邮编:464000\n淮河校区:大学路 1 号 邮编:464300", qr_wechat: "/cms/upload/qr-wechat.png", social_weibo: "https://weibo.com/xxx", social_douyin: "not-a-url", gallery_title: "线上展厅", }); const m = parseSiteConfig(json); expect(m.fields.footer_addresses).toContain("淮河校区"); expect(m.fields.qr_wechat).toBe("/cms/upload/qr-wechat.png"); expect(m.fields.social_weibo).toBe("https://weibo.com/xxx"); expect(m.extras).toEqual([]); // 新键均为已知键,不落扩展行 const qrDef = SITE_CONFIG_FIELD_DEFS.find(d => d.key === "qr_weibo")!; const socialDef = SITE_CONFIG_FIELD_DEFS.find(d => d.key === "social_douyin")!; expect(validateFieldValue(qrDef, "javascript:alert(1)")).toContain("路径"); expect(validateFieldValue(socialDef, "not-a-url")).toContain("http"); // 空值统一剔除:不污染 config_json m.fields.social_bilibili = " "; const out = JSON.parse(serializeSiteConfig(m)); expect(out.footer_addresses).toContain("邮编:464300"); expect(out).not.toHaveProperty("social_bilibili"); expect(out.gallery_title).toBe("线上展厅"); }); it("B3:custom_css 为保留键,不入扩展行,parse/serialize 无损往返", () => { const json = JSON.stringify({ custom_css: ".site-header{padding:16px 0}", my_ext: "v" }); const m = parseSiteConfig(json); expect(m.extras).toEqual([{ key: "my_ext", value: "v" }]); expect(m.preserved.custom_css).toBe(".site-header{padding:16px 0}"); expect(JSON.parse(serializeSiteConfig(m)).custom_css).toBe(".site-header{padding:16px 0}"); }); });