fix: terminal.js fontSize regression and stale test references
Issue 1 (CRITICAL): terminal.js createTerminal() read fontSize from
localStorage.getItem('muxplex.display'), which always returns null after
the server-settings migration. This caused terminal font size to permanently
default to 14 regardless of the server-side fontSize setting.
Fix: Pass getDisplaySettings().fontSize from app.js to window._openTerminal
as a third argument. Update openTerminal(name, remoteId, fontSize) and
createTerminal(fontSize) to use the parameter, removing all localStorage
reads from terminal.js.
Issue 2: Fix 9 stale test references in test_app.mjs that set
_localStorageStore['muxplex.display'] instead of app._setServerSettings().
Tests now correctly exercise the server-settings path.
Issue 3: Fix stale initSidebar test that used
delete _localStorageStore['muxplex.sidebarOpen'] — replaced with
app._setServerSettings(null) since initSidebar reads from _serverSettings.
Also update 5 Python tests in test_frontend_js.py that were checking the
old localStorage-based createTerminal() behavior:
- Renamed test_create_terminal_reads_font_size_from_localstorage to
test_create_terminal_accepts_font_size_parameter
- Renamed test_create_terminal_parses_json_for_font_size to
test_create_terminal_does_not_parse_json_from_localstorage
- Updated 3 remaining tests to use regex that matches createTerminal(fontSize)
instead of createTerminal()
Verification:
- grep -rn 'localStorage' app.js: only tmux-web-device-id (3 lines)
- grep -rn 'muxplex\.display|muxplex\.sidebarOpen' frontend/: zero matches
- All 372 JS tests pass (44 terminal + 328 app)
- All 199 Python frontend_js tests pass
This commit is contained in:
@@ -1234,7 +1234,7 @@ async function openSession(name, opts = {}) {
|
|||||||
await animDone;
|
await animDone;
|
||||||
|
|
||||||
// Mount terminal NOW — /connect has completed, new ttyd is serving the correct session
|
// Mount terminal NOW — /connect has completed, new ttyd is serving the correct session
|
||||||
if (window._openTerminal) window._openTerminal(name, _remoteId);
|
if (window._openTerminal) window._openTerminal(name, _remoteId, getDisplaySettings().fontSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -224,8 +224,9 @@ function initVisualViewport() {
|
|||||||
* Create (or recreate) the xterm.js Terminal and FitAddon instances.
|
* Create (or recreate) the xterm.js Terminal and FitAddon instances.
|
||||||
* Disposes any existing terminal first.
|
* Disposes any existing terminal first.
|
||||||
* Stores the results in module-level _term and _fitAddon.
|
* Stores the results in module-level _term and _fitAddon.
|
||||||
|
* @param {number} [fontSize=14] - font size in pixels, from server display settings
|
||||||
*/
|
*/
|
||||||
function createTerminal() {
|
function createTerminal(fontSize) {
|
||||||
// Dispose any existing instance
|
// Dispose any existing instance
|
||||||
if (_term) {
|
if (_term) {
|
||||||
_term.dispose();
|
_term.dispose();
|
||||||
@@ -233,22 +234,15 @@ function createTerminal() {
|
|||||||
_fitAddon = null;
|
_fitAddon = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read font size from display settings (localStorage key 'muxplex.display')
|
// Use the fontSize passed from app.js (getDisplaySettings().fontSize), defaulting to 14.
|
||||||
var storedFontSize = 14;
|
var storedFontSize = (typeof fontSize === 'number' && fontSize > 0) ? fontSize : 14;
|
||||||
try {
|
|
||||||
var raw = localStorage.getItem('muxplex.display');
|
|
||||||
if (raw) {
|
|
||||||
var parsed = JSON.parse(raw);
|
|
||||||
if (parsed && parsed.fontSize) storedFontSize = parsed.fontSize;
|
|
||||||
}
|
|
||||||
} catch (_) { /* use default 14 */ }
|
|
||||||
|
|
||||||
const mobile = window.innerWidth < 600; // matches MOBILE_THRESHOLD in app.js
|
const mobile = window.innerWidth < 600; // matches MOBILE_THRESHOLD in app.js
|
||||||
const fontSize = mobile ? Math.min(storedFontSize, 12) : storedFontSize;
|
const effectiveFontSize = mobile ? Math.min(storedFontSize, 12) : storedFontSize;
|
||||||
|
|
||||||
_term = new window.Terminal({
|
_term = new window.Terminal({
|
||||||
cursorBlink: true,
|
cursorBlink: true,
|
||||||
fontSize: fontSize,
|
fontSize: effectiveFontSize,
|
||||||
fontFamily: "'SF Mono', 'Fira Code', Consolas, monospace",
|
fontFamily: "'SF Mono', 'Fira Code', Consolas, monospace",
|
||||||
theme: {
|
theme: {
|
||||||
background: '#000000',
|
background: '#000000',
|
||||||
@@ -333,7 +327,7 @@ function _searchPrev() {
|
|||||||
* When provided, the WebSocket connects via the federation proxy path
|
* When provided, the WebSocket connects via the federation proxy path
|
||||||
* ws://host/federation/{remoteId}/terminal/ws (same origin, no cross-origin).
|
* ws://host/federation/{remoteId}/terminal/ws (same origin, no cross-origin).
|
||||||
*/
|
*/
|
||||||
function openTerminal(sessionName, remoteId) {
|
function openTerminal(sessionName, remoteId, fontSize) {
|
||||||
// Null _currentSession first so any in-flight close handler on the old WS won't
|
// Null _currentSession first so any in-flight close handler on the old WS won't
|
||||||
// schedule a reconnect (it checks `if (!_currentSession) return;`).
|
// schedule a reconnect (it checks `if (!_currentSession) return;`).
|
||||||
_currentSession = null;
|
_currentSession = null;
|
||||||
@@ -359,7 +353,7 @@ function openTerminal(sessionName, remoteId) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
createTerminal();
|
createTerminal(fontSize);
|
||||||
|
|
||||||
_term.open(container);
|
_term.open(container);
|
||||||
|
|
||||||
|
|||||||
@@ -1217,6 +1217,32 @@ test('openSession with remoteId passes remoteId to window._openTerminal', async
|
|||||||
globalThis.setTimeout = origSetTimeout;
|
globalThis.setTimeout = origSetTimeout;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('openSession passes getDisplaySettings().fontSize to window._openTerminal as third argument', async () => {
|
||||||
|
// Verify openSession passes getDisplaySettings().fontSize to _openTerminal as third argument.
|
||||||
|
let openTerminalArgs = null;
|
||||||
|
const origFetch = globalThis.fetch;
|
||||||
|
const origGetById = globalThis.document.getElementById;
|
||||||
|
const origQS = globalThis.document.querySelector;
|
||||||
|
const origSetTimeout = globalThis.setTimeout;
|
||||||
|
globalThis.fetch = async () => ({ ok: true });
|
||||||
|
globalThis.document.getElementById = () => ({ textContent: '', style: {}, classList: { remove: () => {}, add: () => {} } });
|
||||||
|
globalThis.document.querySelector = () => null;
|
||||||
|
globalThis.setTimeout = (fn) => { fn(); };
|
||||||
|
globalThis.window._openTerminal = (...args) => { openTerminalArgs = args; };
|
||||||
|
|
||||||
|
app._setServerSettings({ fontSize: 18 });
|
||||||
|
await app.openSession('my-session', { skipAnimation: true });
|
||||||
|
app._setServerSettings(null);
|
||||||
|
|
||||||
|
assert.ok(openTerminalArgs !== null, '_openTerminal should have been called');
|
||||||
|
assert.strictEqual(openTerminalArgs[2], 18,
|
||||||
|
'_openTerminal third arg should be fontSize from getDisplaySettings()');
|
||||||
|
globalThis.fetch = origFetch;
|
||||||
|
globalThis.document.getElementById = origGetById;
|
||||||
|
globalThis.document.querySelector = origQS;
|
||||||
|
globalThis.setTimeout = origSetTimeout;
|
||||||
|
});
|
||||||
|
|
||||||
test('openSession for local session still POSTs to local /api/sessions/{name}/connect', async () => {
|
test('openSession for local session still POSTs to local /api/sessions/{name}/connect', async () => {
|
||||||
const fetchCalls = [];
|
const fetchCalls = [];
|
||||||
const origFetch = globalThis.fetch;
|
const origFetch = globalThis.fetch;
|
||||||
@@ -1746,7 +1772,7 @@ test('renderSidebar does nothing when view is not fullscreen', () => {
|
|||||||
// ─── initSidebar ─────────────────────────────────────────────────────────────
|
// ─── initSidebar ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
test('initSidebar defaults to open (removes sidebar--collapsed) on wide screens when no stored value', () => {
|
test('initSidebar defaults to open (removes sidebar--collapsed) on wide screens when no stored value', () => {
|
||||||
delete _localStorageStore['muxplex.sidebarOpen'];
|
app._setServerSettings(null); // ensure no stored sidebarOpen value in server settings
|
||||||
const origInnerWidth = globalThis.window.innerWidth;
|
const origInnerWidth = globalThis.window.innerWidth;
|
||||||
globalThis.window.innerWidth = 1200;
|
globalThis.window.innerWidth = 1200;
|
||||||
|
|
||||||
@@ -3567,12 +3593,12 @@ test('buildTileHTML shows session-tile--edge-bell class when activityIndicator i
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('buildTileHTML shows both session-tile--bell and session-tile--edge-bell when activityIndicator is both (legacy test updated)', () => {
|
test('buildTileHTML shows both session-tile--bell and session-tile--edge-bell when activityIndicator is both (legacy test updated)', () => {
|
||||||
_localStorageStore['muxplex.display'] = JSON.stringify({ activityIndicator: 'both' });
|
app._setServerSettings({ activityIndicator: 'both' });
|
||||||
const session = { name: 's', bell: { unseen_count: 1, seen_at: null, last_fired_at: 100 }, snapshot: '' };
|
const session = { name: 's', bell: { unseen_count: 1, seen_at: null, last_fired_at: 100 }, snapshot: '' };
|
||||||
const html = app.buildTileHTML(session, 0, false);
|
const html = app.buildTileHTML(session, 0, false);
|
||||||
assert.ok(html.includes('session-tile--bell'), 'session-tile--bell must appear when activityIndicator is both');
|
assert.ok(html.includes('session-tile--bell'), 'session-tile--bell must appear when activityIndicator is both');
|
||||||
assert.ok(html.includes('session-tile--edge-bell'), 'session-tile--edge-bell must appear when activityIndicator is both');
|
assert.ok(html.includes('session-tile--edge-bell'), 'session-tile--edge-bell must appear when activityIndicator is both');
|
||||||
_localStorageStore = {};
|
app._setServerSettings(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('buildTileHTML omits all bell indicator classes when activityIndicator is none', () => {
|
test('buildTileHTML omits all bell indicator classes when activityIndicator is none', () => {
|
||||||
@@ -3594,19 +3620,19 @@ test('buildTileHTML omits session-tile--edge-bell when activityIndicator is glow
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('buildTileHTML adds session-tile--bell when activityIndicator is glow', () => {
|
test('buildTileHTML adds session-tile--bell when activityIndicator is glow', () => {
|
||||||
_localStorageStore['muxplex.display'] = JSON.stringify({ activityIndicator: 'glow' });
|
app._setServerSettings({ activityIndicator: 'glow' });
|
||||||
const session = { name: 's', bell: { unseen_count: 1, seen_at: null, last_fired_at: 100 }, snapshot: '' };
|
const session = { name: 's', bell: { unseen_count: 1, seen_at: null, last_fired_at: 100 }, snapshot: '' };
|
||||||
const html = app.buildTileHTML(session, 0, false);
|
const html = app.buildTileHTML(session, 0, false);
|
||||||
assert.ok(html.includes('session-tile--bell'), 'session-tile--bell must appear when activityIndicator is glow');
|
assert.ok(html.includes('session-tile--bell'), 'session-tile--bell must appear when activityIndicator is glow');
|
||||||
_localStorageStore = {};
|
app._setServerSettings(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('buildTileHTML adds session-tile--bell when activityIndicator is both', () => {
|
test('buildTileHTML adds session-tile--bell when activityIndicator is both', () => {
|
||||||
_localStorageStore['muxplex.display'] = JSON.stringify({ activityIndicator: 'both' });
|
app._setServerSettings({ activityIndicator: 'both' });
|
||||||
const session = { name: 's', bell: { unseen_count: 1, seen_at: null, last_fired_at: 100 }, snapshot: '' };
|
const session = { name: 's', bell: { unseen_count: 1, seen_at: null, last_fired_at: 100 }, snapshot: '' };
|
||||||
const html = app.buildTileHTML(session, 0, false);
|
const html = app.buildTileHTML(session, 0, false);
|
||||||
assert.ok(html.includes('session-tile--bell'), 'session-tile--bell must appear when activityIndicator is both');
|
assert.ok(html.includes('session-tile--bell'), 'session-tile--bell must appear when activityIndicator is both');
|
||||||
_localStorageStore = {};
|
app._setServerSettings(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('buildTileHTML omits session-tile--bell when activityIndicator is none', () => {
|
test('buildTileHTML omits session-tile--bell when activityIndicator is none', () => {
|
||||||
@@ -3700,27 +3726,27 @@ test('api() is same-origin only (no baseUrl parameter support)', async () => {
|
|||||||
// ─── Edge-bar design: failing tests added before implementation ───
|
// ─── Edge-bar design: failing tests added before implementation ───
|
||||||
|
|
||||||
test('buildTileHTML does NOT include tile-bell-dot in HTML (edge bar replaces dot)', () => {
|
test('buildTileHTML does NOT include tile-bell-dot in HTML (edge bar replaces dot)', () => {
|
||||||
_localStorageStore['muxplex.display'] = JSON.stringify({ activityIndicator: 'both' });
|
app._setServerSettings({ activityIndicator: 'both' });
|
||||||
const session = { name: 's', bell: { unseen_count: 1, seen_at: null, last_fired_at: 100 }, snapshot: '' };
|
const session = { name: 's', bell: { unseen_count: 1, seen_at: null, last_fired_at: 100 }, snapshot: '' };
|
||||||
const html = app.buildTileHTML(session, 0, false);
|
const html = app.buildTileHTML(session, 0, false);
|
||||||
assert.ok(!html.includes('tile-bell-dot'), 'tile-bell-dot must NOT appear in HTML — edge bar replaces it');
|
assert.ok(!html.includes('tile-bell-dot'), 'tile-bell-dot must NOT appear in HTML — edge bar replaces it');
|
||||||
_localStorageStore = {};
|
app._setServerSettings(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('buildTileHTML adds session-tile--edge-bell class when activityIndicator is dot', () => {
|
test('buildTileHTML adds session-tile--edge-bell class when activityIndicator is dot', () => {
|
||||||
_localStorageStore['muxplex.display'] = JSON.stringify({ activityIndicator: 'dot' });
|
app._setServerSettings({ activityIndicator: 'dot' });
|
||||||
const session = { name: 's', bell: { unseen_count: 1, seen_at: null, last_fired_at: 100 }, snapshot: '' };
|
const session = { name: 's', bell: { unseen_count: 1, seen_at: null, last_fired_at: 100 }, snapshot: '' };
|
||||||
const html = app.buildTileHTML(session, 0, false);
|
const html = app.buildTileHTML(session, 0, false);
|
||||||
assert.ok(html.includes('session-tile--edge-bell'), 'session-tile--edge-bell must appear when activityIndicator is dot');
|
assert.ok(html.includes('session-tile--edge-bell'), 'session-tile--edge-bell must appear when activityIndicator is dot');
|
||||||
_localStorageStore = {};
|
app._setServerSettings(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('buildTileHTML adds session-tile--edge-bell class when activityIndicator is both', () => {
|
test('buildTileHTML adds session-tile--edge-bell class when activityIndicator is both', () => {
|
||||||
_localStorageStore['muxplex.display'] = JSON.stringify({ activityIndicator: 'both' });
|
app._setServerSettings({ activityIndicator: 'both' });
|
||||||
const session = { name: 's', bell: { unseen_count: 1, seen_at: null, last_fired_at: 100 }, snapshot: '' };
|
const session = { name: 's', bell: { unseen_count: 1, seen_at: null, last_fired_at: 100 }, snapshot: '' };
|
||||||
const html = app.buildTileHTML(session, 0, false);
|
const html = app.buildTileHTML(session, 0, false);
|
||||||
assert.ok(html.includes('session-tile--edge-bell'), 'session-tile--edge-bell must appear when activityIndicator is both');
|
assert.ok(html.includes('session-tile--edge-bell'), 'session-tile--edge-bell must appear when activityIndicator is both');
|
||||||
_localStorageStore = {};
|
app._setServerSettings(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('buildTileHTML does NOT add session-tile--edge-bell when activityIndicator is glow', () => {
|
test('buildTileHTML does NOT add session-tile--edge-bell when activityIndicator is glow', () => {
|
||||||
@@ -3760,19 +3786,19 @@ test('buildSidebarHTML does not have sidebar-item-meta element', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('buildSidebarHTML adds sidebar-item--edge-bell when activityIndicator is dot', () => {
|
test('buildSidebarHTML adds sidebar-item--edge-bell when activityIndicator is dot', () => {
|
||||||
_localStorageStore['muxplex.display'] = JSON.stringify({ activityIndicator: 'dot' });
|
app._setServerSettings({ activityIndicator: 'dot' });
|
||||||
const session = { name: 's', snapshot: '', bell: { unseen_count: 2 } };
|
const session = { name: 's', snapshot: '', bell: { unseen_count: 2 } };
|
||||||
const html = app.buildSidebarHTML(session, '');
|
const html = app.buildSidebarHTML(session, '');
|
||||||
assert.ok(html.includes('sidebar-item--edge-bell'), 'sidebar-item--edge-bell must appear when activityIndicator is dot');
|
assert.ok(html.includes('sidebar-item--edge-bell'), 'sidebar-item--edge-bell must appear when activityIndicator is dot');
|
||||||
_localStorageStore = {};
|
app._setServerSettings(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('buildSidebarHTML adds sidebar-item--edge-bell when activityIndicator is both', () => {
|
test('buildSidebarHTML adds sidebar-item--edge-bell when activityIndicator is both', () => {
|
||||||
_localStorageStore['muxplex.display'] = JSON.stringify({ activityIndicator: 'both' });
|
app._setServerSettings({ activityIndicator: 'both' });
|
||||||
const session = { name: 's', snapshot: '', bell: { unseen_count: 2 } };
|
const session = { name: 's', snapshot: '', bell: { unseen_count: 2 } };
|
||||||
const html = app.buildSidebarHTML(session, '');
|
const html = app.buildSidebarHTML(session, '');
|
||||||
assert.ok(html.includes('sidebar-item--edge-bell'), 'sidebar-item--edge-bell must appear when activityIndicator is both');
|
assert.ok(html.includes('sidebar-item--edge-bell'), 'sidebar-item--edge-bell must appear when activityIndicator is both');
|
||||||
_localStorageStore = {};
|
app._setServerSettings(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('buildSidebarHTML does NOT add sidebar-item--edge-bell when activityIndicator is glow', () => {
|
test('buildSidebarHTML does NOT add sidebar-item--edge-bell when activityIndicator is glow', () => {
|
||||||
@@ -3784,11 +3810,11 @@ test('buildSidebarHTML does NOT add sidebar-item--edge-bell when activityIndicat
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('buildSidebarHTML does NOT include tile-bell-dot in HTML', () => {
|
test('buildSidebarHTML does NOT include tile-bell-dot in HTML', () => {
|
||||||
_localStorageStore['muxplex.display'] = JSON.stringify({ activityIndicator: 'both' });
|
app._setServerSettings({ activityIndicator: 'both' });
|
||||||
const session = { name: 's', snapshot: '', bell: { unseen_count: 2 } };
|
const session = { name: 's', snapshot: '', bell: { unseen_count: 2 } };
|
||||||
const html = app.buildSidebarHTML(session, '');
|
const html = app.buildSidebarHTML(session, '');
|
||||||
assert.ok(!html.includes('tile-bell-dot'), 'tile-bell-dot must NOT appear in sidebar HTML — edge bar replaces it');
|
assert.ok(!html.includes('tile-bell-dot'), 'tile-bell-dot must NOT appear in sidebar HTML — edge bar replaces it');
|
||||||
_localStorageStore = {};
|
app._setServerSettings(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('CSS style.css has .session-tile--edge-bell rule', () => {
|
test('CSS style.css has .session-tile--edge-bell rule', () => {
|
||||||
@@ -3965,9 +3991,15 @@ test('remote instance debounced input listener selector includes .settings-remot
|
|||||||
|
|
||||||
test('killSession closes active session and returns to dashboard', () => {
|
test('killSession closes active session and returns to dashboard', () => {
|
||||||
const source = fs.readFileSync(new URL('../app.js', import.meta.url), 'utf8');
|
const source = fs.readFileSync(new URL('../app.js', import.meta.url), 'utf8');
|
||||||
// Find killSession function body (use 600 chars to ensure closeSession call is included)
|
// Find killSession function body using brace-counting extraction
|
||||||
const fnStart = source.indexOf('function killSession');
|
const fnStart = source.indexOf('function killSession');
|
||||||
const fnBody = source.substring(fnStart, fnStart + 600);
|
const afterStart = source.indexOf('{', fnStart);
|
||||||
|
let depth = 0, bodyEnd = -1;
|
||||||
|
for (let i = afterStart; i < source.length; i++) {
|
||||||
|
if (source[i] === '{') depth++;
|
||||||
|
else if (source[i] === '}') { depth--; if (depth === 0) { bodyEnd = i; break; } }
|
||||||
|
}
|
||||||
|
const fnBody = source.substring(fnStart, bodyEnd + 1);
|
||||||
assert.ok(fnBody.includes('_viewingSession'), 'killSession must check if deleted session is the active one');
|
assert.ok(fnBody.includes('_viewingSession'), 'killSession must check if deleted session is the active one');
|
||||||
assert.ok(fnBody.includes('closeSession'), 'killSession must call closeSession when deleting the active session');
|
assert.ok(fnBody.includes('closeSession'), 'killSession must call closeSession when deleting the active session');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1081,4 +1081,104 @@ test('terminal.js reconnect uses federation connect path for remote sessions', (
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- fontSize: must come from server settings, NOT localStorage ---
|
||||||
|
|
||||||
|
test('terminal.js createTerminal does not read fontSize from localStorage', () => {
|
||||||
|
// Verify createTerminal accepts fontSize as a parameter (no localStorage dependency).
|
||||||
|
const source = fs.readFileSync(new URL('../terminal.js', import.meta.url), 'utf8');
|
||||||
|
const createTermIdx = source.indexOf('function createTerminal(');
|
||||||
|
assert.ok(createTermIdx !== -1, 'createTerminal function must exist');
|
||||||
|
// Extract createTerminal body (up to next top-level function)
|
||||||
|
const afterStart = source.indexOf('{', createTermIdx);
|
||||||
|
let depth = 0;
|
||||||
|
let bodyEnd = -1;
|
||||||
|
for (let i = afterStart; i < source.length; i++) {
|
||||||
|
if (source[i] === '{') depth++;
|
||||||
|
else if (source[i] === '}') {
|
||||||
|
depth--;
|
||||||
|
if (depth === 0) { bodyEnd = i; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const createTermBody = source.substring(createTermIdx, bodyEnd + 1);
|
||||||
|
assert.ok(
|
||||||
|
!createTermBody.includes('localStorage'),
|
||||||
|
'createTerminal must NOT read from localStorage — fontSize must come from the server settings parameter',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('openTerminal uses passed fontSize to configure xterm.js Terminal constructor', () => {
|
||||||
|
// Verify openTerminal forwards fontSize parameter to createTerminal.
|
||||||
|
const modulePath = join(__dirname, '..', 'terminal.js');
|
||||||
|
delete require.cache[require.resolve(modulePath)];
|
||||||
|
|
||||||
|
let capturedTerminalOptions = null;
|
||||||
|
const mockTerm = {
|
||||||
|
cols: 80, rows: 24,
|
||||||
|
open: () => {},
|
||||||
|
onData: () => {},
|
||||||
|
onResize: () => {},
|
||||||
|
loadAddon: () => {},
|
||||||
|
dispose: () => {},
|
||||||
|
write: () => {},
|
||||||
|
focus: () => {},
|
||||||
|
attachCustomKeyEventHandler: () => {},
|
||||||
|
getSelection: () => '',
|
||||||
|
onSelectionChange: () => {},
|
||||||
|
parser: { registerOscHandler: () => {} },
|
||||||
|
options: { fontSize: 14 },
|
||||||
|
};
|
||||||
|
|
||||||
|
globalThis.WebSocket = class MockWS {
|
||||||
|
constructor() { this.readyState = 1; this.binaryType = ''; }
|
||||||
|
addEventListener() {}
|
||||||
|
close() {}
|
||||||
|
send() {}
|
||||||
|
};
|
||||||
|
globalThis.WebSocket.OPEN = 1;
|
||||||
|
globalThis.location = { protocol: 'http:', host: 'localhost' };
|
||||||
|
globalThis.document = {
|
||||||
|
getElementById: (id) => {
|
||||||
|
if (id === 'terminal-container') return { appendChild: () => {} };
|
||||||
|
if (id === 'reconnect-overlay') return { classList: { add: () => {}, remove: () => {} } };
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
querySelector: () => null,
|
||||||
|
querySelectorAll: () => [],
|
||||||
|
addEventListener: () => {},
|
||||||
|
createElement: () => ({ style: {}, classList: { add: () => {}, remove: () => {} } }),
|
||||||
|
};
|
||||||
|
globalThis.window = {
|
||||||
|
addEventListener: () => {},
|
||||||
|
location: { href: '' },
|
||||||
|
innerWidth: 1024,
|
||||||
|
Terminal: function Terminal(options) {
|
||||||
|
capturedTerminalOptions = options;
|
||||||
|
return mockTerm;
|
||||||
|
},
|
||||||
|
FitAddon: { FitAddon: function FitAddon() { return { fit: () => {} }; } },
|
||||||
|
};
|
||||||
|
|
||||||
|
const origSetTimeout = globalThis.setTimeout;
|
||||||
|
globalThis.setTimeout = (_fn, _ms) => 0;
|
||||||
|
|
||||||
|
require(modulePath);
|
||||||
|
|
||||||
|
globalThis.setTimeout = origSetTimeout;
|
||||||
|
|
||||||
|
const openTerminal = globalThis.window._openTerminal;
|
||||||
|
|
||||||
|
const origST2 = globalThis.setTimeout;
|
||||||
|
globalThis.setTimeout = (_fn, _ms) => 0;
|
||||||
|
|
||||||
|
openTerminal('session', '', 20);
|
||||||
|
|
||||||
|
globalThis.setTimeout = origST2;
|
||||||
|
|
||||||
|
assert.ok(capturedTerminalOptions !== null, 'Terminal constructor must have been called');
|
||||||
|
assert.strictEqual(
|
||||||
|
capturedTerminalOptions.fontSize, 20,
|
||||||
|
'openTerminal must pass the fontSize argument to the xterm.js Terminal constructor',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2069,47 +2069,54 @@ def test_js_show_fab_session_input_uses_factory() -> None:
|
|||||||
# ─── Task 7: Apply settings effects (task-7-apply-settings-effects) ──────────
|
# ─── Task 7: Apply settings effects (task-7-apply-settings-effects) ──────────
|
||||||
|
|
||||||
|
|
||||||
# ── terminal.js: createTerminal reads font size from localStorage ─────────────
|
# ── terminal.js: createTerminal receives font size as parameter ───────────────
|
||||||
|
|
||||||
|
|
||||||
def test_create_terminal_reads_font_size_from_localstorage() -> None:
|
def test_create_terminal_accepts_font_size_parameter() -> None:
|
||||||
"""createTerminal() must read font size from localStorage key 'muxplex.display'."""
|
"""createTerminal() must accept a fontSize parameter (from server settings via app.js).
|
||||||
|
The old localStorage-based approach is gone since the server-settings migration.
|
||||||
|
"""
|
||||||
match = re.search(
|
match = re.search(
|
||||||
r"function createTerminal\s*\(\s*\)\s*\{(.*?)(?=\n(?:function|//|window\.))",
|
r"function createTerminal\s*\([^)]*\)\s*\{(.*?)(?=\n(?:function|//|window\.))",
|
||||||
|
_TERMINAL_JS,
|
||||||
|
re.DOTALL,
|
||||||
|
)
|
||||||
|
assert match, "createTerminal function not found in terminal.js"
|
||||||
|
# Verify the function signature accepts a parameter (not zero-arg)
|
||||||
|
sig_match = re.search(r"function createTerminal\s*\(([^)]+)\)", _TERMINAL_JS)
|
||||||
|
assert sig_match, "createTerminal must accept a fontSize parameter"
|
||||||
|
param = sig_match.group(1).strip()
|
||||||
|
assert param, "createTerminal must accept a fontSize parameter (not zero-arg)"
|
||||||
|
assert "localStorage" not in match.group(1), (
|
||||||
|
"createTerminal must NOT read from localStorage — "
|
||||||
|
"fontSize must be passed as a parameter from app.js (getDisplaySettings().fontSize)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_terminal_does_not_parse_json_from_localstorage() -> None:
|
||||||
|
"""createTerminal() must NOT parse JSON from localStorage (server-settings migration).
|
||||||
|
Font size now comes directly from the fontSize parameter passed by app.js.
|
||||||
|
"""
|
||||||
|
match = re.search(
|
||||||
|
r"function createTerminal\s*\([^)]*\)\s*\{(.*?)(?=\n(?:function|//|window\.))",
|
||||||
_TERMINAL_JS,
|
_TERMINAL_JS,
|
||||||
re.DOTALL,
|
re.DOTALL,
|
||||||
)
|
)
|
||||||
assert match, "createTerminal function not found in terminal.js"
|
assert match, "createTerminal function not found in terminal.js"
|
||||||
body = match.group(1)
|
body = match.group(1)
|
||||||
assert "muxplex.display" in body or "DISPLAY_SETTINGS_KEY" in body, (
|
assert "JSON.parse" not in body, (
|
||||||
"createTerminal must read from localStorage key 'muxplex.display'"
|
"createTerminal must NOT use JSON.parse — localStorage is no longer used; "
|
||||||
)
|
"fontSize comes directly as a parameter from app.js"
|
||||||
assert "localStorage" in body, (
|
|
||||||
"createTerminal must use localStorage to read font size"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_create_terminal_parses_json_for_font_size() -> None:
|
|
||||||
"""createTerminal() must parse JSON from localStorage to extract fontSize."""
|
|
||||||
match = re.search(
|
|
||||||
r"function createTerminal\s*\(\s*\)\s*\{(.*?)(?=\n(?:function|//|window\.))",
|
|
||||||
_TERMINAL_JS,
|
|
||||||
re.DOTALL,
|
|
||||||
)
|
|
||||||
assert match, "createTerminal function not found in terminal.js"
|
|
||||||
body = match.group(1)
|
|
||||||
assert "JSON.parse" in body, (
|
|
||||||
"createTerminal must use JSON.parse to extract fontSize from localStorage"
|
|
||||||
)
|
)
|
||||||
assert "fontSize" in body, (
|
assert "fontSize" in body, (
|
||||||
"createTerminal must extract fontSize from parsed display settings"
|
"createTerminal must use the fontSize parameter for terminal configuration"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_create_terminal_applies_mobile_cap_with_math_min() -> None:
|
def test_create_terminal_applies_mobile_cap_with_math_min() -> None:
|
||||||
"""createTerminal() must apply mobile cap using Math.min(storedFontSize, 12)."""
|
"""createTerminal() must apply mobile cap using Math.min(storedFontSize, 12)."""
|
||||||
match = re.search(
|
match = re.search(
|
||||||
r"function createTerminal\s*\(\s*\)\s*\{(.*?)(?=\n(?:function|//|window\.))",
|
r"function createTerminal\s*\([^)]*\)\s*\{(.*?)(?=\n(?:function|//|window\.))",
|
||||||
_TERMINAL_JS,
|
_TERMINAL_JS,
|
||||||
re.DOTALL,
|
re.DOTALL,
|
||||||
)
|
)
|
||||||
@@ -2121,9 +2128,9 @@ def test_create_terminal_applies_mobile_cap_with_math_min() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_create_terminal_uses_stored_font_size_not_hardcoded() -> None:
|
def test_create_terminal_uses_stored_font_size_not_hardcoded() -> None:
|
||||||
"""createTerminal() must use stored fontSize variable, not hardcoded 14 or ternary."""
|
"""createTerminal() must use passed fontSize parameter, not hardcoded 14 or ternary."""
|
||||||
match = re.search(
|
match = re.search(
|
||||||
r"function createTerminal\s*\(\s*\)\s*\{(.*?)(?=\n(?:function|//|window\.))",
|
r"function createTerminal\s*\([^)]*\)\s*\{(.*?)(?=\n(?:function|//|window\.))",
|
||||||
_TERMINAL_JS,
|
_TERMINAL_JS,
|
||||||
re.DOTALL,
|
re.DOTALL,
|
||||||
)
|
)
|
||||||
@@ -2133,21 +2140,21 @@ def test_create_terminal_uses_stored_font_size_not_hardcoded() -> None:
|
|||||||
# Check that there's no raw "mobile ? 12 : 14" pattern anymore
|
# Check that there's no raw "mobile ? 12 : 14" pattern anymore
|
||||||
assert "mobile ? 12 : 14" not in body, (
|
assert "mobile ? 12 : 14" not in body, (
|
||||||
"createTerminal must not use hardcoded 'mobile ? 12 : 14' ternary; "
|
"createTerminal must not use hardcoded 'mobile ? 12 : 14' ternary; "
|
||||||
"use stored font size from localStorage with Math.min cap"
|
"use the passed fontSize parameter with Math.min cap for mobile"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_create_terminal_has_default_font_size_14() -> None:
|
def test_create_terminal_has_default_font_size_14() -> None:
|
||||||
"""createTerminal() must default to fontSize 14 when not set in localStorage."""
|
"""createTerminal() must default to fontSize 14 when no parameter is passed."""
|
||||||
match = re.search(
|
match = re.search(
|
||||||
r"function createTerminal\s*\(\s*\)\s*\{(.*?)(?=\n(?:function|//|window\.))",
|
r"function createTerminal\s*\([^)]*\)\s*\{(.*?)(?=\n(?:function|//|window\.))",
|
||||||
_TERMINAL_JS,
|
_TERMINAL_JS,
|
||||||
re.DOTALL,
|
re.DOTALL,
|
||||||
)
|
)
|
||||||
assert match, "createTerminal function not found in terminal.js"
|
assert match, "createTerminal function not found in terminal.js"
|
||||||
body = match.group(1)
|
body = match.group(1)
|
||||||
assert "14" in body, (
|
assert "14" in body, (
|
||||||
"createTerminal must have default font size of 14 for when localStorage is not set"
|
"createTerminal must have default font size of 14 for when no fontSize parameter is passed"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user