feat: complete TASK-WP4-04 color and dynamic stickers

This commit is contained in:
suyx
2026-08-03 14:11:40 +08:00
parent dd7a23a281
commit 457e1147e4
29 changed files with 2234 additions and 61 deletions
+6 -1
View File
@@ -59,7 +59,12 @@ function writeEvidence(caseId: string, name: string, value: unknown) {
async function routeEditor(page: Page) {
await page.route("**/api/v1/auth/session", (route) => route.fulfill({ body: JSON.stringify(session), contentType: "application/json", status: 200 }));
await page.route(`**/api/v1/projects/${projectId}`, (route) => route.fulfill({ body: JSON.stringify(projectPayload()), contentType: "application/json", status: 200 }));
await page.route(`**/api/v1/private-assets/projects/${projectId}/images/**`, (route) => route.fulfill({ body: Buffer.from("not-an-image"), contentType: "image/png", status: 200 }));
await page.route(`**/api/v1/private-assets/projects/${projectId}/images/**`, (route) => {
const alternate = route.request().url().endsWith(alternateImageId);
const colors = alternate ? ["#102030", "#405060", "#708090", "#A0B0C0", "#D0E0F0"] : ["#F42020", "#20D260", "#1860DC", "#F8D230", "#7840B4"];
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="250" height="250">${colors.map((color, index) => `<rect x="${index * 50}" width="50" height="250" fill="${color}"/>`).join("")}</svg>`;
return route.fulfill({ body: svg, contentType: "image/svg+xml", status: 200 });
});
}
test("TDD-WP4-BG-001 preserves overlays while switching the background", async ({ page }) => {
+16 -2
View File
@@ -1,7 +1,8 @@
import { expect, test, type Page } from "@playwright/test";
import { createServer, type ViteDevServer } from "vite";
import { mkdirSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { join, resolve } from "node:path";
import type { CanvasState } from "@dada/shared-contracts";
let vite: ViteDevServer;
@@ -25,6 +26,12 @@ const session = {
user: { creator_name: "Canvas User", role: "user", social_id: "@canvas_user", status: "active", user_id: "00000000-0000-4000-8000-000000000501" },
};
const stickerRoot = process.env.DADA_STATIC_STICKER_ROOT ?? join(homedir(), "Desktop", "贴纸素材");
const originalStickerFixtures: Readonly<Record<string, string>> = {
STK001: join(stickerRoot, "sticker_part1", "01af6384c2a962d17f55736f9895b505.png"),
STK002: join(stickerRoot, "sticker_part2", "011db0fb6ac4184e4a708374003bce66.png"),
};
function uuid(index: number) {
return `00000000-0000-4000-8000-${String(index).padStart(12, "0")}`;
}
@@ -53,6 +60,7 @@ function writeEvidence(caseId: string, name: string, value: unknown) {
}
async function routeEditor(page: Page, projectId: string, backend: { canvas: CanvasState; saves: number; version: number }) {
if (!Object.values(originalStickerFixtures).every(existsSync)) throw new Error("Archived ordinary sticker fixture is unavailable.");
await page.route("**/api/v1/auth/session", (route) => route.fulfill({ body: JSON.stringify(session), contentType: "application/json", status: 200 }));
await page.route(`**/api/v1/projects/${projectId}`, (route) => route.fulfill({
body: JSON.stringify({
@@ -68,6 +76,12 @@ async function routeEditor(page: Page, projectId: string, backend: { canvas: Can
backend.version += 1;
await route.fulfill({ body: JSON.stringify({ save_status: "saved", state_version: backend.version }), contentType: "application/json", status: 200 });
});
await page.route("**/api/v1/assets/public/fixture-v1/*", (route) => {
const assetId = decodeURIComponent(route.request().url().split("/").at(-1)!);
const source = originalStickerFixtures[assetId];
if (!source) return route.fulfill({ status: 404 });
return route.fulfill({ body: readFileSync(source), contentType: "image/png", status: 200 });
});
}
test("TDD-WP4-CAN-001 keeps fifty elements editable and blocks the fifty-first", async ({ page }) => {
+278
View File
@@ -0,0 +1,278 @@
import { expect, test, type Page } from "@playwright/test";
import { createServer, type ViteDevServer } from "vite";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { join, resolve } from "node:path";
import type { CanvasState } from "@dada/shared-contracts";
let vite: ViteDevServer;
let webUrl: string;
test.beforeAll(async () => {
vite = await createServer({ configFile: resolve("apps/web/vite.config.ts"), root: resolve("apps/web"), server: { host: "127.0.0.1", port: 0 } });
await vite.listen();
const address = vite.httpServer?.address();
if (!address || typeof address === "string") throw new Error("Vite did not expose a test port.");
webUrl = `http://127.0.0.1:${address.port}`;
});
test.afterAll(async () => vite.close());
const userId = "00000000-0000-4000-8000-000000000821";
const session = {
csrf_token: "csrf-wp4-04-fixture-0000000000000000000000000000",
user: { creator_name: "Dada Creator", social_id: "@@dada", user_id: userId },
};
const rawImages: Record<string, string[]> = {
"00000000-0000-4000-8000-000000000811": ["#F42020", "#20D260", "#1860DC", "#F8D230", "#7840B4"],
"00000000-0000-4000-8000-000000000812": ["#102030", "#405060", "#708090", "#A0B0C0", "#D0E0F0"],
};
const dynamicRoot = process.env.DADA_DYNAMIC_ASSET_ROOT ?? join(homedir(), "Desktop", "sticker_interactive", "单模板归档", "templates");
const textRoot = process.env.DADA_TEXT_ASSET_ROOT ?? join(homedir(), "Desktop", "sticker_text");
const dynamicSourceAssets: Readonly<Record<string, { contentType: string; path: string }>> = {
"15974853bc3294ef68e7e6d58fe74fd7": { contentType: "font/ttf", path: join(dynamicRoot, "DYN002", "fonts", "15974853bc3294ef68e7e6d58fe74fd7", "0202b90o6r57rxed4027b5689e0dxe7e142r0yho9d0lljuqj.ttf") },
"46f8336813e4c48d06a1aef294fdccf6": { contentType: "font/ttf", path: join(dynamicRoot, "DYN016", "fonts", "46f8336813e4c48d06a1aef294fdccf6", "9fbfbb420cea1df916d7c7c7ac90b1c88b61e117-PingFang-SC-Semibold-2.ttf") },
"53ca6b704728520da50c145eabb2e635": { contentType: "font/ttf", path: join(dynamicRoot, "DYN007", "fonts", "53ca6b704728520da50c145eabb2e635", "fab6c26a0b21d5e9b57fb5238843ac1fb77a2ce6-HYZhengYuan.ttf") },
"cca5efc0e02fb1bf62349bd68ef30fc1": { contentType: "font/otf", path: join(dynamicRoot, "DYN015", "fonts", "cca5efc0e02fb1bf62349bd68ef30fc1", "e11ced673fc7e63e8b0b4730166d29845d8bebae-NotoSansCJKsc-Regular.otf") },
"dd25b35dcb7ba4476cbaa9a9592e39e2": { contentType: "font/ttf", path: join(dynamicRoot, "DYN001", "fonts", "dd25b35dcb7ba4476cbaa9a9592e39e2", "0202b90o6r57rxed4027b5689e0dxe7e142r0ygbx80porvko.ttf") },
"e4210c9872f0c279b35273f230809821": { contentType: "font/ttf", path: join(dynamicRoot, "DYN011", "fonts", "e4210c9872f0c279b35273f230809821", "06b980259e2104e1211a6819a61bc5ddeca77dcb-DJB-Get-Digital-1.ttf") },
"f4bfd4132df2d6be97ceabadf3853505": { contentType: "font/ttf", path: join(dynamicRoot, "DYN008", "fonts", "f4bfd4132df2d6be97ceabadf3853505", "6ce05a147aedabbb610d9cb3e75bbe60c064c3f5-BarlowCondensed-SemiBold.ttf") },
"DYN001-image28": { contentType: "image/png", path: join(dynamicRoot, "DYN001", "resource", "image28.png") },
"DYN002-image29": { contentType: "image/png", path: join(dynamicRoot, "DYN002", "resource", "image29.png") },
"DYN003-image30": { contentType: "image/png", path: join(dynamicRoot, "DYN003", "resource", "image30.png") },
"DYN004-image32": { contentType: "image/png", path: join(dynamicRoot, "DYN004", "resource", "image32.png") },
"DYN008-backendui0": { contentType: "image/png", path: join(dynamicRoot, "DYN008", "resource", "backendui0.png") },
"DYN011-backendui0": { contentType: "image/png", path: join(dynamicRoot, "DYN011", "resource", "backendui0.png") },
"DYN015-imager2": { contentType: "image/png", path: join(dynamicRoot, "DYN015", "resource", "imager2_2.png") },
"DYN016-image21": { contentType: "image/png", path: join(dynamicRoot, "DYN016", "resource", "image21.png") },
};
const font081Path = join(textRoot, "字体", "面板全量采集", "font_panel_full_20260722", "resources", "font_packages", "FONT081_Lexend Deca", "font_files", "02034l0o6r57rxed4027b5689e0dxe7e142r0vi8920akeqto.ttf");
function rawSvg(colors: string[]) {
return `<svg xmlns="http://www.w3.org/2000/svg" width="250" height="250">${colors.map((color, index) => `<rect x="${index * 50}" y="0" width="50" height="250" fill="${color}"/>`).join("")}</svg>`;
}
function emptyCanvas(assetId: string | null = null): CanvasState {
return {
background: { adjustments: { brightness: 0, contrast: 0, crop: null, filter: "none", fit: "fill", saturation: 0, sharpness: 0, temperature: 0 }, asset_id: assetId },
elements: [], pixel_height: 1440, pixel_width: 1080, ratio: "3:4", schema_version: 1,
};
}
interface Backend {
canvas: CanvasState;
saves: number;
version: number;
}
function writeEvidence(caseId: string, name: string, value: unknown) {
const root = process.env.DADA_EVIDENCE_DIR_COLOR_DYNAMIC;
if (!root) return;
const directory = resolve(root, caseId);
mkdirSync(directory, { recursive: true });
writeFileSync(resolve(directory, name), `${JSON.stringify(value, null, 2)}\n`);
}
async function routeEditor(page: Page, projectId: string, backend: Backend) {
const sourceFiles = [...Object.values(dynamicSourceAssets).map((asset) => asset.path), font081Path];
if (!sourceFiles.every(existsSync)) throw new Error("Archived dynamic source fixture is unavailable.");
await page.route("**/api/v1/auth/session", (route) => route.fulfill({ body: JSON.stringify(session), contentType: "application/json" }));
await page.route(`**/api/v1/projects/${projectId}`, (route) => route.fulfill({
body: JSON.stringify({
canvas_state: backend.canvas, created_at: "2026-08-03T04:00:00.000Z", current_image_id: backend.canvas.background.asset_id,
images: Object.keys(rawImages).map((imageId, index) => ({ created_at: `2026-08-03T0${index + 4}:00:00.000Z`, generation_id: `00000000-0000-4000-8000-00000000081${index + 3}`, image_id: imageId })),
name: "色卡动态画布", project_id: projectId, ratio: "3:4", state_version: backend.version,
}), contentType: "application/json",
}));
await page.route(`**/api/v1/projects/${projectId}/state`, async (route) => {
backend.canvas = (route.request().postDataJSON() as { canvas_state: CanvasState }).canvas_state;
backend.saves += 1;
backend.version += 1;
await route.fulfill({ body: JSON.stringify({ save_status: "saved", state_version: backend.version }), contentType: "application/json" });
});
await page.route(`**/api/v1/private-assets/projects/${projectId}/images/*`, (route) => {
const assetId = route.request().url().split("/").at(-1)!;
const colors = rawImages[assetId];
if (!colors) return route.fulfill({ status: 404 });
return route.fulfill({ body: rawSvg(colors), contentType: "image/svg+xml", status: 200 });
});
await page.route("**/api/v1/assets/recent?asset_kind=text_template", (route) => route.fulfill({ body: JSON.stringify({ items: [] }), contentType: "application/json" }));
await page.route("**/api/v1/assets/public/wp4-fixture-v1/FONT081", (route) => route.fulfill({ body: readFileSync(font081Path), contentType: "font/ttf", status: 200 }));
await page.route("**/api/v1/assets/public/wp4-dynamic-source-v1/*", (route) => {
const assetId = decodeURIComponent(route.request().url().split("/").at(-1)!);
const asset = dynamicSourceAssets[assetId];
if (!asset) return route.fulfill({ status: 404 });
return route.fulfill({ body: readFileSync(asset.path), contentType: asset.contentType, status: 200 });
});
}
async function stageInk(page: Page) {
return page.getByLabel("编辑画布").evaluate((canvas: HTMLCanvasElement) => {
const context = canvas.getContext("2d");
if (!context) throw new Error("Canvas context unavailable.");
const pixels = context.getImageData(0, 0, canvas.width, canvas.height).data;
let nonWhite = 0;
for (let index = 0; index < pixels.length; index += 4) {
if ((pixels[index] ?? 255) < 245 || (pixels[index + 1] ?? 255) < 245 || (pixels[index + 2] ?? 255) < 245) nonWhite += 1;
}
return { canvas_pixels: canvas.width * canvas.height, non_white_pixels: nonWhite };
});
}
async function nudgeSelected(page: Page, directions: Array<{ key: "ArrowDown" | "ArrowLeft" | "ArrowRight" | "ArrowUp"; times: number }>) {
const stage = page.getByLabel("编辑画布");
for (const direction of directions) {
for (let index = 0; index < direction.times; index += 1) await stage.press(`Shift+${direction.key}`);
}
}
test("TDD-WP4-COL-001 extracts once from raw pixels and refreshes only for a new background", async ({ page }) => {
const projectId = "00000000-0000-4000-8000-000000000830";
const initialAsset = Object.keys(rawImages)[0]!;
const backend: Backend = { canvas: emptyCanvas(initialAsset), saves: 0, version: 1 };
const requests: Array<{ method: string; url: string }> = [];
page.on("request", (request) => requests.push({ method: request.method(), url: request.url() }));
await routeEditor(page, projectId, backend);
await page.goto(`${webUrl}/app/projects/${projectId}/editor`);
await page.getByRole("button", { name: "色卡", exact: true }).click();
await expect(page.getByRole("button", { name: /^添加色卡/ })).toHaveCount(4);
await expect(page.getByRole("button", { name: "色卡说明" })).toHaveAttribute("title", "色卡基于原始底图,更换底图时更新,不随裁剪、调色和滤镜变化");
const placements = [
[{ key: "ArrowLeft", times: 15 }, { key: "ArrowUp", times: 14 }],
[{ key: "ArrowRight", times: 15 }, { key: "ArrowUp", times: 14 }],
[{ key: "ArrowLeft", times: 15 }, { key: "ArrowDown", times: 14 }],
[{ key: "ArrowRight", times: 15 }, { key: "ArrowDown", times: 14 }],
] as const;
for (const [index, id] of ["COLOR001", "COLOR002", "COLOR008", "COLOR016"].entries()) {
await page.getByRole("button", { name: new RegExp(`添加色卡 ${id}`) }).click();
await nudgeSelected(page, [...placements[index]!]);
}
await expect.poll(() => backend.canvas.elements.length).toBe(4);
const palettes = backend.canvas.elements.map((element) => element.colors);
expect(palettes.every((palette) => JSON.stringify(palette) === JSON.stringify(palettes[0]))).toBe(true);
expect(backend.canvas.elements.every((element) => element.style_parameters?.palette_algorithm_version === "mmcq-v1")).toBe(true);
const beforeAdjustments = structuredClone(palettes[0]);
await page.getByLabel("编辑画布").press("Escape");
await page.getByRole("button", { name: "底图", exact: true }).click();
await page.locator(".editor-inspector label").filter({ hasText: "亮度" }).locator('input[type="range"]').fill("40");
await page.getByRole("button", { name: "应用调整" }).click();
expect(backend.canvas.elements.every((element) => JSON.stringify(element.colors) === JSON.stringify(beforeAdjustments))).toBe(true);
await page.getByRole("button", { name: "历史", exact: true }).click();
await page.locator(".editor-history-list button").last().click();
await page.getByRole("button", { name: "确认更换" }).click();
await expect.poll(() => backend.canvas.background.asset_id).toBe(Object.keys(rawImages)[1]);
expect(backend.canvas.elements.every((element) => JSON.stringify(element.colors) === JSON.stringify(backend.canvas.elements[0]?.colors))).toBe(true);
expect(backend.canvas.elements[0]?.colors).not.toEqual(beforeAdjustments);
const network = {
image_gets: requests.filter((item) => item.method === "GET" && item.url.includes("/private-assets/")).length,
image_uploads: requests.filter((item) => item.method !== "GET" && item.url.includes("/private-assets/")).length,
};
expect(network.image_uploads).toBe(0);
const pixels = await stageInk(page);
await page.getByLabel("编辑画布").press("Escape");
await page.getByRole("button", { name: "色卡", exact: true }).click();
writeEvidence("TDD-WP4-COL-001-deterministic-palette", "palette.json", { algorithm: "mmcq-v1", initial: beforeAdjustments, replacement: backend.canvas.elements[0]?.colors, styles: backend.canvas.elements.map((item) => item.style_id) });
writeEvidence("TDD-WP4-COL-001-deterministic-palette", "network-timeline.json", network);
writeEvidence("TDD-WP4-COL-001-deterministic-palette", "pixel-diff.json", { ...pixels, four_renderers_visible: true });
writeEvidence("TDD-WP4-COL-001-deterministic-palette", "db-diff.json", { elements: backend.canvas.elements, save_count: backend.saves });
if (process.env.DADA_EVIDENCE_DIR_COLOR_DYNAMIC) await page.screenshot({ fullPage: true, path: resolve(process.env.DADA_EVIDENCE_DIR_COLOR_DYNAMIC, "TDD-WP4-COL-001-deterministic-palette", "color-cards.png") });
});
test("TDD-WP4-DYN-001 snapshots the confirmed local time across clock changes and reopen", async ({ page }) => {
await page.clock.setFixedTime(new Date("2026-08-03T09:07:00+08:00"));
const projectId = "00000000-0000-4000-8000-000000000840";
const backend: Backend = { canvas: emptyCanvas(), saves: 0, version: 2 };
await routeEditor(page, projectId, backend);
await page.goto(`${webUrl}/app/projects/${projectId}/editor`);
await page.getByRole("button", { name: "动态贴纸", exact: true }).click();
for (const id of ["DYN007", "DYN008", "DYN011", "DYN012"]) await page.getByRole("button", { name: new RegExp(`添加动态贴纸 ${id}`) }).click();
await expect.poll(() => backend.canvas.elements.length).toBe(4);
const snapshots = backend.canvas.elements.map((element) => ({ fields: element.dynamic_fields, id: element.template_or_asset_id, value: element.formatted_value }));
await page.clock.setFixedTime(new Date("2026-08-06T23:59:00+08:00"));
await page.reload();
expect(backend.canvas.elements.map((element) => ({ fields: element.dynamic_fields, id: element.template_or_asset_id, value: element.formatted_value }))).toEqual(snapshots);
writeEvidence("TDD-WP4-DYN-001-fixed-time", "canvas-state.json", backend.canvas);
writeEvidence("TDD-WP4-DYN-001-fixed-time", "clock-trace.json", { inserted_at: "2026-08-03T09:07:00+08:00", reopened_at: "2026-08-06T23:59:00+08:00", snapshots });
writeEvidence("TDD-WP4-DYN-001-fixed-time", "pixel-diff.json", { ...(await stageInk(page)), static_canvas_content: true, links: 0 });
});
test("TDD-WP4-DYN-002 gates coordinates behind consent and preserves manual fallback", async ({ page }) => {
const projectId = "00000000-0000-4000-8000-000000000850";
const backend: Backend = { canvas: emptyCanvas(), saves: 0, version: 3 };
await page.addInitScript(() => {
(window as Window & { __dadaLocationCalls?: number }).__dadaLocationCalls = 0;
Object.defineProperty(navigator, "geolocation", {
configurable: true,
value: { getCurrentPosition(success: PositionCallback) { (window as Window & { __dadaLocationCalls?: number }).__dadaLocationCalls = ((window as Window & { __dadaLocationCalls?: number }).__dadaLocationCalls ?? 0) + 1; success({ coords: { latitude: 27.9943, longitude: 120.6994 } } as GeolocationPosition); } },
});
});
let reverseCalls = 0;
let servicePaused = false;
await routeEditor(page, projectId, backend);
await page.route("**/api/v1/location/reverse-geocode", async (route) => {
reverseCalls += 1;
if (servicePaused) return route.fulfill({ status: 503 });
return route.fulfill({ body: JSON.stringify({ formatted_value: "浙江省温州市", service_mode: "mock", status: "resolved" }), contentType: "application/json" });
});
await page.goto(`${webUrl}/app/projects/${projectId}/editor`);
await page.getByRole("button", { name: "动态贴纸", exact: true }).click();
await page.getByRole("button", { name: /添加动态贴纸 DYN004/ }).click();
const dialog = page.getByRole("dialog", { name: "使用自动定位" });
await expect(dialog).toContainText("原始经纬度会保存到当前项目并显示在导出成品中");
await dialog.getByRole("button", { name: "暂不定位" }).click();
expect(await page.evaluate(() => (window as Window & { __dadaLocationCalls?: number }).__dadaLocationCalls)).toBe(0);
expect(reverseCalls).toBe(0);
expect(backend.canvas.elements).toHaveLength(0);
await page.getByRole("button", { name: /添加动态贴纸 DYN004/ }).click();
await page.getByRole("button", { name: "同意并自动定位" }).click();
await expect.poll(() => backend.canvas.elements.length).toBe(1);
expect(backend.canvas.elements[0]?.coordinates).toEqual({ latitude: 27.9943, longitude: 120.6994 });
expect(reverseCalls).toBe(1);
await page.getByRole("button", { name: "删除" }).click();
await expect.poll(() => backend.canvas.elements.length).toBe(0);
servicePaused = true;
await page.getByRole("button", { name: /添加动态贴纸 DYN004/ }).click();
await page.getByRole("button", { name: "同意并自动定位" }).click();
await expect(page.getByText("自动定位不可用,请改用手动地点贴纸。")).toBeVisible();
await page.getByLabel("手动地点文字").fill("温州手动地点");
await page.getByRole("button", { name: "改用手动地点贴纸" }).click();
await expect.poll(() => backend.canvas.elements.length).toBe(1);
expect(backend.canvas.elements[0]?.template_or_asset_id).toBe("DYN001");
expect(backend.canvas.elements[0]?.coordinates).toBeUndefined();
const calls = { geolocation: await page.evaluate(() => (window as Window & { __dadaLocationCalls?: number }).__dadaLocationCalls), reverse_geocode: reverseCalls };
writeEvidence("TDD-WP4-DYN-002-location-consent", "external-calls.json", { ...calls, rejected_dada_prompt_calls: 0, service_mode: "mock", service_paused_manual_available: true });
writeEvidence("TDD-WP4-DYN-002-location-consent", "canvas-state.json", backend.canvas);
writeEvidence("TDD-WP4-DYN-002-location-consent", "db-diff.json", { coordinates_after_delete: null, manual_element: backend.canvas.elements[0] });
});
test("TDD-WP4-DYN-003 keeps identity overrides local and discloses DYN012 FONT081 substitution", async ({ page }) => {
await page.clock.setFixedTime(new Date("2026-08-03T09:07:00+08:00"));
const projectId = "00000000-0000-4000-8000-000000000860";
const backend: Backend = { canvas: emptyCanvas(Object.keys(rawImages)[0]!), saves: 0, version: 4 };
await routeEditor(page, projectId, backend);
await page.goto(`${webUrl}/app/projects/${projectId}/editor`);
await page.getByRole("button", { name: "动态贴纸", exact: true }).click();
await page.getByRole("button", { name: /添加动态贴纸 DYN016/ }).click();
await expect.poll(() => backend.canvas.elements[0]?.formatted_value).toBe("@dada");
await page.getByLabel("动态贴纸显示文字").fill("@single-instance");
await page.getByRole("button", { name: "应用显示文字" }).click();
await expect.poll(() => backend.canvas.elements[0]?.formatted_value).toBe("@single-instance");
await nudgeSelected(page, [{ key: "ArrowUp", times: 18 }]);
await page.getByRole("button", { name: "动态贴纸", exact: true }).click();
await page.getByRole("button", { name: /添加动态贴纸 DYN012/ }).click();
await expect(page.getByText(/当前明确使用 FONT081 · Lexend Deca 替代/)).toBeVisible();
await expect.poll(() => page.evaluate(() => document.fonts.check('16px "Dada_FONT081"'))).toBe(true);
await expect.poll(() => backend.canvas.elements.some((element) => element.template_or_asset_id === "DYN012")).toBe(true);
expect(backend.canvas.elements.find((element) => element.template_or_asset_id === "DYN012")).toMatchObject({
dynamic_fields: { font_substitution: "FONT081" }, font_override: "FONT081",
});
expect(session.user.social_id).toBe("@@dada");
const pixels = await stageInk(page);
writeEvidence("TDD-WP4-DYN-003-identity-font", "canvas-state.json", backend.canvas);
writeEvidence("TDD-WP4-DYN-003-identity-font", "db-diff.json", { account_profile: session.user, instance_override: "@single-instance", profile_changed: false });
writeEvidence("TDD-WP4-DYN-003-identity-font", "font-load.json", { fallback: null, font_id: "FONT081", original_font: "DIN_MediumAlternate.otf", ready: true, substitution_disclosed: true });
writeEvidence("TDD-WP4-DYN-003-identity-font", "pixel-diff.json", { ...pixels, substitution_requires_manual_review: true });
if (process.env.DADA_EVIDENCE_DIR_COLOR_DYNAMIC) await page.screenshot({ fullPage: true, path: resolve(process.env.DADA_EVIDENCE_DIR_COLOR_DYNAMIC, "TDD-WP4-DYN-003-identity-font", "dyn012-substitution.png") });
});