Files
tyx_AI_xhs/tests/unit/wp4-04-palette-dynamic.test.ts

151 lines
7.7 KiB
TypeScript

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);
});
});