180 lines
11 KiB
JavaScript
180 lines
11 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-2");
|
|
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 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 stageAndActionFit(page, selector, label) {
|
|
const metrics = await page.locator(selector).evaluate((action) => {
|
|
const a = action.getBoundingClientRect();
|
|
const s = action.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, viewportWidth: innerWidth, viewportHeight: innerHeight };
|
|
});
|
|
assert.ok(metrics.sLeft >= -0.5 && metrics.sRight <= metrics.viewportWidth + 0.5 && metrics.sTop >= -0.5 && metrics.sBottom <= metrics.viewportHeight + 0.5, `${label} stage: ${JSON.stringify(metrics)}`);
|
|
assert.ok(metrics.aLeft >= metrics.sLeft && metrics.aRight <= metrics.sRight && metrics.aTop >= metrics.sTop && metrics.aBottom <= metrics.sBottom, `${label} action: ${JSON.stringify(metrics)}`);
|
|
}
|
|
|
|
async function hire(page, offerId, expectedBefore, expectedAfter, companyAfter) {
|
|
assert.equal(await page.locator(".p82-resource strong").textContent(), String(expectedBefore));
|
|
assert.equal(await page.locator(".p82-offers > button").count(), 6 - companyAfter, "stable unsold offer count");
|
|
assert.equal(await page.locator(".p82-offers > button.is-focused").count(), 0, "no preselected offer");
|
|
const beforeText = await page.locator(".p82-offers").textContent();
|
|
await page.locator(`[data-market-focus="${offerId}"]`).click();
|
|
assert.equal(await page.locator(".p82-resource strong").textContent(), String(expectedBefore), "inspect spends nothing");
|
|
assert.match(await page.locator(".p82-offer-inspect").textContent(), /LV 1.*XP 0\/30/s);
|
|
assert.match(await page.locator(".p82-offer-inspect").textContent(), /唯一装备槽/);
|
|
assert.ok((await page.locator(".p82-offers").textContent()).includes(beforeText.trim().slice(0, 30)), "inspect does not reroll board");
|
|
await page.locator("[data-market-review]").click();
|
|
assert.match(await page.locator(".p82-transaction").textContent(), /14 金币/);
|
|
assert.match(await page.locator(".p82-transaction").textContent(), new RegExp(`${expectedBefore} → ${expectedAfter}`));
|
|
await page.locator("[data-market-hire-confirm]").click();
|
|
assert.equal(await page.locator(".p82-company-bench article:not(.is-empty)").count(), companyAfter);
|
|
assert.match(await page.locator(".p82-simple-receipt").textContent(), new RegExp(`${expectedBefore} → ${expectedAfter}`));
|
|
assert.match(await page.locator(".p82-simple-receipt").textContent(), new RegExp(`${companyAfter - 1} → ${companyAfter}`));
|
|
}
|
|
|
|
async function completeToEquipment(page) {
|
|
await hire(page, "MKT-R1-01", 52, 38, 3);
|
|
await page.locator("[data-market-continue]").click();
|
|
await hire(page, "MKT-R1-02", 38, 24, 4);
|
|
await page.locator("[data-market-continue]").click();
|
|
}
|
|
|
|
async function buyAndEquip(page) {
|
|
assert.equal(await page.locator(".p82-resource strong").textContent(), "24");
|
|
assert.match(await page.locator(".p82-item-card").textContent(), /12 金币/);
|
|
assert.match(await page.locator(".p82-equipment-offer").textContent(), /不会自动给任何队员装备/);
|
|
await page.locator("[data-market-item-review]").click();
|
|
assert.match(await page.locator(".p82-item-review").textContent(), /24 → 12/);
|
|
assert.match(await page.locator(".p82-item-review").textContent(), /无 · 稍后选择/);
|
|
await page.locator("[data-market-item-confirm]").click();
|
|
assert.match(await page.locator(".p82-item-receipt").textContent(), /24 → 12/);
|
|
assert.match(await page.locator(".p82-item-receipt").textContent(), /无人/);
|
|
await page.locator("[data-market-open-equip]").click();
|
|
assert.equal(await page.locator(".p82-equip-company button.is-selected").count(), 0, "no automatic wearer");
|
|
await page.locator('[data-market-equip-target="OFF-03"]').click();
|
|
assert.match(await page.locator(".p82-equip-delta").textContent(), /EQ-001 → EQ-012/);
|
|
assert.match(await page.locator(".p82-equip-delta").textContent(), /退回库存.*EQ-001/s);
|
|
await page.locator("[data-market-equip-confirm]").click();
|
|
assert.match(await page.locator(".p82-simple-receipt").textContent(), /有效装备数.*1 → 1/s);
|
|
assert.match(await page.locator(".p82-equip-receipt").textContent(), /退回库存.*EQ-001/s);
|
|
}
|
|
|
|
async function orderAndFinish(page) {
|
|
await page.locator("[data-market-open-order]").click();
|
|
const original = await page.locator("[data-market-order-unit]").evaluateAll((buttons) => buttons.map((button) => button.dataset.marketOrderUnit));
|
|
assert.deepEqual(original, ["OFF-03", "OFF-04", "MKT-R1-01", "MKT-R1-02"]);
|
|
assert.ok(await page.locator("[data-market-order-save]").isDisabled(), "cannot save unchanged order");
|
|
await page.locator('[data-market-order-unit="OFF-03"]').click();
|
|
assert.equal(await page.locator("[data-market-order-unit].is-source").count(), 1);
|
|
await page.locator('[data-market-order-unit="MKT-R1-02"]').click();
|
|
const changed = await page.locator("[data-market-order-unit]").evaluateAll((buttons) => buttons.map((button) => button.dataset.marketOrderUnit));
|
|
assert.deepEqual(changed, ["MKT-R1-02", "OFF-04", "MKT-R1-01", "OFF-03"]);
|
|
await page.locator("[data-market-order-undo]").click();
|
|
assert.deepEqual(await page.locator("[data-market-order-unit]").evaluateAll((buttons) => buttons.map((button) => button.dataset.marketOrderUnit)), original, "undo restores order");
|
|
await page.locator('[data-market-order-unit="OFF-04"]').click();
|
|
await page.locator('[data-market-order-unit="MKT-R1-01"]').click();
|
|
assert.ok(await page.locator("[data-market-order-save]").isEnabled());
|
|
await page.locator("[data-market-order-save]").click();
|
|
assert.match(await page.locator(".p82-ready-facts").textContent(), /金币\s*12/);
|
|
assert.match(await page.locator(".p82-ready-facts").textContent(), /队伍 \/ 容量\s*4 \/ 4/);
|
|
assert.match(await page.locator(".p82-ready").textContent(), /P8\.3 尚未开启/);
|
|
}
|
|
|
|
async function desktopAudit(browser, evidence) {
|
|
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 }, deviceScaleFactor: 1 });
|
|
const errors = [];
|
|
page.on("console", (message) => { if (message.type() === "error") errors.push(message.text()); });
|
|
page.on("pageerror", (error) => errors.push(error.message));
|
|
await page.goto(`${baseURL}?package=p8-2#playtest`, { waitUntil: "networkidle" });
|
|
assert.equal(await page.locator("html").getAttribute("lang"), "zh-CN");
|
|
assert.equal(await page.locator(".p82-company-bench article:not(.is-empty)").count(), 2);
|
|
assert.match(await page.locator(".p82-resource").textContent(), /队伍 2 \/ 容量 4/);
|
|
evidence.push(await screenshot(page, "desktop-market-company2.png"));
|
|
await hire(page, "MKT-R1-01", 52, 38, 3);
|
|
evidence.push(await screenshot(page, "desktop-hire1-receipt.png"));
|
|
await page.locator("[data-market-continue]").click();
|
|
await hire(page, "MKT-R1-02", 38, 24, 4);
|
|
evidence.push(await screenshot(page, "desktop-hire2-receipt.png"));
|
|
await page.locator("[data-market-continue]").click();
|
|
evidence.push(await screenshot(page, "desktop-equipment-offer.png"));
|
|
await buyAndEquip(page);
|
|
evidence.push(await screenshot(page, "desktop-equipment-receipt.png"));
|
|
await page.locator("[data-market-open-order]").click();
|
|
await page.locator('[data-market-order-unit="OFF-04"]').click();
|
|
await page.locator('[data-market-order-unit="MKT-R1-01"]').click();
|
|
evidence.push(await screenshot(page, "desktop-order-changed.png"));
|
|
await page.locator("[data-market-order-save]").click();
|
|
evidence.push(await screenshot(page, "desktop-preparation-receipt.png"));
|
|
assert.match(await page.locator(".p82-ready").textContent(), /P8\.3 尚未开启/);
|
|
await page.locator("[data-market-restart]").click();
|
|
assert.equal(await page.locator(".p82-resource strong").textContent(), "52");
|
|
assert.deepEqual(errors, [], `desktop errors: ${errors.join(" | ")}`);
|
|
await page.close();
|
|
}
|
|
|
|
async function responsiveAudit(browser, evidence) {
|
|
for (const fixture of [
|
|
{ name: "phone-landscape-market.png", viewport: { width: 844, height: 390 } },
|
|
{ name: "phone-portrait-market.png", viewport: { width: 390, height: 844 } },
|
|
]) {
|
|
const page = await browser.newPage({ viewport: fixture.viewport, deviceScaleFactor: 1 });
|
|
await page.goto(`${baseURL}?package=p8-2#playtest`, { waitUntil: "networkidle" });
|
|
await page.locator('[data-market-focus="MKT-R1-01"]').click();
|
|
await noHorizontalOverflow(page, fixture.name);
|
|
await stageAndActionFit(page, "[data-market-review]", fixture.name);
|
|
assert.equal(await page.locator(".p82-offers > button:visible").count(), 3);
|
|
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");
|
|
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}?package=p8-2#playtest`, { waitUntil: "load" });
|
|
assert.equal(await page.locator(".p82-offers > button").count(), 3);
|
|
assert.deepEqual(external, []);
|
|
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_P8_2_BROWSER_OK hires=2 hire_price=14 coin=52,38,24,12 company=2,3,4 capacity=4 item_buy=1 item_equip=1 active_slots=1 order_swap=1 undo=1 responsive=desktop,phone_landscape,phone_portrait bundle=offline");
|
|
console.log(`evidence=${evidence.join(",")}`);
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(error.stack || error);
|
|
process.exitCode = 1;
|
|
});
|