diff --git a/test-all.sh b/test-all.sh index 3c61942..8fdf58c 100755 --- a/test-all.sh +++ b/test-all.sh @@ -14,6 +14,7 @@ node test-decoder-spec.js node test-packet-store.js node test-packet-filter.js node test-aging.js +node test-frontend-helpers.js node test-regional-filter.js node test-server-helpers.js node test-db.js diff --git a/test-frontend-helpers.js b/test-frontend-helpers.js new file mode 100644 index 0000000..181eb16 --- /dev/null +++ b/test-frontend-helpers.js @@ -0,0 +1,372 @@ +/* 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('