230 lines
14 KiB
JavaScript
230 lines
14 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-4");
|
|
const browserPath = process.env.ABG_BROWSER_PATH || "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
|
|
|
|
async function screenshot(page, name) {
|
|
const destination = path.join(outputDir, name);
|
|
await page.screenshot({ path: destination, fullPage: true });
|
|
return destination;
|
|
}
|
|
|
|
async function noHorizontalOverflow(page, label) {
|
|
const metrics = await page.evaluate(() => ({
|
|
scrollWidth: document.documentElement.scrollWidth,
|
|
clientWidth: document.documentElement.clientWidth,
|
|
}));
|
|
assert.ok(metrics.scrollWidth <= metrics.clientWidth + 1, `${label}: ${JSON.stringify(metrics)}`);
|
|
}
|
|
|
|
async function stageFits(page, label) {
|
|
const metrics = await page.locator(".game-stage").evaluate((node) => {
|
|
const rect = node.getBoundingClientRect();
|
|
return { left: rect.left, right: rect.right, top: rect.top, bottom: rect.bottom, width: innerWidth, height: innerHeight };
|
|
});
|
|
assert.ok(metrics.left >= -0.5 && metrics.right <= metrics.width + 0.5, `${label} horizontal stage fit: ${JSON.stringify(metrics)}`);
|
|
assert.ok(metrics.top >= -0.5 && metrics.bottom <= metrics.height + 0.5, `${label} vertical stage fit: ${JSON.stringify(metrics)}`);
|
|
const action = await page.locator(".experience-stage-actions").evaluate((node) => {
|
|
const a = node.getBoundingClientRect();
|
|
const s = node.closest(".game-stage").getBoundingClientRect();
|
|
return { aLeft: a.left, aRight: a.right, aTop: a.top, aBottom: a.bottom, sLeft: s.left, sRight: s.right, sTop: s.top, sBottom: s.bottom };
|
|
});
|
|
assert.ok(action.aLeft >= action.sLeft && action.aRight <= action.sRight && action.aTop >= action.sTop && action.aBottom <= action.sBottom, `${label} action fit: ${JSON.stringify(action)}`);
|
|
}
|
|
|
|
async function openPackage(page) {
|
|
await page.goto(`${baseURL}?package=p8-4#playtest`, { waitUntil: "networkidle" });
|
|
await page.locator('[data-p84-state="diagnosis"]').waitFor();
|
|
assert.equal(await page.locator(".p84-progress li").count(), 5, "five visible P8.4 steps");
|
|
assert.match(await page.locator(".p84-diagnosis").textContent(), /队伍赢了,但 Ilya 没有回来/);
|
|
assert.match(await page.locator(".p84-cause-replay").textContent(), /首个决定性分歧/);
|
|
assert.match(await page.locator(".p84-cause-replay").textContent(), /EQ-014|寂静后甲/);
|
|
assert.match(await page.locator(".p84-result-totals").textContent(), /12 → 46/);
|
|
const order = await page.evaluate(() => {
|
|
const cause = document.querySelector(".p84-cause-replay").getBoundingClientRect();
|
|
const totals = document.querySelector(".p84-result-totals").getBoundingClientRect();
|
|
return { causeLeft: cause.left, totalsLeft: totals.left };
|
|
});
|
|
assert.ok(order.causeLeft < order.totalsLeft, "causal diagnosis precedes totals in reading order");
|
|
}
|
|
|
|
async function settleCasualty(page) {
|
|
await page.locator("[data-p84-casualty-review]").click();
|
|
assert.match(await page.locator(".p84-casualty").textContent(), /min\(14, 10 \+ floor\(14 \/ 2\)\) = 14/);
|
|
assert.match(await page.locator(".p84-casualty").textContent(), /EQ-014 Quiet Rearplate → 库存/);
|
|
assert.match(await page.locator(".p84-casualty").textContent(), /Company 4 → 3/);
|
|
await page.locator("[data-p84-back-diagnosis]").click();
|
|
assert.equal(await page.locator('[data-p84-state="diagnosis"]').count(), 1, "casualty review is reversible");
|
|
await page.locator("[data-p84-casualty-review]").click();
|
|
await page.locator("[data-p84-casualty-commit]").click();
|
|
assert.equal(await page.locator("text=CASUALTY-P84-001").count(), 1, "one casualty receipt");
|
|
assert.equal(await page.locator("[data-company-unit]").count(), 3, "three survivors remain");
|
|
assert.equal(await page.locator("[data-company-vacancy]").count(), 1, "vacancy remains explicit");
|
|
assert.match(await page.locator(".p84-receipt").textContent(), /0 → 14/);
|
|
assert.match(await page.locator(".p84-receipt").textContent(), /EQ-014/);
|
|
assert.match(await page.locator(".p84-receipt").textContent(), /1 \/ 1/);
|
|
}
|
|
|
|
async function chooseArtifactReward(page) {
|
|
await page.locator("[data-p84-open-reward]").click();
|
|
assert.equal(await page.locator("[data-p84-reward]").count(), 3, "elite draft has three outer cards");
|
|
assert.equal(await page.locator("[data-p84-reward].is-selected").count(), 0, "reward has no preselection");
|
|
assert.ok(await page.locator("[data-p84-reward-next]").isDisabled(), "reward review is disabled before selection");
|
|
await page.locator('[data-p84-reward="artifact"]').click();
|
|
await page.locator("[data-p84-reward-next]").click();
|
|
assert.equal(await page.locator("[data-p84-artifact]").count(), 3, "Artifact card expands to three fixed IDs");
|
|
assert.equal(await page.locator("[data-p84-artifact].is-selected").count(), 0, "Artifact has no preselection");
|
|
assert.deepEqual(await page.locator("[data-p84-artifact]").evaluateAll((nodes) => nodes.map((node) => node.dataset.p84Artifact)), ["AR-021", "AR-031", "AR-043"]);
|
|
await page.locator('[data-p84-artifact="AR-043"]').click();
|
|
await page.locator("[data-p84-artifact-back]").click();
|
|
assert.equal(await page.locator('[data-p84-reward="artifact"].is-selected').count(), 1, "back preserves the outer card");
|
|
await page.locator("[data-p84-reward-next]").click();
|
|
assert.equal(await page.locator('[data-p84-artifact="AR-043"].is-selected').count(), 1, "back preserves nested Artifact choice");
|
|
await page.locator("[data-p84-artifact-review]").click();
|
|
assert.match(await page.locator(".p84-review").textContent(), /AR-043/);
|
|
await page.locator("[data-p84-reward-revise]").click();
|
|
assert.equal(await page.locator('[data-p84-artifact="AR-043"].is-selected').count(), 1, "reward review is reversible");
|
|
await page.locator("[data-p84-artifact-review]").click();
|
|
await page.locator("[data-p84-reward-commit]").click();
|
|
assert.equal(await page.locator("text=REWARD-P84-001").count(), 1, "one reward receipt");
|
|
assert.match(await page.locator(".p84-receipt").textContent(), /AR-043/);
|
|
assert.match(await page.locator(".p84-receipt").textContent(), /金币\s*46/);
|
|
assert.match(await page.locator(".p84-receipt").textContent(), /凭证\s*14/);
|
|
assert.match(await page.locator(".p84-receipt").textContent(), /1 \/ 1/);
|
|
}
|
|
|
|
async function recoverWithCredit(page) {
|
|
await page.locator("[data-p84-open-recovery]").click();
|
|
assert.equal(await page.locator("[data-p84-recovery]").count(), 3, "three recovery responses");
|
|
assert.equal(await page.locator("[data-p84-recovery].is-selected").count(), 0, "recovery has no preselection");
|
|
assert.match(await page.locator(".p84-recovery").textContent(), /不会自动装备退回的EQ-014/);
|
|
await page.locator('[data-p84-recovery="credit"]').click();
|
|
await page.locator("[data-p84-recovery-review]").click();
|
|
assert.match(await page.locator(".p84-review").textContent(), /46 → 46/);
|
|
assert.match(await page.locator(".p84-review").textContent(), /14 → 0/);
|
|
assert.match(await page.locator(".p84-review").textContent(), /3 → 4/);
|
|
await page.locator("[data-p84-recovery-revise]").click();
|
|
assert.equal(await page.locator('[data-p84-recovery="credit"].is-selected').count(), 1, "recovery review is reversible");
|
|
await page.locator("[data-p84-recovery-review]").click();
|
|
await page.locator("[data-p84-recovery-commit]").click();
|
|
assert.equal(await page.locator("text=RECOVERY-P84-001").count(), 1, "one recovery receipt");
|
|
assert.equal(await page.locator("[data-company-unit]").count(), 4, "replacement restores Company to four");
|
|
assert.equal(await page.locator('[data-company-unit="RCV-P84-01"]').count(), 1, "fixed recovery Recruit joins once");
|
|
assert.match(await page.locator(".p84-receipt").textContent(), /金币\s*46/);
|
|
assert.match(await page.locator(".p84-receipt").textContent(), /凭证\s*0/);
|
|
assert.match(await page.locator(".p84-receipt").textContent(), /EQ-014.*仍在库存/s);
|
|
await page.locator("[data-p84-open-market]").click();
|
|
assert.match(await page.locator(".p84-next-market").textContent(), /第2轮集市已准备/);
|
|
assert.match(await page.locator(".p84-market-ledger").textContent(), /金币\s*46/);
|
|
assert.match(await page.locator(".p84-market-ledger").textContent(), /凭证\s*0/);
|
|
assert.match(await page.locator(".p84-market-ledger").textContent(), /AR-043/);
|
|
assert.match(await page.locator(".p84-next-market").textContent(), /P8\.5 尚未开启/);
|
|
}
|
|
|
|
async function mainPath(browser, evidence) {
|
|
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 }, deviceScaleFactor: 1 });
|
|
await openPackage(page);
|
|
evidence.push(await screenshot(page, "desktop-diagnosis.png"));
|
|
await settleCasualty(page);
|
|
evidence.push(await screenshot(page, "desktop-casualty-receipt.png"));
|
|
await chooseArtifactReward(page);
|
|
evidence.push(await screenshot(page, "desktop-reward-receipt.png"));
|
|
await recoverWithCredit(page);
|
|
evidence.push(await screenshot(page, "desktop-next-market.png"));
|
|
await noHorizontalOverflow(page, "desktop next Market");
|
|
await stageFits(page, "desktop next Market");
|
|
await page.close();
|
|
}
|
|
|
|
async function counterfactual(page, reward, recovery, expected) {
|
|
await openPackage(page);
|
|
await page.locator("[data-p84-casualty-review]").click();
|
|
await page.locator("[data-p84-casualty-commit]").click();
|
|
await page.locator("[data-p84-open-reward]").click();
|
|
if (reward === "decline") await page.locator("[data-p84-reward-decline]").click();
|
|
else {
|
|
await page.locator(`[data-p84-reward="${reward}"]`).click();
|
|
await page.locator("[data-p84-reward-next]").click();
|
|
}
|
|
await page.locator("[data-p84-reward-commit]").click();
|
|
await page.locator("[data-p84-open-recovery]").click();
|
|
await page.locator(`[data-p84-recovery="${recovery}"]`).click();
|
|
await page.locator("[data-p84-recovery-review]").click();
|
|
await page.locator("[data-p84-recovery-commit]").click();
|
|
await page.locator("[data-p84-open-market]").click();
|
|
const text = await page.locator(".p84-next-market").textContent();
|
|
assert.match(text, new RegExp(`金币\\s*${expected.coin}`));
|
|
assert.match(text, new RegExp(`凭证\\s*${expected.credit}`));
|
|
assert.equal(await page.locator("[data-company-unit]").count(), expected.company);
|
|
assert.equal(await page.locator("[data-company-vacancy]").count(), expected.company === 3 ? 1 : 0);
|
|
}
|
|
|
|
async function responsiveAudit(browser, evidence) {
|
|
for (const fixture of [
|
|
{ name: "phone-landscape-recovery.png", viewport: { width: 844, height: 390 }, step: "recovery" },
|
|
{ name: "phone-portrait-diagnosis.png", viewport: { width: 390, height: 844 }, step: "diagnosis" },
|
|
]) {
|
|
const page = await browser.newPage({ viewport: fixture.viewport, deviceScaleFactor: 1 });
|
|
await openPackage(page);
|
|
if (fixture.step === "recovery") {
|
|
await page.locator("[data-p84-casualty-review]").click();
|
|
await page.locator("[data-p84-casualty-commit]").click();
|
|
await page.locator("[data-p84-open-reward]").click();
|
|
await page.locator('[data-p84-reward="equipment"]').click();
|
|
await page.locator("[data-p84-reward-next]").click();
|
|
await page.locator("[data-p84-reward-commit]").click();
|
|
await page.locator("[data-p84-open-recovery]").click();
|
|
}
|
|
await noHorizontalOverflow(page, fixture.name);
|
|
await stageFits(page, fixture.name);
|
|
evidence.push(await screenshot(page, fixture.name));
|
|
await page.close();
|
|
}
|
|
}
|
|
|
|
async function bundleAudit(browser) {
|
|
const bundle = path.join(root, "prototype/gallery/aetherbound-guild-prototype.html");
|
|
assert.ok(fs.statSync(bundle).size > 1_000_000, "Gallery bundle contains embedded media");
|
|
const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
|
|
const requests = [];
|
|
page.on("request", (request) => requests.push(request.url()));
|
|
await page.goto(`file://${bundle}?package=p8-4#playtest`, { waitUntil: "load" });
|
|
assert.equal(await page.locator('[data-p84-state="diagnosis"]').count(), 1, "offline bundle opens P8.4");
|
|
assert.equal(requests.filter((url) => /^https?:/.test(url)).length, 0, "offline bundle makes no network requests");
|
|
await page.close();
|
|
}
|
|
|
|
async function main() {
|
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
const browser = await chromium.launch({ headless: true, executablePath: browserPath });
|
|
const evidence = [];
|
|
try {
|
|
await mainPath(browser, evidence);
|
|
const coinPage = await browser.newPage({ viewport: { width: 1280, height: 800 } });
|
|
await counterfactual(coinPage, "coin", "coin", { coin: 45, credit: 14, company: 4 });
|
|
await coinPage.close();
|
|
const shortPage = await browser.newPage({ viewport: { width: 1280, height: 800 } });
|
|
await counterfactual(shortPage, "decline", "short", { coin: 46, credit: 14, company: 3 });
|
|
await shortPage.close();
|
|
await responsiveAudit(browser, evidence);
|
|
await bundleAudit(browser);
|
|
} finally {
|
|
await browser.close();
|
|
}
|
|
fs.writeFileSync(path.join(outputDir, "screenshots.txt"), `${evidence.map((entry) => path.basename(entry)).join("\n")}\n`);
|
|
console.log("ABG_P8_4_BROWSER_OK result=diagnosis_first battle_coin=34 coin=12,46 casualty=Ilya credit=14 item_return=EQ-014 reward_cards=3 artifact_ids=3 reward_commits=1 recovery_options=3 recovery_commits=1 next_market=dynamic counterfactuals=coin_coin,decline_short responsive=desktop,phone_landscape,phone_portrait bundle=offline p8_5=locked");
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(error.stack || error);
|
|
process.exitCode = 1;
|
|
});
|