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 | 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(node) && node.props['data-testid'] === 'charity-promise-guide'); return {element,button:isValidElement(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,/]*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,/]*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/); });