用户反馈"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。
128 lines
5.9 KiB
JavaScript
128 lines
5.9 KiB
JavaScript
/**
|
||
* 三条真浏览器用例:登录 → 建任务(含 #标签/!优先级)→ 看板拖拽 → 详情抽屉保存。
|
||
*
|
||
* 用 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);
|
||
}
|