import assert from 'node:assert/strict'; import test from 'node:test'; import { createElement, isValidElement, type ReactElement, type ReactNode } from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import { TruthFutureBattleLeader } from '../components/truth-future-battle-leader'; import { createGame, joinGame, newPlayer, viewGame, type GameView } from '../game/engine'; type ElementProps = { children?: ReactNode; [key: string]: unknown }; type Element = ReactElement; function elements(node: ReactNode): Element[] { if (Array.isArray(node)) return node.flatMap(elements); if (!isValidElement(node)) return []; return [node, ...elements(node.props.children)]; } function control(node: ReactNode, id: string): Element { const result = elements(node).find(element => element.props['data-testid'] === id); assert.ok(result, `Expected ${id}`); return result; } function projection(): GameView { const game = createGame('TRUTHFUTURE', newPlayer('asker', 'Asker', 'atreides'), false); joinGame(game, newPlayer('target', 'Target', 'emperor')); joinGame(game, newPlayer('other', 'Other', 'harkonnen')); return viewGame(game, 'asker'); } void test('future battle question lists only the target faction’s public native leaders', () => { const game = projection(); const native = game.allLeaders.find(item => item.faction === 'emperor'); const other = game.allLeaders.find(item => item.faction === 'harkonnen'); assert.ok(native && other); const props = { game, target: 'target', leader: native.id, available: true, busy: false, onLeaderChange: (_leader: string) => {}, onAsk: (_target: string, _leader: string) => {} }; const html = renderToStaticMarkup(createElement(TruthFutureBattleLeader, props)); assert.match(html, /If you submit a battle plan later this turn/); assert.match(html, /first such battle/); assert.match(html, /there is no opportunity/); assert.ok(html.includes(native.name)); assert.ok(!html.includes(other.name)); assert.equal(control(TruthFutureBattleLeader(props), 'truth-future-battle-leader-ask').props.disabled, false); }); void test('authoring callbacks reject unavailable, missing and foreign leaders', () => { const game = projection(); const native = game.allLeaders.filter(item => item.faction === 'emperor'); const foreign = game.allLeaders.find(item => item.faction === 'harkonnen'); assert.ok(native.length >= 2 && foreign); const changes: string[] = [], asks: string[][] = []; const props = { game, target: 'target', leader: '', available: true, busy: false, onLeaderChange: (id: string) => changes.push(id), onAsk: (target: string, id: string) => asks.push([target, id]) }; const change = (tree: ReactNode, value: string) => (control(tree, 'truth-future-battle-leader-select').props.onChange as (event: { target: { value: string } }) => void)({ target: { value } }); const ask = (tree: ReactNode) => (control(tree, 'truth-future-battle-leader-ask').props.onClick as () => void)(); const empty = TruthFutureBattleLeader(props); assert.equal(control(empty, 'truth-future-battle-leader-ask').props.disabled, true); change(empty, foreign.id); ask(empty); assert.deepEqual(changes, []); assert.deepEqual(asks, []); change(empty, native[1].id); assert.deepEqual(changes, [native[1].id]); const selected = TruthFutureBattleLeader({ ...props, leader: native[1].id }); ask(selected); assert.deepEqual(asks, [['target', native[1].id]]); for (const override of [{ busy: true }, { available: false }, { leader: foreign.id }, { target: 'asker' }]) { const tree = TruthFutureBattleLeader({ ...props, leader: native[1].id, ...override }); assert.equal(control(tree, 'truth-future-battle-leader-ask').props.disabled, true); ask(tree); } assert.equal(asks.length, 1); });