import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { registerHooks } from 'node:module'; import { resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; import test from 'node:test'; import { createElement } from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import { createGame, newPlayer, viewGame, type Game } from '../game/engine'; import { MOBILE_STRONGHOLD, location, territory } from '../game/board'; // SSR cannot operate a select. Seed exactly the state normally set by the // destination/sector and force controls while still rendering real GameTable. // Derive hook indices from declaration names so unrelated inserted hooks do not // change the selected state. This does not claim a hydrated browser interaction. const file = resolve('components/game-table.tsx'); const sourceText = readFileSync(file, 'utf8'); const names = [...sourceText.matchAll(/const\s+\[\s*([\w$]+)\s*,[^\]]+\]\s*=\s*useState/g)].map(match => match[1]); const indices = Object.fromEntries(['selectedId', 'sector', 'rideForces'].map(name => { const index = names.indexOf(name); assert.ok(index >= 0, `Missing ${name} table state`); return [name, index]; })); const reactUrl = pathToFileURL(resolve('node_modules/react/index.js')).href; const mockReact = `import {useState as original} from ${JSON.stringify(reactUrl)}; export function useState(initial) { const config = globalThis.__duneWormRideHarness; const index = config.next++; if (index === config.indices.selectedId) initial = config.destination; if (index === config.indices.sector) initial = 0; if (index === config.indices.rideForces) initial = {[config.source]: 4}; return original(initial); }`; const aliases = registerHooks({ resolve(specifier, context, next) { if (specifier === 'react' && context.parentURL?.endsWith('/components/game-table.tsx')) return {url:`data:text/javascript,${encodeURIComponent(mockReact)}`, shortCircuit:true}; return next(specifier === 'next/image' ? 'vinext/shims/image' : specifier === 'next/link' ? 'vinext/shims/link' : specifier, context); }, load(url, context, next) { if (!url.endsWith('.module.css')) return next(url, context); const css = readFileSync(new URL(url), 'utf8'); const classes = Object.fromEntries([...css.matchAll(/\.([a-zA-Z_][\w-]*)/g)].map(match => [match[1], match[1]])); return {format:'module', source:`export default ${JSON.stringify(classes)}`, shortCircuit:true}; }, }); const { GameTable } = await import('../components/game-table'); aliases.deregister(); const sourceSector = territory('the_great_flat').sectors[0]; const sourceLocation = location('the_great_flat', sourceSector); /** Staged Basic Ix worm decision, matching the focused runtime fixture; only * forces, source, storm and mobile placement are staged, not a played journey. */ function fixture(storm: number): Game { const game = createGame('WORMHMSUI', newPlayer('f', 'Fremen', 'fremen'), false, ['ix']); game.players.push(newPlayer('i', 'Ixians', 'ixians'), newPlayer('e', 'Emperor', 'emperor')); Object.assign(game, {status:'playing', phase:1, turn:2, storm, active:'f', order:['f','i','e'], spiceSequence:{pile:1, skipped:[]}, wormRides:[], decision:{kind:'wormRide', player:'f', territory:'the_great_flat'}, mobileStronghold:{location:'arsunt:11'}}); for (const player of game.players) Object.assign(player, {hand:[], forces:{}, reserves:20, spice:10}); game.players[0].forces = {[sourceLocation]:4}; game.players[0].reserves = 16; return game; } function render(game: Game) { const config = {next:0, indices, source:sourceLocation, destination:MOBILE_STRONGHOLD}; (globalThis as unknown as {__duneWormRideHarness: typeof config}).__duneWormRideHarness = config; try { return renderToStaticMarkup(createElement(GameTable, {game:viewGame(game,'f'),busy:false, send:async () => { assert.fail('Server rendering must not send a decision.'); },onExit() {}})); } finally { Reflect.deleteProperty(globalThis, '__duneWormRideHarness'); } } function rideButton(html: string) { const match = html.match(/]*>Ride Shai-Hulud/); assert.ok(match, 'The original Ride button renders'); return match[0]; } void test('selected mobile interior blocks a worm ride through a storm-covered pointer with a readable reason', () => { const game = fixture(11), before = structuredClone(game), html = render(game); assert.match(html, /Hidden Mobile Stronghold points into the storm; a worm ride cannot enter it\./); assert.match(rideButton(html), /\bdisabled=""/); assert.match(html, /