feat: complete TASK-WP2-03 project lifecycle
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
import { mkdirSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { createServer, type ViteDevServer } from "vite";
|
||||
|
||||
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 targetId = "00000000-0000-4000-8000-000000000501";
|
||||
const doomedId = "00000000-0000-4000-8000-000000000502";
|
||||
const deletedAt = "2026-08-02T09:00:00.000Z";
|
||||
const purgeAt = "2026-09-01T09:00:00.000Z";
|
||||
const session = {
|
||||
audience: "user", authenticated: true, credits: { available_balance: 10, reserved_balance: 0 },
|
||||
csrf_token: "csrf-project-trash-fixture-000000000000000000000000000000000",
|
||||
expires_at: "2026-09-02T09:00:00.000Z",
|
||||
user: { creator_name: "Trash User", role: "user", social_id: "@trash_user", status: "active", user_id: "00000000-0000-4000-8000-000000000503" },
|
||||
};
|
||||
|
||||
function summary(projectId: string, name: string, status: "active" | "trashed") {
|
||||
return {
|
||||
current_image_id: null, deleted_at: status === "trashed" ? deletedAt : null, name, project_id: projectId,
|
||||
purge_at: status === "trashed" ? purgeAt : null, ratio: "3:4", state_version: 2, status,
|
||||
successful_image_count: 0, updated_at: deletedAt,
|
||||
};
|
||||
}
|
||||
|
||||
test("TDD-WP2-PROJ-003 exposes safe trash, blocked restore, restore, and permanent purge actions", async ({ page }) => {
|
||||
let activeCount = 20;
|
||||
let targetStatus: "active" | "trashed" = "active";
|
||||
let restored = false;
|
||||
let purged = false;
|
||||
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?status=*", (route) => {
|
||||
const status = new URL(route.request().url()).searchParams.get("status");
|
||||
const projects = status === "active"
|
||||
? targetStatus === "active" ? [summary(targetId, "日落海报", "active")] : []
|
||||
: [
|
||||
...(targetStatus === "trashed" && !restored ? [summary(targetId, "日落海报", "trashed")] : []),
|
||||
...(!purged ? [summary(doomedId, "待永久删除", "trashed")] : []),
|
||||
];
|
||||
return route.fulfill({ body: JSON.stringify({ active_count: activeCount, active_limit: 20, projects }), contentType: "application/json", status: 200 });
|
||||
});
|
||||
await page.route(`**/api/v1/projects/${targetId}/trash`, (route) => {
|
||||
targetStatus = "trashed";
|
||||
activeCount = 19;
|
||||
return route.fulfill({ body: JSON.stringify({ ...summary(targetId, "日落海报", "trashed") }), contentType: "application/json", status: 200 });
|
||||
});
|
||||
await page.route(`**/api/v1/projects/${targetId}/restore`, (route) => {
|
||||
restored = true;
|
||||
targetStatus = "active";
|
||||
activeCount = 20;
|
||||
return route.fulfill({ body: JSON.stringify({ ...summary(targetId, "日落海报", "active") }), contentType: "application/json", status: 200 });
|
||||
});
|
||||
await page.route(`**/api/v1/projects/${doomedId}/purge`, (route) => {
|
||||
purged = true;
|
||||
return route.fulfill({ body: JSON.stringify({ project_id: doomedId, status: "purged" }), contentType: "application/json", status: 200 });
|
||||
});
|
||||
|
||||
await page.goto(`${webUrl}/app/projects`);
|
||||
await page.getByRole("button", { name: "移入回收站:日落海报" }).click();
|
||||
await expect(page.getByText("项目已移入回收站")).toBeVisible();
|
||||
activeCount = 20;
|
||||
await page.getByRole("tab", { name: "回收站" }).click();
|
||||
await expect(page.getByRole("button", { name: "恢复项目:日落海报" })).toBeDisabled();
|
||||
await expect(page.getByText("活动项目已达 20 个,请先释放名额")).toBeVisible();
|
||||
|
||||
activeCount = 19;
|
||||
await page.getByRole("tab", { name: "项目", exact: true }).click();
|
||||
await page.getByRole("tab", { name: "回收站" }).click();
|
||||
await page.getByRole("button", { name: "恢复项目:日落海报" }).click();
|
||||
await expect(page.getByText("项目已恢复")).toBeVisible();
|
||||
await page.getByRole("button", { name: "永久删除:待永久删除" }).click();
|
||||
await expect(page.getByRole("dialog", { name: "永久删除项目" })).toBeVisible();
|
||||
const evidenceRoot = process.env.DADA_EVIDENCE_DIR_PROJECT_TRASH;
|
||||
if (evidenceRoot) {
|
||||
const directory = resolve(evidenceRoot, "screenshots");
|
||||
mkdirSync(directory, { recursive: true });
|
||||
await page.screenshot({ fullPage: true, path: resolve(directory, "trash-confirmation.png") });
|
||||
}
|
||||
await page.getByRole("button", { name: "确认永久删除" }).click();
|
||||
await expect(page.getByText("项目已永久删除,物理清理将在后台完成")).toBeVisible();
|
||||
expect(restored).toBe(true);
|
||||
expect(purged).toBe(true);
|
||||
});
|
||||
Reference in New Issue
Block a user