step 10.9: done
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
// 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)
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user