import test from 'node:test'; import assert from 'node:assert/strict'; import { createElement } from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import { CoalitionNexus, coalitionNexusOptions, type CoalitionNexusView } from '../components/coalition-nexus'; import type { CoalitionGroup } from '../game/coalition-alliance'; const players = ['a', 'b', 'c', 'd', 'e', 'f'].map(id => ({id, name: id.toUpperCase()})); function group(members: string[]): CoalitionGroup { const edges: CoalitionGroup['edges'] = []; for (let left = 0; left < members.length; left++) for (let right = left + 1; right < members.length; right++) edges.push({members: [members[left], members[right]], formedTurn: 1}); return {id: 'group', members, edges, formedTurn: 1}; } function view(me: string, members: string[], unlimited = true): CoalitionNexusView { return {me, groups: [group(members)], players, proposal: null, revision: 7, ...(unlimited ? {variant: 'unlimited' as const} : {})}; } function render(value: CoalitionNexusView, disabled = false) { return renderToStaticMarkup(createElement(CoalitionNexus, {view: value, disabled, onIntent() {}})); } void test('unlimited membership offers entry only to a member or the unallied entrant', () => { const member = view('a', ['a', 'b', 'c']); assert.deepEqual(coalitionNexusOptions(member).invitations.map(row => row.entrant), ['d', 'e', 'f']); assert.match(render(member), /Invite D to A, B, C/); const entrant = view('d', ['a', 'b', 'c']); assert.deepEqual(coalitionNexusOptions(entrant).invitations.map(row => row.entrant), ['d']); assert.match(render(entrant), /Ask to join A, B, C/); assert.deepEqual(coalitionNexusOptions(view('missing', ['a', 'b', 'c'])).invitations, []); assert.deepEqual(coalitionNexusOptions(view('a', ['a', 'b', 'c'], false)).invitations, [], 'An older three-member prototype must not admit a fourth player.'); }); void test('unlimited invitations allow a sixth player but never a seventh', () => { const five = view('a', ['a', 'b', 'c', 'd', 'e']); assert.deepEqual(coalitionNexusOptions(five).invitations.map(row => row.entrant), ['f']); assert.match(render(five), /Invite F to A, B, C, D, E/); assert.match(render(five), /Leaving keeps the other players allied/); const six = view('a', ['a', 'b', 'c', 'd', 'e', 'f']); assert.deepEqual(coalitionNexusOptions(six).invitations, []); assert.match(render(six), /Leave A, B, C, D, E, F/); assert.match(render(six, true), /disabled=""[^>]*>Leave A, B, C, D, E, F/); }); void test('every existing member and the entrant can answer one pending group invitation', () => { const original = group(['a', 'b', 'c']); const value = view('d', original.members); value.proposal = {event: 'event', nexus: 'nexus', turn: 2, groupId: original.id, original, entrant: 'd', proposer: 'a', approvals: ['a', 'b']}; assert.equal(coalitionNexusOptions(value).mayAccept, true); assert.match(render(value), /Every current member and the invited player must agree/); assert.match(render(value), /Approved: A, B/); assert.match(render(value), /Waiting for: C, D/); assert.match(render(value), /Accept invitation/); value.me = 'b'; assert.equal(coalitionNexusOptions(value).mayAccept, false); assert.match(render(value), /Withdraw approval and cancel invitation/); value.me = 'e'; assert.equal(coalitionNexusOptions(value).mayAccept, false); assert.equal(coalitionNexusOptions(value).mayDecline, false); assert.doesNotMatch(render(value), /Accept invitation|cancel invitation/); });