import { describe, expect, it, vi } from "vitest"; import type { CanvasState } from "@dada/shared-contracts"; import { COLOR_CARD_SOURCE_GEOMETRY, P0A_COLOR_CARDS, createColorCardElement, extractMmcqPalette, refreshColorCards, } from "../../apps/web/src/palette-provider.js"; import { DYN012_RENDER_LAYOUT, LocationConsentGate, P0A_DYNAMIC_STICKERS, createDynamicStickerElement, normalizeSocialId, overrideDynamicStickerValue, } from "../../apps/web/src/dynamic-provider.js"; import { DYNAMIC_RENDER_MODELS, DYNAMIC_RESOURCE_VERSION } from "../../apps/web/src/dynamic-render-models.js"; const identity = { createdAt: "2026-08-03T04:00:00.000Z", elementId: "00000000-0000-4000-8000-000000000801" }; function canvas(elements: CanvasState["elements"] = []): CanvasState { return { background: { adjustments: { brightness: 0, contrast: 0, crop: null, filter: "none", fit: "fill", saturation: 0, sharpness: 0, temperature: 0 }, asset_id: "00000000-0000-4000-8000-000000000802" }, elements, pixel_height: 1440, pixel_width: 1080, ratio: "3:4", schema_version: 1, }; } function colorPixels(colors: Array<{ count: number; rgb: [number, number, number] }>) { const values: number[] = []; for (const color of colors) { for (let index = 0; index < color.count; index += 1) values.push(...color.rgb, 255); } return new Uint8ClampedArray(values); } describe("TASK-WP4-04 deterministic color cards", () => { it("keeps the archived 320px color-card geometry instead of enlarged approximations", () => { expect(COLOR_CARD_SOURCE_GEOMETRY).toMatchObject({ style_01: { bounds: { bottom: 76, left: -26, right: 26, top: -74 }, palette: { bottom: 37, left: -23, right: 23, top: -71 } }, style_02: { bounds: { bottom: 69, left: -18, right: 18, top: -77 } }, style_08: { bounds: { bottom: 10, left: -73, right: 73, top: -9 } }, style_16: { bounds: { bottom: 9, left: -78, right: 78, top: -9 } }, }); }); it("uses the four-item P0-A allowlist and produces a stable five-color MMCQ palette", () => { expect(P0A_COLOR_CARDS.map((item) => item.cardId)).toEqual(["COLOR001", "COLOR002", "COLOR008", "COLOR016"]); const pixels = colorPixels([ { count: 50, rgb: [244, 32, 32] }, { count: 40, rgb: [32, 210, 96] }, { count: 30, rgb: [24, 96, 220] }, { count: 20, rgb: [248, 210, 48] }, { count: 10, rgb: [120, 64, 180] }, ]); const first = extractMmcqPalette(pixels); const second = extractMmcqPalette(pixels); expect(first).toEqual(second); expect(first).toHaveLength(5); expect(first).toEqual([...first].sort((left, right) => right.population - left.population || left.rgbValue - right.rgbValue)); expect(first.every((entry) => /^#[0-9A-F]{6}$/.test(entry.hex))).toBe(true); }); it("snapshots five colors and only refreshes them when the raw background changes", () => { const initialPalette = ["#F42020", "#20D260", "#1860DC", "#F8D230", "#7840B4"]; const replacement = ["#111111", "#333333", "#555555", "#777777", "#999999"]; const element = createColorCardElement(P0A_COLOR_CARDS[0]!, initialPalette, identity, 0); const state = canvas([element]); expect(element.style_id).toBe("style_01"); expect(element.style_parameters?.palette_algorithm_version).toBe("mmcq-v1"); expect(element.colors).toEqual(initialPalette); expect(refreshColorCards(state, replacement).elements[0]?.colors).toEqual(replacement); expect(state.elements[0]?.colors).toEqual(initialPalette); }); }); describe("TASK-WP4-04 dynamic providers", () => { const context = { now: new Date("2026-08-03T09:07:00+08:00"), profile: { creatorName: "Dada Creator", socialId: "@@dada" }, }; it("exposes exactly ten P0-A providers and snapshots time and identity", () => { expect(P0A_DYNAMIC_STICKERS.map((item) => item.templateId)).toEqual([ "DYN001", "DYN002", "DYN003", "DYN004", "DYN007", "DYN008", "DYN011", "DYN012", "DYN015", "DYN016", ]); const time = createDynamicStickerElement("DYN012", context, identity, 0); expect(time.formatted_value).toBe("09:07"); expect(time.dynamic_fields).toMatchObject({ font_substitution: "FONT081", hour: "09", minute: "07" }); expect(time.font_override).toBe("FONT081"); const identitySticker = createDynamicStickerElement("DYN016", context, identity, 0); expect(identitySticker.formatted_value).toBe("@dada"); expect(normalizeSocialId("@@@dada")).toBe("@dada"); }); it("uses the archived template categories and only the fields visibly consumed by each source", () => { expect(P0A_DYNAMIC_STICKERS.map(({ category, templateId }) => [templateId, category])).toEqual([ ["DYN001", "location"], ["DYN002", "location"], ["DYN003", "location"], ["DYN004", "location"], ["DYN007", "other"], ["DYN008", "time"], ["DYN011", "time"], ["DYN012", "time"], ["DYN015", "identity"], ["DYN016", "identity"], ]); const other = createDynamicStickerElement("DYN007", context, identity, 0); expect(other.dynamic_fields).toEqual({ nickname: "@dada" }); expect(other.formatted_value).toBe("@dada"); const monthTime = createDynamicStickerElement("DYN008", context, identity, 0); expect(monthTime.dynamic_fields).toEqual({ hour: "09", minute: "07", month: "08" }); }); it("maps every enabled dynamic sticker to its archived source candidate and original resource version", () => { expect(Object.entries(DYNAMIC_RENDER_MODELS).map(([id, model]) => [id, model.sourceCandidateId])).toEqual([ ["DYN001", "l_POI01"], ["DYN002", "l_POI02"], ["DYN003", "l_POI03"], ["DYN004", "l_POI04"], ["DYN007", "diaoyu"], ["DYN008", "l_shijian2"], ["DYN011", "l_shijian6"], ["DYN012", "l_shijian7"], ["DYN015", "0721userna"], ["DYN016", "l_username00"], ]); const element = createDynamicStickerElement("DYN001", { ...context, location: { formattedValue: "温州" } }, identity, 0); expect(element.resource_version).toBe(DYNAMIC_RESOURCE_VERSION); expect(DYNAMIC_RENDER_MODELS.DYN001.imageLayers).toEqual([ { assetId: "DYN001-image28", height: 67, width: 219, x: -17.562, y: 2.203 }, ]); }); it("keeps the source DYN012 split-clock composition instead of a generic time box", () => { expect(DYN012_RENDER_LAYOUT).toEqual({ background: "transparent", divider: { color: "#FFFFFF", height: 42, width: 7, x: 0, y: 0 }, hour: { color: "#FFFFFF", fontId: "FONT081", fontSize: 100, x: -60, y: 0 }, meridiem: { color: "#FFFFFF", fontId: "FONT081", fontSize: 20, x: 94, y: -65 }, minute: { color: "#FFFFFF", fontId: "FONT081", fontSize: 100, x: 60, y: 0 }, }); }); it("keeps a per-instance override separate from the account profile", () => { const profile = structuredClone(context.profile); const element = createDynamicStickerElement("DYN015", context, identity, 0); const changed = overrideDynamicStickerValue(element, "Single Sticker Name"); expect(changed.formatted_value).toBe("Single Sticker Name"); expect(element.formatted_value).toBe("Dada Creator"); expect(context.profile).toEqual(profile); }); it("does not call location or reverse geocoding until Dada consent is confirmed", async () => { const geolocate = vi.fn(async () => ({ latitude: 27.9943, longitude: 120.6994 })); const reverseGeocode = vi.fn(async () => "浙江省温州市"); const gate = new LocationConsentGate({ geolocate, reverseGeocode }); expect(gate.reject()).toBeUndefined(); expect(geolocate).not.toHaveBeenCalled(); expect(reverseGeocode).not.toHaveBeenCalled(); await expect(gate.confirm()).resolves.toEqual({ formattedValue: "浙江省温州市", latitude: 27.9943, longitude: 120.6994 }); expect(geolocate).toHaveBeenCalledTimes(1); expect(reverseGeocode).toHaveBeenCalledTimes(1); }); });