188 lines
6.3 KiB
JavaScript
Executable File
188 lines
6.3 KiB
JavaScript
Executable File
import { readFile } from 'node:fs/promises';
|
|
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
|
|
const html = await readFile(new URL('../index.html', import.meta.url), 'utf8');
|
|
const manifest = JSON.parse(
|
|
await readFile(new URL('../data/manifest.json', import.meta.url), 'utf8')
|
|
);
|
|
const dashboardJs = await readFile(new URL('../js/dashboard.js', import.meta.url), 'utf8');
|
|
|
|
function idsInMarkup() {
|
|
return [...html.matchAll(/\sid=["']([^"']+)["']/g)].map(match => match[1]);
|
|
}
|
|
|
|
test('dashboard keeps the required shell element ids unique', () => {
|
|
const ids = idsInMarkup();
|
|
const requiredIds = [
|
|
'clockDisplay',
|
|
'dateDisplay',
|
|
'lastUpdated',
|
|
'globalStatus',
|
|
'health',
|
|
'healthBadge',
|
|
'serverDot',
|
|
'webDot',
|
|
'webStatus',
|
|
'emacsDot',
|
|
'emacsStatus',
|
|
'uptimeStat',
|
|
'uptimeDays',
|
|
'uptimeHours',
|
|
'uptimeMins',
|
|
'uptimeStart',
|
|
'uptimePct',
|
|
'uptimeFill',
|
|
'pingHistory',
|
|
'quickLinks',
|
|
'widgetGrid',
|
|
'buildGrid',
|
|
'dailyMessage',
|
|
'countdownLabel',
|
|
'countdownDisplay',
|
|
'cd-days',
|
|
'cd-hours',
|
|
'cd-mins',
|
|
'cd-secs',
|
|
'modalOverlay',
|
|
'modalTitle',
|
|
'modalBody',
|
|
'modalCancel',
|
|
'modalConfirm'
|
|
];
|
|
|
|
for (const id of requiredIds) {
|
|
assert.ok(ids.includes(id), `missing #${id}`);
|
|
}
|
|
|
|
const duplicates = ids.filter((id, index) => ids.indexOf(id) !== index);
|
|
assert.deepEqual(duplicates, []);
|
|
});
|
|
|
|
test('manifest v2 defines integrations and widgets', () => {
|
|
assert.equal(manifest.version, 2);
|
|
assert.ok(manifest.integrations?.authoring);
|
|
assert.ok(Array.isArray(manifest.widgets) && manifest.widgets.length >= 3);
|
|
assert.ok(manifest.widgets.some(w => w.id === 'scripts-health'));
|
|
});
|
|
|
|
test('manifest defines unique link and build ids', () => {
|
|
const linkIds = manifest.links.map(l => l.id);
|
|
const buildIds = manifest.builds.map(b => b.id);
|
|
assert.equal(new Set(linkIds).size, linkIds.length);
|
|
assert.equal(new Set(buildIds).size, buildIds.length);
|
|
assert.ok(manifest.links.some(l => l.id === 'timesheet' && l.href === '/time-tracking/'));
|
|
assert.ok(manifest.builds.some(b => b.id === 'org_web'));
|
|
assert.ok(manifest.builds.some(b => b.id === 'emacs'));
|
|
assert.ok(manifest.builds.some(b => b.id === 'adventure_resources'));
|
|
assert.ok(manifest.builds.some(b => b.id === 'guacamole_start'));
|
|
assert.ok(manifest.builds.some(b => b.id === 'guacamole_stop'));
|
|
assert.ok(manifest.builds.some(b => b.id === 'nostalgia_prod'));
|
|
});
|
|
|
|
test('manifest build cards expose stable dom ids', () => {
|
|
for (const build of manifest.builds) {
|
|
assert.ok(build.id, `build needs id`);
|
|
if (build.id === 'org_web') {
|
|
assert.equal(build.start.path, '/api/build-web');
|
|
assert.equal(build.logs.path, '/api/build-web/logs');
|
|
assert.equal(build.lastRunKey, 'org_web');
|
|
}
|
|
if (build.id === 'emacs') {
|
|
assert.equal(build.start.path, '/api/rerun-emacs');
|
|
assert.equal(build.lastRunKey, 'emacs');
|
|
}
|
|
if (build.id === 'adventure_resources') {
|
|
assert.equal(build.start.path, '/api/adventure/resources/free');
|
|
assert.equal(build.lastRunKey, 'adventure_resources');
|
|
}
|
|
if (build.id === 'guacamole_start') {
|
|
assert.equal(build.start.path, '/api/guacamole/start');
|
|
assert.equal(build.lastRunKey, 'guacamole_start');
|
|
}
|
|
if (build.id === 'guacamole_stop') {
|
|
assert.equal(build.start.path, '/api/guacamole/stop');
|
|
assert.equal(build.lastRunKey, 'guacamole_stop');
|
|
}
|
|
if (build.id === 'nostalgia_prod') {
|
|
assert.equal(build.start.path, '/api/nostalgia/production');
|
|
assert.equal(build.lastRunKey, 'nostalgia_prod');
|
|
}
|
|
}
|
|
});
|
|
|
|
test('dashboard module references manifest-driven API endpoints', () => {
|
|
const expectedEndpoints = [
|
|
'/api/health',
|
|
'/api/uptime',
|
|
'/api/last-runs',
|
|
'/api/build-web',
|
|
'/api/build-web/logs',
|
|
'/api/build-web/status',
|
|
'/api/rerun-emacs',
|
|
'/api/rerun-emacs/logs',
|
|
'/api/rerun-emacs/status',
|
|
'/api/adventure/resources/free',
|
|
'/api/adventure/resources/free/logs',
|
|
'/api/adventure/resources/free/status',
|
|
'/api/guacamole/start',
|
|
'/api/guacamole/start/logs',
|
|
'/api/guacamole/start/status',
|
|
'/api/guacamole/stop',
|
|
'/api/guacamole/stop/logs',
|
|
'/api/guacamole/stop/status',
|
|
'/api/nostalgia/production',
|
|
'/api/nostalgia/production/logs',
|
|
'/api/nostalgia/production/status'
|
|
];
|
|
|
|
for (const endpoint of expectedEndpoints) {
|
|
assert.ok(
|
|
manifest.builds.some(b =>
|
|
b.start?.path === endpoint ||
|
|
b.status?.path === endpoint ||
|
|
b.logs?.path === endpoint
|
|
) ||
|
|
Object.values(manifest.status).includes(endpoint) ||
|
|
dashboardJs.includes(endpoint),
|
|
`missing endpoint ${endpoint}`
|
|
);
|
|
}
|
|
|
|
assert.match(dashboardJs, /new EventSource\(url\)/);
|
|
assert.match(dashboardJs, /addEventListener\(['"]log['"]/);
|
|
assert.match(dashboardJs, /\/api\/zone\/manifest/);
|
|
assert.match(dashboardJs, /data\/manifest\.json/);
|
|
assert.match(dashboardJs, /\/api\/zone\/status/);
|
|
assert.match(dashboardJs, /renderWidgets/);
|
|
assert.match(dashboardJs, /buildDomIds/);
|
|
assert.doesNotMatch(dashboardJs, /build-roam/);
|
|
assert.doesNotMatch(dashboardJs, /run-combined/);
|
|
});
|
|
|
|
test('layout classes and accessible structure are present', () => {
|
|
for (const className of ['topbar', 'dashboard-grid', 'rail', 'panel', 'run-grid']) {
|
|
assert.match(html, new RegExp(`class=["'][^"']*\\b${className}\\b`), `missing .${className}`);
|
|
}
|
|
assert.match(dashboardJs, /log-tools/);
|
|
|
|
assert.match(html, /<main class=["']page["']/);
|
|
assert.match(html, /aria-label=["']Build runs["']/);
|
|
assert.match(html, /role=["']dialog["']/);
|
|
});
|
|
|
|
test('dashboard loads external stylesheets and manifest module', () => {
|
|
assert.match(html, /href=["']css\/shared\.css["']/);
|
|
assert.match(html, /href=["']css\/dashboard\.css["']/);
|
|
assert.match(html, /type=["']module["'][^>]*src=["']js\/dashboard\.js(\?[^"']*)?["']/);
|
|
});
|
|
|
|
test('gated manifest schema has required keys', () => {
|
|
for (const key of ['version', 'dashboard', 'links', 'builds', 'status']) {
|
|
assert.ok(key in manifest, `manifest missing ${key}`);
|
|
}
|
|
assert.equal(typeof manifest.dashboard.title, 'string');
|
|
assert.ok(Array.isArray(manifest.links) && manifest.links.length > 0);
|
|
assert.ok(Array.isArray(manifest.builds) && manifest.builds.length > 0);
|
|
});
|