import { describe, expect, test } from "bun:test"; import { areWidgetFrameAppearancesEqual, cloneWidgetFrameAppearance, isWidgetFrameAppearance, type WidgetFrameAppearance, type WidgetFrameResolvedBorder, type WidgetFrameResolvedBorderRadius, type WidgetFrameResolvedBoxShadowLayer, } from "./widget-frame-appearance"; const resolvedBorder: WidgetFrameResolvedBorder = { color: { colorSpace: "srgb", red: 221, green: 221, blue: 221, alpha: 0.8, }, style: "solid", widthPx: 1, }; const resolvedBorderRadius: WidgetFrameResolvedBorderRadius = { topLeft: { horizontalPx: 12, verticalPx: 8 }, topRight: { horizontalPx: 10, verticalPx: 6 }, bottomRight: { horizontalPx: 4, verticalPx: 3 }, bottomLeft: { horizontalPx: 2, verticalPx: 1 }, }; const outerShadow: WidgetFrameResolvedBoxShadowLayer = { color: { colorSpace: "srgb", red: 12, green: 34, blue: 56, alpha: 0.24, }, offsetXPx: 0, offsetYPx: 4, blurRadiusPx: 12, spreadRadiusPx: -2, inset: false, }; const insetShadow: WidgetFrameResolvedBoxShadowLayer = { color: { colorSpace: "srgb", red: 255, green: 255, blue: 255, alpha: 0.5, }, offsetXPx: 1, offsetYPx: 2, blurRadiusPx: 0, spreadRadiusPx: 3, inset: true, }; const resolvedBoxShadow: WidgetFrameResolvedBoxShadowLayer[] = [ outerShadow, insetShadow, ]; describe("widget frame appearance", () => { test("accepts legacy and resolved page appearances", () => { expect(isWidgetFrameAppearance({ kind: "none" })).toBe(true); expect( isWidgetFrameAppearance({ kind: "page", border: "1px solid #ddd", borderRadius: "12px", boxShadow: "0 2px 8px rgb(0 0 0 / 0.12)", resolvedBorder, resolvedBorderRadius, resolvedBoxShadow, }), ).toBe(true); expect( isWidgetFrameAppearance({ kind: "page", border: "none", borderRadius: "0", boxShadow: "none", resolvedBoxShadow: [], }), ).toBe(true); }); test("rejects missing or invalid page fields", () => { expect(isWidgetFrameAppearance(undefined)).toBe(false); expect( isWidgetFrameAppearance({ kind: "page", borderRadius: "12px", boxShadow: "none", }), ).toBe(false); expect( isWidgetFrameAppearance({ kind: "page", border: 1, borderRadius: "12px", boxShadow: "none", }), ).toBe(false); expect( isWidgetFrameAppearance({ kind: "none", border: "unexpected" }), ).toBe(false); expect( isWidgetFrameAppearance({ kind: "page", border: "none", borderRadius: "0", boxShadow: "none", extra: true, }), ).toBe(false); const arrayAppearance = ["unexpected"] as unknown[] & { kind: "none" }; arrayAppearance.kind = "none"; expect(isWidgetFrameAppearance(arrayAppearance)).toBe(false); const inheritedAppearance = Object.create({ kind: "page", border: "none", borderRadius: "0", boxShadow: "none", }) as unknown; expect(isWidgetFrameAppearance(inheritedAppearance)).toBe(false); const nonEnumerableAppearance = {}; for (const [key, value] of Object.entries({ kind: "page", border: "none", borderRadius: "0", boxShadow: "none", })) { Object.defineProperty(nonEnumerableAppearance, key, { value }); } expect(isWidgetFrameAppearance(nonEnumerableAppearance)).toBe(false); const validAppearance = { kind: "page" as const, border: "none", borderRadius: "0", boxShadow: "none", resolvedBorder, }; for (const invalidResolvedBorder of [ { ...resolvedBorder, style: "invalid" }, { ...resolvedBorder, widthPx: -1 }, { ...resolvedBorder, widthPx: Number.NaN }, { ...resolvedBorder, widthPx: Number.POSITIVE_INFINITY }, { ...resolvedBorder, color: { ...resolvedBorder.color, red: 1.5 }, }, { ...resolvedBorder, color: { ...resolvedBorder.color, green: -1 }, }, { ...resolvedBorder, color: { ...resolvedBorder.color, blue: 256 }, }, { ...resolvedBorder, color: { ...resolvedBorder.color, alpha: Number.NaN }, }, { ...resolvedBorder, color: { ...resolvedBorder.color, alpha: 1.1 }, }, ]) { expect( isWidgetFrameAppearance({ ...validAppearance, resolvedBorder: invalidResolvedBorder, }), ).toBe(false); } expect( isWidgetFrameAppearance({ ...validAppearance, resolvedBorder: { ...resolvedBorder, extra: true }, }), ).toBe(false); expect( isWidgetFrameAppearance({ ...validAppearance, resolvedBorder: { ...resolvedBorder, color: { ...resolvedBorder.color, extra: true }, }, }), ).toBe(false); const inheritedResolvedBorder = Object.create({ widthPx: 1 }); Object.assign(inheritedResolvedBorder, { color: resolvedBorder.color, style: resolvedBorder.style, }); expect( isWidgetFrameAppearance({ ...validAppearance, resolvedBorder: inheritedResolvedBorder, }), ).toBe(false); const inheritedColor = Object.create({ red: 221 }); Object.assign(inheritedColor, { colorSpace: "srgb", green: 221, blue: 221, alpha: 0.8, }); expect( isWidgetFrameAppearance({ ...validAppearance, resolvedBorder: { ...resolvedBorder, color: inheritedColor }, }), ).toBe(false); expect( isWidgetFrameAppearance({ ...validAppearance, resolvedBorder: { ...resolvedBorder, color: [] }, }), ).toBe(false); for (const invalidResolvedBorderRadius of [ { ...resolvedBorderRadius, topLeft: undefined }, { ...resolvedBorderRadius, topLeft: { ...resolvedBorderRadius.topLeft, horizontalPx: -1 }, }, { ...resolvedBorderRadius, topLeft: { ...resolvedBorderRadius.topLeft, verticalPx: Number.NaN }, }, { ...resolvedBorderRadius, topLeft: { ...resolvedBorderRadius.topLeft, extra: true }, }, { ...resolvedBorderRadius, extra: true }, ]) { expect( isWidgetFrameAppearance({ ...validAppearance, resolvedBorderRadius: invalidResolvedBorderRadius, }), ).toBe(false); } const inheritedRadius = Object.create(resolvedBorderRadius); expect( isWidgetFrameAppearance({ ...validAppearance, resolvedBorderRadius: inheritedRadius, }), ).toBe(false); const nonEnumerableRadiusAppearance = { ...validAppearance }; Object.defineProperty( nonEnumerableRadiusAppearance, "resolvedBorderRadius", { value: resolvedBorderRadius }, ); expect(isWidgetFrameAppearance(nonEnumerableRadiusAppearance)).toBe(false); for (const invalidResolvedBoxShadow of [ { not: "an array" }, [{ ...outerShadow, blurRadiusPx: -1 }], [{ ...outerShadow, blurRadiusPx: Number.NaN }], [{ ...outerShadow, offsetXPx: Number.POSITIVE_INFINITY }], [{ ...outerShadow, spreadRadiusPx: Number.NEGATIVE_INFINITY }], [{ ...outerShadow, inset: "false" }], [{ ...outerShadow, extra: true }], [ { ...outerShadow, color: { ...outerShadow.color, alpha: 2 }, }, ], new Array(1), ]) { expect( isWidgetFrameAppearance({ ...validAppearance, resolvedBoxShadow: invalidResolvedBoxShadow, }), ).toBe(false); } const sparseShadowWithExtraKey = new Array(1) as unknown[] & { extra?: WidgetFrameResolvedBoxShadowLayer; }; sparseShadowWithExtraKey.extra = outerShadow; expect( isWidgetFrameAppearance({ ...validAppearance, resolvedBoxShadow: sparseShadowWithExtraKey, }), ).toBe(false); const inheritedShadowLayer = Object.create(outerShadow); expect( isWidgetFrameAppearance({ ...validAppearance, resolvedBoxShadow: [inheritedShadowLayer], }), ).toBe(false); const nonEnumerableShadowAppearance = { ...validAppearance }; Object.defineProperty(nonEnumerableShadowAppearance, "resolvedBoxShadow", { value: resolvedBoxShadow, }); expect(isWidgetFrameAppearance(nonEnumerableShadowAppearance)).toBe(false); }); test("compares omitted, none, and page appearances by value", () => { const page: WidgetFrameAppearance = { kind: "page", border: "1px solid #ddd", borderRadius: "12px", boxShadow: "none", resolvedBorder, resolvedBorderRadius, resolvedBoxShadow, }; expect(areWidgetFrameAppearancesEqual(undefined, undefined)).toBe(true); expect(areWidgetFrameAppearancesEqual(undefined, { kind: "none" })).toBe( false, ); expect( areWidgetFrameAppearancesEqual({ kind: "none" }, { kind: "none" }), ).toBe(true); expect(areWidgetFrameAppearancesEqual(page, { ...page })).toBe(true); expect( areWidgetFrameAppearancesEqual(page, { ...page, boxShadow: "0 2px 8px rgb(0 0 0 / 0.12)", }), ).toBe(false); expect( areWidgetFrameAppearancesEqual(page, { ...page, resolvedBoxShadow: [{ ...insetShadow }, { ...outerShadow }], }), ).toBe(false); expect( areWidgetFrameAppearancesEqual( { ...page, resolvedBoxShadow: undefined }, { ...page, resolvedBoxShadow: [] }, ), ).toBe(false); for (const changedOuterShadow of [ { ...outerShadow, offsetXPx: 1 }, { ...outerShadow, offsetYPx: 5 }, { ...outerShadow, blurRadiusPx: 13 }, { ...outerShadow, spreadRadiusPx: 1 }, { ...outerShadow, inset: true }, { ...outerShadow, color: { ...outerShadow.color, red: 13 } }, { ...outerShadow, color: { ...outerShadow.color, green: 35 } }, { ...outerShadow, color: { ...outerShadow.color, blue: 57 } }, { ...outerShadow, color: { ...outerShadow.color, alpha: 0.25 } }, ]) { expect( areWidgetFrameAppearancesEqual(page, { ...page, resolvedBoxShadow: [changedOuterShadow, insetShadow], }), ).toBe(false); } expect( areWidgetFrameAppearancesEqual( { ...page, resolvedBorder: undefined }, { ...page, resolvedBorder: undefined }, ), ).toBe(true); expect( areWidgetFrameAppearancesEqual( { ...page, resolvedBorder: undefined }, page, ), ).toBe(false); for (const changedResolvedBorder of [ { ...resolvedBorder, style: "double" as const }, { ...resolvedBorder, widthPx: 2 }, { ...resolvedBorder, color: { ...resolvedBorder.color, red: 1 }, }, { ...resolvedBorder, color: { ...resolvedBorder.color, green: 1 }, }, { ...resolvedBorder, color: { ...resolvedBorder.color, blue: 1 }, }, { ...resolvedBorder, color: { ...resolvedBorder.color, alpha: 0.4 }, }, ]) { expect( areWidgetFrameAppearancesEqual(page, { ...page, resolvedBorder: changedResolvedBorder, }), ).toBe(false); } expect( areWidgetFrameAppearancesEqual(page, { ...page, resolvedBorderRadius: { ...resolvedBorderRadius, topRight: { ...resolvedBorderRadius.topRight, verticalPx: 7, }, }, }), ).toBe(false); }); test("deep-clones resolved frame values and preserves undefined", () => { const appearance: WidgetFrameAppearance = { kind: "page", border: "1px solid #ddd", borderRadius: "12px", boxShadow: "none", resolvedBorder: { ...resolvedBorder, color: { ...resolvedBorder.color }, }, resolvedBorderRadius: { ...resolvedBorderRadius, topLeft: { ...resolvedBorderRadius.topLeft }, }, resolvedBoxShadow: resolvedBoxShadow.map((layer) => ({ ...layer, color: { ...layer.color }, })), }; const clone = cloneWidgetFrameAppearance(appearance); appearance.border = "none"; if (appearance.resolvedBorder !== undefined) { appearance.resolvedBorder.color.red = 0; } if (appearance.resolvedBorderRadius !== undefined) { appearance.resolvedBorderRadius.topLeft.horizontalPx = 0; } const firstShadow = appearance.resolvedBoxShadow?.[0]; if (firstShadow !== undefined) { firstShadow.color.alpha = 1; firstShadow.offsetYPx = 99; } expect(clone).toEqual({ kind: "page", border: "1px solid #ddd", borderRadius: "12px", boxShadow: "none", resolvedBorder, resolvedBorderRadius, resolvedBoxShadow, }); expect(clone).not.toBe(appearance); if (clone?.kind === "page") { expect(clone.resolvedBorder).not.toBe(appearance.resolvedBorder); expect(clone.resolvedBorder?.color).not.toBe( appearance.resolvedBorder?.color, ); expect(clone.resolvedBorderRadius).not.toBe( appearance.resolvedBorderRadius, ); expect(clone.resolvedBorderRadius?.topLeft).not.toBe( appearance.resolvedBorderRadius?.topLeft, ); expect(clone.resolvedBoxShadow).not.toBe(appearance.resolvedBoxShadow); expect(clone.resolvedBoxShadow?.[0]).not.toBe( appearance.resolvedBoxShadow?.[0], ); expect(clone.resolvedBoxShadow?.[0]?.color).not.toBe( appearance.resolvedBoxShadow?.[0]?.color, ); } const legacyAppearance: WidgetFrameAppearance = { kind: "page", border: "none", borderRadius: "0", boxShadow: "none", }; const legacyClone = cloneWidgetFrameAppearance(legacyAppearance); expect(legacyClone).toEqual(legacyAppearance); expect(Object.hasOwn(legacyClone, "resolvedBorder")).toBe(false); const undefinedResolvedBorderAppearance = { ...legacyAppearance, resolvedBorder: undefined, }; const undefinedResolvedBorderClone = cloneWidgetFrameAppearance( undefinedResolvedBorderAppearance, ); expect(undefinedResolvedBorderClone).toEqual( undefinedResolvedBorderAppearance, ); expect(Object.hasOwn(undefinedResolvedBorderClone, "resolvedBorder")).toBe( true, ); expect(undefinedResolvedBorderClone.resolvedBorder).toBeUndefined(); const undefinedResolvedBoxShadowAppearance = { ...legacyAppearance, resolvedBoxShadow: undefined, }; const undefinedResolvedBoxShadowClone = cloneWidgetFrameAppearance( undefinedResolvedBoxShadowAppearance, ); expect(undefinedResolvedBoxShadowClone).toEqual( undefinedResolvedBoxShadowAppearance, ); expect( Object.hasOwn(undefinedResolvedBoxShadowClone, "resolvedBoxShadow"), ).toBe(true); expect(cloneWidgetFrameAppearance(undefined)).toBeUndefined(); }); });