Files
Dune/tests/native-world-no-field-controls.test.tsx
T
Quintine 7e3d745807
Verify and publish checkpoint container / container (push) Canceled after 0s
Prototype native Homeworld No-Field return
2026-10-29 04:55:44 +11:00

123 lines
6.9 KiB
TypeScript

import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';
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 type { Action, GameView } from '../game/engine';
import { HomeworldNoFieldShipment } from '../components/homeworld-shipment';
import { createRicheseNoField, deployRicheseNoField } from '../game/richese-no-field';
import { homeworldForceGroups, type HomeworldCustody } from '../game/homeworld-custody';
import { homeworldPopulations } from '../game/homeworld-population';
import * as combat from '../game/combat';
import * as options from '../game/homeworld-shipment-options';
/** Personalized public Homeworld view with one real deployed marker. */
function fixture(advanced: boolean): GameView {
const context = { advanced, players: [
{ id: 'r', faction: 'richese' as const, reserves: 12, eliteReserves: 0, ally: null as string | null },
{ id: 'a', faction: 'atreides' as const, reserves: 15, eliteReserves: 0, ally: null as string | null },
{ id: 'g', faction: 'guild' as const, reserves: 20, eliteReserves: 0, ally: null as string | null },
] };
const custody: HomeworldCustody = { salusa: null, visitors: {
'homeworld:atreides': { r: { normal: 2, elite: 0 } },
} };
const populations = homeworldPopulations(context, custody);
const marker = deployRicheseNoField(createRicheseNoField(['zero', 'three', 'five']),
{ tokenId: 'five', controller: 'r', location: { homeworld: 'homeworld:atreides' } });
return {
me: 'r', active: 'r', advanced, status: 'playing', phase: 5,
homeworldShipment: { event: 'homeworld-event', blocked: null }, aid: { available: 0, pledged: 0 },
players: context.players.map(p => ({ ...p, name: p.faction, spice: p.id === 'r' ? 4 : undefined,
shipped: false, elites: { reserves: 0 } })),
homeworlds: { worlds: homeworldForceGroups(context, custody).map(world => ({ ...world,
...populations.find(population => population.location === world.id)! })) },
richeseNoField: { owner: 'r', event: 'marker-event', private: marker,
canShip: false, shipBlock: 'Reveal the existing No-Field before shipping another.' },
} as unknown as GameView;
}
function markup(view: GameView): string {
return renderToStaticMarkup(createElement(HomeworldNoFieldShipment, { game: view, act() {}, busy: false }));
}
function destinations(html: string): string {
const match = /<select aria-label="No-Field destination Homeworld"[^>]*>([\s\S]*?)<\/select>/.exec(html);
assert.ok(match, 'The marker destination selector is present');
return match[1];
}
void test('Advanced deployed marker can select native Homeworld without exposing its value to another player', () => {
const view = fixture(true), before = structuredClone(view), html = markup(view);
assert.match(destinations(html), /value="homeworld:richese"/);
assert.doesNotMatch(destinations(html), /value="homeworld:atreides"/);
assert.match(html, /from Caladan/);
assert.match(html, /No-Field 5/);
assert.doesNotMatch(html, /No-Field 0|No-Field 3/);
assert.match(html, /already in those reserves and are not added again/);
const result = options.richeseHomeworldMarkerChoice(view, 'homeworld:richese', 'five', 0, 'homeworld:atreides');
assert.equal(result.cost, 1);
assert.deepEqual(result.action, { type: 'homeworldShip', event: 'homeworld-event', destination: 'homeworld:richese',
sources: {}, noField: 'five', noFieldEvent: 'marker-event', allyPayment: 0, noFieldFrom: 'homeworld:atreides' });
const observer = { ...view, me: 'a', richeseNoField: { ...view.richeseNoField!, private: null } } as GameView;
assert.equal(markup(observer), '');
assert.deepEqual(view, before);
});
void test('Basic return and fresh Advanced supply placement cannot target the native World', () => {
const basic = fixture(false);
assert.doesNotMatch(destinations(markup(basic)), /value="homeworld:richese"/);
assert.equal(options.richeseHomeworldMarkerChoice(basic, 'homeworld:richese', 'five', 0, 'homeworld:atreides').action, null);
const fresh = fixture(true);
fresh.richeseNoField!.private = createRicheseNoField(['zero', 'three', 'five']);
fresh.richeseNoField!.canShip = true;
assert.doesNotMatch(destinations(markup(fresh)), /value="homeworld:richese"/);
assert.equal(options.richeseHomeworldMarkerChoice(fresh, 'homeworld:richese', 'five', 0).action, null);
});
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)];
}
void test('the marker control sends the selected native return through the normal shipment action', () => {
const view = fixture(true), sent: Action[] = [], states: unknown[] = [];
let cursor = 0;
const exports: { HomeworldNoFieldShipment?: typeof HomeworldNoFieldShipment } = {};
runInNewContext(ts.transpileModule(readFileSync(new URL('../components/homeworld-shipment.tsx', import.meta.url), 'utf8'), {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022, jsx: ts.JsxEmit.ReactJSX },
}).outputText, {
exports,
require(name: string) {
if (name === 'react/jsx-runtime') return jsxRuntime;
if (name === 'react') return { useState(initial: unknown) {
const index = cursor++; if (!(index in states)) states[index] = initial;
return [states[index], (next: unknown) => { states[index] = next; }];
} };
if (name === '@/game/combat') return combat;
if (name === '@/game/homeworld-shipment-options') return options;
if (name === './ui/button') return { Button: 'button' };
if (name === './ui/input') return { Input: 'input' };
throw Error(`Unexpected import ${name}`);
},
});
assert.ok(exports.HomeworldNoFieldShipment);
const render = () => {
cursor = 0;
return elements(exports.HomeworldNoFieldShipment!({ game: view, busy: false, act: action => sent.push(action) }));
};
const select = render().find(element => element.type === 'select' && element.props['aria-label'] === 'No-Field destination Homeworld');
assert.ok(select);
(select.props.onChange as (event: { target: { value: string } }) => void)({ target: { value: 'homeworld:richese' } });
const button = render().find(element => element.type === 'button');
assert.ok(button);
assert.equal(button.props.disabled, false);
(button.props.onClick as () => void)();
assert.deepEqual(JSON.parse(JSON.stringify(sent[0])), { type: 'homeworldShip', event: 'homeworld-event',
destination: 'homeworld:richese', sources: {}, noField: 'five', noFieldEvent: 'marker-event',
allyPayment: 0, noFieldFrom: 'homeworld:atreides' });
});