Files
2026-10-23 01:06:48 +11:00

185 lines
7.3 KiB
JavaScript

// Step 10.9 — Allocation UI tests
// Verifies: allocation UI elements exist in HTML, JS uses textContent,
// allocation flow via API works end-to-end.
const { describe, it, before, after } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const { setup, teardown, cleanup } = require('../helpers');
describe('Step 10.9 — Allocation UI', () => {
let baseUrl;
let loginCookie;
before(async () => {
baseUrl = await setup();
});
after(async () => {
const { resetNow } = require('../../src/utils/time');
resetNow();
await teardown();
cleanup();
});
// ===== HTML/JS structure tests =====
it('game.html contains allocation section with all required elements', () => {
const html = fs.readFileSync(path.resolve(__dirname, '../../public/game.html'), 'utf8');
assert.ok(html.includes('id="allocation-section"'),
'game.html must contain allocation-section div');
assert.ok(html.includes('id="unspent-points"'),
'game.html must contain unspent-points span');
assert.ok(html.includes('id="alloc-rows"'),
'game.html must contain alloc-rows div');
assert.ok(html.includes('id="alloc-apply-btn"'),
'game.html must contain alloc-apply-btn button');
assert.ok(html.includes('id="alloc-total"'),
'game.html must contain alloc-total div');
});
it('allocation section is hidden by default', () => {
const html = fs.readFileSync(path.resolve(__dirname, '../../public/game.html'), 'utf8');
assert.ok(html.includes('id="allocation-section" style="display:none;"'),
'allocation section must be hidden by default');
});
it('game.js defines all allocation functions', () => {
const js = fs.readFileSync(path.resolve(__dirname, '../../public/js/game.js'), 'utf8');
assert.ok(js.includes('function buildAllocationRows'),
'game.js must define buildAllocationRows');
assert.ok(js.includes('function handleAllocIncrement'),
'game.js must define handleAllocIncrement');
assert.ok(js.includes('function updateAllocState'),
'game.js must define updateAllocState');
assert.ok(js.includes('function handleAllocApply'),
'game.js must define handleAllocApply');
assert.ok(js.includes('function initAllocationUI'),
'game.js must define initAllocationUI');
});
it('allocation UI uses textContent, not innerHTML, for dynamic data', () => {
const js = fs.readFileSync(path.resolve(__dirname, '../../public/js/game.js'), 'utf8');
const start = js.indexOf('// ===== Stat allocation UI =====');
const end = js.indexOf('// ===== Init =====');
const allocCode = js.substring(start, end);
assert.ok(!allocCode.includes('innerHTML = '),
'Allocation UI must not use innerHTML with dynamic data');
assert.ok(allocCode.includes('textContent'),
'Allocation UI must use textContent for values');
});
it('loadCharacter calls initAllocationUI with unspent_points', () => {
const js = fs.readFileSync(path.resolve(__dirname, '../../public/js/game.js'), 'utf8');
assert.ok(js.includes('initAllocationUI(character.unspent_points)'),
'loadCharacter must call initAllocationUI with unspent_points');
});
it('init attaches handleAllocApply to the apply button', () => {
const js = fs.readFileSync(path.resolve(__dirname, '../../public/js/game.js'), 'utf8');
assert.ok(js.includes('handleAllocApply'),
'init must attach handleAllocApply to apply button');
});
// ===== Allocation flow via HTTP =====
function registerAndLogin(username) {
return fetch(`${baseUrl}/api/auth/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password: 'password123' }),
}).then(() => fetch(`${baseUrl}/api/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password: 'password123' }),
}));
}
function createHero(cookie, name) {
return fetch(`${baseUrl}/api/character`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Cookie: cookie },
body: JSON.stringify({ name, class: 'warrior' }),
});
}
function allocate(cookie, body) {
return fetch(`${baseUrl}/api/character/allocate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Cookie: cookie },
body: JSON.stringify(body),
});
}
function getCharacter(cookie) {
return fetch(`${baseUrl}/api/character`, {
method: 'GET',
headers: { Cookie: cookie },
});
}
it('full allocation flow: grant points → allocate → verify stats updated', async () => {
const res = await registerAndLogin('ui109');
loginCookie = res.headers.get('set-cookie');
await createHero(loginCookie, 'UI109Hero');
// Grant 5 unspent points via direct DB update
const db = require('../../src/db').getDb();
const userId = Number(db.prepare('SELECT id FROM users WHERE username = ?').get('ui109').id);
const char = db.prepare('SELECT id FROM characters WHERE user_id = ?').get(userId);
db.prepare('UPDATE characters SET unspent_points = 5 WHERE id = ?').run(char.id);
// Verify initial state
const charRes = await getCharacter(loginCookie);
assert.equal(charRes.status, 200);
const charData = await charRes.json();
assert.equal(charData.data.character.unspent_points, 5);
const baseStr = charData.data.character.str;
const baseMaxHp = charData.data.derived.maxHp;
// Allocate 2 STR + 1 VIT = 3 points
const allocRes = await allocate(loginCookie, { str: 2, vit: 1 });
assert.equal(allocRes.status, 200);
const allocData = await allocRes.json();
assert.equal(allocData.ok, true);
// Verify stats updated
assert.equal(allocData.data.character.str, baseStr + 2);
assert.equal(allocData.data.character.unspent_points, 2); // 5 - 3 = 2
// Warrior maxHp = 50 + vit*10; +1 VIT = +10 maxHp
assert.equal(allocData.data.derived.maxHp, baseMaxHp + 10);
// Single-page flow: no page reload needed, server returns full updated profile
assert.ok(allocData.data.character, 'response includes updated character');
assert.ok(allocData.data.derived, 'response includes updated derived stats');
});
it('allocation UI hides when no points remain (allocate all → verify 0 unspent)', async () => {
const res = await registerAndLogin('ui109b');
loginCookie = res.headers.get('set-cookie');
await createHero(loginCookie, 'UI109B');
// Grant 3 points
const db = require('../../src/db').getDb();
const userId = Number(db.prepare('SELECT id FROM users WHERE username = ?').get('ui109b').id);
const char = db.prepare('SELECT id FROM characters WHERE user_id = ?').get(userId);
db.prepare('UPDATE characters SET unspent_points = 3 WHERE id = ?').run(char.id);
// Allocate all 3 points
const allocRes = await allocate(loginCookie, { str: 3 });
assert.equal(allocRes.status, 200);
// Verify 0 unspent points remain
const charRes = await getCharacter(loginCookie);
assert.equal(charRes.status, 200);
const charData = await charRes.json();
assert.equal(charData.data.character.unspent_points, 0);
// initAllocationUI(0) → section.style.display = 'none'
// (verified by code inspection; the section is hidden when unspentPoints is 0)
});
});