173 lines
10 KiB
JavaScript
173 lines
10 KiB
JavaScript
// Browser checks of the round's result box at settlement (ResultDialog; GitHub #235, PR #112 by @Convey123) on the
|
||
// in-match mock harness (public/dev/game-mock.html):
|
||
// SP_E2E=1 node --test test/ui/resultbox.e2e.test.js → screenshots in test/e2e/out/resultbox-*.png
|
||
//
|
||
// The box wears the official dialog's words only (the owner's review of PR #112): title 作战结束, then the LP THIS
|
||
// player was charged — 全员无伤! / 生命值减少 −N — after a 联防 the authority's own per-player figure
|
||
// (m.public.uniteResult.losses). Asserted: `?phase=SETTLE&variant=unite` pops it for a leaker the helpers saved
|
||
// (losses.p1 = 0, nothing got through → 全员无伤!), `unite,through` for the same leaker charged 3 (→ 生命值减少 −3, not
|
||
// its own battle's leaks), `unite,through,helper` for a HELPER whose teammate paid (→ the official title alone),
|
||
// `unite,dead` for a player not in the round (not in `losses` → no box at all); driving the harness' own COMBAT →
|
||
// SETTLE switcher pops the round's own battle's box; the box is big, centred and click-through, closes by itself (and
|
||
// leaves the DOM), and no scenario logs a console error. The words are unit-tested in test/ui/gameLogic.test.js, the
|
||
// per-player charge server-side in test/match/playtest6-matchflow.test.js (#235).
|
||
|
||
import { test, describe, before, after } from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { existsSync, mkdirSync } from 'node:fs';
|
||
import path from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
|
||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
||
const OUT = path.join(ROOT, 'test/e2e/out');
|
||
const CHROME = process.env.CHROME_PATH || '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
|
||
const ENABLED = process.env.SP_E2E === '1' && existsSync(CHROME);
|
||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||
|
||
describe('settlement result box in the browser', { skip: !ENABLED && 'set SP_E2E=1 (and have Chrome) to run' }, () => {
|
||
let srv;
|
||
let browser;
|
||
let base;
|
||
|
||
before(async () => {
|
||
const { startServer } = await import('../../server/index.js');
|
||
const puppeteer = (await import('puppeteer-core')).default;
|
||
srv = await startServer({ port: 0, host: '127.0.0.1', quiet: true });
|
||
base = `http://127.0.0.1:${srv.port}`;
|
||
browser = await puppeteer.launch({ executablePath: CHROME, headless: true, args: ['--no-sandbox', '--force-device-scale-factor=1'] });
|
||
mkdirSync(OUT, { recursive: true });
|
||
});
|
||
after(async () => {
|
||
await browser?.close();
|
||
await srv?.close();
|
||
});
|
||
|
||
/** A mock page; `seen()` tells whether a result box was EVER in the DOM (it closes by itself after ~3 s). */
|
||
async function open(query, { w = 1920, h = 1080 } = {}) {
|
||
const page = await browser.newPage();
|
||
await page.setViewport({ width: w, height: h });
|
||
const problems = [];
|
||
page.on('console', (m) => { if (m.type() === 'error') problems.push(`console: ${m.text()}`); });
|
||
page.on('pageerror', (e) => problems.push(`pageerror: ${e.message}`));
|
||
page.on('requestfailed', (r) => { if (r.failure()?.errorText !== 'net::ERR_ABORTED') problems.push(`requestfailed: ${r.url()} ${r.failure()?.errorText}`); });
|
||
page.on('response', (r) => { if (r.status() >= 400) problems.push(`http ${r.status()}: ${r.url()}`); });
|
||
await page.evaluateOnNewDocument(() => {
|
||
globalThis.__rdSeen = 0;
|
||
new MutationObserver(() => { if (document.querySelector('.rdialog')) globalThis.__rdSeen++; })
|
||
.observe(document, { subtree: true, childList: true });
|
||
});
|
||
// not networkidle0: the box opens when the screen mounts and is gone ~3 s later, art may still be loading then
|
||
await page.goto(`${base}/dev/game-mock.html?render=fallback&${query}`, { waitUntil: 'domcontentloaded' });
|
||
await page.waitForFunction(() => !!document.querySelector('.screen:not(.gload)'), { timeout: 15000 });
|
||
return { page, problems, seen: () => page.evaluate(() => globalThis.__rdSeen > 0) };
|
||
}
|
||
|
||
/** The box as the player sees it: words + where it sits + how it behaves. */
|
||
const boxState = (page) => page.evaluate(() => {
|
||
const el = document.querySelector('.rdialog');
|
||
if (!el) return null;
|
||
const box = el.querySelector('.rdialog__box');
|
||
const b = box.getBoundingClientRect();
|
||
const vw = document.documentElement.clientWidth;
|
||
const vh = document.documentElement.clientHeight;
|
||
return {
|
||
classes: el.className,
|
||
title: el.querySelector('.rdialog__title').textContent,
|
||
sub: el.querySelector('.rdialog__sub')?.textContent ?? '',
|
||
micro: el.querySelector('.rdialog__micro')?.textContent ?? '',
|
||
chevrons: el.querySelectorAll('.rdialog__chev').length,
|
||
ticks: el.querySelectorAll('.rdialog__tick').length,
|
||
titlePx: parseFloat(getComputedStyle(el.querySelector('.rdialog__title')).fontSize),
|
||
pointerEvents: getComputedStyle(el).pointerEvents,
|
||
centerX: Math.round(b.left + b.width / 2 - vw / 2),
|
||
centerY: Math.round(b.top + b.height / 2 - vh / 2),
|
||
visible: b.width > 300 && b.height > 90,
|
||
};
|
||
});
|
||
|
||
test('the official dialog: 作战结束 + this player\'s own LP after a 联防 — and none for a player not in the round', async () => {
|
||
const { page, problems } = await open('shot=1&phase=SETTLE&variant=unite');
|
||
await page.waitForSelector('.rdialog', { timeout: 10000 });
|
||
const ok = await boxState(page);
|
||
// the mock's player p1 leaked and the helpers stopped every enemy: nothing got through, nobody was charged
|
||
assert.equal(ok.title, '作战结束', JSON.stringify(ok));
|
||
assert.equal(ok.sub, '全员无伤!', 'the official line for this player\'s own LP, and nothing else');
|
||
assert.match(ok.micro, /BATTLE OVER/);
|
||
assert.ok(ok.classes.includes('rdialog--mint'), `the held tone: ${ok.classes}`);
|
||
assert.equal(ok.chevrons, 2);
|
||
assert.equal(ok.ticks, 4);
|
||
assert.ok(ok.titlePx >= 40, `a BIG box, not a line of small print (title ${ok.titlePx}px)`);
|
||
assert.ok(ok.visible, 'a real plate, not a hairline');
|
||
assert.ok(Math.abs(ok.centerX) <= 2 && Math.abs(ok.centerY) <= 2, `centred (off by ${ok.centerX},${ok.centerY})`);
|
||
assert.equal(ok.pointerEvents, 'none', 'the settlement stays clickable under it');
|
||
await sleep(700); // the opening wipe done: the whole plate on the screenshot
|
||
await page.screenshot({ path: path.join(OUT, 'resultbox-unite-held.png') });
|
||
assert.deepEqual(problems, []);
|
||
await page.close();
|
||
|
||
// 3 got through and p1 was charged 3: the LP settlement charged (not its own battle's leaks), no verdict line
|
||
const t = await open('shot=1&phase=SETTLE&variant=unite,through');
|
||
await t.page.waitForSelector('.rdialog', { timeout: 10000 });
|
||
const bad = await boxState(t.page);
|
||
assert.equal(bad.title, '作战结束');
|
||
assert.equal(bad.sub, '生命值减少 −3', 'this player\'s own LP, from the authority');
|
||
assert.ok(bad.classes.includes('rdialog--red'), `the loss tone: ${bad.classes}`);
|
||
await sleep(700); // the opening wipe done: the whole plate on the screenshot
|
||
await t.page.screenshot({ path: path.join(OUT, 'resultbox-unite-through.png') });
|
||
assert.deepEqual(t.problems, []);
|
||
await t.page.close();
|
||
|
||
// p1 HELPED: the 联防 leaked and a teammate was charged 4 while p1 paid nothing — 全员无伤! would be false for that
|
||
// teammate and the official dialog has no other line, so the box keeps the official title alone
|
||
const hp = await open('shot=1&phase=SETTLE&variant=unite,through,helper');
|
||
await hp.page.waitForSelector('.rdialog', { timeout: 10000 });
|
||
const spared = await boxState(hp.page);
|
||
assert.equal(spared.title, '作战结束');
|
||
assert.equal(spared.sub, '', 'no 全员无伤! while a teammate was charged');
|
||
assert.ok(spared.classes.includes('rdialog--orange'), `spared tone: ${spared.classes}`);
|
||
await sleep(700); // the opening wipe done: the whole plate on the screenshot
|
||
await hp.page.screenshot({ path: path.join(OUT, 'resultbox-unite-helper-spared.png') });
|
||
assert.deepEqual(hp.problems, []);
|
||
await hp.page.close();
|
||
|
||
// p1 was eliminated before the round: not in `losses` → no box (and no sound), as in a round without 联防
|
||
const d = await open('shot=1&phase=SETTLE&variant=unite,dead');
|
||
await sleep(1500);
|
||
assert.equal(await d.seen(), false, 'no result box for a player not in the round');
|
||
assert.deepEqual(d.problems, []);
|
||
await d.page.close();
|
||
});
|
||
|
||
test('every battle gets one: COMBAT → SETTLE through the harness switcher pops the official dialog', async () => {
|
||
const { page, problems } = await open('phase=COMBAT');
|
||
await sleep(600);
|
||
assert.equal(await page.$('.rdialog'), null, 'no box while the battle runs');
|
||
const clicked = await page.evaluate(() => {
|
||
const btn = [...document.querySelectorAll('#mockbar button')].find((b) => b.textContent.trim() === 'SETTLE');
|
||
if (!btn) return false;
|
||
btn.click();
|
||
return true;
|
||
});
|
||
assert.ok(clicked, 'the switcher has a SETTLE button');
|
||
await page.waitForSelector('.rdialog', { timeout: 8000 });
|
||
const own = await boxState(page);
|
||
assert.equal(own.title, '作战结束', `the round's own battle, no 联防: ${JSON.stringify(own)}`);
|
||
assert.equal(own.sub, '全员无伤!');
|
||
assert.ok(own.classes.includes('rdialog--mint'));
|
||
await sleep(700); // the opening wipe done: the whole plate on the screenshot
|
||
await page.screenshot({ path: path.join(OUT, 'resultbox-own-battle.png') });
|
||
assert.deepEqual(problems, []);
|
||
await page.close();
|
||
});
|
||
|
||
test('it closes by itself: leaving, then gone (it never blocks the next phase)', async () => {
|
||
const { page, problems } = await open('shot=1&phase=SETTLE&variant=unite');
|
||
await page.waitForSelector('.rdialog', { timeout: 10000 });
|
||
await page.waitForSelector('.rdialog.is-leaving', { timeout: 6000 });
|
||
await page.waitForFunction(() => !document.querySelector('.rdialog'), { timeout: 8000 });
|
||
assert.equal(await page.$('.rdialog'), null, 'and it leaves the DOM after ~3 s');
|
||
assert.deepEqual(problems, []);
|
||
await page.close();
|
||
});
|
||
});
|