92 lines
4.8 KiB
TypeScript
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, []);
|
|
});
|