120 lines
7.1 KiB
TypeScript
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);
|
|
});
|