import assert from 'node:assert/strict'; import test from 'node:test'; import { createElement } from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import { FaceDanceActions, faceDanceForceChoices, type FaceDanceActionsProps } from '../components/face-dance-actions'; import { FaceDanceDecision } from '../components/face-dance-decision'; import { createGame, joinGame, newPlayer, viewGame, type Action, type GameView } from '../game/engine'; // These are controlled entitled projections, supplementing the separate native // Arrakis and controlled conserved Homeworld engine sources. No pending receipt, // winning army or private card is installed in an authoritative Game here. // React renders the stateful parent; direct calls enter only the hook-free leaf. function projected(world = false): GameView { const game = createGame('EMPERORFACEDANCECONTROLS', newPlayer('t', 'Tleilaxu', 'tleilaxu'), true, ['ix', 'ecaz']); joinGame(game, newPlayer('e', 'Emperor', 'emperor')); joinGame(game, newPlayer('a', 'Atreides', 'atreides')); joinGame(game, newPlayer('c', 'Ecaz', 'ecaz')); const view = viewGame(game, 't'); view.status = 'playing'; view.phase = 6; // Presence of the public Homeworld table identifies the selected module. Its // populations are unnecessary to this explanation and never become custody. view.homeworlds = { worlds: [] }; const caller = view.players.find(player => player.id === 't')!; caller.reserves = 5; caller.forces = { 'arrakeen:10': 2, 'sietch_tabr:14': 3 }; caller.faceDancers = [{ leader: 'emperor-0', revealed: false }]; view.players.find(player => player.id === 'e')!.forces = { 'hagga_basin:12': 2, 'hagga_basin:13': 2 }; view.players.find(player => player.id === 'c')!.forces = { 'hagga_basin:13': 2 }; view.decision = { kind: 'faceDance', player: 't', winner: 'e', leader: 'emperor-0', identity: 'emperor-0', territory: world ? 'homeworld:tleilaxu' : 'hagga_basin', }; if (world) view.combatLocations.push({ id: 'homeworld:tleilaxu', name: 'Tleilax', kind: 'homeworld', native: 't', forces: { t: { normal: 5, elite: 0 }, e: { normal: 2, elite: 2 }, a: { normal: 1, elite: 0 } }, }); return view; } function markup(game: GameView, busy = false) { return renderToStaticMarkup(createElement(FaceDanceDecision, { game, busy, act() {} })); } function button(html: string, action: 'reveal' | 'decline') { const found = html.match(new RegExp(`]*data-testid="face-dance-${action}")[^>]*>[\\s\\S]*?<\\/button>`))?.[0]; assert.ok(found, `The Face Dance ${action} action is present`); return found; } function propsFor(game: GameView, sources: Record = {}): FaceDanceActionsProps { return { game, sources, sector: 12, busy: false, act() {} }; } type ActionButton = { props: { 'data-testid': string; disabled: boolean; onClick: () => void } }; function click(props: FaceDanceActionsProps, action: 'reveal' | 'decline'): Action[] { const actions: Action[] = []; const leaf = FaceDanceActions({ ...props, act(value) { actions.push(value); } }); const found = (leaf.props.children as ActionButton[]).find(child => child.props['data-testid'] === `face-dance-${action}`); assert.ok(found); found.props.onClick(); return actions; } function provisional(html: string) { assert.match(html, /aria-label="Provisional Emperor Face Dance return"/); assert.match(html, /Provisional return: the Emperor’s normal forces return to Kaitain; Sardaukar return to Salusa Secundus/); assert.match(html, /reserve return with no revival fee or income, new battle or additional battle reward/); assert.doesNotMatch(html, /Confirm|Acknowledge|countdown/); } void test('Advanced Homeworld Emperor returns show persistent provisional typed guidance on Arrakis and a visitor Homeworld', () => { for (const world of [false, true]) { const game = projected(world), before = structuredClone(game); for (let render = 0; render < 3; render++) { const html = markup(game); provisional(html); assert.match(html, /Their battle rewards remain theirs/); assert.match(html, /winner’s 4 remaining forces/); assert.match(html, /selected winning leader goes to the tanks without a spice bounty/); assert.match(html, /Arrakeen · sector 10/); assert.match(html, /Sietch Tabr · sector 14/); assert.doesNotMatch(button(html, 'reveal'), /\sdisabled=/); assert.doesNotMatch(button(html, 'decline'), /\sdisabled=/); if (world) { assert.match(html, /Emperor won in Tleilax/); assert.match(html, /native forces already at Tleilax remain there/); assert.match(html, /0 \/ 4 replacement forces selected/); assert.doesNotMatch(html, /face-source-reserves|face-sector|Replacement sector/); } else { assert.match(html, /Emperor won in Hagga Basin/); assert.match(html, /face-source-reserves|Replacement sector/); assert.match(html, /4 \/ 4 replacement forces selected/); for (const sector of [12, 13]) assert.match(html, new RegExp(`