149 lines
7.7 KiB
JavaScript
149 lines
7.7 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 baseURL = process.env.ABG_PROTOTYPE_URL || "http://127.0.0.1:8766";
|
|
const outputDir = process.env.ABG_PROTOTYPE_EVIDENCE || "/tmp/aetherbound-prototype-review";
|
|
const browserPath = process.env.ABG_BROWSER_PATH || "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
|
|
|
|
async function clickView(page, view) {
|
|
await page.locator(`[data-view="${view}"]`).click();
|
|
await page.locator("main h1").waitFor({ state: "visible" });
|
|
}
|
|
|
|
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} overflow: ${JSON.stringify(dimensions)}`,
|
|
);
|
|
}
|
|
|
|
async function waitForMobileNavClosed(page) {
|
|
await page.waitForFunction(() => {
|
|
const sidebar = document.querySelector("#sidebar");
|
|
if (!sidebar || sidebar.classList.contains("is-open")) return false;
|
|
return sidebar.getBoundingClientRect().right <= 0.5;
|
|
});
|
|
}
|
|
|
|
async function main() {
|
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
const browser = await chromium.launch({ headless: true, executablePath: browserPath });
|
|
try {
|
|
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 must clear");
|
|
assert.equal(await page.locator(".nav-item").count(), 11, "primary view count");
|
|
await noHorizontalOverflow(page, "desktop overview");
|
|
await page.screenshot({ path: path.join(outputDir, "desktop-overview.png"), fullPage: true });
|
|
|
|
for (const view of [
|
|
"screens", "flow", "chapters", "characters", "professions", "enemies",
|
|
"items", "records", "production", "readiness",
|
|
]) {
|
|
await clickView(page, view);
|
|
assert.ok(await page.locator("main h1").isVisible(), `${view} heading`);
|
|
}
|
|
|
|
await clickView(page, "screens");
|
|
assert.equal(await page.locator(".screen-index button").count(), 82, "screen inventory count");
|
|
const ids = await page.locator(".screen-index button span").allTextContents();
|
|
assert.equal(new Set(ids).size, 82, "screen IDs must be unique");
|
|
assert.equal(await page.locator('[data-coverage="dedicated"]').count(), 1, "dedicated page count");
|
|
assert.equal(await page.locator('[data-coverage="family"]').count(), 40, "shared-family page count");
|
|
assert.equal(await page.locator('[data-coverage="missing"]').count(), 41, "missing page count");
|
|
await page.locator('[data-screen-id="SYS-001"]').first().click();
|
|
assert.ok(await page.locator('[data-stage-id="SYS-001"] .boot-stage').isVisible(), "dedicated boot page");
|
|
assert.equal(await page.locator('[data-stage-id="SYS-001"] button').count(), 0, "boot has no player action");
|
|
assert.equal(await page.locator('[data-stage-id="SYS-001"]').getByText(/Cache|Saved|none/).count(), 0, "boot excludes unrelated HUD");
|
|
await page.screenshot({ path: path.join(outputDir, "desktop-boot-screen.png"), fullPage: true });
|
|
await page.locator('[data-screen-id="SYS-004"]').first().click();
|
|
assert.ok(await page.locator('[data-stage-id="SYS-004"] .prototype-missing').isVisible(), "title gap is explicit");
|
|
await page.screenshot({ path: path.join(outputDir, "desktop-title-gap.png"), fullPage: true });
|
|
await page.locator('[data-screen-id="BAT-002"]').first().click();
|
|
assert.ok(await page.locator('[data-stage-id="BAT-002"]').isVisible(), "battle screen stage");
|
|
assert.ok(await page.locator(".battle-telegraph").isVisible(), "battle telegraph");
|
|
await page.screenshot({ path: path.join(outputDir, "desktop-battle-screen.png"), fullPage: true });
|
|
|
|
await clickView(page, "flow");
|
|
assert.equal(await page.locator(".flow-steps button").count(), 17, "flow step count");
|
|
for (let index = 0; index < 17; index += 1) {
|
|
assert.equal(await page.locator(".flow-steps button.is-active").count(), 1, `active flow step ${index}`);
|
|
await page.locator(".flow-controls [data-flow-next]").click();
|
|
}
|
|
assert.ok(await page.locator('[data-stage-id="SYS-004"]').isVisible(), "flow wraps to title");
|
|
|
|
await clickView(page, "chapters");
|
|
assert.equal(await page.locator(".chapter-tabs button").count(), 8, "chapter count");
|
|
const firstChapterArt = await page.locator(".chapter-art img").getAttribute("src");
|
|
await page.locator('[data-chapter="7"]').click();
|
|
assert.match(await page.locator(".chapter-copy h2").textContent(), /A Concord of Many/);
|
|
const lastChapterArt = await page.locator(".chapter-art img").getAttribute("src");
|
|
assert.notEqual(lastChapterArt, firstChapterArt, "chapter art must change from CH-01 to CH-08");
|
|
assert.match(lastChapterArt, /(?:ch_ch08\.webp|^data:image\/webp;base64,)/);
|
|
|
|
await clickView(page, "items");
|
|
await page.locator('[data-catalog-search="items"]').fill("Aetherbound Guildcoat");
|
|
assert.equal(await page.locator(".catalog-entry").count(), 1, "item search result count");
|
|
assert.equal(await page.locator(".catalog-entry strong").textContent(), "Aetherbound Guildcoat");
|
|
|
|
await page.setViewportSize({ width: 1024, height: 768 });
|
|
await page.reload({ waitUntil: "networkidle" });
|
|
await clickView(page, "screens");
|
|
await page.locator('[data-screen-id="BAT-002"]').first().click();
|
|
const tabletStageBox = await page.locator(".game-stage").boundingBox();
|
|
assert.ok(
|
|
tabletStageBox && tabletStageBox.x >= 0 && tabletStageBox.x + tabletStageBox.width <= 1024.5,
|
|
`tablet stage box ${JSON.stringify(tabletStageBox)}`,
|
|
);
|
|
await noHorizontalOverflow(page, "tablet screens");
|
|
await page.screenshot({ path: path.join(outputDir, "tablet-battle-screen.png"), fullPage: true });
|
|
|
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
await page.reload({ waitUntil: "networkidle" });
|
|
await page.locator("#open-nav").click();
|
|
assert.ok((await page.locator("#sidebar").getAttribute("class")).includes("is-open"), "mobile nav opens");
|
|
await page.locator('[data-view="screens"]').click();
|
|
assert.ok(!(await page.locator("#sidebar").getAttribute("class")).includes("is-open"), "mobile nav closes");
|
|
await waitForMobileNavClosed(page);
|
|
const stageBox = await page.locator(".game-stage").boundingBox();
|
|
assert.ok(stageBox && stageBox.x >= 0 && stageBox.x + stageBox.width <= 390.5, `mobile stage box ${JSON.stringify(stageBox)}`);
|
|
await noHorizontalOverflow(page, "phone portrait screens");
|
|
await page.screenshot({ path: path.join(outputDir, "phone-portrait-screens.png"), fullPage: true });
|
|
|
|
await page.setViewportSize({ width: 844, height: 390 });
|
|
await page.reload({ waitUntil: "networkidle" });
|
|
await page.locator("#open-nav").click();
|
|
await page.locator('[data-view="flow"]').click();
|
|
await waitForMobileNavClosed(page);
|
|
await noHorizontalOverflow(page, "phone landscape flow");
|
|
assert.ok(await page.locator(".game-stage").isVisible(), "landscape stage visible");
|
|
await page.screenshot({ path: path.join(outputDir, "phone-landscape-flow.png"), fullPage: true });
|
|
|
|
assert.deepEqual(errors, [], `browser errors: ${errors.join(" | ")}`);
|
|
console.log("ABG_PROTOTYPE_BROWSER_OK views=11 screens=82 flow_steps=17 chapters=8 responsive=desktop,tablet,phone_portrait,phone_landscape");
|
|
console.log(`evidence=${outputDir}`);
|
|
} finally {
|
|
await browser.close();
|
|
}
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(error.stack || error);
|
|
process.exitCode = 1;
|
|
});
|