175 lines
11 KiB
JavaScript
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;
|
|
});
|