Files
aetherbound-guild/prototype/site/verify-r6.js
T

236 lines
12 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/r6");
const browserPath = process.env.ABG_BROWSER_PATH || "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
const criticalScreens = {
"SYS-005": ".opening-composition",
"SHP-001": ".market-composition",
"SHP-002": ".market-review",
"EQP-002": ".equipment-composition",
"PTY-001": ".party-composition",
"BAT-002": ".battle-composition",
"OUT-001": ".result-composition",
"OUT-003": ".reward-composition",
"OUT-004": ".recovery-composition",
"PRO-003": ".records-composition",
"SET-002": ".settings-composition",
};
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, requireVertical = false) {
const metrics = await page.locator(".game-stage").first().evaluate((stage) => {
const box = stage.getBoundingClientRect();
return { left: box.left, right: box.right, top: box.top, bottom: box.bottom, width: box.width, height: box.height, viewportWidth: innerWidth, viewportHeight: innerHeight };
});
assert.ok(metrics.left >= -0.5 && metrics.right <= metrics.viewportWidth + 0.5, `${label} horizontal stage: ${JSON.stringify(metrics)}`);
if (requireVertical) assert.ok(metrics.top >= -0.5 && metrics.bottom <= metrics.viewportHeight + 0.5, `${label} vertical stage: ${JSON.stringify(metrics)}`);
assert.ok(Math.abs(metrics.width / metrics.height - 16 / 9) < 0.02, `${label} aspect ratio: ${JSON.stringify(metrics)}`);
}
async function openView(page, view) {
const desktopButton = page.locator(`[data-view="${view}"]`);
if (!(await desktopButton.isVisible())) {
await page.locator("#open-nav").click();
}
await desktopButton.click();
await page.locator("main h1").waitFor({ state: "visible" });
}
async function waitForMobileNavClosed(page) {
await page.waitForFunction(() => {
const sidebar = document.querySelector("#sidebar");
return sidebar && !sidebar.classList.contains("is-open") && sidebar.getBoundingClientRect().right <= 0.5;
});
}
async function selectScreen(page, id) {
await openView(page, "screens");
const button = page.locator(`.screen-index [data-screen-id="${id}"]`);
await button.evaluate((element) => element.click());
await page.locator(`[data-stage-id="${id}"]`).waitFor({ state: "visible" });
}
async function screenshot(page, name) {
const destination = path.join(outputDir, name);
await page.screenshot({ path: destination, fullPage: true });
return destination;
}
async function desktopAudit(browser, evidence) {
const errors = [];
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 }, deviceScaleFactor: 1 });
page.on("console", (message) => { if (message.type() === "error") errors.push(`console: ${message.text()}`); });
page.on("pageerror", (error) => errors.push(`pageerror: ${error.message}`));
await page.goto(baseURL, { waitUntil: "networkidle" });
assert.equal(await page.locator("#loading").count(), 0, "loading state clears");
assert.equal(await page.locator(".nav-item").count(), 5, "gallery view count");
assert.equal(await page.locator(".flow-progress button").count(), 23, "complete first-play state count");
assert.equal(await page.locator(".recruit-offer").count(), 4, "four provisional offers");
assert.equal(await page.locator(".recruit-offer.is-selected").count(), 0, "opening build is not preselected");
await noHorizontalOverflow(page, "desktop opening");
await stageFitsViewport(page, "desktop opening");
evidence.push(await screenshot(page, "desktop-opening.png"));
await page.locator('[data-offer-id="OFF-01"]').click();
assert.equal(await page.locator(".recruit-offer.is-selected").count(), 1, "first provisional selection");
await page.locator('[data-offer-id="OFF-03"]').click();
assert.equal(await page.locator(".recruit-offer.is-selected").count(), 2, "second provisional selection");
assert.ok(await page.locator(".opening-footer .stage-primary").isEnabled(), "Review Company enabled at 2/2");
await page.locator('[data-flow-index="7"]').click();
assert.equal(await page.locator('[data-stage-id="SHP-001"] .company-meter strong').first().textContent(), "2", "first Market Company count");
evidence.push(await screenshot(page, "desktop-market.png"));
await page.locator('[data-flow-index="12"]').click();
assert.equal(await page.locator('[data-stage-id="PTY-001"] .party-actor').count(), 4, "party line actor count");
assert.equal(await page.locator('[data-stage-id="PTY-001"] .party-actor').last().locator("> b").textContent(), "0", "rightmost position is 0");
evidence.push(await screenshot(page, "desktop-formation.png"));
await page.locator('[data-flow-index="16"]').click();
assert.ok(await page.locator('[data-stage-id="BAT-002"] .forecast-arrow').isVisible(), "battle forecast visible");
assert.equal(await page.locator('[data-stage-id="BAT-002"] .allied-line .party-actor').count(), 4, "battle party line count");
evidence.push(await screenshot(page, "desktop-battle.png"));
await page.locator('[data-flow-index="18"]').click();
assert.ok(await page.locator('[data-stage-id="OUT-001"] .is-divergence').isVisible(), "result divergence visible");
evidence.push(await screenshot(page, "desktop-result.png"));
await page.locator('[data-flow-index="22"]').click();
assert.equal(await page.locator('[data-stage-id="OUT-004"] .recovery-path').count(), 3, "recovery paths visible");
evidence.push(await screenshot(page, "desktop-recovery.png"));
await openView(page, "screens");
assert.equal(await page.locator(".screen-index button").count(), 50, "screen inventory count");
const ids = await page.locator(".screen-index button span").allTextContents();
assert.equal(new Set(ids).size, 50, "screen IDs unique");
for (const id of ids) {
const button = page.locator(`.screen-index [data-screen-id="${id}"]`);
await button.evaluate((element) => element.click());
assert.equal(await page.locator(".game-stage").first().getAttribute("data-stage-id"), id, `${id} owns its composition`);
assert.equal(await page.locator(".prototype-missing").count(), 0, `${id} has no missing placeholder`);
}
for (const [id, selector] of Object.entries(criticalScreens)) {
await selectScreen(page, id);
assert.ok(await page.locator(`[data-stage-id="${id}"] ${selector}`).isVisible(), `${id} critical composition`);
}
await page.locator('[data-screen-variant="error"]').click();
assert.ok(await page.locator(".state-variant-overlay.error").isVisible(), "screen error variant inspectable");
await openView(page, "regions");
assert.equal(await page.locator(".region-tabs button").count(), 8, "region count");
for (let index = 0; index < 8; index += 1) {
await page.locator(`[data-region-index="${index}"]`).click();
await page.waitForFunction(() => {
const image = document.querySelector(".region-stage img");
return image && image.complete && image.naturalWidth > 0;
});
}
evidence.push(await screenshot(page, "desktop-regions.png"));
await openView(page, "catalogs");
assert.equal(await page.locator(".catalog-tabs button").count(), 6, "searchable catalog family count");
await page.locator('[data-catalog-type="professions"]').click();
await page.locator('[data-catalog-filter="base"]').click();
assert.equal(await page.locator(".catalog-entry").count(), 12, "base profession count");
await page.locator('[data-catalog-type="equipment"]').click();
await page.locator("[data-catalog-search]").fill("Stepback Soles");
assert.equal(await page.locator(".catalog-entry").count(), 1, "equipment search result");
assert.equal(await page.locator(".catalog-entry strong").textContent(), "Stepback Soles", "equipment search identity");
evidence.push(await screenshot(page, "desktop-catalog.png"));
await selectScreen(page, "SET-002");
await page.locator('[data-text-scale="130"]').click();
assert.equal(await page.locator("body").getAttribute("data-text-scale"), "130", "130% state applies");
await page.locator('[data-setting-key="contrast"]').click();
assert.match(await page.locator("body").getAttribute("class"), /high-contrast/, "high-contrast state applies");
await page.locator('[data-locale="zh_CN"]').first().click();
assert.equal(await page.locator("html").getAttribute("lang"), "zh-CN", "Simplified Chinese state applies");
assert.match(await page.locator("main h1").textContent(), /显示与文字/, "selected screen localizes");
evidence.push(await screenshot(page, "desktop-settings-zh-130.png"));
await page.locator('[data-text-scale="100"]').click();
await page.locator("#fullscreen-current").click();
assert.ok(await page.locator("#stage-dialog").evaluate((dialog) => dialog.open), "stage viewer opens");
await page.locator("#zoom-in").click();
assert.equal(await page.locator("#zoom-reset").textContent(), "125%", "stage viewer zooms");
await page.locator("#close-dialog").click();
assert.deepEqual(errors, [], `desktop browser errors: ${errors.join(" | ")}`);
await page.close();
}
async function responsiveAudit(browser, evidence) {
const fixtures = [
{ name: "tablet-battle.png", viewport: { width: 1024, height: 768 }, view: "screens", screen: "BAT-002", vertical: false },
{ name: "phone-portrait-opening.png", viewport: { width: 390, height: 844 }, view: "firstplay", vertical: true },
{ name: "phone-landscape-battle.png", viewport: { width: 844, height: 390 }, view: "firstplay", flow: 16, vertical: true },
];
for (const fixture of fixtures) {
const page = await browser.newPage({ viewport: fixture.viewport, deviceScaleFactor: 1 });
const errors = [];
page.on("pageerror", (error) => errors.push(error.message));
await page.goto(`${baseURL}#${fixture.view}`, { waitUntil: "networkidle" });
if (fixture.screen) await selectScreen(page, fixture.screen);
if (Number.isInteger(fixture.flow)) await page.locator(`[data-flow-index="${fixture.flow}"]`).click();
if (fixture.viewport.width < 880) {
await page.locator("#open-nav").click();
assert.match(await page.locator("#sidebar").getAttribute("class"), /is-open/, `${fixture.name} drawer opens`);
await page.locator("#close-nav").click();
assert.doesNotMatch(await page.locator("#sidebar").getAttribute("class"), /is-open/, `${fixture.name} drawer closes`);
await waitForMobileNavClosed(page);
}
await noHorizontalOverflow(page, fixture.name);
await stageFitsViewport(page, fixture.name, fixture.vertical);
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");
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 external = [];
page.on("request", (request) => {
if (!request.url().startsWith("file:") && !request.url().startsWith("data:")) external.push(request.url());
});
await page.goto(`file://${bundle}`, { waitUntil: "load" });
assert.equal(await page.locator(".recruit-offer").count(), 4, "Gallery bundle initializes 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_R6_BROWSER_OK screens=50 flow_states=23 regions=8 catalogs=42,36,320,60,100,16 responsive=desktop,tablet,phone_portrait,phone_landscape bundle=offline");
console.log(`evidence=${evidence.join(",")}`);
}
main().catch((error) => {
console.error(error.stack || error);
process.exitCode = 1;
});