step 10.17: wip (not ticked)
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
// 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');
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user