Files
Dune/tests/nexus-traitor-controls.test.ts
T

135 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import test from 'node:test';
import assert from 'node:assert/strict';
import { registerHooks } from 'node:module';
import { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { viewGame, type GameView } from '../game/engine';
import {
nexusTraitorDrawAction,
nexusTraitorReturnAction,
} from '../game/nexus-traitor-options';
import {
nexusTraitorFixture,
nexusTraitorDraw,
} from './fixture-nexus-traitors';
const aliases = registerHooks({
resolve(specifier, context, next) {
return next(
specifier === 'next/image' ? 'vinext/shims/image' : specifier,
context,
);
},
});
const { NexusTraitors } = await import('../components/nexus-traitors');
aliases.deregister();
function html(game: GameView, busy = false) {
return renderToStaticMarkup(
createElement(NexusTraitors, { game, busy, act() {} }),
);
}
void test('Nexus draw control uses the private current offer and keeps human Cunning available over preserved controls', () => {
const v = viewGame(nexusTraitorFixture(), 'p');
const offer = v.nexusTraitors!.offer!;
const expected = {
type: 'nexusTraitorDraw',
event: offer.event,
mode: 'cunning',
};
assert.deepEqual(nexusTraitorDrawAction(v, offer.event, 'cunning'), expected);
v.decision = { kind: 'nullentropy', player: 'p' };
v.automaticContinuationPending = true;
assert.deepEqual(nexusTraitorDrawAction(v, offer.event, 'cunning'), expected);
assert.match(html(v), /Draw 1 Traitor card/);
assert.match(html(v, true), /disabled/);
assert.equal(nexusTraitorDrawAction(v, 'stale', 'cunning'), null);
assert.equal(nexusTraitorDrawAction(v, offer.event, 'secretAlly'), null);
offer.blocked = 'Finish the current Nexus card draw.';
assert.equal(nexusTraitorDrawAction(v, offer.event, 'cunning'), null);
assert.match(html(v), /Finish the current Nexus card draw/);
offer.blocked = null;
v.nexusCards!.card = 'atreides';
assert.equal(nexusTraitorDrawAction(v, offer.event, 'cunning'), null);
});
void test('owner exchange renders real identities and explicit choice without automatic confirmation', () => {
const v = viewGame(nexusTraitorDraw(nexusTraitorFixture()), 'p');
const pending = v.nexusTraitors!.pending!;
const rendered = html(v);
assert.equal(
(rendered.match(/type="checkbox"/g) ?? []).length,
pending.choices.length,
);
assert.doesNotMatch(rendered, /checked=""/);
assert.match(rendered, /0 of 1 selected/);
assert.match(rendered, /previous game action is paused/);
assert.match(rendered, /disabled=""[^>]*>Return selected card/);
for (const choice of pending.choices) {
const identity = v.allLeaders.find((leader) => leader.id === choice.id)!;
assert.ok(rendered.includes(identity.name));
assert.ok(rendered.includes(`Inspect traitor: ${identity.name}`));
assert.ok(rendered.includes(`Strength ${identity.strength}`));
}
const drawn = pending.choices.find((choice) => choice.drawn)!.id;
assert.deepEqual(nexusTraitorReturnAction(v, pending.event, [drawn]), {
type: 'nexusTraitorReturn',
event: pending.event,
cards: [drawn],
});
});
void test('Face Dancer exchange requires exactly two projected choices and excludes previously revealed dancers', () => {
const g = nexusTraitorFixture({ ownerFaction: 'tleilaxu' });
g.players[0].faceDancers![0].revealed = true;
const excluded = g.players[0].faceDancers![0].leader;
const v = viewGame(nexusTraitorDraw(g), 'p');
const pending = v.nexusTraitors!.pending!;
const selected = pending.choices.slice(0, 2).map((choice) => choice.id);
assert.equal(pending.count, 2);
assert.ok(!pending.choices.some((choice) => choice.id === excluded));
assert.match(html(v), /Inspect Face Dancer:/);
assert.match(html(v), /Previously revealed Face Dancers are not available/);
assert.ok(nexusTraitorReturnAction(v, pending.event, selected));
for (const cards of [
[],
selected.slice(0, 1),
[selected[0], selected[0]],
[selected[0], excluded],
[...selected, excluded],
])
assert.equal(nexusTraitorReturnAction(v, pending.event, cards), null);
assert.equal(nexusTraitorReturnAction(v, 'old-exchange', selected), null);
v.decision = { kind: 'nullentropy', player: 'p' };
v.automaticContinuationPending = true;
assert.ok(nexusTraitorReturnAction(v, pending.event, selected));
v.truthtrance = { stage: 'priority', queue: [], passed: [], question: null };
assert.equal(nexusTraitorReturnAction(v, pending.event, selected), null);
assert.match(html(v), /Finish Truthtrance before returning/);
});
void test('observer exchange controls never read private card choices', () => {
const g = nexusTraitorDraw(nexusTraitorFixture());
for (const id of ['q', 'r']) {
const v = viewGame(g, id);
const pending = v.nexusTraitors!.pending!;
assert.deepEqual(pending.choices, []);
Object.defineProperty(pending, 'choices', {
get() {
throw new Error('Read another seat’s choices');
},
});
const rendered = html(v);
assert.match(rendered, /Waiting for Owner to return 1 card privately/);
assert.doesNotMatch(
rendered,
/checkbox|Inspect traitor|Strength|Newly drawn/,
);
assert.equal(
nexusTraitorReturnAction(v, pending.event, ['anything']),
null,
);
}
});