97 lines
4.5 KiB
TypeScript
97 lines
4.5 KiB
TypeScript
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 { ChoamMarket } from '../components/choam-market';
|
|
import { ChoamMarketAuto } from '../components/choam-market-auto';
|
|
import { createGame, joinGame, newPlayer, viewGame, type Action, type GameView } from '../game/engine';
|
|
|
|
// Staged projections isolate the controls; the engine suite exercises actual
|
|
// market eligibility, turn expiry and the owner-only view.
|
|
function projected(id = 'c'): GameView {
|
|
const game = createGame('MARKETCONTROLS', newPlayer('c', 'CHOAM', 'choam'), false, ['choam']);
|
|
joinGame(game, newPlayer('e', 'Emperor', 'emperor'));
|
|
game.status = 'playing';
|
|
game.phase = 4;
|
|
game.turn = 2;
|
|
game.choamMarket = { owner: 'c', resume: 'phase', blocked: [] };
|
|
game.decision = { kind: 'choamMarket', player: 'c' };
|
|
const view = viewGame(game, id);
|
|
view.choamMarketSkipAvailable = id === 'c';
|
|
view.choamMarketSkipActive = false;
|
|
return view;
|
|
}
|
|
|
|
type ButtonProps = { children?: ReactNode; disabled?: boolean; onClick?: () => void };
|
|
function findButton(node: ReactNode, label: string): ButtonProps | undefined {
|
|
for (const child of Children.toArray(node)) {
|
|
if (!isValidElement<ButtonProps>(child)) continue;
|
|
if (child.props.children === label && child.props.onClick) return child.props;
|
|
const nested = findButton(child.props.children, label);
|
|
if (nested) return nested;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
void test('the market owner can finish this phase or explicitly waive later markets with informed consent', () => {
|
|
const game = projected();
|
|
const actions: Action[] = [];
|
|
const props = { game, busy: false, act(action: Action) { actions.push(action); } };
|
|
const html = renderToStaticMarkup(createElement(ChoamMarket, props));
|
|
assert.match(html, /Finish this phase/);
|
|
assert.match(html, /Finish and skip remaining markets this turn/);
|
|
assert.match(html, /other sales and trades and opportunities to receive gifts/);
|
|
assert.match(html, /enable it again only from a future market/);
|
|
const tree = ChoamMarket(props);
|
|
const finish = findButton(tree, 'Finish this phase');
|
|
const skip = findButton(tree, 'Finish and skip remaining markets this turn');
|
|
assert.ok(finish && skip);
|
|
assert.equal(skip.disabled, false);
|
|
finish.onClick!();
|
|
skip.onClick!();
|
|
assert.deepEqual(actions, [
|
|
{ type: 'decision', done: true },
|
|
{ type: 'decision', done: true, skipMarketsThisTurn: true },
|
|
]);
|
|
const busy = findButton(ChoamMarket({ ...props, busy: true }), 'Finish and skip remaining markets this turn');
|
|
assert.ok(busy?.disabled);
|
|
busy.onClick!();
|
|
assert.equal(actions.length, 2);
|
|
});
|
|
|
|
void test('the skip control is absent without the owner quote or in another market decision', () => {
|
|
for (const game of [projected('e'), { ...projected(), choamMarketSkipAvailable: false }]) {
|
|
assert.doesNotMatch(renderToStaticMarkup(createElement(ChoamMarket, { game, busy: false, act() {} })),
|
|
/Finish and skip remaining markets this turn/);
|
|
}
|
|
const reply = projected();
|
|
reply.decision = { kind: 'choamTradeReply', player: 'c' };
|
|
assert.doesNotMatch(renderToStaticMarkup(createElement(ChoamMarket, { game: reply, busy: false, act() {} })),
|
|
/Finish and skip remaining markets this turn/);
|
|
});
|
|
|
|
void test('the active preference is persistent owner-facing guidance with a busy-safe cancel action', () => {
|
|
const game = projected();
|
|
game.choamMarket = null;
|
|
game.decision = null;
|
|
game.choamMarketSkipAvailable = false;
|
|
game.choamMarketSkipActive = true;
|
|
const actions: Action[] = [];
|
|
const props = { game, busy: false, act(action: Action) { actions.push(action); } };
|
|
const html = renderToStaticMarkup(createElement(ChoamMarketAuto, props));
|
|
assert.match(html, /remaining markets this turn will be skipped/);
|
|
assert.match(html, /other sales and trades and opportunities to receive gifts/);
|
|
assert.match(html, /enable it again only from a future market/);
|
|
const cancel = findButton(ChoamMarketAuto(props), 'Cancel skipping remaining markets');
|
|
assert.ok(cancel);
|
|
assert.equal(cancel.disabled, false);
|
|
cancel.onClick!();
|
|
assert.deepEqual(actions, [{ type: 'cancelChoamMarketSkip' }]);
|
|
const busy = findButton(ChoamMarketAuto({ ...props, busy: true }), 'Cancel skipping remaining markets');
|
|
assert.ok(busy?.disabled);
|
|
busy.onClick!();
|
|
assert.equal(actions.length, 1);
|
|
game.choamMarketSkipActive = false;
|
|
assert.equal(renderToStaticMarkup(createElement(ChoamMarketAuto, props)), '');
|
|
});
|