import assert from 'node:assert/strict'; import test from 'node:test'; import { Children, createElement, isValidElement } from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import type { Action } from '../game/engine'; import { SaphoStationPriority, type SaphoStationPriorityProps } from '../components/sapho-station-priority'; function props(): SaphoStationPriorityProps & { sent: Action[] } { const sent: Action[] = []; return {game: {saphoStationPriority: {event:'station-event', canPass:true, waiting:false}}, busy:false, sent, act(action) { sent.push(action); }}; } const render = (input: SaphoStationPriorityProps) => renderToStaticMarkup(createElement(SaphoStationPriority, input)); /** Invoke the actual rendered Button callback, without mounting a browser. */ function pass(input: SaphoStationPriorityProps) { const section = SaphoStationPriority(input); assert.ok(section); const button = Children.toArray(section.props.children).find(child => isValidElement(child) && (child.props as {'data-testid'?: string})['data-testid'] === 'sapho-station-priority-pass'); assert.ok(isValidElement<{onClick: () => void; disabled: boolean}>(button)); return button; } void test('a neutral Testing Station opportunity stays visible and passes its exact event', () => { const input = props(), html = render(input); assert.match(html, /Testing Station priority/); assert.match(html, /Continue without changing the order/); assert.match(html, /Already completed choices stay finished/); assert.equal(pass(input).props.disabled, false); pass(input).props.onClick(); assert.deepEqual(input.sent, [{type:'saphoStationPass', event:'station-event'}]); }); void test('busy and invalid events disable Continue without sending an action', () => { for (const change of ['busy', 'empty-event'] as const) { const input = props(); if (change === 'busy') input.busy = true; else input.game.saphoStationPriority!.event = ''; assert.match(render(input), /disabled=""/); const button = pass(input); assert.equal(button.props.disabled, true); button.props.onClick(); assert.deepEqual(input.sent, []); } }); void test('waiting and completed viewers see no Continue button or hidden card data', () => { for (const waiting of [true, false]) { const input = props(); input.game.saphoStationPriority = {event:'station-event', canPass:false, waiting}; // Only the server-supplied public opportunity is rendered, even when a // surrounding object contains unrelated private fields. Object.assign(input.game, {hiddenCard:'private-sapho-card-id'}); const html = render(input); assert.doesNotMatch(html, /sapho-station-priority-pass|private-sapho-card-id/); assert.match(html, waiting ? /Waiting for the other players/ : /Your response has been recorded/); assert.deepEqual(input.sent, []); } const input = props(); input.game.saphoStationPriority = null; assert.equal(render(input), ''); }); void test('stale event, consumed response and replaced priority callbacks do not send', () => { for (const change of ['event', 'pass', 'replace', 'close'] as const) { const input = props(), button = pass(input), view = input.game.saphoStationPriority!; if (change === 'event') view.event = 'new-event'; else if (change === 'pass') view.canPass = false; else if (change === 'replace') input.game.saphoStationPriority = {...view}; else input.game.saphoStationPriority = null; button.props.onClick(); assert.deepEqual(input.sent, []); } });