feat: complete TASK-WP4-04 color and dynamic stickers
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user