/** * 三条真浏览器用例:登录 → 建任务(含 #标签/!优先级)→ 看板拖拽 → 详情抽屉保存。 * * 用 playwright-core + 本机缓存的 Chromium,不下载浏览器(这台机器外网被掐)。 * 跑之前先起后端(8100)与前端(5180): * node e2e/smoke.mjs */ import { existsSync, mkdirSync } from "node:fs"; import { chromium } from "playwright-core"; const APP = process.env.APP_URL ?? "http://127.0.0.1:5180"; const CHROME = process.env.CHROME_PATH ?? `${process.env.LOCALAPPDATA}\\ms-playwright\\chromium-1223\\chrome-win64\\chrome.exe`; const SHOT_DIR = new URL("./artifacts/", import.meta.url).pathname.replace(/^\//, ""); if (!existsSync(CHROME)) { console.error(`找不到 Chromium:${CHROME}\n用 CHROME_PATH 指定可执行文件`); process.exit(2); } mkdirSync(SHOT_DIR, { recursive: true }); const phone = `139${String(Date.now()).slice(-8)}`; let step = 0; function pass(message) { console.log(` ✓ ${++step}. ${message}`); } function fail(message) { console.error(` ✗ ${message}`); process.exit(1); } const browser = await chromium.launch({ executablePath: CHROME, headless: true }); const page = await browser.newPage({ viewport: { width: 1280, height: 900 } }); page.setDefaultTimeout(10_000); const consoleErrors = []; page.on("console", (msg) => { if (msg.type() === "error") consoleErrors.push(msg.text()); }); page.on("pageerror", (err) => consoleErrors.push(String(err))); try { // 1. 登录 await page.goto(`${APP}/login`, { waitUntil: "domcontentloaded" }); await page.locator("#phone").fill(phone); await page.getByRole("button", { name: "获取验证码" }).click(); const devCodeText = await page.locator("text=开发环境验证码").first().innerText(); const code = devCodeText.match(/(\d{6})/)?.[1]; if (!code) fail(`没读到开发验证码:${devCodeText}`); await page.locator("#code").fill(code); await page.getByRole("main").getByRole("button", { name: "登录", exact: true }).click(); await page.waitForURL(`${APP}/`); pass("手机号验证码登录成功"); // 2. 建工作区(创建后直接跳进任务页) await page.getByPlaceholder("例如:达道科技 / 我的事项").fill("E2E 测试组"); await page.getByRole("button", { name: "创建" }).click(); await page.waitForURL(/\/w\/[0-9a-f-]{36}$/); await page.getByRole("heading", { name: "任务" }).waitFor(); pass("建工作区并进入任务页"); // 3. 快速录入:#标签 + !优先级 const composer = page.getByPlaceholder(/加一件要做的事/); await composer.fill("写周报初稿 #周报 !高"); await composer.press("Enter"); const row = page.locator("div.card > div").filter({ hasText: "写周报初稿" }).first(); await row.waitFor(); const rowText = await row.innerText(); if (!rowText.includes("#周报")) fail(`标签没解析进标题外:${rowText}`); if (rowText.includes("#周报") && rowText.includes("写周报初稿 #周报")) fail("标题里残留了语法"); if (!rowText.includes("优先级 高")) fail(`优先级没生效:${rowText}`); pass("快速录入解析 #标签 与 !优先级(标题里已剥离语法)"); // 4. 详情抽屉可编辑:改优先级 + 加评论 await page.getByRole("button", { name: "写周报初稿" }).click(); const drawer = page.locator("aside"); await drawer.waitFor(); // 抽屉里现在有两个 select(优先级 + 升级工单选人),取第一个才是优先级 await drawer.locator("select").first().selectOption("2"); await page.waitForTimeout(500); const commentBox = drawer.getByPlaceholder("补充一句进展…"); await commentBox.fill("E2E:初稿已出,等评审"); await commentBox.press("Enter"); await drawer.locator("text=E2E:初稿已出,等评审").waitFor(); await page.screenshot({ path: `${SHOT_DIR}/detail.png` }); await page.getByRole("button", { name: "关闭" }).click(); const rowAfter = await page.locator("div.card > div").filter({ hasText: "写周报初稿" }).first().innerText(); if (!rowAfter.includes("优先级 中")) fail(`详情里改优先级没同步回列表:${rowAfter}`); pass("详情抽屉保存(优先级回写列表)与评论落库"); // 5. 看板拖拽:待办 → 进行中 await page.getByRole("link", { name: "看板" }).click(); await page.getByRole("heading", { name: "看板" }).waitFor(); const columns = page.locator("section"); const todoCol = columns.nth(0); const doingCol = columns.nth(1); const card = todoCol.locator("article").filter({ hasText: "写周报初稿" }).first(); await card.waitFor(); await card.dragTo(doingCol); await doingCol.locator("article").filter({ hasText: "写周报初稿" }).first().waitFor(); await page.screenshot({ path: `${SHOT_DIR}/board.png` }); pass("看板拖拽改状态(待办 → 进行中)落库并刷新"); // 6. 状态机拒绝非法拖拽:进行中 → 待办 之后不能再直接拖到"已完成"以外的非法列(用 blocked→done 验证) const blockedCol = columns.nth(2); await doingCol.locator("article").filter({ hasText: "写周报初稿" }).first().dragTo(blockedCol); await blockedCol.locator("article").filter({ hasText: "写周报初稿" }).first().waitFor(); const doneCol = columns.nth(3); await blockedCol.locator("article").filter({ hasText: "写周报初稿" }).first().dragTo(doneCol); const warning = await page.locator("text=不能直接变成").first().innerText(); if (!warning.includes("不能直接变成")) fail("非法拖拽没有被拦下"); pass(`非法状态流转被拦下并给出原因:${warning}`); if (consoleErrors.length > 0) { fail(`浏览器控制台有报错:\n${consoleErrors.join("\n")}`); } pass("浏览器控制台无报错"); console.log(`\n全部通过,截图在 ${SHOT_DIR}`); await browser.close(); } catch (err) { await page.screenshot({ path: `${SHOT_DIR}/failure.png` }).catch(() => {}); console.error(`\n失败:${err?.message ?? err}`); await browser.close(); process.exit(1); }