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 = { '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, []); });