import assert from 'node:assert/strict'; import test from 'node:test'; import { Children, createElement, isValidElement, type ReactNode } from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import { FremenRevivalGrant } from '../components/fremen-revival-grant'; import { createGame, newPlayer, viewGame, type Action, type GameView } from '../game/engine'; /** Projection-only control fixtures; engine tests own the actual alliance and * ordered Revival transitions. The ally's private revival inventory is not read. */ function projected(group = false): GameView { const game = createGame('ORDEREDGRANTCONTROLS', newPlayer('f', 'Fremen', 'fremen'), true); game.players.push(newPlayer('a', 'Ally', 'atreides')); game.players.push(newPlayer('e', 'Emperor', 'emperor')); Object.assign(game, { status: 'playing', phase: 4, turn: 3, order: ['f', 'e', 'a'] }); game.players[0].ally = 'a'; game.players[1].ally = 'f'; const view = viewGame(game, 'f'); view.players.find(player => player.id === 'a')!.autopilot = 'Easy'; view.players.find(player => player.id === 'e')!.bot = 'Medium'; view.revivalOrder = { version: 1, turn: 3, original: ['f', 'e', 'a'], remaining: ['e', 'a'], completed: ['f'], started: false }; view.fremenRevivalGrant = { recipient: 'a', granted: false, blocked: null, waitReleased: false }; if (group) view.fremenGroupRevivalGrant = [ { recipient: 'a', granted: false, blocked: null, waitReleased: false }, { recipient: 'e', granted: false, blocked: null, waitReleased: false }, ]; return view; } function markup(game: GameView, busy = false) { return renderToStaticMarkup(createElement(FremenRevivalGrant, { game, busy, act() {} })); } function click(node: ReactNode, target: string): boolean { for (const child of Children.toArray(node)) { if (!isValidElement<{ children?: ReactNode; onClick?: () => void; 'data-testid'?: string }>(child)) continue; if (child.props['data-testid'] === `fremen-revival-wait-release-${target}` && child.props.onClick) { child.props.onClick(); return true; } if (click(child.props.children, target)) return true; } return false; } function release(game: GameView, target: string, busy = false): Action[] { const actions: Action[] = []; click(FremenRevivalGrant({ game, busy, act(action) { actions.push(action); } }), target); return actions; } void test('Fremen can release a later automated ally before its ordered Revival turn', () => { const game = projected(); assert.match(markup(game), /Continue without waiting/); assert.match(markup(game), /may still grant later while Revival remains open/); assert.deepEqual(release(game, 'a'), [{type:'releaseFremenRevivalWait', target:'a'}]); }); void test('release is disabled during a busy window or after release without hiding grant', () => { const game = projected(); assert.deepEqual(release(game, 'a', true), []); game.fremenRevivalGrant!.waitReleased = true; assert.match(markup(game), /Ally can continue without waiting for your grant/); assert.match(markup(game), /Grant ally free revival/); assert.deepEqual(release(game, 'a'), []); game.truthtrance = {} as NonNullable; game.fremenRevivalGrant!.waitReleased = false; assert.deepEqual(release(game, 'a'), []); }); void test('group controls release each recipient independently without reading private forces', () => { const game = projected(true); for (const ally of game.players.filter(player => player.id !== 'f')) { for (const field of ['spice', 'tanks', 'deadForces', 'revived', 'freeForcesRevived']) Object.defineProperty(ally, field, {get() { throw new Error(`Read private ${field}`); }}); } assert.deepEqual(release(game, 'a'), [{type:'releaseFremenRevivalWait', target:'a'}]); assert.deepEqual(release(game, 'e'), [{type:'releaseFremenRevivalWait', target:'e'}]); game.fremenGroupRevivalGrant![0].waitReleased = true; assert.deepEqual(release(game, 'a'), []); assert.deepEqual(release(game, 'e'), [{type:'releaseFremenRevivalWait', target:'e'}]); }); void test('release is absent for human, finished, blocked or nonordered allies', () => { for (const condition of ['human', 'finished', 'blocked', 'basic'] as const) { const game = projected(); if (condition === 'human') game.players.find(player => player.id === 'a')!.autopilot = undefined; if (condition === 'finished') game.revivalOrder!.remaining = ['e']; if (condition === 'blocked') game.fremenRevivalGrant!.blocked = 'Grant prevented.'; if (condition === 'basic') game.advanced = false; assert.doesNotMatch(markup(game), /Continue without waiting/, condition); assert.deepEqual(release(game, 'a'), [], condition); } });