test: establish WP4-07 visual performance red harness
Dada P0-A isolated Windows CI / validate-and-package (push) Failing after 4m19s
Dada P0-A isolated Windows CI / validate-and-package (push) Failing after 4m19s
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
export const WP4_07_SOURCE_HASHES = Object.freeze({
|
||||
"DevelopmentPlan.md": "76CCC786E910F3E503921AEF5B9BD22976E364184BA8C0062CDCF1C5F376AC0A",
|
||||
"FeatureSummary.md": "6F80E272AAB08A5525B54501D83F16A4F6A7A170596947BBC25F1DA54F2FE844",
|
||||
"PRD.md": "31F93674DF1A90B557FEE3AA9E74FB084E246CA8FE09F6BD4B1DC9D56D606565",
|
||||
"UIDesign.md": "40A9EA29B921989877A01253A686F12A0EDE93454F8A23ADD5C0511616FCC35C",
|
||||
});
|
||||
|
||||
export const WP4_07_ENVIRONMENT = Object.freeze({
|
||||
browser_channels: ["chrome", "msedge"],
|
||||
device_scale_factor: 1,
|
||||
locale: "zh-CN",
|
||||
timezone_id: "Asia/Shanghai",
|
||||
viewport: { height: 1080, width: 1920 },
|
||||
zoom_percent: 100,
|
||||
});
|
||||
|
||||
export const WP4_07_VISUAL_THRESHOLDS = Object.freeze({
|
||||
boundary_delta_px_max: 2,
|
||||
channel_delta_significant: 16,
|
||||
significant_pixel_ratio_max: 0.01,
|
||||
});
|
||||
|
||||
export const WP4_07_PERFORMANCE_BUDGETS = Object.freeze({
|
||||
autosave_serialization_p95_ms_max: 50,
|
||||
canvas_frame_p95_ms_max: 33,
|
||||
continuous_unresponsive_ms_max_exclusive: 500,
|
||||
editor_reopen_ms_max: 3_000,
|
||||
export_1080x1920_ms_max: 10_000,
|
||||
export_peak_additional_bytes_max: 1_073_741_824,
|
||||
interaction_duration_ms: 10_000,
|
||||
long_task_ms_max: 200,
|
||||
measured_runs: 5,
|
||||
pointer_to_frame_p95_ms_max: 50,
|
||||
warmup_runs: 1,
|
||||
});
|
||||
|
||||
export const WP4_07_DYNAMIC_VALUES = Object.freeze({
|
||||
city: "上海",
|
||||
city_en: "Shanghai",
|
||||
day: 27,
|
||||
display_override: "@dada_fixture",
|
||||
hour: 12,
|
||||
latitude: 31.2304,
|
||||
longitude: 121.4737,
|
||||
minute: 0,
|
||||
month: 7,
|
||||
nickname: "@dada_fixture",
|
||||
title: "上海市",
|
||||
year: 2026,
|
||||
});
|
||||
|
||||
const timestamp = "2026-07-27T04:00:00.000Z";
|
||||
const redResourceVersion = "wp4-07-red-contract-v1";
|
||||
const palette = ["#111111", "#F2F400", "#1769AA", "#C92A24", "#FFFFFF"];
|
||||
const textTemplateIds = [
|
||||
"FLOWER001", "FLOWER003", "FLOWER005", "FLOWER008", "H003", "H004",
|
||||
"H006", "TAG001", "TAG002", "TAG003", "TAG005", "TAG051",
|
||||
];
|
||||
const textFontIds = [
|
||||
"FONT011", "FONT008", "FONT008", "FONT005", "FONT039", "FONT046",
|
||||
"FONT052", "FONT027", "FONT043", "FONT043", "FONT008", "FONT022",
|
||||
];
|
||||
const dynamicIds = [
|
||||
"DYN001", "DYN002", "DYN003", "DYN004", "DYN007",
|
||||
"DYN008", "DYN011", "DYN012", "DYN015", "DYN016",
|
||||
];
|
||||
const colorCards = [
|
||||
["COLOR001", "style_01"],
|
||||
["COLOR002", "style_02"],
|
||||
["COLOR008", "style_08"],
|
||||
["COLOR016", "style_16"],
|
||||
];
|
||||
|
||||
function identity(index) {
|
||||
return `00000000-0000-4000-8000-${String(4_070_000 + index).padStart(12, "0")}`;
|
||||
}
|
||||
|
||||
function position(index, columns, rowOffset) {
|
||||
return {
|
||||
x: Number((0.1 + (index % columns) * (0.8 / Math.max(1, columns - 1))).toFixed(4)),
|
||||
y: Number((rowOffset + Math.floor(index / columns) * 0.105).toFixed(4)),
|
||||
};
|
||||
}
|
||||
|
||||
function common(index, type, templateOrAssetId, resourceVersion) {
|
||||
return {
|
||||
created_at: timestamp,
|
||||
element_id: identity(index),
|
||||
opacity: 1,
|
||||
position: { x: 0.5, y: 0.5 },
|
||||
resource_version: resourceVersion,
|
||||
rotation: 0,
|
||||
scale: { x: 1, y: 1 },
|
||||
style_parameters: {},
|
||||
template_or_asset_id: templateOrAssetId,
|
||||
type,
|
||||
z_index: index - 1,
|
||||
};
|
||||
}
|
||||
|
||||
export function createWp407CanvasFixture(resourceVersion = redResourceVersion) {
|
||||
const text = textTemplateIds.map((templateId, offset) => ({
|
||||
...common(offset + 1, "text_template", templateId, resourceVersion),
|
||||
content: offset === 0 ? "DADA\n视觉预算" : `固定文字 ${String(offset + 1).padStart(2, "0")}`,
|
||||
font_size: 48,
|
||||
position: offset === 0 ? { x: 0.5, y: 0.5 } : position(offset, 4, 0.09),
|
||||
rotation: (offset % 3 - 1) * 4,
|
||||
scale: { x: 0.72, y: 0.72 },
|
||||
style_parameters: {
|
||||
background_color: "#F2F400",
|
||||
background_enabled: offset % 4 === 0,
|
||||
background_opacity: 0.9,
|
||||
default_font_id: textFontIds[offset],
|
||||
fill_color: offset % 2 === 0 ? "#111111" : "#1769AA",
|
||||
letter_spacing: 1,
|
||||
line_height: 1.2,
|
||||
stroke_color: "#FFFFFF",
|
||||
stroke_enabled: offset % 5 === 0,
|
||||
stroke_width: offset % 5 === 0 ? 2 : 0,
|
||||
text_align: "center",
|
||||
},
|
||||
}));
|
||||
const stickers = Array.from({ length: 24 }, (_, offset) => ({
|
||||
...common(offset + 13, "static_sticker", `STK${String(offset + 1).padStart(3, "0")}`, resourceVersion),
|
||||
opacity: 0.84 + (offset % 4) * 0.04,
|
||||
position: position(offset, 6, 0.39),
|
||||
rotation: (offset % 5 - 2) * 6,
|
||||
scale: { x: 0.58 + (offset % 3) * 0.06, y: 0.58 + (offset % 3) * 0.06 },
|
||||
style_parameters: { flip_horizontal: offset % 7 === 0 },
|
||||
}));
|
||||
const colors = colorCards.map(([cardId, styleId], offset) => ({
|
||||
...common(offset + 37, "color_card", cardId, resourceVersion),
|
||||
colors: [...palette],
|
||||
position: { x: 0.16 + offset * 0.22, y: 0.83 },
|
||||
scale: { x: 1.25, y: 1.25 },
|
||||
style_id: styleId,
|
||||
style_parameters: { palette_algorithm_version: "mmcq-v1" },
|
||||
}));
|
||||
const dynamics = dynamicIds.map((dynamicId, offset) => ({
|
||||
...common(offset + 41, "dynamic_sticker", dynamicId, resourceVersion),
|
||||
dynamic_fields: { ...WP4_07_DYNAMIC_VALUES },
|
||||
formatted_value: dynamicId === "DYN012" ? "12:00 PM" : "@dada_fixture",
|
||||
position: { x: 0.09 + (offset % 5) * 0.205, y: 0.9 + Math.floor(offset / 5) * 0.06 },
|
||||
scale: { x: 0.55, y: 0.55 },
|
||||
style_parameters: dynamicId === "DYN012" ? { font_id: "FONT081", known_substitution: true } : {},
|
||||
}));
|
||||
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-000000004079",
|
||||
},
|
||||
elements: [...text, ...stickers, ...colors, ...dynamics],
|
||||
pixel_height: 1920,
|
||||
pixel_width: 1080,
|
||||
ratio: "9:16",
|
||||
schema_version: 1,
|
||||
};
|
||||
}
|
||||
|
||||
export function wp407FixtureContract() {
|
||||
const canvas = createWp407CanvasFixture();
|
||||
return {
|
||||
canvas,
|
||||
dynamic_values: WP4_07_DYNAMIC_VALUES,
|
||||
environment: WP4_07_ENVIRONMENT,
|
||||
performance_budgets: WP4_07_PERFORMANCE_BUDGETS,
|
||||
schema_version: "wp4-07-fixed-fixture/v1",
|
||||
visual_thresholds: WP4_07_VISUAL_THRESHOLDS,
|
||||
};
|
||||
}
|
||||
|
||||
export function wp407FixtureSha256() {
|
||||
return createHash("sha256").update(JSON.stringify(wp407FixtureContract())).digest("hex").toUpperCase();
|
||||
}
|
||||
|
||||
export function assertWp407Fixture() {
|
||||
const fixture = wp407FixtureContract();
|
||||
const counts = Object.fromEntries(["text_template", "static_sticker", "color_card", "dynamic_sticker"].map((type) => [
|
||||
type,
|
||||
fixture.canvas.elements.filter((element) => element.type === type).length,
|
||||
]));
|
||||
if (fixture.canvas.elements.length !== 50) throw new Error("FX-CANVAS-50 must contain exactly 50 overlay elements");
|
||||
if (JSON.stringify(counts) !== JSON.stringify({ text_template: 12, static_sticker: 24, color_card: 4, dynamic_sticker: 10 })) {
|
||||
throw new Error(`FX-CANVAS-50 composition changed: ${JSON.stringify(counts)}`);
|
||||
}
|
||||
if (fixture.canvas.pixel_width !== 1080 || fixture.canvas.pixel_height !== 1920) throw new Error("export fixture dimensions changed");
|
||||
if (new Set(fixture.canvas.elements.map((element) => element.element_id)).size !== 50) throw new Error("fixture element IDs are not unique");
|
||||
if (fixture.performance_budgets.warmup_runs !== 1 || fixture.performance_budgets.measured_runs !== 5) throw new Error("measurement count changed");
|
||||
if (fixture.performance_budgets.interaction_duration_ms !== 10_000) throw new Error("interaction duration changed");
|
||||
if (fixture.environment.viewport.width !== 1920 || fixture.environment.viewport.height !== 1080 || fixture.environment.device_scale_factor !== 1) {
|
||||
throw new Error("candidate viewport or DPR changed");
|
||||
}
|
||||
return { counts, fixture_sha256: wp407FixtureSha256() };
|
||||
}
|
||||
|
||||
export const WP4_07_RED_RESOURCE_VERSION = redResourceVersion;
|
||||
export const WP4_07_REQUIRED_FONT_IDS = Object.freeze([
|
||||
"FONT005", "FONT008", "FONT011", "FONT022", "FONT027", "FONT039", "FONT043", "FONT046", "FONT052", "FONT081",
|
||||
"15974853bc3294ef68e7e6d58fe74fd7", "46f8336813e4c48d06a1aef294fdccf6",
|
||||
"53ca6b704728520da50c145eabb2e635", "cca5efc0e02fb1bf62349bd68ef30fc1",
|
||||
"dd25b35dcb7ba4476cbaa9a9592e39e2", "e4210c9872f0c279b35273f230809821",
|
||||
"f4bfd4132df2d6be97ceabadf3853505",
|
||||
]);
|
||||
@@ -0,0 +1,49 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
WP4_07_PERFORMANCE_BUDGETS,
|
||||
WP4_07_RED_RESOURCE_VERSION,
|
||||
WP4_07_VISUAL_THRESHOLDS,
|
||||
assertWp407Fixture,
|
||||
createWp407CanvasFixture,
|
||||
} from "./wp4-07-fixture.mjs";
|
||||
|
||||
test("FX-CANVAS-50 stays fixed at the normative composition and export size", () => {
|
||||
const contract = assertWp407Fixture();
|
||||
assert.deepEqual(contract.counts, {
|
||||
color_card: 4,
|
||||
dynamic_sticker: 10,
|
||||
static_sticker: 24,
|
||||
text_template: 12,
|
||||
});
|
||||
assert.match(contract.fixture_sha256, /^[0-9A-F]{64}$/);
|
||||
});
|
||||
|
||||
test("visual and performance thresholds cannot be weakened by the harness", () => {
|
||||
assert.deepEqual(WP4_07_VISUAL_THRESHOLDS, {
|
||||
boundary_delta_px_max: 2,
|
||||
channel_delta_significant: 16,
|
||||
significant_pixel_ratio_max: 0.01,
|
||||
});
|
||||
assert.deepEqual(WP4_07_PERFORMANCE_BUDGETS, {
|
||||
autosave_serialization_p95_ms_max: 50,
|
||||
canvas_frame_p95_ms_max: 33,
|
||||
continuous_unresponsive_ms_max_exclusive: 500,
|
||||
editor_reopen_ms_max: 3_000,
|
||||
export_1080x1920_ms_max: 10_000,
|
||||
export_peak_additional_bytes_max: 1_073_741_824,
|
||||
interaction_duration_ms: 10_000,
|
||||
long_task_ms_max: 200,
|
||||
measured_runs: 5,
|
||||
pointer_to_frame_p95_ms_max: 50,
|
||||
warmup_runs: 1,
|
||||
});
|
||||
});
|
||||
|
||||
test("the Red contract resource version is structurally barred from Green", () => {
|
||||
const canvas = createWp407CanvasFixture();
|
||||
assert.equal(new Set(canvas.elements.map((element) => element.resource_version)).size, 1);
|
||||
assert.equal(canvas.elements[0].resource_version, WP4_07_RED_RESOURCE_VERSION);
|
||||
assert.match(WP4_07_RED_RESOURCE_VERSION, /red-contract/);
|
||||
});
|
||||
Reference in New Issue
Block a user