Files
Dune/tests/truth-no-field-shipment-controls.test.tsx
T
Quintine 7fccd62814
Verify and publish checkpoint container / container (push) Canceled after 0s
Support Truthtrance No-Field shipment promises
2026-10-29 10:07:00 +11:00

120 lines
7.1 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { createElement, isValidElement, type ReactElement, type ReactNode } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { ShipmentExpressionFields } from '../components/shipment-expression-fields';
import { TruthShipmentQuestion } from '../components/truth-shipment-question';
import { TruthNoFieldShipmentFields, type NoFieldShipmentInput } from '../components/truth-no-field-shipment-fields';
import { createGame, joinGame, newPlayer, viewGame, type Action } from '../game/engine';
import type { ShipmentExpression } from '../game/shipment-promises';
type ElementProps = { children?: ReactNode; [key: string]: unknown };
type Element = ReactElement<ElementProps>;
function elements(node: ReactNode): Element[] {
if (Array.isArray(node)) return node.flatMap(elements);
if (!isValidElement<ElementProps>(node)) return [];
return [node, ...elements(node.props.children)];
}
function byId(tree: ReactNode, id: string): Element {
const element = elements(tree).find(item => item.props.id === id);
assert.ok(element, `Expected ${id}`);
return element;
}
function choose(element: Element, value: string) {
(element.props.onChange as (event: {currentTarget: {value: string}}) => void)({currentTarget: {value}});
}
void test('No-Field condition authors any destination or one printed territory and sector', () => {
let value: NoFieldShipmentInput = {kind: 'noField'};
const tree = () => TruthNoFieldShipmentFields({id: 'marker', value, disabled: false, onChange(next) { value = next; }});
const html = renderToStaticMarkup(createElement(TruthNoFieldShipmentFields, {id: 'marker', value, disabled: false, onChange() {}}));
assert.match(html, /Any destination/);
assert.doesNotMatch(html, /token ID|token value/i);
choose(byId(tree(), 'marker-no-field-territory'), 'arrakeen');
assert.deepEqual(value, {kind:'noField', territory:'arrakeen'});
choose(byId(tree(), 'marker-no-field-sector'), '10');
assert.deepEqual(value, {kind:'noField', territory:'arrakeen', sector:10});
choose(byId(tree(), 'marker-no-field-territory'), 'carthag');
assert.deepEqual(value, {kind:'noField', territory:'carthag'}, 'changing destination removes the old sector');
choose(byId(tree(), 'marker-no-field-sector'), '10');
assert.deepEqual(value, {kind:'noField', territory:'carthag'}, 'invalid sector is ignored');
choose(byId(tree(), 'marker-no-field-territory'), '');
assert.deepEqual(value, {kind:'noField'});
});
void test('disabled marker fields ignore direct callback invocation', () => {
let changes = 0;
const value: NoFieldShipmentInput = {kind:'noField', territory:'arrakeen', sector:10};
const tree = TruthNoFieldShipmentFields({id:'locked', value, disabled:true, onChange() { changes++; }});
assert.equal(byId(tree, 'locked-no-field-territory').props.disabled, true);
assert.equal(byId(tree, 'locked-no-field-sector').props.disabled, true);
choose(byId(tree, 'locked-no-field-territory'), 'carthag');
choose(byId(tree, 'locked-no-field-sector'), '');
assert.equal(changes, 0);
});
void test('mixed shipment expression can change one condition to No-Field without replacing the physical condition', () => {
let value: ShipmentExpression = {op:'or', terms:[{territory:'carthag',minimum:2}, {territory:'arrakeen',minimum:3}]};
const tree = () => ShipmentExpressionFields({value, id:'question', disabled:false, allowNoField:true, onChange(next) { value = next; }});
choose(byId(tree(), 'question-1.2-condition-type'), 'noField');
assert.deepEqual(value, {op:'or', terms:[{territory:'carthag',minimum:2}, {kind:'noField'}]});
const marker = elements(tree()).find(element => element.type === TruthNoFieldShipmentFields);
assert.ok(marker);
(marker.props.onChange as (claim: NoFieldShipmentInput) => void)({kind:'noField',territory:'arrakeen',sector:10});
assert.deepEqual(value, {op:'or', terms:[{territory:'carthag',minimum:2}, {kind:'noField',territory:'arrakeen',sector:10}]});
choose(byId(tree(), 'question-1.2-condition-type'), 'physical');
assert.deepEqual(value, {op:'or', terms:[{territory:'carthag',minimum:2}, {territory:'carthag',minimum:1}]});
});
void test('a non-Richese target cannot add a No-Field condition and can remove a previously selected one', () => {
let value: ShipmentExpression = {kind:'noField'};
const tree = () => ShipmentExpressionFields({value, id:'other', disabled:false, allowNoField:false, onChange(next) { value = next; }});
const html = renderToStaticMarkup(createElement(ShipmentExpressionFields, {value, id:'other', disabled:false, allowNoField:false, onChange() {}}));
assert.match(html, /Choose Richese for a No-Field shipment condition/);
choose(byId(tree(), 'other-1-condition-type'), 'physical');
assert.deepEqual(value, {territory:'carthag',minimum:1});
assert.equal(elements(tree()).some(element => element.props.id === 'other-1-condition-type'), false);
});
void test('shipment question asks an exact No-Field claim for Richese and blocks it for another faction', () => {
const source = createGame('MARKERQUESTION', newPlayer('asker','Asker','atreides'), false, ['choam']);
joinGame(source, newPlayer('richese','Richese','richese'));
joinGame(source, newPlayer('emperor','Emperor','emperor'));
const game = viewGame(source, 'asker');
game.truthtrance = {stage:'ask', queue:[{player:'asker',card:'truth'}], passed:[], question:null};
game.truthShipmentTargets = ['richese','emperor'];
let target = 'richese';
let claim: ShipmentExpression = {territory:'carthag',minimum:1};
const actions: Action[] = [];
const question = () => ({kind:'shipment' as const, target, claim});
const wrapper = () => TruthShipmentQuestion({game, question:question(), busy:false,
act(action) { actions.push(action); }, onChange(next) { claim = next; }});
const fieldTree = () => {
const field = elements(wrapper()).find(element => element.type === ShipmentExpressionFields);
assert.ok(field);
return ShipmentExpressionFields(field.props as Parameters<typeof ShipmentExpressionFields>[0]);
};
const button = () => {
const found = elements(wrapper()).find(element => element.props['data-testid'] === 'truth-shipment-ask');
assert.ok(found);
return found;
};
choose(byId(fieldTree(), 'truth-shipment-1-condition-type'), 'noField');
assert.deepEqual(claim, {kind:'noField'});
const marker = elements(fieldTree()).find(element => element.type === TruthNoFieldShipmentFields);
assert.ok(marker);
(marker.props.onChange as (value: NoFieldShipmentInput) => void)({kind:'noField', territory:'arrakeen', sector:10});
assert.equal(button().props.disabled, false);
(button().props.onClick as () => void)();
assert.deepEqual(actions, [{type:'truthAsk', question:{kind:'shipment', target:'richese',
claim:{kind:'noField', territory:'arrakeen', sector:10}}}]);
target = 'emperor';
assert.equal(button().props.disabled, true);
(button().props.onClick as () => void)();
assert.equal(actions.length, 1);
choose(byId(fieldTree(), 'truth-shipment-1-condition-type'), 'physical');
assert.deepEqual(claim, {territory:'carthag',minimum:1});
assert.equal(button().props.disabled, false);
});