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:
Brian Krabach
2026-04-08 12:27:34 -07:00
parent 5f09d0b0f0
commit 4b0718eff9
6 changed files with 201 additions and 68 deletions
+1 -1
View File
@@ -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);
} }
/** /**
+8 -14
View File
@@ -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);
+53 -21
View File
@@ -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');
}); });
+100
View File
@@ -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',
);
});
+38 -31
View File
@@ -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"
) )
Generated
+1 -1
View File
@@ -332,7 +332,7 @@ wheels = [
[[package]] [[package]]
name = "muxplex" name = "muxplex"
version = "0.1.0" version = "0.2.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "aiofiles" }, { name = "aiofiles" },