import { describe, expect, it } from "vitest"; import { normalizeStyle } from "../src/vue-helper/style-helper"; describe("normalizeStyle", () => { // ========== 单参数:纯转换 ========== // ---------- 空值 / falsy ---------- it("应将 undefined 转为空对象", () => { expect(normalizeStyle(undefined as any)).toEqual({}); }); it("应将 null 转为空对象", () => { expect(normalizeStyle(null as any)).toEqual({}); }); it("应将 false 转为空对象", () => { expect(normalizeStyle(false as any)).toEqual({}); }); // ---------- CSSProperties 对象 ---------- it("应直接返回 CSSProperties 对象", () => { const style = { color: "red", fontSize: "14px" }; expect(normalizeStyle(style)).toEqual(style); }); it("应保留 CSSProperties 中的数字值", () => { const style = { zIndex: 10, opacity: 0.5 }; expect(normalizeStyle(style)).toEqual({ zIndex: 10, opacity: 0.5 }); }); it("应保留 CSS 变量", () => { const style = { "--custom-color": "red" } as any; expect(normalizeStyle(style)).toEqual({ "--custom-color": "red" }); }); it("空对象应返回空对象", () => { expect(normalizeStyle({})).toEqual({}); }); // ---------- 字符串 ---------- it("应解析简单的 CSS 字符串", () => { expect(normalizeStyle("color: red")).toEqual({ color: "red" }); }); it("应解析多条声明的 CSS 字符串", () => { expect(normalizeStyle("color: red; font-size: 14px")).toEqual({ color: "red", "font-size": "14px", }); }); it("应解析带空格的 CSS 字符串", () => { expect(normalizeStyle(" color : red ; font-size : 14px ")).toEqual({ color: "red", "font-size": "14px", }); }); it("应忽略空声明", () => { expect(normalizeStyle("color: red;; font-size:14px;")).toEqual({ color: "red", "font-size": "14px", }); }); it("应忽略尾部分号后的空项", () => { expect(normalizeStyle("color: red;")).toEqual({ color: "red" }); }); it("应去除 CSS 注释", () => { expect(normalizeStyle("color: red; /* 注释 */ font-size: 14px")).toEqual({ color: "red", "font-size": "14px", }); }); it("应解析含 url() 的值(不拆分括号内的分号)", () => { expect(normalizeStyle("background: url(data:image/png;base64,abc)")).toEqual({ background: "url(data:image/png;base64,abc)", }); }); it("应解析含冒号的值(如 url 中的协议)", () => { expect(normalizeStyle("background: url(http://example.com/img.png)")).toEqual({ background: "url(http://example.com/img.png)", }); }); it("空字符串应返回空对象", () => { expect(normalizeStyle("")).toEqual({}); }); // ---------- 数组 ---------- it("应合并对象数组", () => { expect(normalizeStyle([{ color: "red" }, { fontSize: "14px" }])).toEqual({ color: "red", fontSize: "14px", }); }); it("应合并字符串数组", () => { expect(normalizeStyle(["color: red", "font-size: 14px"])).toEqual({ color: "red", "font-size": "14px", }); }); it("应合并混合类型数组", () => { expect(normalizeStyle(["color: red", { fontSize: "14px" }])).toEqual({ color: "red", fontSize: "14px", }); }); it("应处理嵌套数组", () => { expect(normalizeStyle([["color: red", { fontSize: "14px" }], { fontWeight: "bold" }])).toEqual({ color: "red", fontSize: "14px", fontWeight: "bold", }); }); it("应跳过数组中的 falsy 值", () => { expect(normalizeStyle([null, false, undefined, { color: "red" }] as any)).toEqual({ color: "red", }); }); it("数组中后面的属性应覆盖前面的", () => { expect(normalizeStyle([{ color: "red" }, { color: "blue" }])).toEqual({ color: "blue", }); }); it("空数组应返回空对象", () => { expect(normalizeStyle([])).toEqual({}); }); // ---------- 实际使用场景 ---------- it("应处理 AppButton 典型的 customStyle 用法", () => { // 字符串形式(旧用法兼容) expect(normalizeStyle("color: red; font-size: 28rpx")).toEqual({ color: "red", "font-size": "28rpx", }); // 对象形式(推荐用法) expect(normalizeStyle({ color: "red", fontSize: "28rpx" })).toEqual({ color: "red", fontSize: "28rpx", }); // 数组形式 expect(normalizeStyle([{ color: "red" }, { fontSize: "28rpx" }])).toEqual({ color: "red", fontSize: "28rpx", }); }); // ========== 双参数:合并覆盖 ========== describe("合并覆盖(第二个参数)", () => { it("不传第二个参数时,行为与单参数一致", () => { expect(normalizeStyle({ color: "red" })).toEqual({ color: "red" }); }); it("第二个参数为 undefined 时,行为与单参数一致", () => { expect(normalizeStyle({ color: "red" }, undefined)).toEqual({ color: "red" }); }); it("第二个参数为 null 时,行为与单参数一致", () => { expect(normalizeStyle({ color: "red" }, null as any)).toEqual({ color: "red" }); }); it("overrides 有值的属性应覆盖 base", () => { expect(normalizeStyle({ color: "red" }, { color: "blue" })).toEqual({ color: "blue" }); }); it("overrides 新增的属性应合并进来", () => { expect(normalizeStyle({ color: "red" }, { fontSize: "14px" })).toEqual({ color: "red", fontSize: "14px", }); }); it("overrides 和 base 各有不同的属性时,应全部合并", () => { expect( normalizeStyle({ color: "red", padding: "8px" }, { fontSize: "14px", margin: "4px" }), ).toEqual({ color: "red", padding: "8px", fontSize: "14px", margin: "4px", }); }); // ---------- overrides 为空值时 fallback 到 base ---------- it("overrides 属性为 undefined 时应使用 base 的值", () => { expect(normalizeStyle({ color: "red" }, { color: undefined } as any)).toEqual({ color: "red", }); }); it("overrides 属性为 null 时应使用 base 的值", () => { expect(normalizeStyle({ color: "red" }, { color: null } as any)).toEqual({ color: "red" }); }); it("overrides 属性为空字符串时应使用 base 的值", () => { expect(normalizeStyle({ color: "red" }, { color: "" })).toEqual({ color: "red" }); }); it("overrides 属性为 false 时应使用 base 的值", () => { expect(normalizeStyle({ color: "red" }, { color: false } as any)).toEqual({ color: "red" }); }); // ---------- 两者都为空时过滤属性 ---------- it("base 和 overrides 都为空时,应过滤掉该属性", () => { expect(normalizeStyle({ color: "" }, { color: undefined } as any)).toEqual({}); }); it("base 和 overrides 都为 null 时,应过滤掉该属性", () => { expect(normalizeStyle({ color: null } as any, { color: null } as any)).toEqual({}); }); it("base 有值但属性为空、overrides 未指定该属性时,应过滤掉", () => { // base 有 color:"" (空值), overrides 没有 color → both empty → filtered expect(normalizeStyle({ color: "" }, {})).toEqual({}); }); // ---------- 字符串形式的 overrides ---------- it("overrides 为字符串时应正常解析并合并", () => { expect(normalizeStyle({ color: "red" }, "font-size: 14px")).toEqual({ color: "red", "font-size": "14px", }); }); it("base 为字符串、overrides 为对象时应正常合并", () => { // 字符串解析出 kebab-case 键,对象用 camelCase 键,两者独立 expect(normalizeStyle("color: red", { fontSize: "14px" })).toEqual({ color: "red", fontSize: "14px", }); }); // ---------- 数组形式的 overrides ---------- it("overrides 为数组时应正常解析并合并", () => { expect(normalizeStyle({ color: "red" }, [{ fontSize: "14px" }])).toEqual({ color: "red", fontSize: "14px", }); }); // ---------- 0 是有效值,不算空 ---------- it("数字 0 应视为有效值,不当作空值", () => { expect(normalizeStyle({ opacity: 1 }, { opacity: 0 })).toEqual({ opacity: 0 }); }); it("overrides 为 0 时不应 fallback 到 base", () => { expect(normalizeStyle({ zIndex: 10 }, { zIndex: 0 })).toEqual({ zIndex: 0 }); }); // ---------- 综合场景 ---------- it("应正确处理混合覆盖和 fallback", () => { const base = { color: "red", fontSize: "14px", padding: "8px", margin: "" }; const overrides = { color: "blue", fontSize: undefined, padding: null, margin: "4px" } as any; expect(normalizeStyle(base, overrides)).toEqual({ color: "blue", // overrides 有值,覆盖 fontSize: "14px", // overrides 为 undefined,fallback 到 base padding: "8px", // overrides 为 null,fallback 到 base margin: "4px", // base 为空,overrides 有值,使用 overrides }); }); it("appButton 场景:customStyle 覆盖内部样式", () => { // 模拟 AppButton 内部样式 + customStyle const internal = { width: "200rpx", backgroundColor: "#3adceb", color: "#111" }; const custom = { width: "128rpx", backgroundColor: undefined, color: "" } as any; expect(normalizeStyle(internal, custom)).toEqual({ width: "128rpx", // overrides 有值,覆盖 backgroundColor: "#3adceb", // overrides 为 undefined,fallback color: "#111", // overrides 为空字符串,fallback }); }); }); });