Files
Dune/tests/leader-skills-controls.test.ts
T
Quintine 6e6fecc9d4 Prototype Leader Skills setup, battle disciplines and capture
Connect all fourteen physical skills and private setup to five card-role battle
modifiers, public posture, Harkonnen skilled capture, death return and optional
own-leader revival through controls, legal AI and persisted room actions.
Nine skill effects and combined-mode/publication acceptance remain unfinished.
Captured replacement entitlement and foreign ghola integration stay guarded.

Fresh publisher/designer research supports skilled-capture publicity through
its unique known physical card; this is recorded as a component inference.
Independent review found and fixed Voice/must-use-leader and invalid saved
assignment/offer cases. A pre-existing Discovery privacy assertion confused
card ID prefixes; the corrected deterministic physical-pair scenario was
independently reviewed. The first broad run was 4410/4411; corrected final
verification passes types/lint, all 4411 offline tests, build and 43 HTTP tests.
Final durations: check 149741ms, build 6659ms, integration 28274ms.
Source fingerprint: aef655ac1ebc0eb2f64294e13d3d27337167312cf8ab17568a166ee9c39f6a85.
Private final report: 20260913-leader-skills/checks-final/report.json.

Three seeded prototype games completed with no rejected bot candidates. Browser
setup/card inspection/refresh passed. Another user-reported power outage stopped
the server; backup and necessary restart restored the same private seat. All
292 games at restart and their original application records remain unchanged;
33 isolated HTTP-test rooms bring the current total to 325. No database reset.

Efficiency: one shared setup/lifecycle implementation supports five connected
role effects; selective existing agents handled catalog/UI/tests/research and
independent reviews. One repeated broad check was required by the reproduced
ID-prefix assertion failure. Aggregate goal counter rose from 65529766 to
67313673 through this checkpoint (1783907), including inherited context and
coordination; this is not a billing or savings estimate. Full goal remains active.
2026-09-13 17:31:37 +10:00

214 lines
6.4 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.doesNotMatch(publicOnly, /Bureaucrat|Spice Banker|Diplomat/);
});
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('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, /Bureaucrat|Diplomat|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);
});
void test('the panel states the exact five connected role modifiers and unfinished boundary', () => {
const html = markup(view());
for (const name of [
'Warmaster',
'Master of Assassins',
'Swordmaster of Ginaz',
'Killer Medic',
'Prana-Bindu Adept',
])
assert.match(html, new RegExp(name));
assert.match(html, /development-only module/);
assert.match(html, /Other Leader Skill effects are unfinished/);
});