diff --git a/muxplex/frontend/app.js b/muxplex/frontend/app.js index 0e21afe..4e65092 100644 --- a/muxplex/frontend/app.js +++ b/muxplex/frontend/app.js @@ -898,187 +898,14 @@ function _setViewingSession(name) { _viewingSession = name; } -// ─── Command palette state ──────────────────────────────────────────────────── -const PALETTE_MAX_ITEMS = 9; -let _paletteSelectedIndex = 0; -let _paletteFilteredSessions = []; -let _paletteOpen = false; -let _paletteInputListener = null; - -// ─── Command palette functions ──────────────────────────────────────────────── - -/** - * Render the filtered session list inside #palette-list. - * Shows up to 9 items. Each item is a
  • with index number, - * session name, optional bell emoji, and timestamp. - */ -function renderPaletteList() { - const list = $('palette-list'); - if (!list) return; - - const items = _paletteFilteredSessions.slice(0, PALETTE_MAX_ITEMS); - list.innerHTML = items - .map((session, i) => { - const isBell = sessionPriority(session) === 'bell'; - const bell = isBell ? ' 🔔' : ''; - const time = formatTimestamp(session.last_activity_at || null); - const name = escapeHtml(session.name || ''); - return `
  • ${i + 1} ${name}${bell} ${escapeHtml(time)}
  • `; - }) - .join(''); - - // Bind click handlers on each item - list.querySelectorAll('.palette-item').forEach((item) => { - on(item, 'click', () => { - const idx = parseInt(item.dataset.index, 10); - const session = _paletteFilteredSessions[idx]; - if (session) { - closePalette(); - openSession(session.name).catch((err) => console.error('[renderPaletteList]', err)); - } - }); - }); - - highlightPaletteItem(_paletteSelectedIndex); -} - -/** - * Toggle the palette-item--selected class on the item at `index`. - * @param {number} index - */ -function highlightPaletteItem(index) { - const list = $('palette-list'); - if (!list) return; - list.querySelectorAll('.palette-item').forEach((item, i) => { - if (i === index) { - item.classList.add('palette-item--selected'); - } else { - item.classList.remove('palette-item--selected'); - } - }); -} - -/** - * Open the command palette. - * Shows #command-palette, copies _currentSessions to _paletteFilteredSessions, - * renders the list, resets selection index, focuses #palette-input, and binds - * the input event listener. - */ -function openPalette() { - _paletteOpen = true; - _paletteFilteredSessions = _currentSessions.slice(); - _paletteSelectedIndex = 0; - - const palette = $('command-palette'); - if (palette) palette.classList.remove('hidden'); // palette starts with hidden class - - renderPaletteList(); - - const input = $('palette-input'); - if (input) { - input.value = ''; - input.focus(); - if (_paletteInputListener) { - input.removeEventListener('input', _paletteInputListener); - } - _paletteInputListener = onPaletteInput; - input.addEventListener('input', _paletteInputListener); - } -} - -/** - * Close the command palette. - * Hides #command-palette and removes the input event listener. - */ -function closePalette() { - _paletteOpen = false; - - const palette = $('command-palette'); - if (palette) palette.classList.add('hidden'); - - const input = $('palette-input'); - if (input && _paletteInputListener) { - input.removeEventListener('input', _paletteInputListener); - _paletteInputListener = null; - } -} - -/** - * Handle input events on #palette-input. - * Filters sessions by the current query, re-renders the list, resets selection. - * @param {Event} e - */ -function onPaletteInput(e) { - const query = e && e.target ? e.target.value : ''; - _paletteFilteredSessions = filterByQuery(_currentSessions, query); - _paletteSelectedIndex = 0; - renderPaletteList(); -} - -/** - * Handle keydown events inside the command palette. - * ArrowDown/Up moves selection, Enter opens selected session, - * Escape closes palette, G closes palette + returns to grid, - * number keys 1-9 jump directly to that item. - * @param {KeyboardEvent} e - * @returns {Promise} - */ -async function handlePaletteKeydown(e) { - const visibleCount = Math.min(_paletteFilteredSessions.length, PALETTE_MAX_ITEMS); - - if (e.key === 'Escape') { - e.preventDefault(); - closePalette(); - } else if (e.key === 'g' || e.key === 'G') { - e.preventDefault(); - closePalette(); - await closeSession(); - } else if (visibleCount > 0) { - if (e.key === 'ArrowDown') { - e.preventDefault(); - _paletteSelectedIndex = (_paletteSelectedIndex + 1) % visibleCount; - highlightPaletteItem(_paletteSelectedIndex); - } else if (e.key === 'ArrowUp') { - e.preventDefault(); - _paletteSelectedIndex = (_paletteSelectedIndex - 1 + visibleCount) % visibleCount; - highlightPaletteItem(_paletteSelectedIndex); - } else if (e.key === 'Enter') { - e.preventDefault(); - const session = _paletteFilteredSessions[_paletteSelectedIndex]; - if (session) { - closePalette(); - await openSession(session.name); - } - } else if (e.key >= '1' && e.key <= '9') { - const idx = parseInt(e.key, 10) - 1; - if (idx < visibleCount) { - e.preventDefault(); - const session = _paletteFilteredSessions[idx]; - closePalette(); - await openSession(session.name); - } - } - } -} - /** * Global keydown handler. - * When palette is open: delegates to handlePaletteKeydown. - * When in fullscreen with palette closed: backtick or Ctrl+K opens palette, - * Escape returns to grid. + * When in fullscreen: Escape returns to grid. * @param {KeyboardEvent} e */ function handleGlobalKeydown(e) { - if (_paletteOpen) { - handlePaletteKeydown(e).catch((err) => console.error('[handleGlobalKeydown]', err)); - return; - } - if (_viewMode === 'fullscreen') { - if (e.key === '`' || (e.ctrlKey && e.key === 'k')) { - e.preventDefault(); - openPalette(); - } else if (e.key === 'Escape') { + if (e.key === 'Escape') { e.preventDefault(); closeSession(); } @@ -1166,8 +993,6 @@ function bindStaticEventListeners() { on($('sidebar-toggle-btn'), 'click', toggleSidebar); on($('sidebar-collapse-btn'), 'click', toggleSidebar); bindSidebarClickAway(); - on($('palette-trigger'), 'click', openPalette); - on($('palette-backdrop'), 'click', closePalette); document.addEventListener('keydown', handleGlobalKeydown); on($('session-pill'), 'click', openBottomSheet); on($('sheet-backdrop'), 'click', closeBottomSheet); @@ -1216,36 +1041,6 @@ function _setCurrentSessions(sessions) { _currentSessions = sessions; } -/** Test-only: set _paletteFilteredSessions directly. */ -function _setPaletteFilteredSessions(sessions) { - _paletteFilteredSessions = sessions; -} - -/** Test-only: get _paletteFilteredSessions. */ -function _getPaletteFilteredSessions() { - return _paletteFilteredSessions; -} - -/** Test-only: set _paletteSelectedIndex directly. */ -function _setPaletteSelectedIndex(index) { - _paletteSelectedIndex = index; -} - -/** Test-only: get _paletteSelectedIndex. */ -function _getPaletteSelectedIndex() { - return _paletteSelectedIndex; -} - -/** Test-only: set _paletteOpen directly. */ -function _setPaletteOpen(val) { - _paletteOpen = val; -} - -/** Test-only: get _paletteOpen. */ -function _isPaletteOpen() { - return _paletteOpen; -} - /** Test-only: set _viewMode directly. */ function _setViewMode(mode) { _viewMode = mode; @@ -1301,13 +1096,6 @@ if (typeof module !== 'undefined' && module.exports) { openSession, closeSession, _setViewingSession, - // Command palette - renderPaletteList, - highlightPaletteItem, - openPalette, - closePalette, - onPaletteInput, - handlePaletteKeydown, handleGlobalKeydown, bindStaticEventListeners, openBottomSheet, @@ -1323,12 +1111,6 @@ if (typeof module !== 'undefined' && module.exports) { hidePreview, // Test-only helpers _setCurrentSessions, - _setPaletteFilteredSessions, - _getPaletteFilteredSessions, - _setPaletteSelectedIndex, - _getPaletteSelectedIndex, - _setPaletteOpen, - _isPaletteOpen, _setViewMode, }; } diff --git a/muxplex/tests/__pycache__/test_frontend_js.cpython-312-pytest-9.0.2.pyc b/muxplex/tests/__pycache__/test_frontend_js.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000..9468866 Binary files /dev/null and b/muxplex/tests/__pycache__/test_frontend_js.cpython-312-pytest-9.0.2.pyc differ diff --git a/muxplex/tests/test_frontend_js.py b/muxplex/tests/test_frontend_js.py new file mode 100644 index 0000000..6137fad --- /dev/null +++ b/muxplex/tests/test_frontend_js.py @@ -0,0 +1,330 @@ +"""Tests for frontend/app.js — verifies palette code removal and handleGlobalKeydown simplification.""" + +import pathlib +import re + +JS_PATH = pathlib.Path(__file__).parent.parent / "frontend" / "app.js" + +# Read once per module — tests are read-only so sharing is safe. +_JS: str = JS_PATH.read_text() + + +# ── Palette state variables must be removed ────────────────────────────────── + +def test_no_palette_max_items_constant() -> None: + """PALETTE_MAX_ITEMS constant must be removed.""" + assert "PALETTE_MAX_ITEMS" not in _JS, ( + "PALETTE_MAX_ITEMS must be removed from app.js" + ) + + +def test_no_palette_selected_index_variable() -> None: + """_paletteSelectedIndex variable must be removed.""" + assert "_paletteSelectedIndex" not in _JS, ( + "_paletteSelectedIndex must be removed from app.js" + ) + + +def test_no_palette_filtered_sessions_variable() -> None: + """_paletteFilteredSessions variable must be removed.""" + assert "_paletteFilteredSessions" not in _JS, ( + "_paletteFilteredSessions must be removed from app.js" + ) + + +def test_no_palette_open_variable() -> None: + """_paletteOpen variable must be removed.""" + assert "_paletteOpen" not in _JS, ( + "_paletteOpen must be removed from app.js" + ) + + +def test_no_palette_input_listener_variable() -> None: + """_paletteInputListener variable must be removed.""" + assert "_paletteInputListener" not in _JS, ( + "_paletteInputListener must be removed from app.js" + ) + + +# ── Palette functions must be removed ──────────────────────────────────────── + +def test_no_render_palette_list_function() -> None: + """renderPaletteList function must be removed.""" + assert "renderPaletteList" not in _JS, ( + "renderPaletteList must be removed from app.js" + ) + + +def test_no_highlight_palette_item_function() -> None: + """highlightPaletteItem function must be removed.""" + assert "highlightPaletteItem" not in _JS, ( + "highlightPaletteItem must be removed from app.js" + ) + + +def test_no_open_palette_function() -> None: + """openPalette function must be removed.""" + assert "openPalette" not in _JS, ( + "openPalette must be removed from app.js" + ) + + +def test_no_close_palette_function() -> None: + """closePalette function must be removed.""" + assert "closePalette" not in _JS, ( + "closePalette must be removed from app.js" + ) + + +def test_no_on_palette_input_function() -> None: + """onPaletteInput function must be removed.""" + assert "onPaletteInput" not in _JS, ( + "onPaletteInput must be removed from app.js" + ) + + +def test_no_handle_palette_keydown_function() -> None: + """handlePaletteKeydown function must be removed.""" + assert "handlePaletteKeydown" not in _JS, ( + "handlePaletteKeydown must be removed from app.js" + ) + + +# ── handleGlobalKeydown must be simplified ─────────────────────────────────── + +def test_handle_global_keydown_exists() -> None: + """handleGlobalKeydown function must exist.""" + assert "function handleGlobalKeydown" in _JS, ( + "handleGlobalKeydown must still exist in app.js" + ) + + +def test_handle_global_keydown_no_palette_open_check() -> None: + """handleGlobalKeydown must not check _paletteOpen.""" + # Extract the function body + match = re.search( + r"function handleGlobalKeydown\s*\(e\)\s*\{(.*?)\n\}", + _JS, + re.DOTALL, + ) + assert match, "handleGlobalKeydown function not found" + body = match.group(1) + assert "_paletteOpen" not in body, ( + "handleGlobalKeydown must not reference _paletteOpen" + ) + + +def test_handle_global_keydown_no_open_palette_call() -> None: + """handleGlobalKeydown must not call openPalette.""" + match = re.search( + r"function handleGlobalKeydown\s*\(e\)\s*\{(.*?)\n\}", + _JS, + re.DOTALL, + ) + assert match, "handleGlobalKeydown function not found" + body = match.group(1) + assert "openPalette" not in body, ( + "handleGlobalKeydown must not call openPalette" + ) + + +def test_handle_global_keydown_handles_escape_in_fullscreen() -> None: + """handleGlobalKeydown must call closeSession() on Escape in fullscreen mode.""" + match = re.search( + r"function handleGlobalKeydown\s*\(e\)\s*\{(.*?)\n\}", + _JS, + re.DOTALL, + ) + assert match, "handleGlobalKeydown function not found" + body = match.group(1) + assert "fullscreen" in body, "handleGlobalKeydown must check for fullscreen mode" + assert "Escape" in body, "handleGlobalKeydown must handle Escape key" + assert "closeSession" in body, "handleGlobalKeydown must call closeSession" + + +# ── bindStaticEventListeners must have no palette references ───────────────── + +def test_bind_static_event_listeners_no_palette_trigger() -> None: + """bindStaticEventListeners must not bind palette-trigger click.""" + match = re.search( + r"function bindStaticEventListeners\s*\(\s*\)\s*\{(.*?)\n\}", + _JS, + re.DOTALL, + ) + assert match, "bindStaticEventListeners function not found" + body = match.group(1) + assert "palette-trigger" not in body, ( + "bindStaticEventListeners must not bind palette-trigger click" + ) + + +def test_bind_static_event_listeners_no_palette_backdrop() -> None: + """bindStaticEventListeners must not bind palette-backdrop click.""" + match = re.search( + r"function bindStaticEventListeners\s*\(\s*\)\s*\{(.*?)\n\}", + _JS, + re.DOTALL, + ) + assert match, "bindStaticEventListeners function not found" + body = match.group(1) + assert "palette-backdrop" not in body, ( + "bindStaticEventListeners must not bind palette-backdrop click" + ) + + +# ── Palette test-only helpers must be removed ───────────────────────────────── + +def test_no_set_palette_filtered_sessions_helper() -> None: + """_setPaletteFilteredSessions test helper must be removed.""" + assert "_setPaletteFilteredSessions" not in _JS, ( + "_setPaletteFilteredSessions must be removed from app.js" + ) + + +def test_no_get_palette_filtered_sessions_helper() -> None: + """_getPaletteFilteredSessions test helper must be removed.""" + assert "_getPaletteFilteredSessions" not in _JS, ( + "_getPaletteFilteredSessions must be removed from app.js" + ) + + +def test_no_set_palette_selected_index_helper() -> None: + """_setPaletteSelectedIndex test helper must be removed.""" + assert "_setPaletteSelectedIndex" not in _JS, ( + "_setPaletteSelectedIndex must be removed from app.js" + ) + + +def test_no_get_palette_selected_index_helper() -> None: + """_getPaletteSelectedIndex test helper must be removed.""" + assert "_getPaletteSelectedIndex" not in _JS, ( + "_getPaletteSelectedIndex must be removed from app.js" + ) + + +def test_no_set_palette_open_helper() -> None: + """_setPaletteOpen test helper must be removed.""" + assert "_setPaletteOpen" not in _JS, ( + "_setPaletteOpen must be removed from app.js" + ) + + +def test_no_is_palette_open_helper() -> None: + """_isPaletteOpen test helper must be removed.""" + assert "_isPaletteOpen" not in _JS, ( + "_isPaletteOpen must be removed from app.js" + ) + + +# ── module.exports must not include palette exports ─────────────────────────── + +def test_exports_no_render_palette_list() -> None: + """module.exports must not export renderPaletteList.""" + match = re.search( + r"module\.exports\s*=\s*\{(.*?)\};", + _JS, + re.DOTALL, + ) + assert match, "module.exports block not found" + exports = match.group(1) + assert "renderPaletteList" not in exports, ( + "module.exports must not export renderPaletteList" + ) + + +def test_exports_no_highlight_palette_item() -> None: + """module.exports must not export highlightPaletteItem.""" + match = re.search( + r"module\.exports\s*=\s*\{(.*?)\};", + _JS, + re.DOTALL, + ) + assert match, "module.exports block not found" + exports = match.group(1) + assert "highlightPaletteItem" not in exports, ( + "module.exports must not export highlightPaletteItem" + ) + + +def test_exports_no_open_palette() -> None: + """module.exports must not export openPalette.""" + match = re.search( + r"module\.exports\s*=\s*\{(.*?)\};", + _JS, + re.DOTALL, + ) + assert match, "module.exports block not found" + exports = match.group(1) + assert "openPalette" not in exports, ( + "module.exports must not export openPalette" + ) + + +def test_exports_no_close_palette() -> None: + """module.exports must not export closePalette.""" + match = re.search( + r"module\.exports\s*=\s*\{(.*?)\};", + _JS, + re.DOTALL, + ) + assert match, "module.exports block not found" + exports = match.group(1) + assert "closePalette" not in exports, ( + "module.exports must not export closePalette" + ) + + +def test_exports_no_on_palette_input() -> None: + """module.exports must not export onPaletteInput.""" + match = re.search( + r"module\.exports\s*=\s*\{(.*?)\};", + _JS, + re.DOTALL, + ) + assert match, "module.exports block not found" + exports = match.group(1) + assert "onPaletteInput" not in exports, ( + "module.exports must not export onPaletteInput" + ) + + +def test_exports_no_handle_palette_keydown() -> None: + """module.exports must not export handlePaletteKeydown.""" + match = re.search( + r"module\.exports\s*=\s*\{(.*?)\};", + _JS, + re.DOTALL, + ) + assert match, "module.exports block not found" + exports = match.group(1) + assert "handlePaletteKeydown" not in exports, ( + "module.exports must not export handlePaletteKeydown" + ) + + +def test_exports_still_has_handle_global_keydown() -> None: + """module.exports must still export handleGlobalKeydown.""" + match = re.search( + r"module\.exports\s*=\s*\{(.*?)\};", + _JS, + re.DOTALL, + ) + assert match, "module.exports block not found" + exports = match.group(1) + assert "handleGlobalKeydown" in exports, ( + "module.exports must still export handleGlobalKeydown" + ) + + +def test_exports_still_has_bind_static_event_listeners() -> None: + """module.exports must still export bindStaticEventListeners.""" + match = re.search( + r"module\.exports\s*=\s*\{(.*?)\};", + _JS, + re.DOTALL, + ) + assert match, "module.exports block not found" + exports = match.group(1) + assert "bindStaticEventListeners" in exports, ( + "module.exports must still export bindStaticEventListeners" + )