Files
ark-stronghold-protocol/test/ui/uiRepair3.test.js
T
Chun'an Shi 3f05c65dec Q / X hotkeys retreat and sell the selected operator in prep (#114)
Q retreats and X sells the selected operator during prep, through the same buttons (never an item, never while typing in a field); the buttons show [Q] / [X]. Contributed by @SpiritedAwayCN.
2026-10-05 14:18:44 +08:00

93 lines
5.5 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.
// Regression tests for in-match UI defects (round-3 hunt): the 暂离 (AI 托管) flag leaking into the next match, game
// shortcuts acting behind the 本局信息 / 敌方情报 drawer, two elimination banners during SETTLE, a right-click /
// long-press detail card that a press on the field did not close, and the touch hit areas of the 交流 pager (static
// check here; the in-browser check is in devices.e2e.test.js).
import { test, describe } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { shortcutBlocked, showDeadPill, closesOnFieldPress } from '../../public/js/ui/gameLogic.js';
import { PHASE } from '../../shared/constants.js';
describe('暂离 (AI 托管) is scoped to one match', () => {
test('the flag survives in-match updates and is dropped when the match ends or its state is cleared', async () => {
const { awayStore, awayEnds } = await import('../../public/js/ui/matchChrome.js');
const { store, emptyMatch } = await import('../../public/js/store.js');
const inMatch = (phase) => ({ ...emptyMatch(), public: { phase, players: [] } });
store.set({ match: inMatch(PHASE.PREP) });
awayStore.set({ away: true });
store.patch('match', { public: { phase: PHASE.COMBAT, players: [] } });
assert.equal(awayStore.get().away, true, 'still away while the match runs');
// result → 返回同盟 (result.js back): the match slice is cleared
store.patch('match', { result: { victory: false } });
assert.equal(awayStore.get().away, false, 'the settlement ends 暂离');
awayStore.set({ away: true });
store.set({ match: emptyMatch() });
assert.equal(awayStore.get().away, false, 'clearing the match (返回同盟 / a new match / room closed) ends 暂离');
// the next match opens without the overlay flag
store.set({ match: inMatch(PHASE.INFO_CHECK) });
assert.equal(awayStore.get().away, false);
assert.equal(awayEnds({ match: inMatch(PHASE.PREP) }, { match: inMatch(PHASE.PREP) }), false);
assert.equal(awayEnds({ match: emptyMatch() }, { match: emptyMatch() }), false, 'nothing to end outside a match');
store.set({ match: emptyMatch() });
});
});
describe('game shortcuts behind overlays', () => {
test('the 本局信息 / 敌方情报 drawer swallows R / F / D / Space, Esc still closes it', () => {
for (const act of ['refresh', 'freeze', 'levelUp', 'retreat', 'sell', 'ready']) {
assert.equal(shortcutBlocked(act, { drawer: true }), true, act);
assert.equal(shortcutBlocked(act, {}), false, act);
assert.equal(shortcutBlocked(act, { modal: true }), true, act);
}
assert.equal(shortcutBlocked('escape', { drawer: true }), false);
assert.equal(shortcutBlocked('escape', { modal: true }), true, 'a modal handles its own Esc');
assert.equal(shortcutBlocked(null, {}), true);
});
});
describe('elimination banners', () => {
test('the HUD pill shows outside combat only — never during SETTLE, where the combat HUD already says it', () => {
assert.equal(showDeadPill(false, PHASE.PREP), true);
assert.equal(showDeadPill(false, PHASE.SP_DRAFT), true);
assert.equal(showDeadPill(false, PHASE.SETTLE), false);
for (const p of [PHASE.COMBAT, PHASE.UNITE, PHASE.FINAL_ASSAULT, PHASE.HIDDEN_CORE]) assert.equal(showDeadPill(false, p), false, p);
assert.equal(showDeadPill(true, PHASE.PREP), false);
});
});
describe('detail card vs a press on the field', () => {
test('a card opened from the field (tap, right-click, long press, battle unit) closes; others stay', () => {
assert.equal(closesOnFieldPress({ kind: 'piece', uid: 3 }), true);
assert.equal(closesOnFieldPress({ kind: 'unit', unitId: 7 }), true);
for (const kind of ['chess', 'item', 'enemy']) assert.equal(closesOnFieldPress({ kind, id: 'x' }), false, kind);
assert.equal(closesOnFieldPress(null), false);
});
test('the field press handler closes the card whether or not a piece is selected', () => {
const src = readFileSync(new URL('../../public/js/screens/game.js', import.meta.url), 'utf8');
const h = src.slice(src.indexOf('const onDown = () => {'), src.indexOf("host.addEventListener('pointerdown', onDown, true)"));
assert.ok(h.length > 0);
assert.match(h, /setDetail\(\(d\) => \(closesOnFieldPress\(d\) \? null : d\)\)/);
assert.doesNotMatch(h, /if \(L\.sel\) \{[^}]*setDetail/, 'not only while a piece is selected');
});
});
describe('交流 pager touch hit areas (css/devices.css)', () => {
const css = readFileSync(new URL('../../public/css/devices.css', import.meta.url), 'utf8');
test('‹ / › hit areas start under the emote cells and grow outwards; dots only up to half the gap', () => {
assert.match(css, /\.sp-coarse \.ewheel__panel \.ewheel__nav::before \{ top: \.03rem; bottom: -\.15rem; height: auto; transform: none; \}/);
assert.match(css, /\.sp-coarse \.ewheel__panel \.ewheel__nav\.is-prev::before \{ left: auto; right: 0; \}/);
assert.match(css, /\.sp-coarse \.ewheel__panel \.ewheel__nav\.is-next::before \{ left: 0; right: auto; \}/);
assert.match(css, /\.sp-coarse \.ewheel__dot::before \{ inset: -\.02rem -\.05rem -\.2rem;/);
});
});
describe('dev hook', () => {
test('useFieldView clears globalThis.__SP_VIEW__ when it destroys the view', () => {
const src = readFileSync(new URL('../../public/js/ui/fieldHost.js', import.meta.url), 'utf8');
const cleanup = src.slice(src.indexOf('return () => {', src.indexOf('export function useFieldView')));
assert.match(cleanup.slice(0, 600), /globalThis\.__SP_VIEW__ === viewRef\.current\) globalThis\.__SP_VIEW__ = null/);
});
});