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; const projectId = "00000000-0000-4000-8000-000000000701"; const imageId = "00000000-0000-4000-8000-000000000702"; 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()); test("TDD-WP2-EXP-002 shows the no-export state and only its two allowed actions", async ({ page }) => { await page.route("**/api/v1/auth/session", (route) => route.fulfill({ body: JSON.stringify({ csrf_token: "csrf-latest-export-fixture-000000000000000000000000000000", user: { creator_name: "Export User" } }), contentType: "application/json", status: 200 })); await page.route(`**/api/v1/projects/${projectId}`, (route) => route.fulfill({ body: JSON.stringify({ canvas_state: { background: { adjustments: { brightness: 0, contrast: 0, crop: null, filter: "none", fit: "fill", saturation: 0, sharpness: 0, temperature: 0 }, asset_id: imageId }, elements: [], pixel_height: 1440, pixel_width: 1080, ratio: "3:4", schema_version: 1 }, created_at: "2026-08-03T08:00:00.000Z", current_image_id: imageId, deleted_at: null, draft_prompt: "导出空状态", generations: [], images: [{ created_at: "2026-08-03T08:00:00.000Z", generation_id: "00000000-0000-4000-8000-000000000703", image_id: imageId }], latest_exports: [], name: "导出空状态", pixel_height: 1440, pixel_width: 1080, project_id: projectId, purge_at: null, ratio: "3:4", save_status: "saved", state_version: 2, status: "active", successful_image_count: 1, updated_at: "2026-08-03T08:00:00.000Z", }), contentType: "application/json", status: 200, })); await page.goto(`${webUrl}/app/projects/${projectId}`); const region = page.getByRole("region", { name: "最新成品" }); await expect(region.getByText("暂无导出成品", { exact: true })).toBeVisible(); await expect(region.getByRole("link", { name: "进入编辑并导出" })).toHaveAttribute("href", `/app/projects/${projectId}/editor`); await expect(region.getByRole("link", { name: "下载原始生成图" })).toHaveAttribute("href", `/api/v1/private-assets/projects/${projectId}/images/${imageId}`); await expect(region.getByRole("link")).toHaveCount(2); expect(await region.getByRole("button").count()).toBe(0); const evidenceRoot = process.env.DADA_EVIDENCE_DIR_LATEST_EXPORTS; if (evidenceRoot) { const directory = resolve(evidenceRoot, "TDD-WP2-EXP-002-empty-latest", "screenshots"); mkdirSync(directory, { recursive: true }); await page.screenshot({ fullPage: true, path: resolve(directory, "no-export.png") }); } }); test("TDD-WP2-EXP-001 exposes the frozen browser result matrix without conflating download and persistence", async ({ page }) => { await page.goto(webUrl); const matrix = await page.evaluate(async () => { const { exportResultCopy, runExportFlow } = await import("/src/export-flow.ts"); const run = async (downloadFails: boolean, persistFails: boolean) => runExportFlow({ compose: async () => new Blob(["browser-export"], { type: "image/png" }), download: async () => { if (downloadFails) throw new Error("download_failed"); }, persist: async () => { if (persistFails) throw new Error("persist_failed"); }, }); const results = [await run(false, false), await run(false, true), await run(true, false)]; return results.map((result) => ({ copy: exportResultCopy[result.status], status: result.status })); }); expect(matrix).toEqual([ { copy: "已下载并保存为最新成品", status: "downloaded_and_saved" }, { copy: "文件已下载,但未保存为最新成品", status: "downloaded_not_saved" }, { copy: "浏览器下载失败,可重新下载同一次合成结果", status: "download_failed" }, ]); await page.evaluate((items) => { document.body.innerHTML = `

导出结果矩阵

${items.map((item) => `

${item.copy}

`).join("")}
`; }, matrix); const evidenceRoot = process.env.DADA_EVIDENCE_DIR_LATEST_EXPORTS; if (evidenceRoot) { const directory = resolve(evidenceRoot, "TDD-WP2-EXP-001-result-matrix", "screenshots"); mkdirSync(directory, { recursive: true }); await page.screenshot({ fullPage: true, path: resolve(directory, "result.png") }); } }); test("TDD-WP2-EXP-001 lists only the newest saved artifact for each format", async ({ page }) => { await page.route("**/api/v1/auth/session", (route) => route.fulfill({ body: JSON.stringify({ csrf_token: "csrf-latest-list-fixture-00000000000000000000000000000000", user: { creator_name: "Export User" } }), contentType: "application/json", status: 200 })); await page.route(`**/api/v1/projects/${projectId}`, (route) => route.fulfill({ body: JSON.stringify({ canvas_state: { background: { adjustments: { brightness: 0, contrast: 0, crop: null, filter: "none", fit: "fill", saturation: 0, sharpness: 0, temperature: 0 }, asset_id: imageId }, elements: [], pixel_height: 1440, pixel_width: 1080, ratio: "3:4", schema_version: 1 }, created_at: "2026-08-03T08:00:00.000Z", current_image_id: imageId, deleted_at: null, draft_prompt: "最新成品", generations: [], images: [{ created_at: "2026-08-03T08:00:00.000Z", generation_id: "00000000-0000-4000-8000-000000000703", image_id: imageId }], latest_exports: ["jpg", "png"].map((format, index) => ({ byte_size: 100 + index, created_at: "2026-08-03T08:00:00.000Z", download_url: `/api/v1/projects/${projectId}/latest-exports/${format}`, export_id: `00000000-0000-4000-8000-00000000071${index}`, format, pixel_height: 1440, pixel_width: 1080, sha256: String(index).repeat(64), state_version: 2, })), name: "最新成品", pixel_height: 1440, pixel_width: 1080, project_id: projectId, purge_at: null, ratio: "3:4", save_status: "saved", state_version: 2, status: "active", successful_image_count: 1, updated_at: "2026-08-03T08:00:00.000Z", }), contentType: "application/json", status: 200, })); await page.goto(`${webUrl}/app/projects/${projectId}`); const region = page.getByRole("region", { name: "最新成品" }); await expect(region.getByRole("link", { name: "重新下载" })).toHaveCount(2); await expect(region.getByText("JPG", { exact: true })).toBeVisible(); await expect(region.getByText("PNG", { exact: true })).toBeVisible(); await expect(region.getByText("暂无导出成品", { exact: true })).toHaveCount(0); });