Files
Dune/tests/richese-reveal-interruption-controls.test.tsx
T
Quintine 0d55333d5d
Verify and publish checkpoint container / container (push) Canceled after 0s
Allow Richese No-Field reveal during Basic openings and markets
2026-10-29 07:27:08 +11:00

92 lines
4.8 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { readFileSync } from 'node:fs';
import { runInNewContext } from 'node:vm';
import { createElement, isValidElement, type ReactElement, type ReactNode } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import * as jsxRuntime from 'react/jsx-runtime';
import ts from 'typescript';
import { RicheseNoFieldControls } from '../components/richese-no-field';
import { applyAction, viewGame, type Action, type GameView } from '../game/engine';
import * as catalog from '../game/catalog';
import * as board from '../game/board';
import * as advisors from '../game/advisors';
import * as shipmentPrice from '../game/shipment-price';
import * as noField from '../game/richese-no-field';
import * as homeworldCards from '../game/homeworld-cards';
import * as smuggler from '../game/smuggler-no-field';
import * as homeworldShipment from '../game/richese-homeworld-shipment';
import { smugglerNoFieldAction, smugglerNoFieldGame } from './smuggler-no-field-fixture';
// Stage the admitted quote in a personalized view; engine admission is tested
// independently. The marker itself comes from a real Basic shipment.
function projection(viewer: 'r' | 'h'): GameView {
let game = smugglerNoFieldGame();
game = applyAction(game, 'r', smugglerNoFieldAction(game, 3, false));
const view = viewGame(game, viewer);
view.phase = 3;
view.decision = { kind: 'auctionPayment', player: 'h' };
if (viewer === 'r') {
view.richeseNoField!.canReveal = true;
view.richeseNoField!.revealBlock = null;
}
return view;
}
function props(game: GameView, busy: boolean, act: (action: Action) => void) {
return { game, busy, act, destination: 'arrakeen', sector: 10, allyPayment: '' };
}
type Element = ReactElement<{ children?: ReactNode; [key: string]: unknown }>;
function elements(node: ReactNode): Element[] {
if (Array.isArray(node)) return node.flatMap(elements);
if (!isValidElement<{ children?: ReactNode; [key: string]: unknown }>(node)) return [];
return [node, ...elements(node.props.children)];
}
// Invoke the actual component with deterministic hooks to exercise its callback.
function control(game: GameView, busy: boolean, sent: Action[]) {
const exports: { RicheseNoFieldControls?: typeof RicheseNoFieldControls } = {};
runInNewContext(ts.transpileModule(readFileSync(new URL('../components/richese-no-field.tsx', import.meta.url), 'utf8'), {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022, jsx: ts.JsxEmit.ReactJSX },
}).outputText, { exports, require(name: string) {
const modules: Record<string, unknown> = {
'react/jsx-runtime': jsxRuntime,
react: { useId: () => 'fixture', useState: (initial: unknown) => [initial, () => {}] },
'@/game/catalog': catalog, '@/game/board': board, '@/game/advisors': advisors,
'@/game/shipment-price': shipmentPrice, '@/game/richese-no-field': noField,
'@/game/homeworld-cards': homeworldCards, '@/game/smuggler-no-field': smuggler,
'@/game/richese-homeworld-shipment': homeworldShipment,
'./richese-homeworld-forces': {}, './shipment-quote': {}, './ui/button': { Button: 'button' },
};
if (!(name in modules)) throw Error(`Unexpected import ${name}`);
return modules[name];
} });
assert.ok(exports.RicheseNoFieldControls);
return elements(exports.RicheseNoFieldControls(props(game, busy, action => sent.push(action))));
}
void test('the owner can reveal the current marker during an admitted Basic auction payment', () => {
const game = projection('r'), sent: Action[] = [];
const html = renderToStaticMarkup(createElement(RicheseNoFieldControls, props(game, false, () => {})));
assert.match(html, /Reveal your No-Field/);
const button = control(game, false, sent).find(element => element.type === 'button' && element.props.children === 'Reveal your No-Field');
assert.ok(button);
assert.equal(button.props.disabled, false);
(button.props.onClick as () => void)();
assert.deepEqual(JSON.parse(JSON.stringify(sent)), [{ type: 'revealNoField',
event: game.richeseNoField!.event, token: game.richeseNoField!.private!.deployed!.tokenId }]);
});
void test('the observer has no private inventory or reveal action; busy blocks the owner callback', () => {
const observer = projection('h'), sent: Action[] = [];
assert.equal(observer.richeseNoField!.private, null);
assert.equal(renderToStaticMarkup(createElement(RicheseNoFieldControls, props(observer, false, () => {}))), '');
assert.deepEqual(control(observer, false, sent), []);
const button = control(projection('r'), true, sent).find(element => element.type === 'button' && element.props.children === 'Reveal your No-Field');
assert.ok(button);
assert.equal(button.props.disabled, true);
(button.props.onClick as () => void)();
assert.deepEqual(sent, []);
});