Files
tyx_AI_xhs/tests/e2e/wp4-01-editor-background.spec.ts
T

141 lines
9.5 KiB
TypeScript

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";
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 projectId = "00000000-0000-4000-8000-000000000451";
const currentImageId = "00000000-0000-4000-8000-000000000452";
const alternateImageId = "00000000-0000-4000-8000-000000000453";
const elementId = "00000000-0000-4000-8000-000000000454";
const session = {
audience: "user", authenticated: true,
credits: { available_balance: 10, reserved_balance: 0 },
csrf_token: "csrf-editor-fixture-000000000000000000000000000000000000",
expires_at: "2026-09-02T08:00:00.000Z",
user: { creator_name: "Editor User", role: "user", social_id: "@editor_user", status: "active", user_id: "00000000-0000-4000-8000-000000000455" },
};
const initialCanvasState = {
background: { adjustments: { brightness: 12, contrast: -8, crop: null, filter: "sepia", fit: "fit", saturation: 20, sharpness: 30, temperature: -10 }, asset_id: currentImageId },
elements: [{ colors: ["#111111", "#222222", "#333333", "#444444", "#555555"], created_at: "2026-08-02T08:00:00.000Z", element_id: elementId, opacity: 1, position: { x: 0.4, y: 0.3 }, resource_version: "v1", rotation: 12, scale: { x: 1.25, y: 0.8 }, template_or_asset_id: "palette-card", type: "color_card", z_index: 0 }],
pixel_height: 1440, pixel_width: 1080, ratio: "3:4", schema_version: 1,
};
function projectPayload() {
return {
canvas_state: initialCanvasState,
created_at: "2026-08-02T08:00:00.000Z", current_image_id: currentImageId,
draft_prompt: "城市工作室", generations: [],
images: [
{ created_at: "2026-08-02T08:00:00.000Z", generation_id: "00000000-0000-4000-8000-000000000456", image_id: currentImageId },
{ created_at: "2026-08-02T08:05:00.000Z", generation_id: "00000000-0000-4000-8000-000000000457", image_id: alternateImageId },
],
name: "城市工作室", pixel_height: 1440, pixel_width: 1080, project_id: projectId,
ratio: "3:4", save_status: "saved", state_version: 7, status: "active", successful_image_count: 2,
updated_at: "2026-08-02T08:05:00.000Z",
};
}
function writeEvidence(caseId: string, name: string, value: unknown) {
const root = process.env.DADA_EVIDENCE_DIR_EDITOR;
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) {
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 }));
}
test("TDD-WP4-BG-001 preserves overlays while switching the background", async ({ page }) => {
await routeEditor(page);
const saves: Array<Record<string, unknown>> = [];
let version = 7;
await page.route(`**/api/v1/projects/${projectId}/state`, async (route) => {
const payload = route.request().postDataJSON() as Record<string, unknown>;
saves.push(payload);
version += 1;
await route.fulfill({ body: JSON.stringify({ save_status: "saved", state_version: version }), contentType: "application/json", status: 200 });
});
await page.goto(`${webUrl}/app/projects/${projectId}/editor`);
await expect(page.getByRole("heading", { name: "底图参数" })).toBeVisible();
await expect(page.getByText("城市工作室")).toBeVisible();
await expect(page.getByRole("button", { name: /生成图/ }).last()).toBeVisible();
await page.getByRole("button", { name: /生成图/ }).last().click();
const dialog = page.getByRole("dialog", { name: "更换底图" });
await expect(dialog.getByText("覆盖元素会保留,底图编辑参数将重置。", { exact: true })).toBeVisible();
writeEvidence("TDD-WP4-BG-001-switch-background", "canvas-before.json", initialCanvasState);
await dialog.getByRole("button", { name: "确认更换" }).click();
await expect(page.getByText("已更换底图,覆盖元素保留,底图处理已重置")).toBeVisible();
await expect.poll(() => saves.length, { timeout: 4_000 }).toBeGreaterThan(0);
const saved = saves.at(-1) as { canvas_state: typeof initialCanvasState };
expect(saved.canvas_state.background.asset_id).toBe(alternateImageId);
expect(saved.canvas_state.background.adjustments).toEqual({ brightness: 0, contrast: 0, crop: null, filter: "none", fit: "fill", saturation: 0, sharpness: 0, temperature: 0 });
expect(saved.canvas_state.elements[0]?.position).toEqual(initialCanvasState.elements[0]?.position);
expect(saved.canvas_state.elements[0]?.scale).toEqual(initialCanvasState.elements[0]?.scale);
expect(saved.canvas_state.elements[0]?.rotation).toBe(initialCanvasState.elements[0]?.rotation);
expect(saved.canvas_state.elements[0]?.colors).not.toEqual(initialCanvasState.elements[0]?.colors);
await page.screenshot({ fullPage: true, path: process.env.DADA_EVIDENCE_DIR_EDITOR ? resolve(process.env.DADA_EVIDENCE_DIR_EDITOR, "TDD-WP4-BG-001-switch-background", "switch-background.png") : undefined });
writeEvidence("TDD-WP4-BG-001-switch-background", "canvas-after.json", saved.canvas_state);
writeEvidence("TDD-WP4-BG-001-switch-background", "db-diff.json", { state_version_before: 7, state_version_after: version, saves: saves.length });
writeEvidence("TDD-WP4-BG-001-switch-background", "pixel-diff.json", { overlay_position_preserved: true, background_processing_reset: true, palette_refreshed: true });
});
test("TDD-WP4-BG-002 commits, reopens, undoes, and resets background processing", async ({ page }) => {
await routeEditor(page);
const saves: Array<Record<string, unknown>> = [];
let version = 7;
await page.route(`**/api/v1/projects/${projectId}/state`, async (route) => {
saves.push(route.request().postDataJSON() as Record<string, unknown>);
version += 1;
await route.fulfill({ body: JSON.stringify({ save_status: "saved", state_version: version }), contentType: "application/json", status: 200 });
});
await page.goto(`${webUrl}/app/projects/${projectId}/editor`);
const ranges = page.locator("input[type=range]");
await ranges.nth(0).fill("25");
await ranges.nth(1).fill("15");
await ranges.nth(2).fill("-20");
await ranges.nth(3).fill("30");
await ranges.nth(4).fill("60");
await page.getByLabel("滤镜").selectOption("grayscale");
await page.getByLabel("适配方式").selectOption("crop");
await expect(page.getByRole("button", { name: "应用调整" })).toBeEnabled();
await page.getByRole("button", { name: "应用调整" }).click();
await expect.poll(() => saves.length, { timeout: 4_000 }).toBeGreaterThan(0);
const committed = saves.at(-1) as { canvas_state: typeof initialCanvasState };
expect(committed.canvas_state.background.adjustments).toMatchObject({ brightness: 25, contrast: 15, saturation: -20, temperature: 30, sharpness: 60, filter: "grayscale", fit: "crop" });
await expect(page.getByText("已保存", { exact: true })).toBeVisible({ timeout: 4_000 });
await page.getByRole("button", { name: "撤销" }).click();
await expect.poll(() => saves.length, { timeout: 4_000 }).toBeGreaterThan(1);
const undone = saves.at(-1) as { canvas_state: typeof initialCanvasState };
expect(undone.canvas_state.background.adjustments).toEqual(initialCanvasState.background.adjustments);
await page.getByRole("button", { name: "恢复原图" }).click();
await expect(page.getByRole("button", { name: "应用调整" })).toBeEnabled();
await page.getByRole("button", { name: "应用调整" }).click();
await expect.poll(() => saves.length, { timeout: 4_000 }).toBeGreaterThan(2);
const reset = saves.at(-1) as { canvas_state: typeof initialCanvasState };
expect(reset.canvas_state.background.adjustments).toEqual({ brightness: 0, contrast: 0, crop: null, filter: "none", fit: "fill", saturation: 0, sharpness: 0, temperature: 0 });
expect(reset.canvas_state.background.asset_id).toBe(currentImageId);
expect(reset.canvas_state.elements[0]?.position).toEqual(initialCanvasState.elements[0]?.position);
writeEvidence("TDD-WP4-BG-002-processing-controls", "canvas-state.json", reset.canvas_state);
for (const [key, value] of Object.entries(reset.canvas_state.background.adjustments)) writeEvidence(`TDD-WP4-BG-002-processing-controls/${key}`, "canvas-state.json", { [key]: value });
writeEvidence("TDD-WP4-BG-002-processing-controls", "db-diff.json", { state_version_before: 7, state_version_after: version, saves: saves.length });
writeEvidence("TDD-WP4-BG-002-processing-controls", "pixel-diff.json", { preview_commit_undo_reset: true, export_source_canvas_state_stable: true });
await page.screenshot({ fullPage: true, path: process.env.DADA_EVIDENCE_DIR_EDITOR ? resolve(process.env.DADA_EVIDENCE_DIR_EDITOR, "TDD-WP4-BG-002-processing-controls", "processing-controls.png") : undefined });
});