Files
ark-stronghold-protocol/test/ui/terrain-tip.e2e.test.js
T
Cartethyia f6f5ed50cd A special terrain tile explains itself on a tap, and 活性源石 is one material on both boards (GitHub #184) (#185)
Tapping a 活性源石 / 沼泽 / 深水区 / 排气格栅 / gate / teleport tile opens a read-only card with that stage's own terrain numbers (the boss-prep board picks the board tile once, through prepXf.toBoard); the 2D atlas cell and the 3D shader now share one 活性源石 palette and recipe and draw to the tile edge. Contributed by @Convey123.
2026-10-06 14:08:18 +08:00

173 lines
9.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Browser checks of the special-terrain tip (GitHub issue #184 「建议加入对于特殊地形的单击信息提示」) on the in-match mock
// harness (public/dev/game-mock.html):
// SP_E2E=1 CHROME_PATH=… node --test test/ui/terrain-tip.e2e.test.js
//
// A tap on the GROUND — nothing stands there — makes the tile explain itself, on both field implementations: the engine
// canvas (render/app.js `tileClick`, reached by clicking the tile's own screen position) and the DOM fallback
// (ui/fallbackField.js, the tile div). Asserted: the mock's own stage (act2autochess_m01) carries gates, a tap on 蓝门 /
// 红门 opens the card with its mechanism line, a tap on an ordinary floor tile opens nothing (and closes the card that was
// open), and no scenario logs a console error. The words and the numbers are unit-tested in test/ui/gameLogic.test.js
// (terrainInfo, against every terrain of the real stages) — this file is about the tap reaching them.
import { test, describe, before, after } from 'node:test';
import assert from 'node:assert/strict';
import { existsSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
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));
/** Tiles of the mock's stage (act2autochess_m01) the tip must answer for, and one ordinary tile it must not. */
const GATE = { row: 9, col: 10, name: '红门' };
const GOAL = { row: 9, col: 2, name: '蓝门' };
const FLOOR = { row: 10, col: 5, name: null };
describe('special terrain tip 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'] });
});
after(async () => {
await browser?.close();
await srv?.close();
});
async function open(url, { w = 1600, h = 900, waitUntil = 'networkidle0' } = {}) {
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}`); });
// the engine keeps fetching art (and SwiftShader is slow to build the board), so it waits on __SP_VIEW__ instead
await page.goto(`${base}${url}`, { waitUntil, timeout: 60000 });
return { page, problems };
}
/** The open detail card's text (null while none is open). */
const card = (page) => page.evaluate(() => {
const el = document.querySelector('.dpanel');
return el ? el.textContent.replace(/\s+/g, ' ').trim() : null;
});
test('the fallback (DOM) board: a tap on 蓝门 / 红门 opens the card, an ordinary tile opens nothing', async () => {
const { page, problems } = await open('/dev/game-mock.html?phase=PREP&render=fallback');
await page.waitForSelector('.ff-tile', { timeout: 15000 });
await sleep(400);
const tap = (row, col) => page.evaluate(([r, c]) => {
const el = document.querySelector(`.ff-tile[data-row="${r}"][data-col="${c}"]`);
if (!el) return false;
const b = el.getBoundingClientRect();
el.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, cancelable: true, button: 0, pointerId: 1, pointerType: 'mouse', clientX: b.left + b.width / 2, clientY: b.top + b.height / 2 }));
return true;
}, [row, col]);
assert.ok(await tap(GOAL.row, GOAL.col), 'the mock board draws the 蓝门 tile');
await page.waitForSelector('.dpanel', { timeout: 5000 });
let text = await card(page);
assert.match(text, /蓝门/, text);
assert.match(text, /目标生命值/, text);
assert.match(text, /保护目标/, text);
// the 红门 (the enemies' own entrance) says its own thing
assert.ok(await tap(GATE.row, GATE.col));
await sleep(200);
text = await card(page);
assert.match(text, /红门/, text);
assert.match(text, /从这里出场/, text);
// an ordinary floor tile: nothing to explain, and the card that was open closes with the press
assert.ok(await tap(FLOOR.row, FLOOR.col));
await sleep(200);
assert.equal(await card(page), null, 'an ordinary tile opens nothing');
assert.deepEqual(problems, []);
await page.close();
});
test('the engine canvas: the same tap on a tile\'s own screen position opens the card', async () => {
const { page, problems } = await open('/dev/game-mock.html?phase=PREP&render=engine', { waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => !!globalThis.__SP_VIEW__?.tileScreen, { timeout: 30000 });
await sleep(1200); // models / camera
const at = (row, col) => page.evaluate(([r, c]) => {
const t = globalThis.__SP_VIEW__.tileScreen(r, c);
return t && { x: t.x, y: t.y };
}, [row, col]);
const pt = await at(GOAL.row, GOAL.col);
assert.ok(pt, 'the 蓝门 tile has a screen position');
await page.mouse.click(pt.x, pt.y);
await page.waitForSelector('.dpanel', { timeout: 5000 });
let text = await card(page);
assert.match(text, /蓝门/, text);
assert.match(text, /目标生命值/, text);
const floor = await at(FLOOR.row, FLOOR.col);
await page.mouse.click(floor.x, floor.y);
await sleep(250);
assert.equal(await card(page), null, 'the floor tile closes it again');
assert.deepEqual(problems, []);
await page.close();
});
test('Final Assault prep: the tap reports the BOARD tile, so the boss field explains ITS tile (review on #185)', async () => {
const { page, problems } = await open('/dev/game-mock.html?phase=PREP&variant=boss&render=engine', { waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => !!globalThis.__SP_VIEW__?.tileScreen, { timeout: 30000 });
await page.waitForFunction(() => globalThis.__SP_VIEW__.raw.prepField?.().kind === 'bossPrep', { timeout: 15000 });
await sleep(1200); // camera / models (a deployment with the local board art also builds the 3D board here)
// A board tile of the player's half + the stage tile it draws (stage 2–5 shown as board 9–12): the card must be about
// the stage tile. Reporting the drawn tile instead (the old groundTile) made the screen convert a second time, which
// on this board explains a DIFFERENT tile — or none at all.
const target = await page.evaluate(async () => {
const { data } = await import('/js/data.js');
const gl = await import('/js/ui/gameLogic.js');
const S = globalThis.__MOCK__.S();
const stage = data.lookup('stages', S.pub.stageId);
for (let row = 9; row <= 12; row++) {
for (let col = 0; col <= 10; col++) {
const [sr, sc] = gl.fieldTile('bossL', row, col);
const info = gl.terrainInfo(stage, sr, sc);
if (!info) continue;
const t = globalThis.__SP_VIEW__.tileScreen(row, col);
if (t) return { row, col, sr, sc, name: info.name, x: t.x, y: t.y };
}
}
return null;
});
assert.ok(target, 'the boss field has a special tile on the player half');
// The boss-prep camera is still settling when the board appears (and building the 3D board shifts a machine's timing),
// so wait for the tile's screen position to stop moving, and re-read it before each try.
const at = () => page.evaluate(([r, c]) => {
const t = globalThis.__SP_VIEW__.tileScreen(r, c);
return t && { x: t.x, y: t.y };
}, [target.row, target.col]);
let settled = null;
for (let i = 0; i < 40; i++) {
const p = await at();
if (p && settled && Math.abs(p.x - settled.x) < 1 && Math.abs(p.y - settled.y) < 1) break;
settled = p;
await sleep(150);
}
assert.ok(settled, 'the boss-field tile still has a screen position');
let text = null;
for (let i = 0; i < 3 && text === null; i++) {
const p = (await at()) || settled;
await page.mouse.click(p.x, p.y);
try { await page.waitForSelector('.dpanel', { timeout: 3000 }); } catch { continue; } // a tap that missed: retry
text = await card(page);
}
assert.ok(text !== null, 'the tap opened the terrain card');
assert.match(text, new RegExp(target.name), `board (${target.row},${target.col}) → stage (${target.sr},${target.sc}) is ${target.name}, card: ${text}`);
assert.deepEqual(problems, []);
await page.close();
});
});