Files
DadaoTask/web/e2e/smoke.mjs
dadao 5be55fdccb feat(web): 重做设计系统与全部页面,前端观感对齐当下审美
用户反馈"UI 太丑、太简陋"。不是换配色,而是先立 token 再让页面去用:

- tailwind.config.js:纸白 paper / 墨绿 primary / 陶土橙 clay + 语义色
  sage/wheat/rust/sky,字号阶、圆角阶、投影、动效
- index.css:卡片、按钮五种变体、输入、pill/chip、导航、骨架屏
- 新增 icons.tsx(线性图标集)与 ui.tsx(Button/Sheet/Toast/空态/骨架/PageHeader)
- 新增 AppShell 取代 Layout + WorkspaceNav:桌面侧栏 + 移动顶部横条,工单未读角标
- 六个页面全部重写:登录、首页、任务、看板、工单、知识、成员、邀请落地
- 详情抽屉改用 Sheet,工单态补齐需求人/办理人/退回次数/动作按钮

e2e 因 DOM 升级同步调整(抽屉按 role=dialog 定位、下拉改用 aria-label),
三套用例(7 + 8 + 7 项)全部重跑通过;tsc 0 错,构建 45 modules / gzip 76.70 kB。
2026-10-28 18:37:35 +08:00

128 lines
5.9 KiB
JavaScript
Raw Permalink 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.getByRole("dialog");
await drawer.waitFor();
// 抽屉里现在有两个 select(优先级 + 升级工单选人),取第一个才是优先级
await drawer.getByLabel("优先级").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);
}