Files
Dune/tests/leader-skills-controls.test.ts
T

225 lines
7.1 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { LeaderSkillsPanel } from '../components/leader-skills';
import { LEADER_SKILL_CARDS } from '../game/leader-skill-cards';
import type { LeaderSkillsView } from '../game/leader-skills';
const leaders = [
{ id: 'duke-leto', name: 'Duke Leto Atreides' },
{ id: 'lady-jessica', name: 'Lady Jessica' },
{ id: 'thufir-hawat', name: 'Thufir Hawat' },
] as const;
const players = [
{ id: 'a', name: 'Atreides' },
{ id: 'e', name: 'Emperor' },
] as const;
function view(overrides: Partial<LeaderSkillsView> = {}): LeaderSkillsView {
return {
assignments: [],
offer: null,
eligibleLeaders: [],
battleChoice: null,
...overrides,
};
}
function markup(skills: LeaderSkillsView | null, busy = false): string {
return renderToStaticMarkup(
createElement(LeaderSkillsPanel, {
skills,
leaders,
players,
busy,
act() {},
}),
);
}
function disabledControls(html: string): number {
return (html.match(/<(?:button|input|select)\b[^>]*\sdisabled=""/g) ?? [])
.length;
}
void test('renders nothing when the projected module is absent and never invents a private offer', () => {
assert.equal(markup(null), '');
const publicOnly = markup(
view({
assignments: [
{
owner: 'a',
controller: 'a',
captured: false,
leader: 'duke-leto',
skill: 'warmaster',
faceUp: true,
},
],
}),
);
assert.match(publicOnly, /Public Leader Skill assignments/);
assert.match(publicOnly, /Atreides · Duke Leto Atreides · Face up/);
assert.doesNotMatch(
publicOnly,
/Your private offer|Choose one private skill/,
);
assert.deepEqual(
[...publicOnly.matchAll(/aria-label="([^"]+) Leader Skill card"/g)].map((match) => match[1]),
['Warmaster'],
'General guidance may name skills; only the public assignment gets a card face.',
);
});
void test('every public assignment card exposes its complete canonical normal and battle text', () => {
for (const card of LEADER_SKILL_CARDS) {
const html = markup(
view({
assignments: [
{
owner: 'a',
controller: 'a',
captured: false,
leader: 'duke-leto',
skill: card.id,
faceUp: false,
},
],
}),
);
assert.match(
html,
new RegExp(`aria-label="${card.name} Leader Skill card"`),
);
assert.match(html, new RegExp(`Inspect ${card.name}`));
assert.match(html, /Atreides · Duke Leto Atreides · Behind shield/);
assert.ok(html.includes(`aria-label="${card.name} normal effect"`));
assert.ok(html.includes(`aria-label="${card.name} battle effect"`));
for (const line of [...card.normal, ...card.battle])
assert.ok(html.includes(line), `${card.name} omitted: ${line}`);
}
});
void test('a private setup offer renders only its two physical cards and eligible leader selection', () => {
const skills = view({
offer: {
event: 'setup-event',
cards: ['bureaucrat', 'diplomat'],
leader: null,
},
eligibleLeaders: [leaders[0], leaders[1]],
});
const html = markup(skills);
assert.match(html, /aria-label="Choose a Leader Skill"/);
assert.match(html, /Keep Bureaucrat/);
assert.match(html, /Keep Diplomat/);
assert.match(html, /Bureaucrat Leader Skill card/);
assert.match(html, /Diplomat Leader Skill card/);
assert.doesNotMatch(
html,
/Mentat Leader Skill card|Spice Banker Leader Skill card/,
);
assert.match(html, /aria-label="Eligible Leader Skill leader"/);
assert.match(html, /Duke Leto Atreides/);
assert.match(html, /Lady Jessica/);
assert.match(html, /Assign selected skill/);
assert.equal((html.match(/type="radio"/g) ?? []).length, 2);
assert.equal((html.match(/checked=""/g) ?? []).length, 1);
const disabled = markup(skills, true);
assert.equal(disabledControls(disabled), 4);
});
void test('an unavailable offered skill remains inspectable while the other card is selected', () => {
const reason = 'This Leader Skill is unavailable in the current profile.';
const html = markup(view({
offer: { event: 'guarded-offer', cards: ['suk-graduate', 'warmaster'], leader: null },
eligibleLeaders: [leaders[0]],
unavailableSkills: { 'suk-graduate': reason },
}));
const blocked = html.match(/<input\b[^>]*value="suk-graduate"[^>]*>/)?.[0];
const available = html.match(/<input\b[^>]*value="warmaster"[^>]*>/)?.[0];
assert.ok(blocked && available);
assert.match(blocked, /disabled=""/);
assert.doesNotMatch(blocked, /checked=""/);
assert.match(available, /checked=""/);
assert.doesNotMatch(available, /disabled=""/);
assert.ok(html.includes(reason));
assert.match(html, /Inspect Suk Graduate/);
assert.match(html, /Assign selected skill/);
assert.equal(disabledControls(html), 1, 'the legal assignment remains usable');
});
void test('revival asks before preview and does not offer a decline after cards are drawn', () => {
const undrawn = markup(
view({
offer: { event: 'revival-event', cards: [], leader: 'thufir-hawat' },
eligibleLeaders: [leaders[2]],
}),
);
assert.match(undrawn, /Skill for Thufir Hawat/);
assert.match(undrawn, /before seeing either private card/);
assert.match(undrawn, /Draw two skills/);
assert.match(undrawn, /Continue without a skill/);
assert.doesNotMatch(undrawn, /Leader Skill card"|Your private offer/);
assert.equal(
disabledControls(
markup(
view({
offer: { event: 'revival-event', cards: [], leader: 'thufir-hawat' },
eligibleLeaders: [leaders[2]],
}),
true,
),
),
2,
);
const drawn = markup(
view({
offer: {
event: 'revival-event',
cards: ['bureaucrat', 'diplomat'],
leader: 'thufir-hawat',
},
eligibleLeaders: [leaders[2]],
}),
);
assert.match(drawn, /Assigned leader: <strong>Thufir Hawat<\/strong>/);
assert.match(drawn, /choosing one is mandatory/);
assert.match(drawn, /Assign selected skill/);
assert.doesNotMatch(drawn, /Continue without a skill|Draw two skills/);
assert.doesNotMatch(drawn, /aria-label="Eligible Leader Skill leader"/);
});
void test('battle visibility choice names the skilled leader and disables both actions while busy', () => {
const skills = view({
assignments: [
{
owner: 'a',
controller: 'a',
captured: false,
leader: 'duke-leto',
skill: 'master-of-assassins',
faceUp: true,
},
],
battleChoice: {
event: 'battle-event',
leader: 'duke-leto',
skill: 'master-of-assassins',
},
});
const html = markup(skills);
assert.match(html, /aria-label="Leader Skill battle visibility"/);
assert.match(html, /Duke Leto Atreides in this battle/);
assert.match(html, /Keep leader face up/);
assert.match(html, /Move leader behind shield/);
assert.equal((html.match(/disabled=""/g) ?? []).length, 0);
assert.equal(disabledControls(markup(skills, true)), 2);
});