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.
214 lines
6.4 KiB
TypeScript
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/);
|
|
});
|