test: complete WP4-07 visual and performance budgets
Dada P0-A isolated Windows CI / validate-and-package (push) Failing after 2m34s

This commit is contained in:
suyx
2026-08-04 12:55:25 +08:00
parent 15f7afbe4d
commit c0a8bd6d43
23 changed files with 516 additions and 122 deletions
+10 -3
View File
@@ -76,7 +76,7 @@ async function routeEditor(page: Page, projectId: string, backend: Backend) {
backend.recent = [item, ...backend.recent.filter((entry) => entry.asset_id !== item.asset_id)];
await route.fulfill({ body: JSON.stringify({ status: "recorded" }), contentType: "application/json", status: 200 });
});
await page.route("**/api/v1/assets/public/wp4-fixture-v1/*", (route) => route.fulfill({ body: fontBytes, contentType: "font/ttf", status: 200 }));
await page.route("**/api/v1/assets/public/p0a-complex-v1/*", (route) => route.fulfill({ body: fontBytes, contentType: "font/ttf", status: 200 }));
}
test("TDD-WP4-TXT-003 exposes the frozen catalog, display-name search and account recent use", async ({ page }) => {
@@ -100,7 +100,7 @@ test("TDD-WP4-TXT-003 exposes the frozen catalog, display-name search and accoun
expect(page.getByText("添加普通文字", { exact: true })).toHaveCount(0);
await page.getByRole("button", { name: /FLOWER001 春日计划/ }).click();
await expect(page.getByText("对象 1 / 50")).toBeVisible();
await expect.poll(() => backend.recent).toEqual([{ asset_id: "FLOWER001", asset_kind: "text_template", resource_version: "wp4-fixture-v1" }]);
await expect.poll(() => backend.recent).toEqual([{ asset_id: "FLOWER001", asset_kind: "text_template", resource_version: "p0a-complex-v1" }]);
await page.reload();
await page.getByRole("button", { name: "文字模板", exact: true }).click();
await expect(page.getByLabel("最近使用文字模板").getByText("FLOWER001", { exact: true })).toBeVisible();
@@ -158,7 +158,14 @@ test("TDD-WP4-TXT-001 preserves multiline content and transforms across a templa
expect(pixelEvidence.line_one_ink_pixels).toBeGreaterThan(100);
expect(pixelEvidence.line_two_ink_pixels).toBeGreaterThan(100);
await page.reload();
await page.getByLabel("编辑画布").click({ position: { x: 270, y: 360 } });
const reopenedStage = page.getByLabel("编辑画布");
const reopenedBounds = await reopenedStage.boundingBox();
const reopenedText = backend.canvas.elements[0];
if (!reopenedBounds || !reopenedText) throw new Error("Reopened text geometry is unavailable.");
await reopenedStage.click({ position: {
x: reopenedBounds.width * reopenedText.position.x,
y: reopenedBounds.height * reopenedText.position.y,
} });
await expect(page.getByLabel("文字内容")).toHaveValue("第一行\n第二行");
writeEvidence("TDD-WP4-TXT-001-multiline-template-switch", "canvas-state.json", backend.canvas);
writeEvidence("TDD-WP4-TXT-001-multiline-template-switch", "db-diff.json", { content: backend.canvas.elements[0]?.content, saves: backend.saves, template: backend.canvas.elements[0]?.template_or_asset_id });
+2 -3
View File
@@ -99,10 +99,9 @@ async function routeEditor(page: Page, projectId: string, backend: Backend) {
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) => {
await page.route("**/api/v1/assets/public/p0a-complex-v1/*", (route) => {
const assetId = decodeURIComponent(route.request().url().split("/").at(-1)!);
const asset = dynamicSourceAssets[assetId];
const asset = assetId === "FONT081" ? { contentType: "font/ttf", path: font081Path } : dynamicSourceAssets[assetId];
if (!asset) return route.fulfill({ status: 404 });
return route.fulfill({ body: readFileSync(asset.path), contentType: asset.contentType, status: 200 });
});
+1 -1
View File
@@ -75,7 +75,7 @@ async function routeEditor(page: Page, projectId: string, backend: Backend, opti
}));
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/recent", (route) => route.fulfill({ body: JSON.stringify({ status: "recorded" }), contentType: "application/json" }));
await page.route("**/api/v1/assets/public/wp4-fixture-v1/*", (route) => route.fulfill({ body: readFileSync(windowsFont), contentType: "font/ttf" }));
await page.route("**/api/v1/assets/public/p0a-complex-v1/*", (route) => route.fulfill({ body: readFileSync(windowsFont), contentType: "font/ttf" }));
}
test("TDD-WP4-EXP-001 cancel keeps a pending text edit outside history and export", async ({ page }) => {
+68 -40
View File
@@ -7,28 +7,29 @@ import { resolve } from "node:path";
// @ts-expect-error no declaration file is needed for the test-only ESM fixture.
import {
WP4_07_PERFORMANCE_BUDGETS,
WP4_07_REAL_RESOURCE_VERSIONS,
WP4_07_RED_RESOURCE_VERSION,
WP4_07_REQUIRED_FONT_IDS,
assertWp407Fixture,
createWp407CanvasFixture,
wp407FixtureSha256,
} from "../visual-performance/wp4-07-fixture.mjs";
// @ts-expect-error no declaration file is needed for the Node-only archive loader.
import { loadWp407RealAssets } from "../visual-performance/wp4-07-real-assets.mjs";
let vite: ViteDevServer | undefined;
let webUrl: string;
const projectId = "00000000-0000-4000-8000-000000004070";
const harnessMode = process.env.DADA_WP4_07_HARNESS_MODE;
const fixedCanvas = createWp407CanvasFixture(WP4_07_RED_RESOURCE_VERSION);
if (!new Set(["red_contract", "real_archive"]).has(harnessMode ?? "")) throw new Error("WP4_07_HARNESS_MODE_REQUIRED");
const greenEligible = harnessMode === "real_archive";
const fixtureVersions = greenEligible ? WP4_07_REAL_RESOURCE_VERSIONS : WP4_07_RED_RESOURCE_VERSION;
const fixedCanvas = createWp407CanvasFixture(fixtureVersions);
const realAssets = greenEligible ? loadWp407RealAssets() : undefined;
test.beforeAll(async () => {
assertWp407Fixture();
if (harnessMode !== "red_contract") {
const realUrl = process.env.DADA_WP4_07_APP_URL;
if (!realUrl) throw new Error("WP4_07_REAL_APP_URL_REQUIRED");
webUrl = realUrl;
return;
}
assertWp407Fixture(fixtureVersions);
vite = await createServer({
configFile: resolve("apps/web/vite.config.ts"),
root: resolve("apps/web"),
@@ -49,7 +50,7 @@ const session = {
csrf_token: "csrf-wp4-07-red-contract-000000000000000000000000",
expires_at: "2026-09-03T08:00:00.000Z",
user: {
creator_name: "WP4-07 Fixture",
creator_name: "WP4-07 Archive",
role: "user",
social_id: "@dada_fixture",
status: "active",
@@ -87,14 +88,14 @@ function svgForAsset(assetId: string) {
return `<svg xmlns="http://www.w3.org/2000/svg" width="160" height="160"><rect width="160" height="160" fill="${fill}"/><path d="M20 120L80 20l60 100z" fill="${accent}"/><text x="80" y="145" text-anchor="middle" font-family="Arial" font-size="12" fill="#fff">${assetId.replaceAll("&", "")}</text></svg>`;
}
async function routeRedContractEditor(page: Page, backend: BackendState) {
const fontBytes = readFileSync("C:\\Windows\\Fonts\\arial.ttf");
async function routeEditor(page: Page, backend: BackendState) {
const fontBytes = greenEligible ? undefined : readFileSync("C:\\Windows\\Fonts\\arial.ttf");
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({
canvas_state: backend.canvas,
created_at: "2026-07-27T04:00:00.000Z",
current_image_id: null,
current_image_id: fixedCanvas.background.asset_id,
draft_prompt: "WP4-07 fixed visual and performance fixture",
generations: [],
images: [],
@@ -122,29 +123,38 @@ async function routeRedContractEditor(page: Page, backend: BackendState) {
backend.latestSaves += 1;
await route.fulfill({ body: JSON.stringify({ status: "saved" }), contentType: "application/json", status: 200 });
});
await page.route(`**/api/v1/private-assets/projects/${projectId}/images/*`, (route) => route.fulfill({
body: `<svg xmlns="http://www.w3.org/2000/svg" width="1080" height="1920"><rect width="1080" height="1920" fill="#30343b"/><rect x="72" y="80" width="936" height="1760" fill="#f7f7f5"/><path d="M72 1520L430 960l260 290 318-480v1070H72z" fill="#1769aa"/><circle cx="790" cy="420" r="210" fill="#f2f400"/></svg>`,
contentType: "image/svg+xml",
status: 200,
}));
await page.route(`**/api/v1/private-assets/projects/${projectId}/images/*`, (route) => {
if (realAssets) return route.fulfill({ body: readFileSync(realAssets.background.path), contentType: realAssets.background.content_type, status: 200 });
return route.fulfill({
body: `<svg xmlns="http://www.w3.org/2000/svg" width="1080" height="1920"><rect width="1080" height="1920" fill="#30343b"/><rect x="72" y="80" width="936" height="1760" fill="#f7f7f5"/><path d="M72 1520L430 960l260 290 318-480v1070H72z" fill="#1769aa"/><circle cx="790" cy="420" r="210" fill="#f2f400"/></svg>`,
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/recent", (route) => route.fulfill({ body: JSON.stringify({ status: "recorded" }), contentType: "application/json" }));
await page.route("**/api/v1/assets/public/**", (route) => {
const url = decodeURIComponent(route.request().url());
if (url.includes("/missing-fixture/")) return route.fulfill({ status: 404 });
const assetId = url.split("/").filter(Boolean).at(-1) ?? "asset";
if (WP4_07_REQUIRED_FONT_IDS.some((fontId: string) => url.endsWith(`/${fontId}`))) {
return route.fulfill({ body: fontBytes, contentType: "font/ttf", status: 200 });
const url = new URL(route.request().url());
const parts = decodeURIComponent(url.pathname).split("/").filter(Boolean);
const assetId = parts.at(-1) ?? "asset";
const resourceVersion = parts.at(-2) ?? "missing";
if (realAssets) {
const expectedVersion = assetId.startsWith("STK") ? WP4_07_REAL_RESOURCE_VERSIONS.static : WP4_07_REAL_RESOURCE_VERSIONS.complex;
const asset = realAssets.publicAssets.get(assetId);
if (resourceVersion !== expectedVersion || !asset) return route.fulfill({ status: 404 });
return route.fulfill({ body: readFileSync(asset.path), contentType: asset.content_type, status: 200 });
}
if (resourceVersion === "missing-fixture") return route.fulfill({ status: 404 });
if (WP4_07_REQUIRED_FONT_IDS.some((fontId: string) => url.pathname.endsWith(`/${fontId}`))) {
return route.fulfill({ body: fontBytes!, contentType: "font/ttf", status: 200 });
}
return route.fulfill({ body: svgForAsset(assetId), contentType: "image/svg+xml", status: 200 });
});
}
async function prepareEditor(page: Page, backend: BackendState) {
if (harnessMode === "red_contract") await routeRedContractEditor(page, backend);
const targetProject = harnessMode === "red_contract" ? projectId : process.env.DADA_WP4_07_PROJECT_ID;
if (!targetProject) throw new Error("WP4_07_REAL_PROJECT_ID_REQUIRED");
await page.goto(`${webUrl}/app/projects/${targetProject}/editor`, { waitUntil: "domcontentloaded" });
await routeEditor(page, backend);
await page.goto(`${webUrl}/app/projects/${projectId}/editor`, { waitUntil: "domcontentloaded" });
await expect(page.getByText("对象 50 / 50")).toBeVisible();
await page.evaluate(() => document.fonts.ready);
await page.waitForTimeout(250);
@@ -156,23 +166,25 @@ function percentile(values: readonly number[], ratio: number) {
return sorted[Math.min(sorted.length - 1, Math.max(0, Math.ceil(sorted.length * ratio) - 1))]!;
}
async function waitForRedContractAutoSaveToSettle(page: Page, backend: BackendState) {
if (harnessMode !== "red_contract") return;
async function waitForAutoSaveToSettle(page: Page, backend: BackendState) {
let priorSaves = -1;
for (let attempt = 0; attempt < 5; attempt += 1) {
await page.waitForTimeout(1_100);
if (backend.projectSaves === priorSaves) return;
priorSaves = backend.projectSaves;
}
throw new Error("red contract autosave queue did not settle before export failure isolation");
throw new Error("autosave queue did not settle before export failure isolation");
}
test("TDD-WP4-VIS-001 captures fixed Chrome and Edge editor/export evidence", async ({ page }, testInfo) => {
test("TDD-WP4-VIS-001 captures fixed Chrome and Edge editor/export evidence", async ({ context, page }, testInfo) => {
test.skip(process.env.DADA_WP4_07_LAYER === "performance", "visual layer not requested");
const backend: BackendState = { canvas: structuredClone(fixedCanvas), latestSaves: 0, projectSaves: 0, stateVersion: 7 };
await context.tracing.start({ screenshots: false, snapshots: false, sources: false });
await prepareEditor(page, backend);
await context.tracing.stop({ path: evidencePath(testInfo, "trace.zip") });
const layoutSelectors = {
canvas: ".editor-canvas-frame",
canvas_frame: ".editor-canvas-frame",
canvas_surface: ".editor-canvas",
footer: ".editor-statusbar",
left_panel: ".editor-assets-panel",
right_panel: ".editor-inspector",
@@ -181,6 +193,13 @@ test("TDD-WP4-VIS-001 captures fixed Chrome and Edge editor/export evidence", as
};
const layoutBoxes: Record<string, unknown> = {};
for (const [name, selector] of Object.entries(layoutSelectors)) layoutBoxes[name] = await page.locator(selector).boundingBox();
const frameBox = await page.locator(layoutSelectors.canvas_frame).boundingBox();
const surfaceBox = await page.locator(layoutSelectors.canvas_surface).boundingBox();
expect(frameBox).not.toBeNull();
expect(surfaceBox).not.toBeNull();
expect(Math.abs(frameBox!.width - surfaceBox!.width)).toBeLessThanOrEqual(2);
expect(Math.abs(frameBox!.height - surfaceBox!.height)).toBeLessThanOrEqual(2);
expect(Math.abs(surfaceBox!.width / surfaceBox!.height - fixedCanvas.pixel_width / fixedCanvas.pixel_height)).toBeLessThan(0.002);
await page.screenshot({ animations: "disabled", path: evidencePath(testInfo, "editor.png") });
await page.getByLabel("编辑画布").screenshot({ animations: "disabled", path: evidencePath(testInfo, "canvas.png") });
await page.getByRole("button", { name: "导出", exact: true }).click();
@@ -192,20 +211,23 @@ test("TDD-WP4-VIS-001 captures fixed Chrome and Edge editor/export evidence", as
});
writeEvidence(testInfo, "layout-boxes.json", {
browser,
eligible_for_green: false,
fixture_sha256: wp407FixtureSha256(),
eligible_for_green: greenEligible,
fixture_sha256: wp407FixtureSha256(fixtureVersions),
harness_mode: harnessMode,
layout_boxes: layoutBoxes,
viewport: { device_scale_factor: 1, height: 1080, width: 1920 },
});
if (realAssets) writeEvidence(testInfo, "asset-sources.json", realAssets.evidence);
});
test("TDD-WP4-PERF-001 measures the fixed 50-element budget without dilution", async ({ page }, testInfo) => {
test("TDD-WP4-PERF-001 measures the fixed 50-element budget without dilution", async ({ context, page }, testInfo) => {
test.skip(process.env.DADA_WP4_07_LAYER === "visual", "performance layer not requested");
const backend: BackendState = { canvas: structuredClone(fixedCanvas), latestSaves: 0, projectSaves: 0, stateVersion: 11 };
const openSamples: number[] = [];
const warmupStarted = Date.now();
await context.tracing.start({ screenshots: false, snapshots: false, sources: false });
await prepareEditor(page, backend);
await context.tracing.stop({ path: evidencePath(testInfo, "trace.zip") });
const warmupOpenMs = Date.now() - warmupStarted;
for (let index = 0; index < WP4_07_PERFORMANCE_BUDGETS.measured_runs; index += 1) {
const started = Date.now();
@@ -319,12 +341,12 @@ test("TDD-WP4-PERF-001 measures the fixed 50-element budget without dilution", a
if (run > 0) exportRuns.push(result);
}
await waitForRedContractAutoSaveToSettle(page, backend);
await waitForAutoSaveToSettle(page, backend);
const savesBeforeFailure = { latest: backend.latestSaves, project: backend.projectSaves };
const exportFailure = await page.evaluate(async ({ canvas, fontIds, targetProjectId }) => {
const exportFailure = await page.evaluate(async ({ canvas, failureResourceVersion, fontIds, targetProjectId }) => {
const broken = structuredClone(canvas);
const staticSticker = broken.elements.find((element: { type: string }) => element.type === "static_sticker");
staticSticker.resource_version = "missing-fixture";
staticSticker.resource_version = failureResourceVersion;
const statuses = Object.fromEntries(fontIds.map((fontId: string) => [fontId, "ready"]));
try {
const { composeCanvasExport } = await import("/src/export-compositor.ts");
@@ -333,17 +355,22 @@ test("TDD-WP4-PERF-001 measures the fixed 50-element budget without dilution", a
} catch (error) {
return error instanceof Error ? error.message : String(error);
}
}, { canvas: fixedCanvas, fontIds: WP4_07_REQUIRED_FONT_IDS, targetProjectId: projectId });
}, {
canvas: fixedCanvas,
failureResourceVersion: greenEligible ? "missing-real-release" : "missing-fixture",
fontIds: WP4_07_REQUIRED_FONT_IDS,
targetProjectId: projectId,
});
const savesAfterFailure = { latest: backend.latestSaves, project: backend.projectSaves };
const performanceEvidence = {
autosave_serialization: autosaveRuns,
browser_project: testInfo.project.name,
editor_reopen: { max_ms: Math.max(...openSamples), p50_ms: percentile(openSamples, 0.5), p95_ms: percentile(openSamples, 0.95), samples_ms: openSamples, warmup_ms: warmupOpenMs },
eligible_for_green: false,
eligible_for_green: greenEligible,
export_1080x1920: exportRuns,
export_failure: { observed_error: exportFailure, saves_after: savesAfterFailure, saves_before: savesBeforeFailure },
fixture_sha256: wp407FixtureSha256(),
fixture_sha256: wp407FixtureSha256(fixtureVersions),
harness_mode: harnessMode,
interaction: interactionRuns,
normative_budgets: WP4_07_PERFORMANCE_BUDGETS,
@@ -359,4 +386,5 @@ test("TDD-WP4-PERF-001 measures the fixed 50-element budget without dilution", a
top_count: topDomCount,
});
writeEvidence(testInfo, "environment.json", await page.evaluate(() => ({ device_pixel_ratio: devicePixelRatio, user_agent: navigator.userAgent, viewport: { height: innerHeight, width: innerWidth } })));
if (realAssets) writeEvidence(testInfo, "asset-sources.json", realAssets.evidence);
});
@@ -57,7 +57,7 @@ test("TDD-WP5-CAT-001 keeps the 1,407 sticker directory virtual and loads origin
const backend = { saves: 0, version: 1 };
const requests = { original: 0, thumbnails: 0, thumbnailIds: new Set<string>() };
await routeEditor(page, backend);
await page.route("**/api/v1/assets/public/fixture-v1/*", async (route) => {
await page.route("**/api/v1/assets/public/p0a-static-v1/*", async (route) => {
const url = new URL(route.request().url());
const assetId = decodeURIComponent(url.pathname.split("/").at(-1) ?? "");
if (url.searchParams.get("variant") === "thumbnail") {
+2 -2
View File
@@ -64,8 +64,8 @@ async function routeEditor(page: Page, backend: Backend) {
});
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/recent", (route) => route.fulfill({ body: JSON.stringify({ status: "recorded" }), contentType: "application/json" }));
await page.route("**/api/v1/assets/public/wp4-fixture-v1/*", (route) => route.fulfill({ body: fontBytes, contentType: "font/ttf" }));
await page.route("**/api/v1/assets/public/fixture-v1/*", (route) => route.fulfill({ body: png, contentType: "image/png" }));
await page.route("**/api/v1/assets/public/p0a-complex-v1/*", (route) => route.fulfill({ body: fontBytes, contentType: "font/ttf" }));
await page.route("**/api/v1/assets/public/p0a-static-v1/*", (route) => route.fulfill({ body: png, contentType: "image/png" }));
await page.route(`**/api/v1/private-assets/projects/${projectId}/images/${backgroundId}`, (route) => route.fulfill({ body: rawSvg(), contentType: "image/svg+xml" }));
}