Files
Dune/tests/caladan-arrival-controls.test.tsx
T

97 lines
5.4 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { applyAction, viewGame, type Game } from '../game/engine';
import { CaladanReinforcement } from '../components/caladan-reinforcement';
import { caladanReinforcementCanAct, caladanReinforcementChoice } from '../game/caladan-reinforcement-options';
import {
caladanVictoryFixture, prepareVictoryBattle, commitVictoryPlans, finishVictoryCalls,
victoryPlayer,
} from './fixture-caladan-victory';
// Existing controlled victory source: genuine Homeworld setup/deal, staged
// later phase/armies/wallets and storm position, then real battle packets yield
// the choice. This is not original-game progression or an HMS entry programme.
// SSR and shared-adapter checks do not operate the stateful React chooser.
const sources = new Map<boolean, Game>();
function choiceGame(advanced = true): Game {
let source = sources.get(advanced);
if (!source) {
const initial = caladanVictoryFixture({ advanced });
initial.storm = 13;
source = finishVictoryCalls(commitVictoryPlans(prepareVictoryBattle(initial)));
assert.equal(source.decision?.kind, 'caladanReinforcement');
assert.equal(source.lastBattleContext?.winner, 'a');
sources.set(advanced, source);
}
return structuredClone(source);
}
function html(game: Game, owner = 'a', busy = false) {
return renderToStaticMarkup(createElement(CaladanReinforcement, { game: viewGame(game, owner), busy,
act() { assert.fail('Rendering the chooser must not submit a decision.'); } }));
}
void test('the engine-owned Caladan choice keeps its default target and original exact one-force adapter packet', () => {
const game = choiceGame(), view = viewGame(game, 'a'), offer = view.caladanReinforcement;
assert.ok(offer);
const target = offer.destinations.find(destination => !destination.blocked); assert.ok(target);
assert.ok(target.id.startsWith('hagga_basin:'));
const markup = html(game);
assert.ok(markup.includes(`<option value="${target.id}" selected="">`));
const choice = caladanReinforcementChoice(view, target.id);
assert.deepEqual(choice, { blocked: null, action: { type: 'decision', event: offer.event,
destination: target.id, amount: 1 } });
assert.ok(choice.action);
const before = victoryPlayer(game, 'a');
const after = applyAction(game, 'a', choice.action), placed = victoryPlayer(after, 'a');
assert.equal(placed.reserves, before.reserves - 1);
assert.equal(placed.forces[target.id], (before.forces[target.id] ?? 0) + 1);
});
void test('arrival guidance distinguishes placement from shipment and preserves the original Face Dance explanation', () => {
const game = choiceGame(), before = structuredClone(game);
for (let render = 0; render < 2; render++) {
const markup = html(game);
assert.match(markup, /original surviving winner at the original battle location/);
assert.match(markup, /including inside the Hidden Mobile Stronghold/);
assert.match(markup, /direct-shipment ban does not block this placement/);
assert.match(markup, /No spice, shipment, movement or revival allowance is spent/);
assert.match(markup, /no Spiritual Advisor opportunity is created/);
assert.match(markup, /Storm-sector entry remains unavailable/);
assert.match(markup, /Advanced Bene Gesserit Intrusion resolves first/);
assert.match(markup, /Provisional arrival classification: eligible Ambassador and Terror reactions then follow their original arrival order/);
assert.match(markup, /aria-label="Provisional Face Dance order"/);
assert.match(markup, /joins the original victorious army before any later Face Dance/);
assert.match(markup, /does not create another victory or grant another Caladan reinforcement/);
}
assert.deepEqual(game, before);
});
void test('actual storm and foreign targets stay unavailable and another viewer receives no owned chooser', () => {
const game = choiceGame(), view = viewGame(game, 'a'), offer = view.caladanReinforcement;
assert.ok(offer);
const storm = offer.destinations.find(destination => destination.id === 'hagga_basin:13'); assert.ok(storm);
assert.match(storm.blocked ?? '', /storm/i);
assert.equal(caladanReinforcementChoice(view, storm.id).action, null);
assert.equal(caladanReinforcementChoice(view, 'carthag:11').action, null);
assert.match(html(game), /<option[^>]*value="hagga_basin:13"[^>]*disabled=""/);
const rival = viewGame(game, 'g');
assert.equal(caladanReinforcementCanAct(rival), false);
assert.equal(caladanReinforcementChoice(rival, 'hagga_basin:12').action, null);
assert.equal(html(game, 'g'), '');
});
void test('busy disables the existing chooser while Basic keeps the placement guidance without an Advanced Intrusion claim', () => {
const game = choiceGame(), busy = html(game, 'a', true);
assert.match(busy, /<select[^>]*disabled=""/);
assert.equal((busy.match(/<button[^>]*disabled=""/g) ?? []).length, 2);
assert.match(busy, /Provisional arrival classification/);
const basic = choiceGame(false), markup = html(basic);
assert.match(markup, /no Spiritual Advisor opportunity is created/);
assert.match(markup, /Provisional arrival classification/);
assert.doesNotMatch(markup, /Advanced Bene Gesserit Intrusion resolves first/);
const view = viewGame(basic, 'a');
assert.ok(caladanReinforcementChoice(view, 'hagga_basin:12').action);
});