// 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) }); });