Files
cunkebao_v3/Server/tests/batch3_frontend_e2e.mjs
Manus AI 03ae8c07b6 feat: 同步本地全量开发到 GitHub(前端/后端/超管/开发文档规则)
含 AI 助手、抖音 OAuth、流量池推送、支付获客、SuperAdmin 设备与场景管理等本地迭代;以本地为准单向推送,未拉取远程。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-26 22:12:40 +08:00

268 lines
9.6 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.

#!/usr/bin/env node
/**
* Batch3 · 存客宝前端 62 路由 E2EPlaywright headless
* 用法: node Server/tests/batch3_frontend_e2e.mjs
*/
import { chromium } from "playwright";
const WEB = process.env.CKB_WEB || "http://localhost:3100";
const API = process.env.CKB_API || "http://localhost:8082";
const ACCOUNT = process.env.CKB_ACCOUNT || "karuo";
const PASSWORD = process.env.CKB_PASSWORD || "zhiqun1984";
async function apiLogin() {
const res = await fetch(`${API}/v1/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ account: ACCOUNT, password: PASSWORD, typeId: 1 }),
});
const j = await res.json();
if (j.code !== 200) throw new Error("login failed: " + JSON.stringify(j));
const info = await fetch(`${API}/v1/auth/info`, {
headers: { Authorization: `Bearer ${j.data.token}` },
}).then((r) => r.json());
if (info.code !== 200) throw new Error("auth/info failed");
return { token: j.data.token, user: info.data, deviceTotal: info.data.deviceTotal ?? 1 };
}
async function fetchIds(token) {
const h = { Authorization: `Bearer ${token}` };
const get = (path) => fetch(`${API}${path}`, { headers: h }).then((r) => r.json());
const first = (j, key = "list") => {
const d = j.data;
const list = Array.isArray(d) ? d : (d?.list ?? []);
return list[0] ?? null;
};
const [dev, wx, wb, ct, plans] = await Promise.all([
get("/v1/devices?page=1&limit=1"),
get("/v1/wechats?page=1&limit=1"),
get("/v1/workbench/list?page=1&limit=1"),
get("/v1/content/library/list?page=1&limit=1"),
get("/v1/plan/list?page=1&limit=5&sceneId=8"),
]);
const device = first(dev);
const wechat = first(wx);
const workbench = first(wb);
const content = first(ct);
const payPlan = first(plans) || { id: 759 };
return {
deviceId: device?.id ?? 13287,
wechatId: wechat?.wechatId ?? wechat?.id ?? "wxid_test",
wechatRowId: wechat?.id ?? 1,
workbenchId: workbench?.id ?? 1,
contentId: content?.id ?? 88,
planId: payPlan?.id ?? 759,
scenarioId: 8,
scenarioName: encodeURIComponent("付款码获客"),
sceneName: encodeURIComponent("付款码获客"),
trafficPoolId: 1,
groupId: 1,
materialId: 1,
orderId: 1,
aiKey: "default",
reportToken: "qa-preview",
};
}
function buildRoutes(p) {
const r = (path, auth = true, note = "") => ({ path, auth, note });
return [
r("/", true),
r("/init", true),
r("/login", false),
r("/guide", false),
r("/pay/acquire?planId=" + p.planId, false, "公开付款页"),
r("/scenarios", true),
r("/scenarios/settings", true),
r("/scenarios/report/" + p.reportToken, false),
r("/scenarios/new", true),
r("/scenarios/new/8", true),
r("/scenarios/edit/" + p.planId, true),
r("/scenarios/list/8/付款码获客", true),
r("/scenarios/channel/8/付款码获客", true),
r("/scenarios/channel/8/付款码获客/new", true),
r("/scenarios/channel/8/付款码获客/edit/" + p.planId, true),
r("/oauth/douyin/callback", false),
r("/scenarios/ai/" + p.aiKey, true),
r("/workspace", true),
r("/workspace/auto-like", true),
r("/workspace/auto-like/new", true),
r("/workspace/auto-like/record/" + p.workbenchId, true),
r("/workspace/auto-like/edit/" + p.workbenchId, true),
r("/workspace/auto-group", true),
r("/workspace/auto-group/new", true),
r("/workspace/auto-group/" + p.workbenchId, true),
r("/workspace/auto-group/" + p.workbenchId + "/edit", true),
r("/workspace/group-create", true),
r("/workspace/group-create/new", true),
r("/workspace/group-create/" + p.workbenchId, true),
r("/workspace/group-create/" + p.workbenchId + "/groups", true),
r("/workspace/group-create/" + p.workbenchId + "/groups/" + p.groupId, true),
r("/workspace/group-create/" + p.workbenchId + "/edit", true),
r("/workspace/group-push", true),
r("/workspace/group-push/" + p.workbenchId, true),
r("/workspace/group-push/new", true),
r("/workspace/group-push/edit/" + p.workbenchId, true),
r("/workspace/moments-sync", true),
r("/workspace/moments-sync/new", true),
r("/workspace/moments-sync/record/" + p.workbenchId, true),
r("/workspace/moments-sync/edit/" + p.workbenchId, true),
r("/workspace/ai-assistant", true),
r("/workspace/ai-analyzer", true),
r("/workspace/ai-strategy", true, "占位页"),
r("/workspace/ai-forecast", true, "占位页"),
r("/workspace/traffic-distribution", true),
r("/workspace/traffic-distribution/new", true),
r("/workspace/traffic-distribution/edit/1", true),
r("/workspace/traffic-distribution/1", true),
r("/workspace/contact-import/list", true),
r("/workspace/contact-import/form", true),
r("/workspace/contact-import/form/1", true),
r("/workspace/contact-import/detail/1", true),
r("/workspace/ai-knowledge", true),
r("/workspace/ai-knowledge/new", true),
r("/workspace/ai-knowledge/1", true),
r("/workspace/ai-knowledge/1/edit", true),
r("/workspace/distribution-management", true),
r("/workspace/distribution-management/1", true),
r("/workspace/group-welcome", true),
r("/workspace/group-welcome/new", true),
r("/workspace/group-welcome/1", true),
r("/workspace/group-welcome/edit/1", true),
r("/mine", true),
r("/mine/devices", true),
r("/mine/devices/" + p.deviceId, true),
r("/mine/traffic-pool", true),
r("/mine/traffic-pool/list2", true),
r("/mine/traffic-pool/create", true),
r("/mine/traffic-pool/userList/" + p.trafficPoolId, true),
r("/mine/traffic-pool/detail/" + p.wechatId + "/" + p.trafficPoolId, true),
r("/wechat-accounts", true),
r("/wechat-accounts/detail/" + p.wechatRowId, true),
r("/recharge", true),
r("/recharge/order", true),
r("/recharge/order/" + p.orderId, true),
r("/recharge/buy-power", true),
r("/recharge/usage-records", true),
r("/settings", true),
r("/security", true),
r("/about", true),
r("/privacy", true),
r("/userSet", true),
r("/mine/content", true),
r("/mine/content/new", true),
r("/mine/content/edit/" + p.contentId, true),
r("/mine/content/materials/" + p.contentId, true),
r("/mine/content/materials/new/" + p.contentId, true),
r("/mine/content/materials/edit/" + p.contentId + "/1", true),
r("/test", true),
r("/test/select", true),
r("/test/upload", true),
r("/test/update-notification", true),
r("/test/iframe", true),
];
}
function persistUserStore(user, token) {
return {
value: {
state: {
user,
token,
token2: null,
isLoggedIn: true,
},
version: 1,
},
timestamp: Date.now(),
config: { compress: false, encrypt: false },
};
}
async function injectAuth(page, session) {
await page.goto(WEB + "/login", { waitUntil: "domcontentloaded" });
await page.evaluate(
({ store, tok }) => {
localStorage.setItem("token", tok);
localStorage.setItem("user-store", JSON.stringify(store));
},
{ store: persistUserStore(session.user, session.token), tok: session.token },
);
}
async function main() {
const session = await apiLogin();
const params = await fetchIds(session.token);
const routes = buildRoutes(params);
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({ viewport: { width: 390, height: 844 } });
const page = await context.newPage();
const consoleErrors = [];
page.on("pageerror", (e) => consoleErrors.push(String(e.message)));
page.on("console", (msg) => {
if (msg.type() === "error") consoleErrors.push(msg.text());
});
await injectAuth(page, session);
const results = [];
for (const route of routes) {
consoleErrors.length = 0;
const url = WEB + route.path;
try {
await page.goto(url, { waitUntil: "networkidle", timeout: 25000 });
await page.waitForTimeout(800);
const finalPath = new URL(page.url()).pathname;
const body = await page.content();
let status = "pass";
let reason = "";
if (route.auth && finalPath.startsWith("/login")) {
status = "fail";
reason = "被重定向到登录页";
} else if (/Unexpected Application Error|Cannot read properties of undefined/i.test(body)) {
status = "fail";
reason = "页面运行时错误";
} else if (consoleErrors.some((e) => /TypeError|ReferenceError|SyntaxError/i.test(e))) {
status = "fail";
reason = consoleErrors[0]?.slice(0, 80);
} else if (body.length < 200 && !route.note.includes("占位")) {
status = "warn";
reason = "内容过短";
}
results.push({ path: route.path, status, reason, note: route.note });
process.stdout.write(status === "pass" ? "." : status === "warn" ? "?" : "F");
} catch (e) {
results.push({ path: route.path, status: "fail", reason: String(e.message).slice(0, 100), note: route.note });
process.stdout.write("E");
}
}
console.log("\n");
await browser.close();
const passed = results.filter((x) => x.status === "pass").length;
const warned = results.filter((x) => x.status === "warn").length;
const failed = results.filter((x) => x.status === "fail").length;
const total = results.length;
const summary = {
batch: "Batch3",
total,
passed,
warned,
failed,
pass_rate: Math.round((passed / total) * 1000) / 10,
planId_scene8: params.planId,
failures: results.filter((x) => x.status === "fail"),
warnings: results.filter((x) => x.status === "warn"),
};
console.log(JSON.stringify(summary, null, 2));
process.exit(failed > 0 ? 1 : 0);
}
main().catch((e) => {
console.error(e);
process.exit(2);
});