Files
DadaoTask/web/e2e/smoke.mjs
T
dadao 6e33e1024d feat(web,android): 看板拖拽、详情侧栏编辑、快速录入与安卓任务列表
Web:
- 看板四列可拖拽,非法流转当场说明原因;勾选后可批量移动
- 详情侧栏全部可编辑(标题/优先级/截止/标签/备注/评论),409 自动刷新
- 快速录入 #标签 !优先级;键盘流 j/k、Enter、e、Esc
- Playwright 真浏览器 e2e 7 项断言全绿(web/e2e/smoke.mjs)
- 1000 条任务实测首屏 P50 38-55ms

Android(未编译验证:本机无 JDK/Gradle):
- 任务列表/详情/勾选完成/快速新建,冲突时提示并刷新
- 修:User/Member 字段名与后端 nickmame/phone 对齐,原先必然反序列化失败
2026-10-28 04:05:41 +08:00

127 lines
5.8 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();
await drawer.locator("select").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);
}