test: complete WP4-07 visual and performance budgets
Dada P0-A isolated Windows CI / validate-and-package (push) Failing after 2m34s
Dada P0-A isolated Windows CI / validate-and-package (push) Failing after 2m34s
This commit is contained in:
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user