Files
wownerogue/test/pvpUiPolish.test.js
jwinterm fad2a695bd Wownerogue
A browser-based roguelike synchronized with crypto block timing. A run is a race
against the next block: reach the exit before the block lands, or the house wins.
The same codebase runs the solo dungeon, the social Tavern, and operator-selected
PvP and race rulesets, with separate free and paid leaderboards.

Self-hostable under the MIT License. See README.md to run it, docs/ARCHITECTURE.md
for the design, and docs/DEPLOY.md to operate an instance.
2026-07-27 08:23:01 -04:00

144 lines
7.2 KiB
JavaScript

const fs = require('fs');
const path = require('path');
const vm = require('vm');
function loadMatchCopy() {
const context = {};
context.window = context;
vm.createContext(context);
vm.runInContext(
fs.readFileSync(path.join(__dirname, '../html/js/ui/matchUiCopy.js'), 'utf8'),
context
);
return context.MatchUiCopy;
}
describe('multiplayer UI copy contract', () => {
const copy = loadMatchCopy();
test.each([
['race', 'racer', 'Reach the exit'],
['last-alive', 'contender', 'Stay alive'],
['score-attack', 'delver', 'highest score'],
['coop-escape', 'teammate', 'Work together']
])('%s exposes its own objective and participant language', (id, participant, objective) => {
const mode = copy.normalize({ id });
expect(mode.participant).toBe(participant);
expect(mode.objective).toContain(objective);
});
test.each([
['race', '🏆 You escaped first!'],
['last-alive', '🏆 You are the last delver standing!'],
['score-attack', '🏆 You set the high score!']
])('%s renders a mode-specific competitive victory', (id, expected) => {
const result = copy.finalResult({
ruleset: { id },
winnerId: 'me',
players: [{ id: 'me', placement: 1, score: 812 }]
}, 'me');
expect(result.won).toBe(true);
expect(result.text).toContain(expected);
if (id === 'score-attack') expect(result.text).toContain('score 812');
});
test('co-op reports one team outcome and never promotes placement #1 as an individual winner', () => {
const result = copy.finalResult({
ruleset: { id: 'coop-escape' },
reason: 'all_escaped',
winnerId: null,
players: [
{ id: 'me', placement: 2, escaped: true },
{ id: 'ally', placement: 1, escaped: true }
]
}, 'me');
expect(result.won).toBe(true);
expect(result.text).toContain('🤝 Team escaped!');
expect(result.text).toContain('you reached safety');
expect(result.text).not.toMatch(/placed|winner/i);
});
test('live summaries use the active mode rather than race-only placement copy', () => {
expect(copy.liveSummary({
ruleset: { id: 'last-alive' },
players: [{ id: 'me', alive: true }, { id: 'out', alive: false }]
}, 'me')).toBe('1 contender still in');
expect(copy.liveSummary({
ruleset: { id: 'coop-escape' },
players: [{ id: 'me', escaped: true, finished: true }, { id: 'ally', alive: true }]
}, 'me')).toBe('1/2 safe · 1 still exploring');
});
});
describe('public multiplayer controls and boards', () => {
const matchHtml = fs.readFileSync(path.join(__dirname, '../html/match.html'), 'utf8');
const tavernHtml = fs.readFileSync(path.join(__dirname, '../html/tavern.html'), 'utf8');
const matchClient = fs.readFileSync(path.join(__dirname, '../html/js/matchClient.js'), 'utf8');
const socketHandlers = fs.readFileSync(path.join(__dirname, '../html/js/network/socketHandlers.js'), 'utf8');
const helpModal = fs.readFileSync(path.join(__dirname, '../html/js/ui/helpModal.js'), 'utf8');
const leaderboard = fs.readFileSync(path.join(__dirname, '../html/js/ui/leaderboard.js'), 'utf8');
const paymentUi = fs.readFileSync(path.join(__dirname, '../html/js/ui/paymentUI.js'), 'utf8');
const designDoc = fs.readFileSync(path.join(__dirname, '../docs/TAVERN_AND_MULTIPLAYER.md'), 'utf8');
test('match and Tavern D-pads are named groups activated through native click', () => {
expect(matchHtml).toMatch(/id="matchDpad"[^>]*role="group"[^>]*aria-label="Match movement controls"/);
expect(tavernHtml).toMatch(/id="tavernDpad"[^>]*role="group"[^>]*aria-label="Tavern movement controls"/);
expect(tavernHtml).toMatch(/id="spectatorDpad"[^>]*role="group"[^>]*aria-label="Match movement controls"/);
expect(matchClient).toContain("b.addEventListener('click'");
expect(tavernHtml).toContain("b.addEventListener('click'");
});
test('anonymous Tavern admission has explicit connection, pending, and retry states', () => {
expect(tavernHtml).toContain('Walk in anonymously');
expect(tavernHtml).toContain('No account required');
expect(tavernHtml).toMatch(/id="joinPanel"[^>]*>[\s\S]*id="joinBtn"[^>]*disabled/);
expect(tavernHtml).toContain("joinPanel.addEventListener('submit'");
expect(tavernHtml).toContain("socket.on('disconnect'");
expect(tavernHtml).toContain("socket.on('connect_error'");
expect(tavernHtml).toContain('Preparing your anonymous session');
expect(tavernHtml).toContain('!socket.connected || !joinReady');
expect(tavernHtml).toContain('The tavern did not respond');
});
test('Prestige is a distinct public board with an explicit non-mixing disclosure', () => {
expect(leaderboard).toContain('data-board="prestige"');
expect(leaderboard).toContain('Credit-entry competitive PvP scores only');
expect(leaderboard).toContain('never mixed with Free or the Hall of Champions');
});
test('arena renders only server-advertised economies, never locked out-of-profile cards', () => {
expect(matchClient).toContain('availableKeys.forEach(function (key)');
expect(matchClient).not.toContain('Object.keys(ECONOMIES).forEach(function (key)');
expect(matchClient).not.toContain("avail.textContent = locked ? 'NOT ENABLED'");
expect(tavernHtml).toContain('if (!economies[key]) return;');
});
test('Wownero-facing solo and leaderboard copy uses entry/credits, not stake claims', () => {
expect(socketHandlers).toContain('Choose your entry');
expect(socketHandlers).not.toContain('Choose your stakes');
expect(socketHandlers).toContain("Buy credits with ' + currency");
expect(helpModal).toContain('Prefer a free entry?');
expect(helpModal).not.toContain('Prefer no stakes?');
expect(leaderboard).toContain("this._profileId === 'such-play-wow-prestige'");
expect(leaderboard).toContain('Credit-entry solo scores only');
expect(leaderboard).toContain('historical crypto-match results recorded while that mode was enabled');
expect(leaderboard).not.toContain('crypto-stakes');
});
test('direct-entry payment copy never claims it adds a spendable credit', () => {
expect(socketHandlers).toContain('Buy a single entry or credits with ');
expect(socketHandlers).toContain('SocketHandlers._directModeEnabled');
expect(paymentUi).toContain('Single-game entry');
expect(paymentUi).toContain('no spendable credit is added');
expect(paymentUi).not.toContain('Single game <span');
expect(paymentUi).not.toContain('Counts as 1 credit toward cosmetic unlocks');
});
test('operator docs require restart for mode/ruleset changes and do not promise future combat', () => {
expect(designDoc).toContain('restart the service');
expect(designDoc).toContain('does not hot-reload match modes or rulesets');
expect(designDoc).not.toMatch(/PvP combat (?:remains|is planned)/i);
});
});