151 lines
6.3 KiB
JavaScript
151 lines
6.3 KiB
JavaScript
import { chromium, request } from "playwright";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
const webBase = process.env.MEMBER_WEB_URL || "http://192.168.3.90:3003";
|
|
const apiBase = process.env.MEMBER_API_URL || "http://192.168.3.90:8001/api/v1";
|
|
const artifactDir = path.resolve(__dirname, "..", "..", "project-docs", "qa-artifacts", "member-contract-access", stamp);
|
|
fs.mkdirSync(artifactDir, { recursive: true });
|
|
|
|
const result = {
|
|
webBase,
|
|
apiBase,
|
|
artifactDir,
|
|
username: process.env.MEMBER_EMAIL || null,
|
|
login: null,
|
|
contractsPage: null,
|
|
reviewPage: null,
|
|
reviewsPage: null,
|
|
errors: [],
|
|
};
|
|
|
|
function apiUrl(route) {
|
|
return `${apiBase.replace(/\/$/, "")}${route}`;
|
|
}
|
|
|
|
function assert(condition, message, details = {}) {
|
|
if (!condition) {
|
|
const error = new Error(message);
|
|
error.details = details;
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
function summarizeLoginBody(body) {
|
|
const user = body?.data?.user;
|
|
return {
|
|
code: body?.code,
|
|
message: body?.message,
|
|
hasAccessToken: Boolean(body?.data?.access_token),
|
|
user: user
|
|
? {
|
|
id: user.id,
|
|
tenant_id: user.tenant_id,
|
|
username: user.username,
|
|
display_name: user.display_name,
|
|
role: user.role,
|
|
status: user.status,
|
|
}
|
|
: null,
|
|
};
|
|
}
|
|
|
|
async function loginMember(api) {
|
|
const username = process.env.MEMBER_EMAIL || `member-contract-access-${Date.now()}@example.com`;
|
|
const password = process.env.MEMBER_PASSWORD || "123456";
|
|
|
|
if (!process.env.MEMBER_EMAIL) {
|
|
const registerResp = await api.post(apiUrl("/auth/register"), {
|
|
data: { username, password, display_name: "Member Contract Access Check" },
|
|
});
|
|
const registerBody = await registerResp.json().catch(() => ({}));
|
|
assert(registerResp.ok(), "member registration failed", { status: registerResp.status(), body: registerBody });
|
|
}
|
|
|
|
const loginResp = await api.post(apiUrl("/auth/login"), {
|
|
data: { account: username, password },
|
|
});
|
|
const loginBody = await loginResp.json().catch(() => ({}));
|
|
result.login = { status: loginResp.status(), body: summarizeLoginBody(loginBody) };
|
|
assert(loginResp.ok(), "member login failed", result.login);
|
|
assert(loginBody?.data?.access_token, "member login did not return access token", loginBody);
|
|
assert(loginBody?.data?.user?.role !== "admin", "test account must be a non-admin member", loginBody?.data?.user);
|
|
result.username = username;
|
|
return loginBody.data;
|
|
}
|
|
|
|
async function main() {
|
|
const api = await request.newContext();
|
|
const browser = await chromium.launch({ headless: true });
|
|
|
|
try {
|
|
const { access_token: token, user } = await loginMember(api);
|
|
const context = await browser.newContext({ viewport: { width: 1440, height: 1000 } });
|
|
await context.addInitScript(({ t, u }) => {
|
|
window.localStorage.setItem("access_token", t);
|
|
window.localStorage.setItem("auth-store", JSON.stringify({ state: { accessToken: t, currentUser: u }, version: 0 }));
|
|
}, { t: token, u: user });
|
|
|
|
const page = await context.newPage();
|
|
page.on("console", (msg) => {
|
|
if (msg.type() === "error") result.errors.push(`console:${msg.text()}`);
|
|
});
|
|
page.on("pageerror", (err) => {
|
|
result.errors.push(`pageerror:${err.message}`);
|
|
});
|
|
|
|
await page.goto(`${webBase.replace(/\/$/, "")}/contracts`, { waitUntil: "domcontentloaded" });
|
|
await page.waitForTimeout(1500);
|
|
await page.screenshot({ path: path.join(artifactDir, "contracts-page.png"), fullPage: true });
|
|
const contractsText = await page.locator("body").innerText();
|
|
const uploadButton = page.getByRole("button", { name: "上传已有合同审查" });
|
|
result.contractsPage = {
|
|
url: page.url(),
|
|
blockedByAdminGuard: contractsText.includes("仅管理员可管理合同"),
|
|
hasUploadReviewAction: await uploadButton.isVisible().catch(() => false),
|
|
};
|
|
assert(!result.contractsPage.blockedByAdminGuard, "member is blocked from contracts page by admin-only guard", result.contractsPage);
|
|
assert(result.contractsPage.hasUploadReviewAction, "contracts page does not expose upload review action for member", result.contractsPage);
|
|
|
|
await uploadButton.click();
|
|
await page.waitForURL(/\/review(?:\?|$)/, { timeout: 10000 });
|
|
await page.waitForTimeout(1000);
|
|
await page.screenshot({ path: path.join(artifactDir, "review-page-from-contracts.png"), fullPage: true });
|
|
const reviewText = await page.locator("body").innerText();
|
|
result.reviewPage = {
|
|
url: page.url(),
|
|
acceptsUploadedContracts: reviewText.includes("PDF / DOCX / TXT / PNG / JPG / JPEG") && reviewText.includes("提交审查"),
|
|
};
|
|
assert(result.reviewPage.acceptsUploadedContracts, "review page is not ready for uploaded contract review", result.reviewPage);
|
|
|
|
await page.goto(`${webBase.replace(/\/$/, "")}/reviews`, { waitUntil: "domcontentloaded" });
|
|
await page.waitForTimeout(1500);
|
|
await page.screenshot({ path: path.join(artifactDir, "reviews-page.png"), fullPage: true });
|
|
const reviewsText = await page.locator("body").innerText();
|
|
result.reviewsPage = {
|
|
url: page.url(),
|
|
blockedByAdminGuard: reviewsText.includes("仅管理员可查看审查记录"),
|
|
hasCreateReviewAction: await page.getByRole("button", { name: "发起新审查" }).isVisible().catch(() => false),
|
|
};
|
|
assert(!result.reviewsPage.blockedByAdminGuard, "member is blocked from reviews page by admin-only guard", result.reviewsPage);
|
|
assert(result.reviewsPage.hasCreateReviewAction, "reviews page does not expose create review action for member", result.reviewsPage);
|
|
|
|
fs.writeFileSync(path.join(artifactDir, "member-contract-access-result.json"), JSON.stringify(result, null, 2), "utf8");
|
|
console.log(JSON.stringify(result, null, 2));
|
|
} finally {
|
|
await browser.close().catch(() => undefined);
|
|
await api.dispose().catch(() => undefined);
|
|
}
|
|
}
|
|
|
|
main().catch((error) => {
|
|
result.fatal = { message: error.message, details: error.details };
|
|
fs.writeFileSync(path.join(artifactDir, "member-contract-access-result.json"), JSON.stringify(result, null, 2), "utf8");
|
|
console.error(error);
|
|
process.exit(1);
|
|
});
|