Files
Dune/tests/ecaz-duke-reacquisition-controls.test.tsx
T

112 lines
6.4 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { registerHooks } from 'node:module';
import { readFileSync } from 'node:fs';
import { createElement, isValidElement, type ReactElement, type ReactNode } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { viewGame, type Action, type Game } from '../game/engine';
import { acquireDuke, DUKE_VIDAL_ID } from '../game/duke-vidal';
import { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
import { EcazDukeAcquisitionChoices } from '../components/ecaz-duke-acquisition-choices';
import { actEcazDukeAlly as act, createEcazDukeAllyFixture as fixture,
ecazDukeAllyInventory as inventory, ecazDukeAllyPhysical as physical,
finishEcazDukeAlly as finish } from './fixture-ecaz-duke-ally';
const aliases=registerHooks({
resolve(specifier,context,next) {
return next(specifier === 'next/image' ? 'vinext/shims/image' : specifier === 'next/link' ? 'vinext/shims/link' : specifier,context);
},
load(url,context,next) {
if (!url.endsWith('.module.css')) return next(url,context);
const css=readFileSync(new URL(url),'utf8');
const classes=Object.fromEntries([...css.matchAll(/\.([a-zA-Z_][\w-]*)/g)].map(match => [match[1],match[1]]));
return {format:'module',source:`export default ${JSON.stringify(classes)}`,shortCircuit:true};
},
});
const { EcazEntry } = await import('../components/ecaz-entry');
const { GameTable } = await import('../components/game-table');
aliases.deregister();
type ButtonProps={ 'data-testid'?:string; disabled?:boolean; onClick?:() => void; children?:ReactNode };
function button(node: ReactNode, id:string): ReactElement<ButtonProps> | null {
if (Array.isArray(node)) {
for (const child of node) { const found=button(child,id); if (found) return found; }
return null;
}
if (!isValidElement<ButtonProps>(node)) return null;
if (node.props['data-testid'] === id) return node;
return button(node.props.children,id);
}
/** The fixture earns the alliance, Ecaz Ambassador placement, normal shared
* Duke, paid entry and pending offer through real actions. This single,
* explicitly controlled supplement changes the original shared disc's
* existing control to the already-allied recipient under an ally-loan tenure.
* It does not install a leader, card, force, payment or pending event. */
function loan(advanced:boolean) {
const f=fixture({advanced}), game=structuredClone(f.game);
game.dukeVidal=acquireDuke(game.dukeVidal!,f.ally,game.turn,'ally');
assert.equal(game.dukeVidal.controller,f.ally); assert.equal(game.dukeVidal.source,'ally');
assert.deepEqual(inventory(game),f.originalInventory);
return {f,game};
}
function checkCompleted(f: ReturnType<typeof fixture>, before:Game, game:Game, recipient=f.ally) {
assert.deepEqual([game.dukeVidal!.controller,game.dukeVidal!.source,game.dukeVidal!.acquiredTurn],
[recipient,'ecaz',before.turn]);
assert.deepEqual(game.dukeVidal!.leader,before.dukeVidal!.leader);
assert.deepEqual(physical(game),physical(before));
assert.equal(game.ecazAmbassadors!.tokens.find(token => token.id === f.token)!.zone,'supply');
assert.ok(game.players.every(seat => !seat.leaders.some(leader => leader.id === DUKE_VIDAL_ID)));
assert.deepEqual(inventory(game),f.originalInventory);
}
for (const advanced of [false,true]) {
const mode=advanced ? 'Advanced' : 'Basic';
void test(`${mode} actual Ecaz table renders and submits same-recipient Duke renewal`, () => {
const {f,game:staged}=loan(advanced), before=structuredClone(staged);
const view=viewGame(staged,f.owner), entry=view.ambassadorEntry; assert.ok(entry);
assert.equal(entry.effect,'ecaz'); assert.equal(entry.stage,'offer');
assert.equal(entry.beneficiaries.find(row => row.player === f.ally)?.blocked,null);
const entryHtml=renderToStaticMarkup(createElement(EcazEntry,{game:view,busy:false,act() {}}));
const tableHtml=renderToStaticMarkup(createElement(GameTable,{game:view,busy:false,send:async () => {},onExit() {}}));
for (const html of [entryHtml,tableHtml]) {
assert.match(html,/Acquire Duke Vidal for/);
assert.match(html,/renew that control/);
assert.ok(html.includes(`data-testid="ecaz-duke-acquisition-${f.ally}"`));
}
const sent:Action[]=[];
const control=EcazDukeAcquisitionChoices({game:view,busy:false,act(action) {sent.push(action);}});
const choice=button(control,`ecaz-duke-acquisition-${f.ally}`); assert.ok(choice);
assert.equal(choice.props.disabled,false); assert.ok(choice.props.onClick);
choice.props.onClick();
assert.deepEqual(sent,[{type:'decision',event:f.event,trigger:true,beneficiary:f.ally,choice:'duke'}]);
let game=act(staged,f.owner,sent[0]); game=finish(f,game);
assert.deepEqual(staged,before,'The actual callback does not mutate its input');
checkCompleted(f,before,game);
// Server-rendered controls and this leaf callback are exercised; a browser
// hydration/navigation or external deployed session is not represented.
});
void test(`${mode} busy renewal controls do not submit and all four bots choose a legal native acquisition`, () => {
const {f,game:staged}=loan(advanced), original=structuredClone(staged);
const view=viewGame(staged,f.owner), sent:Action[]=[];
const control=EcazDukeAcquisitionChoices({game:view,busy:true,act(action) {sent.push(action);}});
const choice=button(control,`ecaz-duke-acquisition-${f.ally}`); assert.ok(choice);
assert.equal(choice.props.disabled,true); choice.props.onClick?.(); assert.deepEqual(sent,[]);
for (const difficulty of DIFFICULTIES) {
const bot=viewGame(staged,f.owner);
bot.players.find(row => row.id === f.owner)!.bot=difficulty;
const action=botActions(bot)[0]; assert.ok(action,difficulty);
assert.equal(action.type,'decision'); assert.equal(action.event,f.event);
assert.equal(action.trigger,true); assert.equal(action.choice,'duke');
assert.equal(typeof action.beneficiary,'string');
const recipient=String(action.beneficiary);
assert.ok(bot.ambassadorEntry?.beneficiaries.some(row => row.player === recipient && !row.blocked),
'The bot selects a server-projected legal recipient without imposing a strategic preference');
let game=act(structuredClone(staged),f.owner,action); game=finish(f,game);
checkCompleted(f,original,game,recipient);
}
assert.deepEqual(staged,original);
});
}