Files

175 lines
11 KiB
JavaScript

#!/usr/bin/env node
"use strict";
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const { chromium } = require("playwright");
const root = path.resolve(__dirname, "../..");
const baseURL = process.env.ABG_PROTOTYPE_URL || "http://127.0.0.1:8766";
const outputDir = process.env.ABG_PROTOTYPE_EVIDENCE || path.join(root, "prototype/gallery/evidence/p8-1");
const browserPath = process.env.ABG_BROWSER_PATH || "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
async function noHorizontalOverflow(page, label) {
const dimensions = await page.evaluate(() => ({
scrollWidth: document.documentElement.scrollWidth,
clientWidth: document.documentElement.clientWidth,
}));
assert.ok(dimensions.scrollWidth <= dimensions.clientWidth + 1, `${label}: ${JSON.stringify(dimensions)}`);
}
async function stageFitsViewport(page, label) {
const metrics = await page.locator(".game-stage").evaluate((stage) => {
const box = stage.getBoundingClientRect();
return { left: box.left, right: box.right, top: box.top, bottom: box.bottom, viewportWidth: innerWidth, viewportHeight: innerHeight };
});
assert.ok(metrics.left >= -0.5 && metrics.right <= metrics.viewportWidth + 0.5, `${label} horizontal: ${JSON.stringify(metrics)}`);
assert.ok(metrics.top >= -0.5 && metrics.bottom <= metrics.viewportHeight + 0.5, `${label} vertical: ${JSON.stringify(metrics)}`);
}
async function actionFitsStage(page, selector, label) {
const metrics = await page.locator(selector).evaluate((button) => {
const action = button.getBoundingClientRect();
const stage = button.closest(".game-stage").getBoundingClientRect();
return { actionLeft: action.left, actionRight: action.right, actionTop: action.top, actionBottom: action.bottom, stageLeft: stage.left, stageRight: stage.right, stageTop: stage.top, stageBottom: stage.bottom };
});
assert.ok(metrics.actionLeft >= metrics.stageLeft && metrics.actionRight <= metrics.stageRight && metrics.actionTop >= metrics.stageTop && metrics.actionBottom <= metrics.stageBottom, `${label}: ${JSON.stringify(metrics)}`);
}
async function screenshot(page, name) {
const destination = path.join(outputDir, name);
await page.screenshot({ path: destination, fullPage: true });
return destination;
}
async function selectContract(page) {
assert.equal(await page.locator(".experience-progress .is-current strong").textContent(), "契约与难度");
assert.equal(await page.locator("[data-experience-difficulty]:enabled").count(), 0, "difficulty locked before Contract");
assert.ok(await page.locator("[data-experience-next]").isDisabled(), "continue disabled before Contract and difficulty");
await page.locator('[data-experience-contract="CTR-R1-01"]').click();
assert.equal(await page.locator("[data-experience-difficulty]:enabled").count(), 3, "three authoritative difficulties enabled");
await page.locator('[data-experience-difficulty="standard"]').click();
assert.ok(await page.locator("[data-experience-next]").isEnabled(), "continue enabled after Contract and difficulty");
await page.locator("[data-experience-next]").click();
}
async function inspectAndChoose(page) {
assert.equal(await page.locator(".experience-offer").count(), 4, "four stable offers");
assert.deepEqual(await page.locator(".experience-offer-art > span").allTextContents(), ["OFF-01", "OFF-02", "OFF-03", "OFF-04"], "stable offer order");
assert.equal(await page.locator(".experience-offer.is-selected").count(), 0, "no recommended pair");
const offerSnapshot = await page.locator(".experience-offer").allTextContents();
for (const id of ["OFF-01", "OFF-02", "OFF-03", "OFF-04"]) await page.locator(`[data-experience-inspect="${id}"]`).click();
assert.equal(await page.locator(".experience-stage-actions").textContent().then((text) => /已查看\s*4\/4/.test(text)), true, "all offers inspected");
assert.deepEqual(await page.locator(".experience-offer").allTextContents(), offerSnapshot.map((text) => text.replace("查看", "已查看")), "inspection does not reroll offer values");
await page.locator('[data-experience-offer="OFF-01"]').click();
await page.locator('[data-experience-offer="OFF-02"]').click();
assert.equal(await page.locator(".experience-offer.is-selected").count(), 2, "exactly two selected");
await page.locator('[data-experience-offer="OFF-03"]').click();
assert.equal(await page.locator(".experience-offer.is-selected").count(), 2, "third selection rejected");
assert.match(await page.locator(".experience-notice").textContent(), /已经选择两名队员/, "rejection reason visible");
await page.locator('[data-experience-offer="OFF-02"]').click();
await page.locator('[data-experience-offer="OFF-04"]').click();
assert.deepEqual(await page.locator(".experience-offer.is-selected .experience-offer-art > span").allTextContents(), ["OFF-01", "OFF-04"], "player-selected pair retained");
}
async function reviewAndCommit(page) {
assert.equal(await page.locator(".experience-progress .is-current strong").textContent(), "创建前确认");
assert.match(await page.locator(".experience-review-grid").textContent(), /首次穿越/);
assert.match(await page.locator(".experience-review-grid").textContent(), /标准/);
assert.match(await page.locator(".experience-review-grid").textContent(), /金币消耗\s*0/);
assert.equal(await page.locator(".experience-review-grid .recruit-marker.is-selected").count(), 2, "two selected in review");
assert.equal(await page.locator(".experience-review-grid .recruit-marker:not(.is-selected)").count(), 2, "two unchosen in review");
await page.locator("[data-experience-revise]").click();
assert.deepEqual(await page.locator(".experience-offer.is-selected .experience-offer-art > span").allTextContents(), ["OFF-01", "OFF-04"], "revise preserves same pair");
assert.deepEqual(await page.locator(".experience-offer-art > span").allTextContents(), ["OFF-01", "OFF-02", "OFF-03", "OFF-04"], "revise preserves same offer set");
await page.locator("[data-experience-next]").click();
await page.locator("[data-experience-commit]").click();
assert.equal(await page.locator(".experience-progress .is-current strong").textContent(), "创建回执");
assert.equal(await page.locator("text=P8-RUN-001").count(), 1, "one creation receipt");
assert.match(await page.locator(".experience-receipt").textContent(), /队伍 2 \/ 容量 4/);
assert.match(await page.locator(".experience-receipt").textContent(), /P8\.2 尚未开启/);
assert.equal(await page.locator("[data-experience-commit]").count(), 0, "commit unavailable after receipt");
}
async function desktopAudit(browser, evidence) {
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 }, deviceScaleFactor: 1 });
const errors = [];
page.on("console", (message) => { if (message.type() === "error") errors.push(message.text()); });
page.on("pageerror", (error) => errors.push(error.message));
await page.goto(`${baseURL}?package=p8-1#playtest`, { waitUntil: "networkidle" });
assert.equal(await page.locator("html").getAttribute("lang"), "zh-CN", "Chinese default");
assert.equal(await page.locator(".nav-item").count(), 7, "P8.1 plus six atlas views");
await noHorizontalOverflow(page, "desktop Contract");
evidence.push(await screenshot(page, "desktop-contract.png"));
await selectContract(page);
await inspectAndChoose(page);
evidence.push(await screenshot(page, "desktop-company-selected.png"));
await page.locator("[data-experience-next]").click();
evidence.push(await screenshot(page, "desktop-review.png"));
await page.locator("[data-experience-revise]").click();
await page.locator("[data-experience-next]").click();
await page.locator("[data-experience-commit]").click();
evidence.push(await screenshot(page, "desktop-receipt.png"));
assert.equal(await page.locator("text=P8-RUN-001").count(), 1);
await page.locator("[data-experience-restart]").click();
assert.equal(await page.locator(".experience-contract-card.is-selected").count(), 0, "restart clears Contract");
assert.equal(await page.locator(".experience-progress .is-current strong").textContent(), "契约与难度");
assert.deepEqual(errors, [], `desktop browser errors: ${errors.join(" | ")}`);
await page.close();
}
async function responsiveAudit(browser, evidence) {
for (const fixture of [
{ name: "phone-landscape-offers.png", viewport: { width: 844, height: 390 } },
{ name: "phone-portrait-offers.png", viewport: { width: 390, height: 844 } },
]) {
const page = await browser.newPage({ viewport: fixture.viewport, deviceScaleFactor: 1 });
const errors = [];
page.on("pageerror", (error) => errors.push(error.message));
await page.goto(`${baseURL}?package=p8-1#playtest`, { waitUntil: "networkidle" });
await selectContract(page);
await page.locator('[data-experience-inspect="OFF-01"]').click();
await page.locator('[data-experience-offer="OFF-01"]').click();
await page.locator('[data-experience-offer="OFF-04"]').click();
await noHorizontalOverflow(page, fixture.name);
await stageFitsViewport(page, fixture.name);
await actionFitsStage(page, ".experience-stage-actions [data-experience-next]", `${fixture.name} primary action`);
assert.equal(await page.locator(".experience-offer:visible").count(), 4, `${fixture.name} four offers visible`);
evidence.push(await screenshot(page, fixture.name));
assert.deepEqual(errors, [], `${fixture.name} browser errors: ${errors.join(" | ")}`);
await page.close();
}
}
async function bundleAudit(browser) {
const bundle = path.join(root, "prototype/gallery/aetherbound-guild-prototype.html");
const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
const external = [];
page.on("request", (request) => { if (!request.url().startsWith("file:") && !request.url().startsWith("data:")) external.push(request.url()); });
await page.goto(`file://${bundle}?package=p8-1#playtest`, { waitUntil: "load" });
assert.equal(await page.locator(".experience-contract-card").count(), 1, "self-contained P8.1 starts offline");
assert.deepEqual(external, [], `Gallery external requests: ${external.join(" | ")}`);
await page.close();
}
async function main() {
fs.mkdirSync(outputDir, { recursive: true });
const browser = await chromium.launch({ headless: true, executablePath: browserPath });
const evidence = [];
try {
await desktopAudit(browser, evidence);
await responsiveAudit(browser, evidence);
await bundleAudit(browser);
} finally {
await browser.close();
}
console.log("ABG_P8_1_BROWSER_OK states=4 contracts=1 difficulties=3 offers=4 selected=2 commit_receipts=1 responsive=desktop,phone_landscape,phone_portrait bundle=offline");
console.log(`evidence=${evidence.join(",")}`);
}
main().catch((error) => {
console.error(error.stack || error);
process.exitCode = 1;
});