feat(dashboard, host): cross-repo parity — a11y, messaging, validation, tooling
CI / Build & Test (push) Has been cancelled

- Dashboard: skip link, main landmark, focus-visible, toast live region, keyboard
  shortcuts modal (?), aria-current on nav, shared state-panel CSS, theme spacing tokens
- messaging: sendToNativeResult + notifyNativeFailure; SSH/RDP use them for failures
- Host: validate setVirtualHost/removeVirtualHost/startServer/stopServer in message-router;
  log on handler errors; logWarn in logger
- SW: skipWaiting + clients.claim; register-sw controllerchange hook
- Tests: test/paths.js + env HOLESAIL_HOST_ROOT / HOLESAIL_DASH_DATA_ROOT; mirror tests in
  Holesail; data/package.json for CJS under ESM root; eslint.config.cjs + lint script
- Data: install-commands cross-repo note; extension/dashboard/data package.json (Browser)
This commit is contained in:
Raven Scott
2026-03-27 02:11:19 -04:00
parent 5867087960
commit 24f189ec5c
21 changed files with 278 additions and 28 deletions
+1
View File
@@ -73,6 +73,7 @@ async function init() {
} catch (_) {} } catch (_) {}
setupNavigation(); setupNavigation();
setupKeyboardShortcutsHelp();
setupEvents(); setupEvents();
setupCertValidator(); setupCertValidator();
setupSshEvents(); setupSshEvents();
+43
View File
@@ -13,6 +13,49 @@ function sendToNative(type, payload) {
}); });
} }
/**
* Like sendToNative but resolves with a normalized result (checks chrome.runtime.lastError).
* @returns {Promise<{ ok: true, data: object } | { ok: false, error: string }>}
*/
function sendToNativeResult(type, payload) {
return new Promise((resolve) => {
chrome.runtime.sendMessage(
{ target: 'holesail-native', action: 'send', payload: { type, payload } },
(response) => {
const lastErr = chrome.runtime.lastError;
if (lastErr) {
resolve({ ok: false, error: lastErr.message || 'Extension messaging failed' });
return;
}
if (!response) {
resolve({ ok: false, error: 'No response from extension' });
return;
}
if (response.error != null && response.ok !== true) {
resolve({ ok: false, error: String(response.error) });
return;
}
if (response.ok === false) {
resolve({ ok: false, error: response.error ? String(response.error) : 'Request failed' });
return;
}
resolve({ ok: true, data: response });
}
);
});
}
/**
* Show a toast for a failed native call (user-initiated actions).
* @param {string} [context] - Short label, e.g. "SSH"
* @param {string} [error] - Error message
*/
function notifyNativeFailure(context, error) {
const msg = error && String(error).trim() ? String(error) : 'Something went wrong';
const prefix = context && String(context).trim() ? String(context).trim() + ': ' : '';
if (typeof showToast === 'function') showToast(prefix + msg, 'error');
}
/** Fetch the full extension state from the background service worker. */ /** Fetch the full extension state from the background service worker. */
async function fetchState() { async function fetchState() {
return new Promise((resolve) => { return new Promise((resolve) => {
+32 -2
View File
@@ -25,16 +25,44 @@ const PAGE_TITLES = {
* @param {string} page - Page key (e.g. `'dashboard'`, `'connections'`, `'ssh'`). * @param {string} page - Page key (e.g. `'dashboard'`, `'connections'`, `'ssh'`).
*/ */
function navigateTo(page) { function navigateTo(page) {
document.querySelectorAll('.nav-item').forEach(i => i.classList.remove('active')); document.querySelectorAll('.nav-item').forEach(i => {
i.classList.remove('active');
i.removeAttribute('aria-current');
});
document.querySelectorAll('.page').forEach(p => p.classList.remove('active')); document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
const navItem = document.querySelector(`.nav-item[data-page="${page}"]`); const navItem = document.querySelector(`.nav-item[data-page="${page}"]`);
if (navItem) navItem.classList.add('active'); if (navItem) {
navItem.classList.add('active');
navItem.setAttribute('aria-current', 'page');
}
const pageEl = $(`page-${page}`); const pageEl = $(`page-${page}`);
if (pageEl) pageEl.classList.add('active'); if (pageEl) pageEl.classList.add('active');
const titleEl = $('topbarTitle'); const titleEl = $('topbarTitle');
if (titleEl) titleEl.textContent = PAGE_TITLES[page] || page; if (titleEl) titleEl.textContent = PAGE_TITLES[page] || page;
} }
/**
* Toggle the keyboard shortcuts dialog with ? (Shift+/ on US layouts) when focus is not in a field.
* Depends on modal.js (openModal, closeModal, topmostOpenModalBackdrop) — call after scripts load.
*/
function setupKeyboardShortcutsHelp() {
document.addEventListener('keydown', (e) => {
if (e.defaultPrevented) return;
if (e.ctrlKey || e.metaKey || e.altKey) return;
if (e.key !== '?' && !(e.shiftKey && e.key === '/')) return;
const t = e.target;
if (t && typeof t.closest === 'function' && t.closest('input, textarea, select, [contenteditable="true"]')) return;
const open = typeof topmostOpenModalBackdrop === 'function' ? topmostOpenModalBackdrop() : null;
if (open && open.id !== 'modal-keyboardShortcuts') return;
e.preventDefault();
if (open && open.id === 'modal-keyboardShortcuts') {
closeModal('modal-keyboardShortcuts');
return;
}
openModal('modal-keyboardShortcuts');
});
}
/** /**
* Attach click and keyboard listeners to all `.nav-item` elements. * Attach click and keyboard listeners to all `.nav-item` elements.
* Called once during dashboard initialisation. * Called once during dashboard initialisation.
@@ -51,4 +79,6 @@ function setupNavigation() {
} }
}); });
}); });
const initial = document.querySelector('.nav-item.active[data-page]');
if (initial) initial.setAttribute('aria-current', 'page');
} }
+80
View File
@@ -27,6 +27,9 @@
--sidebar-w: 224px; --sidebar-w: 224px;
--transition: 0.15s ease; --transition: 0.15s ease;
--terminal-bg: #0d0d0f; --terminal-bg: #0d0d0f;
/* Vertical rhythm: use for section/card gaps (theme / density tuning) */
--space-section: 16px;
--space-card-gap: 12px;
} }
[data-theme="light"] { [data-theme="light"] {
@@ -76,6 +79,40 @@
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
} }
/* Skip link (visible on focus) */
.skip-link {
position: absolute;
left: -9999px;
top: 0;
z-index: 2147483647;
padding: 10px 16px;
background: var(--elevated);
border: 1px solid var(--border2);
border-radius: var(--radius);
color: var(--text);
font-family: inherit;
font-size: 13px;
text-decoration: none;
}
.skip-link:focus {
left: 12px;
top: 12px;
outline: 2px solid var(--cyan);
outline-offset: 2px;
}
/* Focus visibility for keyboard / assistive tech */
a:focus-visible,
button:focus-visible,
.btn:focus-visible,
.nav-item:focus-visible,
input:not([type="hidden"]):focus-visible,
textarea:focus-visible,
select:focus-visible {
outline: 2px solid var(--cyan-mid);
outline-offset: 2px;
}
/* ── Scrollbar ─────────────────────────────────── */ /* ── Scrollbar ─────────────────────────────────── */
::-webkit-scrollbar { width: 6px; height: 6px; } ::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-track { background: transparent; }
@@ -628,6 +665,21 @@
transition: transform 0.2s ease; transition: transform 0.2s ease;
} }
.modal-backdrop.open .modal { transform: translateY(0) scale(1); } .modal-backdrop.open .modal { transform: translateY(0) scale(1); }
.modal.modal--narrow { max-width: 360px; }
.keyboard-shortcuts-body { padding-top: 8px; }
.keyboard-shortcuts-hint { font-size: 13px; color: var(--text2); line-height: 1.5; margin: 0; }
.keyboard-shortcuts-list { margin: 12px 0 0; padding: 0 0 0 1.2em; }
.keyboard-shortcuts-list li { margin: 8px 0; font-size: 13px; color: var(--text2); line-height: 1.45; }
.kbd {
display: inline-block;
padding: 2px 6px;
font-size: 11px;
font-family: 'JetBrains Mono', ui-monospace, monospace;
background: var(--elevated);
border: 1px solid var(--border2);
border-radius: 4px;
color: var(--text);
}
.modal-header { .modal-header {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -648,6 +700,34 @@
} }
.modal-error { font-size: 12px; color: var(--red); display: none; } .modal-error { font-size: 12px; color: var(--red); display: none; }
/* ── Dashboard state panels (empty / loading / error) ─ */
.dashboard-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
padding: 32px 20px;
text-align: center;
color: var(--text3);
font-size: 13px;
border-radius: var(--radius);
border: 1px dashed var(--border);
background: var(--elevated);
}
.dashboard-state__title { font-weight: 600; color: var(--text2); font-size: 14px; }
.dashboard-state__actions { margin-top: 8px; display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; }
.dashboard-state--error { color: var(--red); border-color: var(--red-mid); border-style: solid; }
.dashboard-spinner {
width: 22px;
height: 22px;
border: 2px solid var(--border2);
border-top-color: var(--cyan);
border-radius: 50%;
animation: dashboard-spin 0.7s linear infinite;
}
@keyframes dashboard-spin { to { transform: rotate(360deg); } }
/* ── Confirm inline ────────────────────────────── */ /* ── Confirm inline ────────────────────────────── */
.confirm-inline { .confirm-inline {
display: none; display: none;
+27 -3
View File
@@ -15,6 +15,7 @@
<script type="module" src="../vendor/novnc.js"></script> <script type="module" src="../vendor/novnc.js"></script>
</head> </head>
<body> <body>
<a href="#main-content" class="skip-link">Skip to main content</a>
<!-- Boot splash: removed by init.js after first state refresh (sync-aware messaging). --> <!-- Boot splash: removed by init.js after first state refresh (sync-aware messaging). -->
<div id="bootSplash" class="boot-splash" role="status" aria-live="polite" aria-busy="true"> <div id="bootSplash" class="boot-splash" role="status" aria-live="polite" aria-busy="true">
<div class="boot-splash__glow" aria-hidden="true"></div> <div class="boot-splash__glow" aria-hidden="true"></div>
@@ -149,7 +150,7 @@
</nav> </nav>
<!-- ── Main ─────────────────────────────────────── --> <!-- ── Main ─────────────────────────────────────── -->
<main class="main"> <main class="main" id="main-content" tabindex="-1">
<div class="topbar"> <div class="topbar">
<span class="topbar-title" id="topbarTitle">Overview</span> <span class="topbar-title" id="topbarTitle">Overview</span>
<div class="topbar-actions" id="topbarActions"></div> <div class="topbar-actions" id="topbarActions"></div>
@@ -947,8 +948,31 @@
</main> </main>
</div><!-- /layout --> </div><!-- /layout -->
<!-- ── Toast ──────────────────────────────────────────--> <!-- ── Toast (aria-live for screen reader announcements) -->
<div class="toast" id="toast"></div> <div class="toast" id="toast" role="status" aria-live="polite" aria-atomic="true"></div>
<!-- ── Keyboard shortcuts (toggle with ? outside inputs) ─ -->
<div class="modal-backdrop" id="modal-keyboardShortcuts" aria-hidden="true">
<div class="modal modal--narrow" role="dialog" aria-modal="true" aria-labelledby="modal-keyboardShortcuts-title">
<div class="modal-header">
<span class="modal-title" id="modal-keyboardShortcuts-title">Keyboard shortcuts</span>
<button type="button" class="btn-icon" data-close-modal="modal-keyboardShortcuts" aria-label="Close">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<div class="modal-body keyboard-shortcuts-body">
<p class="keyboard-shortcuts-hint">Press <kbd class="kbd">?</kbd> to open or close this panel (when not typing in a field).</p>
<ul class="keyboard-shortcuts-list">
<li><kbd class="kbd">Enter</kbd> / <kbd class="kbd">Space</kbd> Activate sidebar item</li>
<li><kbd class="kbd">Escape</kbd> Close topmost dialog</li>
<li><kbd class="kbd">Tab</kbd> Move focus inside dialogs</li>
</ul>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-close-modal="modal-keyboardShortcuts">Close</button>
</div>
</div>
</div>
<!-- ── Modal: Add Virtual Host ───────────────────────── --> <!-- ── Modal: Add Virtual Host ───────────────────────── -->
<div class="modal-backdrop" id="modal-addVhost"> <div class="modal-backdrop" id="modal-addVhost">
@@ -1,6 +1,7 @@
/** /**
* One-liner install commands for the native host, by platform. * One-liner install commands for the native host, by platform.
* Used by the onboarding card when the native host is not found. * Used by the onboarding card when the native host is not found.
* Mirrored in Holesail (`renderer/dashboard/data/install-commands.js`).
* Testable from Node (see test/install-commands.test.js). * Testable from Node (see test/install-commands.test.js).
*/ */
+4
View File
@@ -0,0 +1,4 @@
{
"private": true,
"type": "commonjs"
}
+6 -5
View File
@@ -1,7 +1,7 @@
/** /**
* Remote Desktop page — connection grid, add/edit modal, VNC viewer (noVNC over WebSocket), * Remote Desktop page — connection grid, add/edit modal, VNC viewer (noVNC over WebSocket),
* and RDP bitmap viewer (node-rdpjs-2 over WebSocket with offscreen canvas rendering). * and RDP bitmap viewer (node-rdpjs-2 over WebSocket with offscreen canvas rendering).
* Depends on: core/utils.js ($, escapeHtml, log), core/messaging.js (sendToNative), * Depends on: core/utils.js ($, escapeHtml, log), core/messaging.js (sendToNative, sendToNativeResult, notifyNativeFailure),
* ui/toast.js (showToast), ui/modal.js (openModal, closeModal, showModalError) * ui/toast.js (showToast), ui/modal.js (openModal, closeModal, showModalError)
*/ */
@@ -395,7 +395,7 @@ async function connectRdp(conn) {
await disconnectRdp(); await disconnectRdp();
const result = await sendToNative('startRdpSession', { const r = await sendToNativeResult('startRdpSession', {
type: conn.type, type: conn.type,
hsUrl: conn.hsUrl, hsUrl: conn.hsUrl,
port: conn.port, port: conn.port,
@@ -407,14 +407,15 @@ async function connectRdp(conn) {
label: conn.label || '' label: conn.label || ''
}); });
if (!result || !result.ok) { if (!r.ok) {
notifyNativeFailure('Remote desktop', r.error);
$('rdpStatusDot').className = 'terminal-status-dot disconnected'; $('rdpStatusDot').className = 'terminal-status-dot disconnected';
$('rdpStateDisplay').textContent = 'Failed: ' + ((result && result.error) || 'Unknown error'); $('rdpStateDisplay').textContent = 'Failed: ' + (r.error || 'Unknown error');
$('rdpStateDisplay').style.color = 'var(--red)'; $('rdpStateDisplay').style.color = 'var(--red)';
return; return;
} }
const { sessionId, wsPort } = result; const { sessionId, wsPort } = r.data;
let viewer = null; let viewer = null;
if (conn.type === 'vnc') { if (conn.type === 'vnc') {
+6 -5
View File
@@ -3,7 +3,7 @@
* Session lifecycle: start Holesail tunnel open WebSocket spawn SSH via PTY on native host * Session lifecycle: start Holesail tunnel open WebSocket spawn SSH via PTY on native host
* bridge PTY WebSocket xterm.js in the browser. * bridge PTY WebSocket xterm.js in the browser.
* Handles auto-reconnect with exponential backoff and PTY resize via ResizeObserver. * Handles auto-reconnect with exponential backoff and PTY resize via ResizeObserver.
* Depends on: core/utils.js ($, escapeHtml, log), core/messaging.js (sendToNative), * Depends on: core/utils.js ($, escapeHtml, log), core/messaging.js (sendToNative, sendToNativeResult, notifyNativeFailure),
* ui/toast.js (showToast, copyToClipboard), ui/modal.js (openModal, closeModal, showModalError) * ui/toast.js (showToast, copyToClipboard), ui/modal.js (openModal, closeModal, showModalError)
*/ */
@@ -260,7 +260,7 @@ async function _connectSshImpl(conn) {
const cols = term.cols || 80; const cols = term.cols || 80;
const rows = term.rows || 24; const rows = term.rows || 24;
const result = await sendToNative('startSshSession', { const r = await sendToNativeResult('startSshSession', {
hsUrl: conn.hsUrl, hsUrl: conn.hsUrl,
username: conn.username, username: conn.username,
password: conn.password || '', password: conn.password || '',
@@ -269,8 +269,9 @@ async function _connectSshImpl(conn) {
label: conn.label || conn.username label: conn.label || conn.username
}); });
if (!result || !result.ok) { if (!r.ok) {
const errMsg = (result && result.error) || 'Unknown error'; notifyNativeFailure('SSH', r.error);
const errMsg = r.error || 'Unknown error';
term.writeln('\x1b[31mFailed to start session: ' + errMsg + '\x1b[0m'); term.writeln('\x1b[31mFailed to start session: ' + errMsg + '\x1b[0m');
$('termStatusDot').className = 'terminal-status-dot disconnected'; $('termStatusDot').className = 'terminal-status-dot disconnected';
$('termStateDisplay').textContent = 'Error'; $('termStateDisplay').textContent = 'Error';
@@ -279,7 +280,7 @@ async function _connectSshImpl(conn) {
return; return;
} }
const { sessionId, wsPort } = result; const { sessionId, wsPort } = r.data;
term.writeln('\x1b[90mTunnel ready — connecting SSH…\x1b[0m'); term.writeln('\x1b[90mTunnel ready — connecting SSH…\x1b[0m');
let ws; let ws;
+3
View File
@@ -1,4 +1,7 @@
// Registers the PWA service worker required for installability. // Registers the PWA service worker required for installability.
if ('serviceWorker' in navigator) { if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('sw.js').catch(() => {}); navigator.serviceWorker.register('sw.js').catch(() => {});
navigator.serviceWorker.addEventListener('controllerchange', () => {
// New worker took control after reload; optional hook for future UX.
});
} }
+10 -3
View File
@@ -1,3 +1,10 @@
// Minimal service worker required for PWA installability. // Minimal service worker for PWA installability.
// No fetch handler needed — the extension bundle is already local // skipWaiting + clients.claim so a new SW activates when the dashboard tab reloads.
// and Chrome does not require one for the install prompt since v112.
self.addEventListener('install', (event) => {
event.waitUntil(self.skipWaiting());
});
self.addEventListener('activate', (event) => {
event.waitUntil(self.clients.claim());
});
+3
View File
@@ -16,6 +16,7 @@ function openModal(id) {
if (!el) return; if (!el) return;
_modalPreviousFocus = document.activeElement; _modalPreviousFocus = document.activeElement;
el.classList.add('open'); el.classList.add('open');
el.setAttribute('aria-hidden', 'false');
el.setAttribute('aria-modal', 'true'); el.setAttribute('aria-modal', 'true');
el.setAttribute('role', 'dialog'); el.setAttribute('role', 'dialog');
const modalTitle = el.querySelector('.modal-title'); const modalTitle = el.querySelector('.modal-title');
@@ -40,6 +41,7 @@ function closeModal(id) {
const el = $(id); const el = $(id);
if (!el) return; if (!el) return;
el.classList.remove('open'); el.classList.remove('open');
el.setAttribute('aria-hidden', 'true');
el.removeAttribute('aria-modal'); el.removeAttribute('aria-modal');
el.removeAttribute('role'); el.removeAttribute('role');
el.removeAttribute('aria-labelledby'); el.removeAttribute('aria-labelledby');
@@ -61,6 +63,7 @@ function showModalError(modalId, errorId, msg) {
if (!el) return; if (!el) return;
el.textContent = msg; el.textContent = msg;
el.style.display = 'block'; el.style.display = 'block';
el.setAttribute('role', 'alert');
} }
// Close modals on backdrop click or close button // Close modals on backdrop click or close button
+9 -1
View File
@@ -44,4 +44,12 @@ function debugLog(...args) {
if (stream) stream.write('[' + new Date().toISOString() + '] ' + msg + '\n'); if (stream) stream.write('[' + new Date().toISOString() + '] ' + msg + '\n');
} }
module.exports = { log, debugLog }; /** Non-fatal issues (also written to log file when available). */
function logWarn(...args) {
const msg = '[host:warn] ' + args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ');
if (process.stderr) process.stderr.write(msg + '\n');
const stream = getLogStream();
if (stream) stream.write('[' + new Date().toISOString() + '] ' + msg + '\n');
}
module.exports = { log, debugLog, logWarn };
+25 -1
View File
@@ -7,6 +7,19 @@
const { STORAGE_PATH } = require('./paths.js'); const { STORAGE_PATH } = require('./paths.js');
const { log, debugLog } = require('./logger.js'); const { log, debugLog } = require('./logger.js');
const {
validateSetVirtualHost,
validateRemoveVirtualHost,
validateStartServer,
validateStopServer
} = require('./payload-schemas.js');
const PAYLOAD_VALIDATORS = new Map([
['setVirtualHost', validateSetVirtualHost],
['removeVirtualHost', validateRemoveVirtualHost],
['startServer', validateStartServer],
['stopServer', validateStopServer]
]);
const { initStartup, getProxiesReadyPromise, getTunnelsRestoredPromise, setTunnelsRestoredPromise, restorePersistedTunnels } = require('./startup.js'); const { initStartup, getProxiesReadyPromise, getTunnelsRestoredPromise, setTunnelsRestoredPromise, restorePersistedTunnels } = require('./startup.js');
const { buildHandlers } = require('./handlers/index.js'); const { buildHandlers } = require('./handlers/index.js');
@@ -112,12 +125,23 @@ async function handleMessageAsync(send, msg) {
const handle = _handlers.get(type); const handle = _handlers.get(type);
if (handle) { if (handle) {
const validator = PAYLOAD_VALIDATORS.get(type);
if (validator) {
const v = validator(payload);
if (!v.ok) {
reply({ ok: false, error: v.error });
} else {
await handle(payload, reply); await handle(payload, reply);
}
} else {
await handle(payload, reply);
}
} else { } else {
reply({ ok: false, error: `Unknown command: ${type}` }); reply({ ok: false, error: `Unknown command: ${type}` });
} }
} catch (err) { } catch (err) {
reply({ ok: false, error: err.message }); log('handleMessage error:', type, err && err.message);
reply({ ok: false, error: err.message || String(err) });
if (process.stderr) { if (process.stderr) {
process.stderr.write(`[holesail-browser-host] ${err.stack}\n`); process.stderr.write(`[holesail-browser-host] ${err.stack}\n`);
} }
+2 -1
View File
@@ -3,8 +3,9 @@
* Run with: node --test test/hostname-validator.test.js * Run with: node --test test/hostname-validator.test.js
*/ */
const path = require('path'); const path = require('path');
const { DASH_DATA_ROOT } = require('./paths.js');
require(path.join(__dirname, 'setup-validator.js')); require(path.join(__dirname, 'setup-validator.js'));
const { isValidVhostHostname, extractBaseDomain, extractActiveTlds } = require(path.join(__dirname, '../extension/dashboard/data/hostname-validator.js')); const { isValidVhostHostname, extractBaseDomain, extractActiveTlds } = require(path.join(DASH_DATA_ROOT, 'hostname-validator.js'));
const { test, describe } = require('node:test'); const { test, describe } = require('node:test');
const assert = require('node:assert'); const assert = require('node:assert');
+2 -1
View File
@@ -3,7 +3,8 @@
* Run with: node --test test/install-commands.test.js * Run with: node --test test/install-commands.test.js
*/ */
const path = require('path'); const path = require('path');
const { getInstallCommand, INSTALL_SCRIPT_BASE_URL } = require(path.join(__dirname, '../extension/dashboard/data/install-commands.js')); const { DASH_DATA_ROOT } = require('./paths.js');
const { getInstallCommand, INSTALL_SCRIPT_BASE_URL } = require(path.join(DASH_DATA_ROOT, 'install-commands.js'));
const { test, describe } = require('node:test'); const { test, describe } = require('node:test');
const assert = require('node:assert'); const assert = require('node:assert');
+3 -2
View File
@@ -3,13 +3,14 @@
* Uses mocked dependencies so we don't need the full Bare runtime or real managers. * Uses mocked dependencies so we don't need the full Bare runtime or real managers.
* Run with: node --test test/message-router.test.js * Run with: node --test test/message-router.test.js
* *
* Note: This test loads native-host/host/handlers/index.js and the handler modules. * Note: This test loads host/host/handlers/index.js (see test/paths.js) and the handler modules.
* It does NOT load message-router.js or the real managers (holesail-manager, etc.), * It does NOT load message-router.js or the real managers (holesail-manager, etc.),
* so we test the handler registry and handler logic with mocks. * so we test the handler registry and handler logic with mocks.
*/ */
const path = require('path'); const path = require('path');
const { buildHandlers } = require(path.join(__dirname, '../native-host/host/handlers/index.js')); const { HOST_ROOT } = require('./paths.js');
const { buildHandlers } = require(path.join(HOST_ROOT, 'host/handlers/index.js'));
const { test, describe } = require('node:test'); const { test, describe } = require('node:test');
const assert = require('node:assert'); const assert = require('node:assert');
+14
View File
@@ -0,0 +1,14 @@
/**
* Resolves native host and dashboard data roots for tests.
* Holesail (Pear) sets HOLESAIL_HOST_ROOT=host and HOLESAIL_DASH_DATA_ROOT=renderer/dashboard/data.
*/
const path = require('path');
const HOST_ROOT = path.join(__dirname, '..', process.env.HOLESAIL_HOST_ROOT || 'native-host');
const DASH_DATA_ROOT = path.join(
__dirname,
'..',
process.env.HOLESAIL_DASH_DATA_ROOT || 'extension/dashboard/data'
);
module.exports = { HOST_ROOT, DASH_DATA_ROOT };
+2 -1
View File
@@ -3,12 +3,13 @@
* Run with: node --test test/payload-schemas.test.js * Run with: node --test test/payload-schemas.test.js
*/ */
const path = require('path'); const path = require('path');
const { HOST_ROOT } = require('./paths.js');
const { const {
validateSetVirtualHost, validateSetVirtualHost,
validateRemoveVirtualHost, validateRemoveVirtualHost,
validateStartServer, validateStartServer,
validateStopServer validateStopServer
} = require(path.join(__dirname, '../native-host/host/payload-schemas.js')); } = require(path.join(HOST_ROOT, 'host/payload-schemas.js'));
const { test, describe } = require('node:test'); const { test, describe } = require('node:test');
const assert = require('node:assert'); const assert = require('node:assert');
+2 -1
View File
@@ -3,6 +3,7 @@
* Must be required before hostname-validator so REAL_TLDS/REAL_SLD_TLDS are set. * Must be required before hostname-validator so REAL_TLDS/REAL_SLD_TLDS are set.
*/ */
const path = require('path'); const path = require('path');
const tlds = require(path.join(__dirname, '../extension/dashboard/data/tlds.js')); const { DASH_DATA_ROOT } = require('./paths.js');
const tlds = require(path.join(DASH_DATA_ROOT, 'tlds.js'));
global.REAL_TLDS = tlds.REAL_TLDS; global.REAL_TLDS = tlds.REAL_TLDS;
global.REAL_SLD_TLDS = tlds.REAL_SLD_TLDS; global.REAL_SLD_TLDS = tlds.REAL_SLD_TLDS;
+2 -1
View File
@@ -3,7 +3,8 @@
* Run with: node --test test/tlds.test.js * Run with: node --test test/tlds.test.js
*/ */
const path = require('path'); const path = require('path');
const { REAL_TLDS, REAL_SLD_TLDS } = require(path.join(__dirname, '../extension/dashboard/data/tlds.js')); const { DASH_DATA_ROOT } = require('./paths.js');
const { REAL_TLDS, REAL_SLD_TLDS } = require(path.join(DASH_DATA_ROOT, 'tlds.js'));
const { test, describe } = require('node:test'); const { test, describe } = require('node:test');
const assert = require('node:assert'); const assert = require('node:assert');