99 lines
5.1 KiB
TypeScript
99 lines
5.1 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { registerHooks } from 'node:module';
|
|
import { readFileSync } from 'node:fs';
|
|
import { createElement } from 'react';
|
|
import { renderToStaticMarkup } from 'react-dom/server';
|
|
import { applyAction, viewGame, type Game, type GameView } from '../game/engine';
|
|
import { EmperorAuctionGift } from '../components/emperor-auction-gift';
|
|
import { advanceTruthAlliance, createTruthAllianceFixture, truthAllianceClean } from './fixture-truth-alliance';
|
|
|
|
const aliases = registerHooks({
|
|
resolve(specifier, context, next) {
|
|
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();
|
|
|
|
/** Original public Advanced setup, natural worm and actual alliance, followed
|
|
* by ordinary bidding actions. Wallet amounts are deliberately staged. */
|
|
function fixture(emperorSpice: number): Game {
|
|
const source = createTruthAllianceFixture({advanced:true, consent:'none'});
|
|
let game = source.game;
|
|
game = applyAction(game, 'harkonnen', {type:'alliance', target:'emperor'});
|
|
game = applyAction(game, 'emperor', {type:'alliance', target:'harkonnen'});
|
|
game = advanceTruthAlliance(game, state => state.phase === 3 && !!state.auction && truthAllianceClean(state));
|
|
for (let count = 0; game.auction && game.auction.active !== 'harkonnen' && count < game.players.length; count++)
|
|
game = applyAction(game, game.auction.active, {type:'passBid'});
|
|
assert.equal(game.auction?.active, 'harkonnen');
|
|
game.players.find(player => player.id === 'harkonnen')!.spice = 6;
|
|
game.players.find(player => player.id === 'emperor')!.spice = emperorSpice;
|
|
game = applyAction(game, 'harkonnen', {type:'bid', amount:4, allyPayment:0});
|
|
for (let count = 0; game.auction && !game.response && !game.decision && count < 12; count++)
|
|
game = applyAction(game, game.auction.active, {type:'passBid'});
|
|
assert.equal(game.decision?.kind, 'auctionPayment');
|
|
assert.deepEqual(viewGame(game, 'emperor').emperorAuctionGiftPending, {owner:'emperor'});
|
|
return game;
|
|
}
|
|
|
|
const component = (game: GameView, busy = false) =>
|
|
renderToStaticMarkup(createElement(EmperorAuctionGift, {game, busy, act() {}}));
|
|
const table = (game: GameView, busy = false) =>
|
|
renderToStaticMarkup(createElement(GameTable, {game, busy, send:async () => {}, onExit() {}}));
|
|
const enabled = (label: string) => new RegExp(`<button\\b(?![^>]*disabled)[^>]*>${label}<\\/button>`);
|
|
const disabled = (label: string) => new RegExp(`<button\\b[^>]*disabled=""[^>]*>${label}<\\/button>`);
|
|
|
|
void test('the owner sees the quoted recipient, amount and continuation on the actual table', () => {
|
|
const game = fixture(10), own = viewGame(game, 'emperor');
|
|
assert.equal(own.emperorAuctionGift?.recipient, 'harkonnen');
|
|
assert.equal(own.emperorAuctionGift?.maximum, 10);
|
|
const html = component(own);
|
|
assert.match(html, /Share spice with Harkonnen/);
|
|
assert.match(html, /committed ally payment split remain unchanged/);
|
|
assert.match(html, /type="number"[^>]*min="1"[^>]*max="10"/);
|
|
assert.match(html, enabled('Share spice'));
|
|
assert.match(html, enabled('Continue to payment'));
|
|
assert.match(table(own), /Share spice with Harkonnen/);
|
|
assert.match(table(own), enabled('Continue to payment'));
|
|
});
|
|
|
|
void test('zero available spice still permits the owner to continue, but not to give', () => {
|
|
const own = viewGame(fixture(0), 'emperor');
|
|
assert.equal(own.emperorAuctionGift?.maximum, 0);
|
|
const html = component(own);
|
|
assert.match(html, disabled('Share spice'));
|
|
assert.match(html, enabled('Continue to payment'));
|
|
assert.match(table(own), enabled('Continue to payment'));
|
|
});
|
|
|
|
void test('nonowners see the public wait without an owner quote or private amount', () => {
|
|
const game = fixture(10);
|
|
for (const id of ['harkonnen', 'atreides']) {
|
|
const view = viewGame(game, id);
|
|
assert.equal(view.emperorAuctionGift, null);
|
|
assert.deepEqual(view.emperorAuctionGiftPending, {owner:'emperor'});
|
|
assert.equal(component(view), '');
|
|
const html = table(view);
|
|
assert.match(html, /Waiting for the Emperor to finish the allied gift opportunity/);
|
|
assert.doesNotMatch(html, /Emperor auction gift/);
|
|
assert.doesNotMatch(html, /Continue to payment/);
|
|
assert.doesNotMatch(html, /Spice to share/);
|
|
}
|
|
});
|
|
|
|
void test('busy controls disable both owner actions while preserving the waiting state', () => {
|
|
const own = viewGame(fixture(10), 'emperor');
|
|
const html = component(own, true);
|
|
assert.match(html, disabled('Share spice'));
|
|
assert.match(html, disabled('Continue to payment'));
|
|
assert.match(table(own, true), disabled('Continue to payment'));
|
|
});
|