/* Unit tests for frontend helper functions (tested via VM sandbox) */ 'use strict'; const vm = require('vm'); const fs = require('fs'); const assert = require('assert'); let passed = 0, failed = 0; function test(name, fn) { try { fn(); passed++; console.log(` ✅ ${name}`); } catch (e) { failed++; console.log(` ❌ ${name}: ${e.message}`); } } // --- Build a browser-like sandbox --- function makeSandbox() { const ctx = { window: { addEventListener: () => {}, dispatchEvent: () => {} }, document: { readyState: 'complete', createElement: () => ({ id: '', textContent: '', innerHTML: '' }), head: { appendChild: () => {} }, getElementById: () => null, addEventListener: () => {}, querySelectorAll: () => [], querySelector: () => null, }, console, Date, Infinity, Math, Array, Object, String, Number, JSON, RegExp, Error, TypeError, parseInt, parseFloat, isNaN, isFinite, encodeURIComponent, decodeURIComponent, setTimeout: () => {}, clearTimeout: () => {}, setInterval: () => {}, clearInterval: () => {}, fetch: () => Promise.resolve({ json: () => Promise.resolve({}) }), performance: { now: () => Date.now() }, localStorage: (() => { const store = {}; return { getItem: k => store[k] || null, setItem: (k, v) => { store[k] = String(v); }, removeItem: k => { delete store[k]; }, }; })(), location: { hash: '' }, CustomEvent: class CustomEvent {}, Map, Promise, URLSearchParams, addEventListener: () => {}, dispatchEvent: () => {}, requestAnimationFrame: (cb) => setTimeout(cb, 0), }; vm.createContext(ctx); return ctx; } function loadInCtx(ctx, file) { vm.runInContext(fs.readFileSync(file, 'utf8'), ctx); // Copy window.* to global context so bare references work for (const k of Object.keys(ctx.window)) { ctx[k] = ctx.window[k]; } } // ===== APP.JS TESTS ===== console.log('\n=== app.js: timeAgo ==='); { const ctx = makeSandbox(); loadInCtx(ctx, 'public/roles.js'); loadInCtx(ctx, 'public/app.js'); const timeAgo = ctx.timeAgo; test('null returns dash', () => assert.strictEqual(timeAgo(null), '—')); test('undefined returns dash', () => assert.strictEqual(timeAgo(undefined), '—')); test('empty string returns dash', () => assert.strictEqual(timeAgo(''), '—')); test('30 seconds ago', () => { const d = new Date(Date.now() - 30000).toISOString(); assert.strictEqual(timeAgo(d), '30s ago'); }); test('5 minutes ago', () => { const d = new Date(Date.now() - 300000).toISOString(); assert.strictEqual(timeAgo(d), '5m ago'); }); test('2 hours ago', () => { const d = new Date(Date.now() - 7200000).toISOString(); assert.strictEqual(timeAgo(d), '2h ago'); }); test('3 days ago', () => { const d = new Date(Date.now() - 259200000).toISOString(); assert.strictEqual(timeAgo(d), '3d ago'); }); } console.log('\n=== app.js: escapeHtml ==='); { const ctx = makeSandbox(); loadInCtx(ctx, 'public/roles.js'); loadInCtx(ctx, 'public/app.js'); const escapeHtml = ctx.escapeHtml; test('escapes < and >', () => assert.strictEqual(escapeHtml('