100 lines
5.4 KiB
TypeScript
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/);
|
|
});
|