236 lines
12 KiB
JavaScript
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;
|
|
});
|