71 lines
4.5 KiB
TypeScript
71 lines
4.5 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { createElement, type ReactElement } from 'react';
|
|
import { renderToStaticMarkup } from 'react-dom/server';
|
|
import { SemutaReaction } from '../components/semuta-reaction';
|
|
import { applyAction, viewGame, type Action, type Game } from '../game/engine';
|
|
import { createSemutaIxAllyCostFixture, allowSemutaIxAllyCost } from './fixture-semuta-ix-ally-cost';
|
|
|
|
type Props = Parameters<typeof SemutaReaction>[0];
|
|
type Control = { children?: unknown; disabled?: boolean; onClick: () => void };
|
|
function controls(node: unknown): Control[] {
|
|
if (Array.isArray(node)) return node.flatMap(controls);
|
|
if (!node || typeof node !== 'object' || !('props' in node)) return [];
|
|
const props = (node as ReactElement<{children?: unknown; disabled?: boolean; onClick?: () => void}>).props;
|
|
return [...(props.onClick ? [{...props, onClick:props.onClick}] : []), ...controls(props.children)];
|
|
}
|
|
function label(node: unknown): string {
|
|
if (Array.isArray(node)) return node.map(label).join('');
|
|
if (typeof node === 'string' || typeof node === 'number') return String(node);
|
|
if (!node || typeof node !== 'object' || !('props' in node)) return '';
|
|
return label((node as ReactElement<{children?: unknown}>).props.children);
|
|
}
|
|
const markup = (props: Props) => renderToStaticMarkup(createElement(SemutaReaction, props));
|
|
function buttonAction(game: Game, actor: string, name: string, expected: Action): Action {
|
|
const sent: Action[] = [], props: Props = {game:viewGame(game, actor), busy:false,
|
|
act(action) { sent.push(action); }};
|
|
const button = controls(SemutaReaction(props)).find(item => label(item.children) === name);
|
|
assert.ok(button, `Expected the ${name} control`); assert.equal(button.disabled, false);
|
|
const busy = controls(SemutaReaction({...props, busy:true})).find(item => label(item.children) === name);
|
|
assert.ok(busy); assert.equal(busy.disabled, true); busy.onClick(); assert.deepEqual(sent, []);
|
|
button.onClick(); assert.deepEqual(sent, [expected]); return sent[0];
|
|
}
|
|
|
|
void test('independent Ecaz-card Ixian replacement exposes a neutral offer and privately claims its single card', () => {
|
|
const f = createSemutaIxAllyCostFixture({independent:true});
|
|
assert.equal(f.game.advanced, false); assert.equal(f.game.ecazTreachery, true);
|
|
assert.equal(f.game.semutaRecruitsPreview, true);
|
|
let game = allowSemutaIxAllyCost(structuredClone(f.game));
|
|
const pending = game.pendingTreacheryDiscard; assert.ok(pending?.reaction);
|
|
assert.equal(pending.continuation.kind, 'ixAllyCard'); assert.equal(pending.reaction.stage, 'offer');
|
|
const event = pending.batch.event, discarded = f.deliveredCard;
|
|
assert.deepEqual(pending.batch.entries.map(row => ({id:row.card.id, owner:row.discardedBy, face:row.publicFace})),
|
|
[{id:discarded.id, owner:f.buyer, face:false}]);
|
|
const holder = viewGame(game, f.holder), observer = viewGame(game, f.owner);
|
|
assert.equal(holder.semutaReaction?.canCommit, true);
|
|
assert.equal(observer.semutaReaction?.canCommit, false);
|
|
assert.deepEqual(holder.semutaReaction?.candidates, []);
|
|
assert.deepEqual(observer.semutaReaction?.candidates, []);
|
|
const holderHtml = markup({game:holder, act() {}, busy:false});
|
|
const observerHtml = markup({game:observer, act() {}, busy:false});
|
|
for (const html of [holderHtml, observerHtml]) {
|
|
assert.match(html, /Fresh Treachery discard/); assert.match(html, />Continue<\/button>/);
|
|
assert.ok(!html.includes(discarded.id)); assert.ok(!html.includes(discarded.name));
|
|
}
|
|
assert.match(holderHtml, />Commit Semuta Drug<\/button>/);
|
|
assert.doesNotMatch(observerHtml, /Commit Semuta Drug/);
|
|
const pass = buttonAction(game, f.owner, 'Continue', {type:'semutaPass', event});
|
|
game = applyAction(game, f.owner, pass);
|
|
assert.match(markup({game:viewGame(game, f.owner), act() {}, busy:false}), /You continued/);
|
|
const commit = buttonAction(game, f.holder, 'Commit Semuta Drug', {type:'semutaCommit', event});
|
|
game = applyAction(game, f.holder, commit);
|
|
assert.equal(game.pendingTreacheryDiscard, null, 'A sole physical candidate is claimed without an extra confirmation.');
|
|
const own = viewGame(game, f.holder), rival = viewGame(game, f.owner);
|
|
assert.ok(own.players.find(player => player.id === f.holder)?.hand?.some(card => card.id === discarded.id));
|
|
assert.equal(rival.players.find(player => player.id === f.holder)?.hand, undefined);
|
|
assert.equal(SemutaReaction({game:own, act() {}, busy:false}), null);
|
|
const saved = structuredClone(game);
|
|
assert.throws(() => applyAction(game, f.holder, commit));
|
|
assert.deepEqual(game, saved);
|
|
});
|