Files
DadaoTask/web/e2e/smoke.mjs
T
dadao b1dc3cffa6 feat(web): 工单页与升级面板,修掉视图竞态与标签缺失导致的崩溃
- 新增 /w/:wsId/tickets:派给我的 / 我发起的、状态过滤、列表内联快操作
- 详情抽屉按角色显示受理/开工/提交/验收/驳回/改派/取消;任务态底部可升级成工单
- 导航抽成 WorkspaceNav,带未读角标(30 秒对齐,操作后立刻刷新)
- 修竞态:切视图/切 tab 连发请求时,慢的回包会把新结果盖掉(reqId 序号丢弃过期响应)
- 登录支持 ?next= 站内回跳,邀请链接不再'登录完掉回首页'
- e2e/tickets.mjs:双人双上下文的完整闭环,8 项断言全绿
2026-10-28 13:20:18 +08:00

128 lines
5.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 三条真浏览器用例:登录 → 建任务(含 #标签/!优先级)→ 看板拖拽 → 详情抽屉保存。
*
* 用 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);
}