Files

157 lines
8.4 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-5");
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 fit(page, label) {
const metrics = await page.evaluate(() => ({
scrollWidth: document.documentElement.scrollWidth,
clientWidth: document.documentElement.clientWidth,
clientHeight: innerHeight,
stage: (() => { const r = document.querySelector(".game-stage").getBoundingClientRect(); return { left: r.left, right: r.right, top: r.top, bottom: r.bottom }; })(),
}));
assert.ok(metrics.scrollWidth <= metrics.clientWidth + 1, `${label} horizontal overflow: ${JSON.stringify(metrics)}`);
assert.ok(metrics.stage.left >= -1 && metrics.stage.right <= metrics.clientWidth + 1, `${label} stage horizontal fit`);
assert.ok(metrics.stage.top >= -1 && metrics.stage.bottom <= metrics.clientHeight + 1, `${label} stage vertical fit`);
}
async function open(page) {
await page.goto(`${baseURL}?package=p8-5#playtest`, { waitUntil: "networkidle" });
await page.locator(".p85-intro").waitFor();
assert.equal(await page.locator(".p85-progress li").count(), 12, "twelve P8.5 steps");
assert.match(await page.locator(".p85-intro").textContent(), /完整的公会循环|complete Guild cycle/);
}
async function advanceToBattle1(page) {
await page.locator("[data-p85-start]").click();
await page.locator("[data-p85-opening-commit]").click();
for (const id of ["mael", "ilya", "equip"]) await page.locator(`[data-p85-market="${id}"]`).click();
assert.match(await page.locator(".p85-market").textContent(), /52.*38.*24.*12|52.*38.*24.*12/);
await page.locator("[data-p85-market-next]").click();
await page.locator("[data-p85-risk1-commit]").click();
assert.equal(await page.locator(".p85-battle").count(), 1, "first battle opens after risk commit");
}
async function advanceBattle(page, second = false, beforeDone = null) {
const next = second ? "[data-p85-battle2-next]" : "[data-p85-battle1-next]";
const done = second ? "[data-p85-battle2-done]" : "[data-p85-battle1-done]";
const expected = second ? 5 : 6;
for (let i = 0; i < expected - 1; i++) await page.locator(next).click();
assert.equal(await page.locator(".p85-battle-log .is-reached").count(), expected, `${second ? "second" : "first"} battle event count before terminal`);
if (beforeDone) await beforeDone();
await page.locator(done).click();
}
async function mainPath(browser, evidence) {
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 }, deviceScaleFactor: 1 });
await open(page);
evidence.push(await screenshot(page, "desktop-intro.png"));
await advanceToBattle1(page);
assert.equal(await page.locator(".p83-arena").count(), 1, "first cycle uses dynamic horizontal arena");
assert.equal(await page.locator(".p83-fighter.is-ally").count(), 4, "first cycle shows four moving allies");
evidence.push(await screenshot(page, "desktop-battle-one.png"));
await advanceBattle(page);
await page.locator("[data-p85-result1-next]").click();
await page.locator('[data-p85-reward="AR-043"]').click();
await page.locator("[data-p85-reward-claim]").click();
assert.match(await page.locator(".p85-recovery").textContent(), /EQ-014/);
await page.locator('[data-p85-recovery="credit"]').click();
await page.locator("[data-p85-recovery-commit]").click();
assert.match(await page.locator(".p85-loadout").textContent(), /46.*0.*4|46.*Company/);
await page.locator('[data-p85-equip="equip"]').click();
await page.locator("[data-p85-equip-commit]").click();
assert.match(await page.locator(".p85-risk").textContent(), /开阔跨线|针雨走廊|Open Span|Needle Rain/);
await page.locator('[data-p85-risk2="RSK-P85-02"]').click();
await page.locator("[data-p85-risk2-commit]").click();
assert.equal(await page.locator(".p85-dynamic-arena").count(), 1, "second cycle uses distinct dynamic arena");
assert.equal(await page.locator('[data-fighter-id="RCV-P84-01"]').count(), 1, "Vela replaces Ilya in cycle two");
assert.equal(await page.locator(".p85-audio-caption").count(), 1, "battle event has a caption");
assert.equal(await page.locator("[data-p85-audio]").count(), 1, "prototype sound cue toggle is visible");
await advanceBattle(page, true, async () => { evidence.push(await screenshot(page, "desktop-battle-two.png")); });
await page.locator("[data-p85-result2-next]").click();
assert.equal(await page.locator(".p85-receipt").count(), 1, "integrated receipt exists");
assert.match(await page.locator(".p85-receipt").textContent(), /EQ-014.*Vela/s);
assert.match(await page.locator(".p85-receipt").textContent(), /RSK-P85-02/);
await page.locator("[data-p85-restart]").click();
assert.equal(await page.locator(".p85-intro").count(), 1, "restart returns to fresh intro");
await fit(page, "desktop");
await page.close();
}
async function counterfactual(browser) {
const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
await open(page);
await advanceToBattle1(page);
await advanceBattle(page);
await page.locator("[data-p85-result1-next]").click();
await page.locator('[data-p85-reward="AR-021"]').click();
await page.locator("[data-p85-reward-claim]").click();
await page.locator('[data-p85-recovery="short"]').click();
await page.locator("[data-p85-recovery-commit]").click();
await page.locator('[data-p85-equip="leave"]').click();
await page.locator("[data-p85-equip-commit]").click();
await page.locator('[data-p85-risk2="RSK-P85-01"]').click();
await page.locator("[data-p85-risk2-commit]").click();
assert.match(await page.locator(".p85-battle").textContent(), /Open Span|Gale Hounds|疾风猎犬/);
await page.close();
}
async function responsive(browser, evidence) {
for (const fixture of [
{ name: "phone-landscape.png", viewport: { width: 844, height: 390 } },
{ name: "phone-portrait.png", viewport: { width: 390, height: 844 } },
]) {
const page = await browser.newPage({ viewport: fixture.viewport });
await open(page);
await page.locator('[data-locale="zh_CN"]').click();
await fit(page, fixture.name);
evidence.push(await screenshot(page, fixture.name));
await page.close();
}
}
async function offline(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-5#playtest`, { waitUntil: "load" });
assert.equal(await page.locator(".p85-intro").count(), 1, "offline P8.5 opens");
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);
await counterfactual(browser);
await responsive(browser, evidence);
await offline(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_5_BROWSER_OK fresh_load=1 steps=12 cycle_one=market,risk,battle,result,recovery cycle_two=loadout,risk,battle,result continuation=1 casualty_inherited=Ilya item_return=EQ-014 reward=AR-043 recovery=credit equip_counterfactual=leave risk_counterfactual=Open_Span responsive=desktop,phone_landscape,phone_portrait locale=zh_CN bundle=offline duplicate_commit=1");
}
const timeout = setTimeout(() => { console.error("P8.5 verifier timeout"); process.exitCode = 1; }, 120000);
main().then(() => clearTimeout(timeout)).catch((error) => { clearTimeout(timeout); console.error(error.stack || error); process.exitCode = 1; });