97 lines
5.6 KiB
TypeScript
97 lines
5.6 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { createElement } from 'react';
|
|
import { renderToStaticMarkup } from 'react-dom/server';
|
|
import type { GameView } from '../game/engine';
|
|
import type { DiscoveryEntryOffer } from '../game/discovery-entry';
|
|
import {
|
|
discoveryEntryBotActions, discoveryEntryDeclineAction, discoveryEntryMoveAction,
|
|
} from '../game/discovery-entry-options';
|
|
import { DiscoveryEntryDecision } from '../components/discovery-entry';
|
|
|
|
/** Controlled public projection only. These cases establish rendering and exact
|
|
* action construction; they do not establish a native reveal/entry chronology
|
|
* or claim to execute the stateful chooser's React event handlers. */
|
|
function view(physical = 0, marker = true): GameView {
|
|
const offer: DiscoveryEntryOffer = { event: 'entry-round:0', token: 'discovery-token-1', owner: 'r',
|
|
parent: 'meridian', destination: 'jacurutu-sietch', sector: 0,
|
|
sources: physical ? [{ source: 'meridian:2', normal: physical, elite: 0 }] : [],
|
|
...(marker ? { noField: { tokenId: 'private-marker-id', event: 'private-marker-event', source: 'meridian:2' } } : {}),
|
|
blocked: null, arrival: null };
|
|
return { status: 'playing', phase: 0, turn: 3, me: 'r',
|
|
decision: { kind: 'discoveryEntry', player: 'r', event: offer.event }, discoveryEntry: offer,
|
|
players: [{ id: 'r', name: 'Richese', faction: 'richese', autopilot: null }] } as unknown as GameView;
|
|
}
|
|
function html(game: GameView, busy = false) {
|
|
return renderToStaticMarkup(createElement(DiscoveryEntryDecision, { game, busy, act() {} }));
|
|
}
|
|
|
|
void test('marker-only entry defaults to a selected independent checkbox and explains concealed free relocation without a physical counter', () => {
|
|
const game = view(), markup = html(game);
|
|
assert.match(markup, /type="checkbox"[^>]*checked=""/);
|
|
assert.match(markup, /aria-label="Move concealed No-Field inside"/);
|
|
assert.match(markup, /Selected: 0 physical forces · 1 concealed marker/);
|
|
assert.match(markup, /Move 0 physical forces and 1 concealed marker inside/);
|
|
assert.match(markup, /Provisional concealed No-Field entry/);
|
|
assert.match(markup, /without revealing its value or materializing reserves/);
|
|
assert.match(markup, /existing shipment history stays unchanged/);
|
|
assert.match(markup, /costs no spice and uses no shipment or normal movement/);
|
|
assert.match(markup, /Select all forces and marker/);
|
|
assert.match(markup, /Clear selection/);
|
|
assert.match(markup, /Leave forces and marker outside/);
|
|
assert.doesNotMatch(markup, /type="number"|private-marker-id|private-marker-event/);
|
|
});
|
|
|
|
void test('marker-only and combined adapters bind both current events while legacy physical packets stay unchanged', () => {
|
|
for (const physical of [0, 2]) {
|
|
const game = view(physical), offer = game.discoveryEntry!;
|
|
const expected = { type: 'decision', event: offer.event, accept: true, groups: offer.sources,
|
|
noField: offer.noField!.tokenId, noFieldEvent: offer.noField!.event };
|
|
assert.deepEqual(discoveryEntryMoveAction(game, offer.sources, offer.noField), expected);
|
|
assert.deepEqual(discoveryEntryBotActions(game), [expected,
|
|
{ type: 'decision', event: offer.event, accept: false }]);
|
|
assert.match(html(game), new RegExp(`Selected: ${physical} physical forces · 1 concealed marker`));
|
|
}
|
|
const game = view(2), offer = game.discoveryEntry!;
|
|
assert.deepEqual(discoveryEntryMoveAction(game, offer.sources), {
|
|
type: 'decision', event: offer.event, accept: true, groups: offer.sources,
|
|
});
|
|
assert.equal(discoveryEntryMoveAction(game, []), null);
|
|
assert.deepEqual(discoveryEntryDeclineAction(game), { type: 'decision', event: offer.event, accept: false });
|
|
const legacy = html(view(2, false));
|
|
assert.match(legacy, /Move 2 forces inside/);
|
|
assert.match(legacy, /Select all forces/);
|
|
assert.doesNotMatch(legacy, /checkbox|concealed marker|Provisional/);
|
|
});
|
|
|
|
void test('stale token marker-event parent-source and Discovery-event selections cannot construct an entry', () => {
|
|
const game = view(), marker = game.discoveryEntry!.noField!;
|
|
for (const stale of [
|
|
{ ...marker, tokenId: 'old-marker' }, { ...marker, event: 'old-marker-event' },
|
|
{ ...marker, source: 'meridian:3' }, { ...marker, source: 'imperial_basin:10' },
|
|
]) assert.equal(discoveryEntryMoveAction(game, [], stale), null);
|
|
const changed = structuredClone(game);
|
|
changed.discoveryEntry!.noField!.event = 'new-marker-event';
|
|
assert.equal(discoveryEntryMoveAction(changed, [], marker), null);
|
|
const wrongDecision = structuredClone(game);
|
|
wrongDecision.discoveryEntry!.event = 'old-entry-round';
|
|
assert.equal(discoveryEntryMoveAction(wrongDecision, [], marker), null);
|
|
assert.equal(html(wrongDecision), '');
|
|
const observer = structuredClone(game); observer.me = 'a';
|
|
assert.equal(discoveryEntryMoveAction(observer, [], marker), null);
|
|
assert.equal(html(observer), '');
|
|
});
|
|
|
|
void test('busy autopilot and blocked marker-only panels cannot offer an enabled move or selection control', () => {
|
|
const busy = html(view(), true);
|
|
assert.equal((busy.match(/<button[^>]*disabled=""/g) ?? []).length, 4);
|
|
assert.match(busy, /<input[^>]*type="checkbox"[^>]*disabled=""/);
|
|
const automatic = view(); automatic.players[0].autopilot = 'Hard';
|
|
assert.equal((html(automatic).match(/<button[^>]*disabled=""/g) ?? []).length, 4);
|
|
const blocked = view(); blocked.discoveryEntry!.blocked = 'The location is full.';
|
|
assert.equal(discoveryEntryMoveAction(blocked, [], blocked.discoveryEntry!.noField), null);
|
|
assert.deepEqual(discoveryEntryBotActions(blocked), []);
|
|
assert.match(html(blocked), /The location is full/);
|
|
assert.equal((html(blocked).match(/<button[^>]*disabled=""/g) ?? []).length, 4);
|
|
});
|