import { expect, test, type APIRequestContext, type Page } from "@playwright/test" const PASSWORD = "SmokePass123!" const apiBase = process.env.E2E_API_BASE || "/api/v1" const apiOrigin = apiBase.endsWith("/api/v1") ? apiBase.slice(0, -"/api/v1".length) : "" async function routeBrowserApiToTestApi(page: Page) { if (!apiOrigin) return await page.route("**/api/v1/**", async (route) => { const sourceUrl = new URL(route.request().url()) const response = await route.fetch({ url: `${apiOrigin}${sourceUrl.pathname}${sourceUrl.search}`, }) await route.fulfill({ response }) }) } async function loginWithRetry(request: APIRequestContext, email: string, password: string) { for (let i = 0; i <= 2; i++) { const r = await request.post(`${apiBase}/auth/login`, { data: { email, password } }) if (r.status() !== 429) { expect(r.ok(), `login: ${await r.text()}`).toBeTruthy() return (await r.json()).access_token as string } console.log(`[douyin] 429 retry ${i + 1}/2`) await new Promise((res) => setTimeout(res, 65000)) } throw new Error("Login retries exhausted") } /** * #1972 抖音文案提取冒烟 * * 路径:文案库页面 → 点「🎬 从抖音提取」→ 粘贴分享文案 → 点「开始提取」 * → mock /api/v1/scripts/extract-from-douyin 返回稳定文案 → 断言「新建文案」弹窗中预填了非空文案 */ test.describe("Douyin Script Extraction (#1972)", () => { test("extract flow: open modal, paste link, text prefilled in create modal", async ({ page, request, }) => { test.setTimeout(180_000) await page.setViewportSize({ width: 1440, height: 900 }) const suffix = Math.random().toString(36).slice(2, 8) const email = `e2e-douyin-${suffix}@example.com` await request.post(`${apiBase}/auth/register`, { data: { email, password: PASSWORD, username: `e2e_dy_${suffix}` }, }) const token = await loginWithRetry(request, email, PASSWORD) const authHeader = { Authorization: `Bearer ${token}` } const proj = await request.post(`${apiBase}/projects`, { headers: authHeader, data: { name: `Smoke Douyin ${suffix}` }, }) const projectId = (await proj.json()).id ?? (await proj.json()).project_id await request.post(`${apiBase}/asset-libraries`, { headers: authHeader, data: { project_id: projectId, name: "Smoke", kind: "video" }, }) await page.addInitScript((t: string) => { window.localStorage.setItem("access_token", t) window.localStorage.setItem( "auth-storage", JSON.stringify({ state: { token: t, user: null } }), ) }, token) await routeBrowserApiToTestApi(page) // Mock 抖音提取接口返回稳定文案 const extractedText = "大家好,今天给大家推荐一款超好用的产品,性价比非常高,快来看看吧!" await page.route("**/api/v1/scripts/extract-from-douyin", (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ text: extractedText, duration_seconds: 15 }), }), ) // 文案列表空态 await page.route( (url) => url.pathname.endsWith("/scripts") && !url.pathname.includes("extract-from-douyin"), (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ items: [], total: 0, page: 1, page_size: 20 }), }), ) await page.goto("/app/scripts") // 文案库页面加载 await expect(page.getByText(/文案库|文案/).first()).toBeVisible({ timeout: 30000 }) // 点「🎬 从抖音提取」按钮 await page.getByRole("button", { name: /从抖音提取/ }).click() await expect(page.getByText("从抖音视频提取文案")).toBeVisible({ timeout: 5000 }) // 在 TextArea 粘贴"抖音分享文案" const textarea = page.locator(".ant-modal textarea").first() await expect(textarea).toBeVisible() await textarea.fill("8.88 复制打开抖音,看看【推荐视频】https://v.douyin.com/abcDEF/") // 点「开始提取」 await page.getByRole("button", { name: "开始提取" }).click() await expect(page.getByText(/提取中/)).toBeVisible({ timeout: 3000 }) // 等待抖音弹窗关闭,「新建文案」弹窗打开并预填提取文案 await expect(page.getByText("从抖音视频提取文案")).not.toBeVisible({ timeout: 15000 }) await expect(page.getByText("新建文案")).toBeVisible({ timeout: 5000 }) const createTextarea = page.locator(".ant-modal textarea").first() await expect(createTextarea).toBeVisible() await expect(createTextarea).toHaveValue(new RegExp(extractedText.slice(0, 10))) console.log("[douyin] Extraction flow completed ✓, text length:", extractedText.length) }) })