feat(admin): Phase 2 邀请码发放管理 — apps/adminpanel 跨团队端点 + Admin 邀请码页
后端:新建 apps/adminpanel(跨团队后台容器,/api/admin/);GET/POST invitations(列跨团队码+筛选分页 / 平台发 create_team 开团队码,写审计)+ revoke(写审计),全程 IsPlatformAdmin。 前端:adminApi 分层;Admin 邀请码页(类型筛选段控 + 表格 + 发码弹窗 + 复制邀请链接 + 撤销)接入 AdminApp toast; Invitation 类型补 kind/team_name;admin-page.css 补段控/表格/弹窗(仅 token)。 测试:adminpanel 9 + accounts 22 = 31 单测过;无头 e2e _admin-p2.mjs 6 断言过 + 0 console error + 端到端凭 UI 发的码注册开团队 201;tsc+build 绿。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
3f8547564c
commit
45a4ceae51
@@ -0,0 +1,87 @@
|
||||
// Phase 2 e2e:平台超管邀请码发放管理页 —— 列表加载、发开团队码弹窗、复制链接反馈、端到端凭码注册开团队。
|
||||
// 沿用 _wave35-auth.mjs / _admin-p0.mjs 模式:headless + token 注入 + 真点击 + 断言 + 截图 + 0 console error。
|
||||
import { chromium } from "playwright";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const BASE = process.env.BASE || "http://127.0.0.1:5188";
|
||||
const API = process.env.API || "http://127.0.0.1:8010";
|
||||
const OUT = path.resolve("output/admin");
|
||||
fs.mkdirSync(OUT, { recursive: true });
|
||||
const stamp = Date.now();
|
||||
|
||||
async function apiLogin(u, p) {
|
||||
const res = await fetch(`${API}/api/auth/login/`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username: u, password: p }),
|
||||
});
|
||||
if (!res.ok) throw new Error(`login ${u} -> ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
const admin = await apiLogin("admin", "admin123");
|
||||
const r = { page: {}, create: {}, copy: {}, endToEnd: {}, consoleErrors: [], pass: false };
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const hook = (p, tag) => {
|
||||
p.on("console", (m) => { if (m.type() === "error") r.consoleErrors.push(`${tag}:${m.text()}`); });
|
||||
p.on("pageerror", (e) => r.consoleErrors.push(`${tag}:PAGEERR:${e.message}`));
|
||||
};
|
||||
|
||||
{
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 }, permissions: ["clipboard-read", "clipboard-write"] });
|
||||
await ctx.addInitScript((tk) => localStorage.setItem("airshelf_token", tk), admin.token);
|
||||
const p = await ctx.newPage(); hook(p, "invites");
|
||||
await p.goto(BASE + "/admin/invites", { waitUntil: "load" });
|
||||
await p.waitForSelector(".admin-app", { timeout: 12000 });
|
||||
await p.waitForSelector(".admin-table, .empty-state.show", { timeout: 8000 }).catch(() => {});
|
||||
await p.waitForTimeout(400);
|
||||
r.page.title = (await p.locator(".content h1").first().innerText().catch(() => "")).trim();
|
||||
r.page.hasCreateBtn = (await p.locator(".page-head .actions .btn-primary").count()) >= 1;
|
||||
await p.screenshot({ path: path.join(OUT, "p2-invites-list.png"), fullPage: true });
|
||||
|
||||
// 发开团队码 → 弹窗 → 确认
|
||||
await p.locator(".page-head .actions .btn-primary").click();
|
||||
await p.waitForSelector(".modal", { timeout: 6000 });
|
||||
r.create.modalShown = (await p.locator(".modal").count()) === 1;
|
||||
await p.screenshot({ path: path.join(OUT, "p2-create-modal.png") });
|
||||
await p.locator(".modal-f .btn-primary").click();
|
||||
await p.waitForTimeout(1300);
|
||||
r.create.modalClosed = (await p.locator(".modal").count()) === 0;
|
||||
r.create.rowCount = await p.locator(".admin-table tbody tr").count();
|
||||
r.create.newCode = (await p.locator(".admin-table tbody tr:first-child .admin-code").innerText().catch(() => "")).trim();
|
||||
await p.screenshot({ path: path.join(OUT, "p2-after-create.png"), fullPage: true });
|
||||
|
||||
// 复制首行链接 → 按钮反馈「已复制」
|
||||
await p.locator(".admin-table tbody tr:first-child .btn-ghost").first().click();
|
||||
await p.waitForTimeout(400);
|
||||
r.copy.btnText = (await p.locator(".admin-table tbody tr:first-child .btn-ghost").first().innerText().catch(() => "")).trim();
|
||||
|
||||
await ctx.close();
|
||||
}
|
||||
|
||||
// 端到端:用刚发的开团队码注册 → 应开出新团队(201)
|
||||
if (r.create.newCode) {
|
||||
const reg = await fetch(`${API}/api/auth/register/`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username: `p2-ct-${stamp}`, password: "strong-pass-1", team_name: `P2团队${stamp}`, invite_code: r.create.newCode }),
|
||||
});
|
||||
r.endToEnd.registerStatus = reg.status;
|
||||
r.endToEnd.ok = reg.status === 201;
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
const checks = {
|
||||
pageLoads: r.page.title === "邀请码" && r.page.hasCreateBtn === true,
|
||||
createModal: r.create.modalShown === true,
|
||||
created: !!r.create.newCode && r.create.newCode.startsWith("TEAM-") && r.create.modalClosed === true,
|
||||
copyFeedback: r.copy.btnText === "已复制",
|
||||
endToEndRegister: r.endToEnd.ok === true,
|
||||
zeroConsoleErrors: r.consoleErrors.length === 0,
|
||||
};
|
||||
r.checks = checks;
|
||||
r.pass = Object.values(checks).every(Boolean);
|
||||
console.log(JSON.stringify(r, null, 2));
|
||||
fs.writeFileSync(path.join(OUT, "p2-summary.json"), JSON.stringify(r, null, 2));
|
||||
process.exit(r.pass ? 0 : 1);
|
||||
Reference in New Issue
Block a user