Files
Dune/tests/truth-charity-ordered-controls.test.tsx
T
Quintine 68c7434a20
Verify and publish checkpoint container / container (push) Canceled after 0s
Support Truthtrance in ordered Charity
2026-10-29 08:57:58 +11:00

100 lines
5.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 { Children, createElement, isValidElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { viewGame, type Action, type GameView } from '../game/engine';
import { actTruthCharityPayments as act, createTruthCharityPaymentsFixture,
declareTruthCharityPayments, truthCharityPaymentsPlayer as player } from './fixture-truth-charity-payments';
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 {TruthCharityPromise} = await import('../components/truth-charity-promise');
const {CharityClaim} = await import('../components/charity-claim');
aliases.deregister();
let cached: ReturnType<typeof createTruthCharityPaymentsFixture> | undefined;
const source = () => cached ??= createTruthCharityPaymentsFixture({source:'phase2',advanced:true});
/** Genuine Advanced setup, Amal, an actual eligibility bribe and Truthtrance
* declaration/answer. The original Charity order is advanced only by real
* Ready actions; no promise, phase, order or wallet is staged. */
function projected(waiting:boolean, answer = true): GameView {
const f = source(); let game = structuredClone(f.game);
const queue = game.charityOrder; assert.ok(queue && queue.remaining.length >= 3);
const target = queue.remaining.find(id => id !== f.asker && id !== queue.remaining[0]); assert.ok(target);
const amount = player(game,target).spice - 1;
if (amount > 0) game = act(game,target,{type:'bribe',target:f.asker,amount});
game = declareTruthCharityPayments(f,game).game;
game = act(game,f.asker,{type:'truthAsk',question:{kind:'charity',target,turn:game.turn}});
game = act(game,target,{type:'truthAnswer',answer:answer ? 'yes' : 'no'});
if (!waiting) for (let count = 0; game.charityOrder?.remaining[0] !== target && count < 6; count++) {
const actor = game.charityOrder?.remaining[0]; assert.ok(actor);
game = act(game,actor,{type:'ready'});
}
const view = viewGame(game,target);
assert.ok(view.charityOrder && view.phase === 2 && view.advanced);
return view;
}
type ButtonProps = {'data-testid'?:string;disabled?:boolean;onClick?:()=>void};
function guide(view:GameView,busy = false) {
const sent:Action[] = [];
const element = TruthCharityPromise({game:view,busy,send(action){sent.push(action);}});
assert.ok(element);
const button = Children.toArray(element.props.children).find(node =>
isValidElement<ButtonProps>(node) && node.props['data-testid'] === 'charity-promise-guide');
return {element,button:isValidElement<ButtonProps>(button) ? button : null,sent};
}
const markup = (view:GameView) => renderToStaticMarkup(createElement(TruthCharityPromise,
{game:view,busy:false,send() { assert.fail('Rendering must not send.'); }}));
void test('ordered Charity shows a waiting promise without an active guided claim', () => {
const view = projected(true), {button} = guide(view);
assert.equal(view.truthCharityGuide,null); assert.equal(button,null);
assert.match(markup(view),/[Ww]ait[^<]*[Cc]harity/);
assert.doesNotMatch(markup(view),/data-testid="charity-promise-guide"/);
const claim = renderToStaticMarkup(createElement(CharityClaim,{game:view,busy:false,act() {}}));
assert.match(claim,/Your turn comes later in the public order/);
assert.match(claim,/<button(?=[^>]*data-testid="charity-claim-submit")(?=[^>]*disabled="")[^>]*>/);
});
void test('the current ordered seat receives the exact native guided claim and a busy state cannot send', () => {
const view = projected(false), {button,sent} = guide(view);
const claim = renderToStaticMarkup(createElement(CharityClaim,{game:view,busy:false,act() {}}));
assert.match(claim,/<button(?=[^>]*data-testid="charity-turn-finish")(?=[^>]*disabled="")[^>]*>/);
assert.match(claim,/Complete your Truthtrance Charity Yes before finishing/);
assert.ok(button); assert.equal(button.props.disabled,false);
assert.ok(button.props.onClick); button.props.onClick();
assert.deepEqual(sent,[view.truthCharityGuide!.action]);
const busy = guide(view,true); assert.ok(busy.button); assert.equal(busy.button.props.disabled,true);
busy.button.props.onClick?.(); assert.deepEqual(busy.sent,[]);
});
void test('a saved guide callback cannot claim after the ordered turn changes', () => {
const view = projected(false), {button,sent} = guide(view);
assert.ok(button?.props.onClick); assert.equal(button.props.disabled,false);
const order = view.charityOrder!;
const prior = order.order.find(id => id !== view.me); assert.ok(prior);
order.remaining = [prior,view.me];
button.props.onClick();
assert.deepEqual(sent,[]);
});
void test('an ordered No retains its restriction without presenting a guided action', () => {
const view = projected(true,false), {button} = guide(view);
assert.equal(button,null); assert.match(markup(view),/You answered No/);
assert.match(markup(view),/No forbids claiming/);
});