Files
2026-10-23 07:01:49 +11:00

180 lines
6.5 KiB
JavaScript

// Step 10.17: Leaderboard UI + nav links on all logged-in pages.
const { describe, it, before, after } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { setup, teardown, cleanup } = require('../helpers');
const PUBLIC_DIR = path.resolve(__dirname, '../../public');
// ─── Helpers ─────────────────────────────────────────────────────────────
function extractLinks(html) {
const links = [];
const linkRegex = /<a[^>]*href=["']([^"']*)["'][^>]*>([^<]*)<\/a>/gi;
let match;
while ((match = linkRegex.exec(html)) !== null) {
links.push({ href: match[1], text: match[2].trim() });
}
return links;
}
async function registerAndLogin(baseUrl, username, password) {
await fetch(`${baseUrl}/api/auth/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
});
const res = await fetch(`${baseUrl}/api/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
});
assert.equal(res.status, 200);
const json = await res.json();
assert.equal(json.ok, true);
const setCookie = res.headers.get('set-cookie');
assert.ok(setCookie, 'Login should set a session cookie');
const cookie = setCookie.split(';')[0];
return cookie;
}
function authHeaders(cookie) {
return { Cookie: cookie };
}
// ─── DOM-free HTML structure tests ───────────────────────────────────────
describe('nav links in logged-in pages', () => {
const navLinks = [
{ href: '/game.html', text: 'Game' },
{ href: '/shop.html', text: 'Shop' },
{ href: '/leaderboard.html', text: 'Leaderboard' },
{ href: '/api/auth/logout', text: 'Logout' },
];
function checkNavLinks(filename) {
const html = fs.readFileSync(path.join(PUBLIC_DIR, filename), 'utf-8');
const links = extractLinks(html);
for (const expected of navLinks) {
assert.ok(
links.some((l) => l.href === expected.href && l.text === expected.text),
`${filename} should link to ${expected.href}`
);
}
}
it('game.html has nav links: Game, Shop, Leaderboard, Logout', () => {
checkNavLinks('game.html');
});
it('shop.html has nav links: Game, Shop, Leaderboard, Logout', () => {
checkNavLinks('shop.html');
});
it('create-hero.html has nav links: Game, Shop, Leaderboard, Logout', () => {
checkNavLinks('create-hero.html');
});
it('leaderboard.html has nav links: Game, Shop, Leaderboard, Logout', () => {
checkNavLinks('leaderboard.html');
});
});
describe('leaderboard page structure', () => {
it('leaderboard.html has Level, Gold, Kills tabs', () => {
const html = fs.readFileSync(path.join(PUBLIC_DIR, 'leaderboard.html'), 'utf-8');
assert.ok(html.includes('data-type="level"'), 'should have Level tab');
assert.ok(html.includes('data-type="gold"'), 'should have Gold tab');
assert.ok(html.includes('data-type="kills"'), 'should have Kills tab');
});
it('leaderboard.html has leaderboard table with rank/name/class/value columns', () => {
const html = fs.readFileSync(path.join(PUBLIC_DIR, 'leaderboard.html'), 'utf-8');
assert.ok(html.includes('Rank'), 'should have Rank column');
assert.ok(html.includes('Name'), 'should have Name column');
assert.ok(html.includes('Class'), 'should have Class column');
assert.ok(html.includes('id="leaderboard-body"'), 'should have tbody container');
assert.ok(html.includes('id="leaderboard-table"'), 'should have table');
});
it('leaderboard.html loads leaderboard.js', () => {
const html = fs.readFileSync(path.join(PUBLIC_DIR, 'leaderboard.html'), 'utf-8');
assert.ok(html.includes('src="/js/leaderboard.js"'), 'should load leaderboard.js');
});
});
describe('leaderboard.js code quality', () => {
it('leaderboard.js does not use innerHTML', () => {
const js = fs.readFileSync(path.join(PUBLIC_DIR, 'js/leaderboard.js'), 'utf-8');
assert.ok(
!js.includes('innerHTML'),
'leaderboard.js must not use innerHTML'
);
});
});
describe('leaderboard page API integration', () => {
let baseUrl, cookie;
before(async () => {
baseUrl = await setup();
cookie = await registerAndLogin(baseUrl, 'navtest', 'navtest123');
// Create a character
await fetch(`${baseUrl}/api/character`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeaders(cookie) },
body: JSON.stringify({ name: 'NavHero', class: 'warrior' }),
});
});
after(async () => {
await teardown();
cleanup();
});
it('leaderboard page returns 200 HTML', async () => {
const res = await fetch(`${baseUrl}/leaderboard.html`, {
headers: authHeaders(cookie),
});
assert.equal(res.status, 200);
assert.ok(res.headers.get('content-type')?.includes('text/html'));
});
it('leaderboard page shows hero names linking to profile.html', async () => {
// The hero names are rendered dynamically by JS, so we verify the
// JS code generates the correct profile link format.
const js = fs.readFileSync(path.join(PUBLIC_DIR, 'js/leaderboard.js'), 'utf-8');
assert.ok(
js.includes('profile.html?name=') && js.includes('encodeURIComponent(entry.name)'),
'leaderboard.js should generate profile links with encodeURIComponent'
);
});
it('leaderboard API returns correct data shape', async () => {
const res = await fetch(`${baseUrl}/api/leaderboard?type=level&page=1`, {
headers: authHeaders(cookie),
});
assert.equal(res.status, 200);
const json = await res.json();
assert.equal(json.ok, true);
assert.ok(Array.isArray(json.data.entries), 'entries should be an array');
assert.ok(json.data.pagination, 'should have pagination');
assert.equal(typeof json.data.pagination.page, 'number');
assert.equal(typeof json.data.pagination.totalPages, 'number');
// Each entry should have rank, name, class, and the value field
if (json.data.entries.length > 0) {
const entry = json.data.entries[0];
assert.equal(typeof entry.rank, 'number');
assert.equal(typeof entry.name, 'string');
assert.equal(typeof entry.class, 'string');
assert.equal(typeof entry.level, 'number');
assert.equal(typeof entry.gold, 'number');
assert.equal(typeof entry.kills, 'number');
}
});
});