-
-
+
{name} is replaced with the session name
diff --git a/muxplex/frontend/style.css b/muxplex/frontend/style.css
index a5a948a..caaab0c 100644
--- a/muxplex/frontend/style.css
+++ b/muxplex/frontend/style.css
@@ -1687,6 +1687,11 @@ body {
background: var(--border-subtle);
}
+.view-dropdown__separator--strong {
+ border-color: var(--border);
+ margin: 6px 0;
+}
+
.view-dropdown__action {
display: flex;
align-items: center;
@@ -1731,14 +1736,16 @@ body {
============================================================ */
.device-badge {
- display: inline-block;
- font-size: 9px;
+ display: inline-flex;
+ align-items: center;
+ font-size: 12px;
color: var(--accent);
background: var(--accent-dim);
border: 1px solid var(--accent);
border-radius: 4px;
- padding: 1px 4px;
+ padding: 3px 6px;
line-height: 1.4;
+ min-height: 22px;
vertical-align: middle;
}
@@ -1818,7 +1825,7 @@ body {
/* —— Add Sessions Panel —————————————————————————————————————————————————————————————————————————— */
-.add-sessions-panel {
+.manage-view-panel {
position: fixed;
inset: 0;
z-index: 250;
@@ -1827,13 +1834,13 @@ body {
justify-content: center;
}
-.add-sessions-panel__backdrop {
+.manage-view-panel__backdrop {
position: absolute;
inset: 0;
background: var(--bg-overlay);
}
-.add-sessions-panel__content {
+.manage-view-panel__content {
position: relative;
width: 90%;
max-width: 440px;
@@ -1846,7 +1853,7 @@ body {
overflow: hidden;
}
-.add-sessions-panel__header {
+.manage-view-panel__header {
display: flex;
align-items: center;
justify-content: space-between;
@@ -1854,14 +1861,14 @@ body {
border-bottom: 1px solid var(--border-subtle);
}
-.add-sessions-panel__title {
+.manage-view-panel__title {
font-size: 15px;
font-weight: 600;
color: var(--text);
margin: 0;
}
-.add-sessions-panel__close {
+.manage-view-panel__close {
width: 28px;
height: 28px;
display: flex;
@@ -1875,25 +1882,25 @@ body {
border-radius: 4px;
}
-.add-sessions-panel__close:hover {
+.manage-view-panel__close:hover {
background: var(--bg-surface);
color: var(--text);
}
-.add-sessions-panel__list {
+.manage-view-panel__list {
overflow-y: auto;
padding: 8px 0;
flex: 1;
}
-.add-sessions-panel__empty {
+.manage-view-panel__empty {
padding: 24px 16px;
text-align: center;
color: var(--text-muted);
font-size: 13px;
}
-.add-sessions-item {
+.manage-view-item {
display: flex;
align-items: center;
gap: 10px;
@@ -1901,20 +1908,20 @@ body {
cursor: pointer;
}
-.add-sessions-item:hover {
+.manage-view-item:hover {
background: var(--bg-surface);
}
-.add-sessions-item--hidden {
+.manage-view-item--hidden {
opacity: 0.5;
}
-.add-sessions-item__checkbox {
+.manage-view-item__checkbox {
flex-shrink: 0;
accent-color: var(--accent);
}
-.add-sessions-item__name {
+.manage-view-item__name {
font-size: 13px;
color: var(--text);
flex: 1;
@@ -1924,13 +1931,13 @@ body {
white-space: nowrap;
}
-.add-sessions-item__device {
+.manage-view-item__device {
font-size: 11px;
color: var(--text-dim);
white-space: nowrap;
}
-.add-sessions-item__badge {
+.manage-view-item__badge {
font-size: 10px;
color: var(--text-dim);
background: var(--bg);
@@ -1939,7 +1946,7 @@ body {
padding: 1px 5px;
}
-.add-sessions-item__disclosure {
+.manage-view-item__disclosure {
width: 100%;
padding: 2px 16px 6px 42px;
font-size: 11px;
@@ -1949,7 +1956,7 @@ body {
/* Mobile: full-screen sheet for Add Sessions */
@media (max-width: 599px) {
- .add-sessions-panel__content {
+ .manage-view-panel__content {
width: 100%;
max-width: 100%;
max-height: 100%;
@@ -1958,6 +1965,66 @@ body {
}
}
+.manage-view-panel__name-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.manage-view-panel__name {
+ font-size: 15px;
+ font-weight: 600;
+ color: var(--text);
+ margin: 0;
+ cursor: pointer;
+}
+
+.manage-view-panel__name:hover::after {
+ content: ' \270E';
+ font-size: 12px;
+ color: var(--text-muted);
+}
+
+.manage-view-panel__summary {
+ font-size: 12px;
+ color: var(--text-muted);
+ margin: 4px 0 0;
+}
+
+.manage-view-panel__footer {
+ padding: 12px 16px;
+ border-top: 1px solid var(--border-subtle);
+ display: flex;
+ justify-content: flex-end;
+}
+
+.manage-view-panel__close-btn {
+ padding: 6px 16px;
+ border-radius: 6px;
+ border: 1px solid var(--border);
+ background: var(--bg-surface);
+ color: var(--text);
+ font-size: 13px;
+ cursor: pointer;
+}
+
+.manage-view-panel__close-btn:hover {
+ border-color: var(--accent);
+ color: var(--accent);
+}
+
+.manage-view-panel__name-input {
+ font-size: 15px;
+ font-weight: 600;
+ color: var(--text);
+ background: var(--bg-secondary);
+ border: 1px solid var(--accent);
+ border-radius: 4px;
+ padding: 2px 6px;
+ width: 200px;
+ max-width: 100%;
+}
+
/* ============================================================
Responsive overlay sidebar at <960px
============================================================ */
@@ -2195,6 +2262,25 @@ body {
background: rgba(239, 68, 68, 0.1);
}
+.views-settings-btn--manage {
+ background: var(--bg-surface);
+ color: var(--accent);
+ border-color: var(--accent);
+}
+
+.views-settings-btn--manage:hover:not(:disabled) {
+ background: var(--accent-dim);
+}
+
+.views-settings-btn--new {
+ margin-top: 8px;
+}
+
+.views-settings-new-row {
+ padding: 8px 0 4px;
+ border-top: 1px solid var(--border-subtle);
+}
+
.views-settings-confirm {
display: inline-flex;
align-items: center;
diff --git a/muxplex/tests/test_frontend_css.py b/muxplex/tests/test_frontend_css.py
index 76acf9c..529b8d4 100644
--- a/muxplex/tests/test_frontend_css.py
+++ b/muxplex/tests/test_frontend_css.py
@@ -2214,17 +2214,16 @@ def test_flyout_bottom_sheet_styled() -> None:
# ============================================================
-def test_add_sessions_panel_styled() -> None:
- """style.css must contain .add-sessions-panel styles."""
+def test_manage_view_panel_styled() -> None:
+ """style.css must contain .manage-view-panel styles (renamed from .add-sessions-panel)."""
css = read_css()
- assert ".add-sessions-panel" in css, "style.css must style .add-sessions-panel"
+ assert ".manage-view-panel" in css, "style.css must style .manage-view-panel"
-def test_add_sessions_item_styled() -> None:
- """style.css must contain .add-sessions-item styles."""
+def test_manage_view_item_styled() -> None:
+ """style.css must contain .manage-view-item styles (renamed from .add-sessions-item)."""
css = read_css()
- assert ".add-sessions-item" in css, "style.css must style .add-sessions-item"
- assert ".add-sessions-item" in css, "style.css must style .add-sessions-item"
+ assert ".manage-view-item" in css, "style.css must style .manage-view-item"
# ─── Phase 3 COE findings regression tests ──────────────────────────────────
@@ -2246,11 +2245,11 @@ def test_disclosure_not_hidden_by_css() -> None:
import re as _re
match = _re.search(
- r"\.add-sessions-item__disclosure\s*\{([^}]*)\}",
+ r"\.manage-view-item__disclosure\s*\{([^}]*)\}",
css,
_re.DOTALL,
)
- assert match, ".add-sessions-item__disclosure rule not found in style.css"
+ assert match, ".manage-view-item__disclosure rule not found in style.css"
body = match.group(1)
assert "display: none" not in body and "display:none" not in body.replace(" ", ""), (
".add-sessions-item__disclosure must NOT have display:none — "
@@ -2309,3 +2308,57 @@ def test_no_sidebar_delete_css() -> None:
".sidebar-delete CSS rules must be removed from style.css — "
"the button was removed from buildSidebarHTML(); orphaned CSS is dead code"
)
+
+
+# ============================================================
+# UX Overhaul CSS — Issues 3 and 8
+# ============================================================
+
+
+def test_manage_view_panel_styles_exist() -> None:
+ """.manage-view-panel CSS rule must exist (renamed from .add-sessions-panel).
+
+ Issue 3: The Add Sessions panel was redesigned as the Manage View panel.
+ All CSS classes must be renamed from add-sessions-* to manage-view-*.
+ """
+ css = read_css()
+ assert ".manage-view-panel" in css, (
+ ".manage-view-panel CSS rule must exist in style.css — "
+ "renamed from .add-sessions-panel (Issue 3)"
+ )
+ assert ".add-sessions-panel" not in css, (
+ ".add-sessions-panel CSS rule must be removed — "
+ "renamed to .manage-view-panel (Issue 3)"
+ )
+
+
+def test_device_badge_height_matches_flyout_button() -> None:
+ """.device-badge must have a height/line-height matching the ⋮ button (~22-24px).
+
+ Issue 8: The device badge was shorter than the ⋮ flyout button (24x24px),
+ making the tile header look uneven. Fix: increase .device-badge dimensions.
+ """
+ import re
+
+ css = read_css()
+ # Find the BASE .device-badge rule (not a qualified selector like .parent .device-badge)
+ # We look for the standalone rule at the start of a line (or after whitespace)
+ match = re.search(r"(?:^|\n)\.device-badge\s*\{([^}]*)\}", css, re.DOTALL)
+ assert match, ".device-badge CSS rule not found"
+ rule = match.group(1)
+ # Should have a line-height or min-height that results in ~22-24px
+ # Check for either explicit height/min-height or a large enough line-height/padding
+ has_height = (
+ "min-height" in rule
+ or ("line-height" in rule and any(
+ f"line-height: {n}" in rule or f"line-height:{n}" in rule
+ for n in ["1.6", "1.7", "1.8", "1.9", "2", "22px", "23px", "24px"]
+ ))
+ or ("padding" in rule and "padding: 2px" in rule)
+ or ("padding: 3px" in rule)
+ or ("padding: 4px" in rule)
+ )
+ assert has_height, (
+ ".device-badge must have increased height to match the ~24px ⋮ button — "
+ "add min-height or increase padding/line-height so the badge is ~22-24px tall"
+ )
diff --git a/muxplex/tests/test_frontend_html.py b/muxplex/tests/test_frontend_html.py
index b3cb0c7..bf5367b 100644
--- a/muxplex/tests/test_frontend_html.py
+++ b/muxplex/tests/test_frontend_html.py
@@ -1540,20 +1540,49 @@ def test_no_hidden_sessions_checkbox_list_in_settings() -> None:
# ============================================================
-def test_add_sessions_panel_exists() -> None:
- """index.html must contain an add-sessions-panel element."""
+def test_manage_view_panel_exists() -> None:
+ """index.html must contain a manage-view-panel element (renamed from add-sessions-panel)."""
soup = _SOUP
- assert soup.find(id="add-sessions-panel"), (
- "index.html must contain an element with id='add-sessions-panel'"
+ assert soup.find(id="manage-view-panel"), (
+ "index.html must contain an element with id='manage-view-panel'"
)
-def test_add_sessions_panel_has_role_dialog() -> None:
- """add-sessions-panel must have role='dialog' and aria-modal='true'."""
+def test_manage_view_panel_has_role_dialog() -> None:
+ """manage-view-panel must have role='dialog' and aria-modal='true'."""
soup = _SOUP
- panel = soup.find(id="add-sessions-panel")
- assert panel, "Missing #add-sessions-panel"
- assert panel.get("role") == "dialog", "add-sessions-panel must have role='dialog'"
+ panel = soup.find(id="manage-view-panel")
+ assert panel, "Missing #manage-view-panel"
+ assert panel.get("role") == "dialog", "manage-view-panel must have role='dialog'"
assert panel.get("aria-modal") == "true", (
- "add-sessions-panel must have aria-modal='true'"
+ "manage-view-panel must have aria-modal='true'"
+ )
+
+
+def test_add_sessions_btn_removed() -> None:
+ """index.html must NOT contain #add-sessions-btn (removed in Issue 5).
+
+ The '+ Add' header button was removed — Manage View is now accessed via
+ the dropdown's 'Manage [ViewName]...' item.
+ """
+ soup = _SOUP
+ assert not soup.find(id="add-sessions-btn"), (
+ "index.html must NOT contain id='add-sessions-btn' — "
+ "the button was removed; use the dropdown 'Manage [ViewName]...' item instead"
+ )
+
+
+def test_kill_session_command_label() -> None:
+ """Settings Commands tab must use 'Kill session command' not 'Delete session command'.
+
+ Terminology must match the flyout menu which uses 'Kill Session'.
+ """
+ html = HTML_PATH.read_text()
+ assert "Kill session command" in html or "Kill Session Command" in html, (
+ "Settings Commands tab must use 'Kill session command' — "
+ "terminology must match the flyout's 'Kill Session' action"
+ )
+ assert "Delete session command" not in html and "Delete Session Command" not in html, (
+ "Settings Commands tab must not use 'Delete session command' — "
+ "rename to 'Kill session command' to match the flyout"
)
diff --git a/muxplex/tests/test_frontend_js.py b/muxplex/tests/test_frontend_js.py
index 987209e..ba84c3a 100644
--- a/muxplex/tests/test_frontend_js.py
+++ b/muxplex/tests/test_frontend_js.py
@@ -3502,8 +3502,11 @@ def test_delete_active_view_persists_active_view_to_server() -> None:
)
-def test_rename_active_view_persists_active_view_to_server() -> None:
- """When renaming the active view in renderViewsSettingsTab, PATCH /api/state with active_view."""
+def test_views_settings_tab_no_inline_rename_commit() -> None:
+ """renderViewsSettingsTab must NOT have inline rename (commitRename removed in Issue 6).
+
+ Rename now lives in the Manage View panel, not the settings tab.
+ """
match = re.search(
r"function renderViewsSettingsTab\s*\(\s*\)\s*\{(.*?)(?=\nfunction |\n// )",
_JS,
@@ -3511,15 +3514,10 @@ def test_rename_active_view_persists_active_view_to_server() -> None:
)
assert match, "renderViewsSettingsTab function not found"
body = match.group(1)
- # Find the commitRename block or rename path
- rename_idx = body.find("commitRename")
- assert rename_idx >= 0, "commitRename not found in renderViewsSettingsTab"
- rename_block = body[rename_idx:]
- assert "api(" in rename_block, (
- "renderViewsSettingsTab rename path must call api() to persist active_view"
- )
- assert "active_view" in rename_block, (
- "renderViewsSettingsTab rename path must PATCH /api/state with active_view"
+ # Inline rename (commitRename) must be gone — it moved to the Manage View panel
+ assert "commitRename" not in body, (
+ "renderViewsSettingsTab must not have inline commitRename — "
+ "rename moved to the Manage View panel (Issue 6)"
)
@@ -3567,8 +3565,8 @@ def test_show_new_view_input_reserved_name_check_is_case_insensitive() -> None:
)
-def test_rename_reserved_name_check_is_case_insensitive() -> None:
- """The rename commitRename reserved name check must use toLowerCase()."""
+def test_rename_reserved_name_check_moved_to_manage_view_panel() -> None:
+ """renderViewsSettingsTab must NOT contain commitRename — it moved to Manage View panel (Issue 6)."""
match = re.search(
r"function renderViewsSettingsTab\s*\(\s*\)\s*\{(.*?)(?=\nfunction |\n// )",
_JS,
@@ -3576,19 +3574,17 @@ def test_rename_reserved_name_check_is_case_insensitive() -> None:
)
assert match, "renderViewsSettingsTab function not found"
body = match.group(1)
- rename_idx = body.find("commitRename")
- assert rename_idx >= 0, "commitRename not found in renderViewsSettingsTab"
- rename_block = body[rename_idx:]
- assert "toLowerCase" in rename_block, (
- "commitRename reserved name check must use toLowerCase() — spec requires case-insensitive"
+ # commitRename was removed from settings tab — it now lives in openManageViewPanel
+ assert "commitRename" not in body, (
+ "renderViewsSettingsTab must not have commitRename — rename moved to Manage View panel (Issue 6)"
)
# ─── Fix 8: rename input maxLength ───────────────────────────────────────────
-def test_rename_input_has_max_length() -> None:
- """renderViewsSettingsTab rename input must have maxLength = 30."""
+def test_rename_input_not_in_settings_tab() -> None:
+ """renderViewsSettingsTab must NOT have views-settings-rename-input (Issue 6: inline rename removed)."""
match = re.search(
r"function renderViewsSettingsTab\s*\(\s*\)\s*\{(.*?)(?=\nfunction |\n// )",
_JS,
@@ -3596,12 +3592,9 @@ def test_rename_input_has_max_length() -> None:
)
assert match, "renderViewsSettingsTab function not found"
body = match.group(1)
- rename_idx = body.find("views-settings-rename-input")
- assert rename_idx >= 0, "rename input not found in renderViewsSettingsTab"
- rename_block = body[rename_idx : rename_idx + 400]
- assert "maxLength" in rename_block, (
- "renderViewsSettingsTab rename input must set maxLength = 30 "
- "(matches creation input constraint)"
+ assert "views-settings-rename-input" not in body, (
+ "renderViewsSettingsTab must NOT have views-settings-rename-input — "
+ "inline rename was moved to the Manage View panel (Issue 6)"
)
@@ -3889,40 +3882,40 @@ def test_do_kill_inline_shows_confirmation_buttons() -> None:
# ============================================================
-def test_open_add_sessions_panel_function_exists() -> None:
- """app.js must define an openAddSessionsPanel function."""
- assert "function openAddSessionsPanel" in _JS, (
- "app.js must contain an openAddSessionsPanel function"
+def test_open_manage_view_panel_function_exists() -> None:
+ """app.js must define an openManageViewPanel function (renamed from openAddSessionsPanel)."""
+ assert "function openManageViewPanel" in _JS, (
+ "app.js must contain an openManageViewPanel function"
)
-def test_close_add_sessions_panel_function_exists() -> None:
- """app.js must define a closeAddSessionsPanel function."""
- assert "function closeAddSessionsPanel" in _JS, (
- "app.js must contain a closeAddSessionsPanel function"
+def test_close_manage_view_panel_function_exists() -> None:
+ """app.js must define a closeManageViewPanel function (renamed from closeAddSessionsPanel)."""
+ assert "function closeManageViewPanel" in _JS, (
+ "app.js must contain a closeManageViewPanel function"
)
-def test_render_add_sessions_list_function_exists() -> None:
- """app.js must define a renderAddSessionsList function."""
- assert "function renderAddSessionsList" in _JS, (
- "app.js must contain a renderAddSessionsList function"
+def test_render_manage_view_list_function_exists() -> None:
+ """app.js must define a renderManageViewList function (renamed from renderAddSessionsList)."""
+ assert "function renderManageViewList" in _JS, (
+ "app.js must contain a renderManageViewList function"
)
-def test_add_sessions_uses_immediate_commit() -> None:
- """renderAddSessionsList must PATCH immediately on checkbox change (no batch Done)."""
- fn_body = _JS.split("function renderAddSessionsList")[1].split("\nfunction ")[0]
+def test_manage_view_uses_immediate_commit() -> None:
+ """renderManageViewList must PATCH immediately on checkbox change (no batch Done)."""
+ fn_body = _JS.split("function renderManageViewList")[1].split("\nfunction ")[0]
assert "PATCH" in fn_body or "api(" in fn_body, (
- "renderAddSessionsList must fire PATCH on each checkbox change (immediate commit)"
+ "renderManageViewList must fire PATCH on each checkbox change (immediate commit)"
)
-def test_add_sessions_shows_device_name() -> None:
- """renderAddSessionsList must show device name next to each session."""
- fn_body = _JS.split("function renderAddSessionsList")[1].split("\nfunction ")[0]
+def test_manage_view_shows_device_name() -> None:
+ """renderManageViewList must show device name next to each session."""
+ fn_body = _JS.split("function renderManageViewList")[1].split("\nfunction ")[0]
assert "deviceName" in fn_body or "device" in fn_body, (
- "renderAddSessionsList must show device name for disambiguation"
+ "renderManageViewList must show device name for disambiguation"
)
@@ -3947,23 +3940,22 @@ def test_open_flyout_menu_checks_mobile() -> None:
# ─── Task 11: Add Sessions entry point in grid ───────────────────────────────
-def test_render_grid_has_add_sessions_affordance() -> None:
- """app.js must have an 'Add Sessions' affordance for user views.
+def test_render_view_dropdown_has_manage_view_affordance() -> None:
+ """renderViewDropdown must include a 'Manage [ViewName]...' action for user views.
- The affordance moved from a tile in renderGrid to a header button (#add-sessions-btn)
- managed by updateAddSessionsButton(). Either the old tile approach or the new header
- button approach satisfies this requirement.
+ The affordance moved from a header button to the dropdown's 'Manage [ViewName]...' item.
+ This item should open the Manage View panel for the current user view.
"""
- fn_body = _JS.split("function renderGrid")[1].split("\nfunction ")[0]
- # Old approach: tile in the grid | New approach: header button via updateAddSessionsButton
- has_affordance = (
- "add-sessions" in fn_body.lower()
- or "openAddSessionsPanel" in fn_body
- or "updateAddSessionsButton" in _JS
+ match = re.search(
+ r"function renderViewDropdown\s*\(\s*\)\s*\{(.*?)(?=\nfunction |\n// )",
+ _JS,
+ re.DOTALL,
)
- assert has_affordance, (
- "app.js must have an 'Add Sessions' affordance for user views — "
- "either as a tile in renderGrid or as a header button via updateAddSessionsButton"
+ assert match, "renderViewDropdown function not found"
+ body = match.group(1)
+ assert "manage-view" in body, (
+ "renderViewDropdown must include a 'Manage [ViewName]...' action with data-action='manage-view' "
+ "for user views — this replaced the old #add-sessions-btn header button"
)
@@ -4028,21 +4020,21 @@ def test_flyout_delegation_handler_no_stop_propagation() -> None:
# ── BUG 2: disclosure has no hover-based show/hide in renderAddSessionsList ──
-def test_render_add_sessions_list_no_hover_disclosure() -> None:
- """renderAddSessionsList must NOT have mouseover/mouseout handlers for disclosure.
+def test_render_manage_view_list_no_hover_disclosure() -> None:
+ """renderManageViewList must NOT have mouseover/mouseout handlers for disclosure.
BUG: The disclosure was shown only on hover via JS mouseover/mouseout. This broke
on mobile (no hover) and was inconsistent. Fix: disclose statically — remove CSS
display:none and remove the hover handlers. The disclosure appears whenever the
hidden item is in the HTML (it's already conditionally rendered for hidden items).
"""
- fn_body = _JS.split("function renderAddSessionsList")[1].split("\nfunction ")[0]
+ fn_body = _JS.split("function renderManageViewList")[1].split("\nfunction ")[0]
assert "onmouseover" not in fn_body, (
- "renderAddSessionsList must not use onmouseover to show disclosure — "
+ "renderManageViewList must not use onmouseover to show disclosure — "
"the disclosure must be statically visible (BUG 2 fix)"
)
assert "onmouseout" not in fn_body, (
- "renderAddSessionsList must not use onmouseout to hide disclosure — "
+ "renderManageViewList must not use onmouseout to hide disclosure — "
"the disclosure must be statically visible (BUG 2 fix)"
)
@@ -4349,8 +4341,8 @@ def test_render_view_dropdown_shows_user_view_session_count() -> None:
# — Issue 3: Empty new view opens Add Sessions panel ———————————————————
-def test_show_new_view_input_calls_open_add_sessions_panel() -> None:
- """showNewViewInput must call openAddSessionsPanel after creating a new view."""
+def test_show_new_view_input_calls_open_manage_view_panel() -> None:
+ """showNewViewInput must call openManageViewPanel after creating a new view."""
match = re.search(
r"function showNewViewInput\s*\(\s*\)\s*\{(.*?)(?=\nasync function |\nfunction |\n// )",
_JS,
@@ -4358,14 +4350,14 @@ def test_show_new_view_input_calls_open_add_sessions_panel() -> None:
)
assert match, "showNewViewInput function not found"
body = match.group(1)
- assert "openAddSessionsPanel" in body, (
- "showNewViewInput must call openAddSessionsPanel() after creating a new view — "
- "so the user immediately sees the Add Sessions panel for their empty view"
+ assert "openManageViewPanel" in body, (
+ "showNewViewInput must call openManageViewPanel() after creating a new view — "
+ "so the user immediately sees the Manage View panel for their empty view"
)
-def test_show_sidebar_new_view_input_calls_open_add_sessions_panel() -> None:
- """showSidebarNewViewInput must call openAddSessionsPanel after creating a new view."""
+def test_show_sidebar_new_view_input_calls_open_manage_view_panel() -> None:
+ """showSidebarNewViewInput must call openManageViewPanel after creating a new view."""
match = re.search(
r"function showSidebarNewViewInput\s*\(\s*\)\s*\{(.*?)(?=\nasync function |\nfunction |\n// )",
_JS,
@@ -4373,8 +4365,8 @@ def test_show_sidebar_new_view_input_calls_open_add_sessions_panel() -> None:
)
assert match, "showSidebarNewViewInput function not found"
body = match.group(1)
- assert "openAddSessionsPanel" in body, (
- "showSidebarNewViewInput must call openAddSessionsPanel() after creating a new view"
+ assert "openManageViewPanel" in body, (
+ "showSidebarNewViewInput must call openManageViewPanel() after creating a new view"
)
@@ -4417,16 +4409,16 @@ def test_open_flyout_submenu_new_view_creates_and_switches() -> None:
# — Issue 5: Add Sessions header button ————————————————————————————————
-def test_update_add_sessions_button_function_exists() -> None:
- """updateAddSessionsButton function must exist in app.js."""
- assert "function updateAddSessionsButton" in _JS, (
- "updateAddSessionsButton must be defined in app.js — "
- "shows/hides the '+ Add' header button based on active view"
+def test_update_add_sessions_button_removed() -> None:
+ """updateAddSessionsButton must be REMOVED — the + Add button is gone (Issue 5)."""
+ assert "function updateAddSessionsButton" not in _JS, (
+ "updateAddSessionsButton must be removed — the #add-sessions-btn header button "
+ "was removed and replaced by the 'Manage [ViewName]...' dropdown item"
)
-def test_switch_view_calls_update_add_sessions_button() -> None:
- """switchView must call updateAddSessionsButton to update header button visibility."""
+def test_switch_view_no_update_add_sessions_button() -> None:
+ """switchView must NOT call updateAddSessionsButton (it was removed in Issue 5)."""
match = re.search(
r"function switchView\s*\(\w+\)\s*\{(.*?)(?=\nfunction |\nasync function |\n// )",
_JS,
@@ -4434,22 +4426,22 @@ def test_switch_view_calls_update_add_sessions_button() -> None:
)
assert match, "switchView function not found"
body = match.group(1)
- assert "updateAddSessionsButton" in body, (
- "switchView must call updateAddSessionsButton() to show/hide the header Add button"
+ assert "updateAddSessionsButton" not in body, (
+ "switchView must not call updateAddSessionsButton() — it was removed in Issue 5"
)
def test_render_grid_no_muxplex_app_onclick() -> None:
- """renderGrid must not use the broken window.MuxplexApp.openAddSessionsPanel onclick."""
+ """renderGrid must not use the broken window.MuxplexApp.openManageViewPanel onclick."""
fn_body = _JS.split("function renderGrid")[1].split("\nfunction ")[0]
assert "MuxplexApp.openAddSessionsPanel" not in fn_body, (
"renderGrid must not use onclick='window.MuxplexApp.openAddSessionsPanel()' — "
- "this was broken; the Add Sessions entry point moved to the header button"
+ "this was broken; the Manage View entry point moved to the dropdown"
)
-def test_bind_static_event_listeners_binds_add_sessions_btn() -> None:
- """bindStaticEventListeners must bind #add-sessions-btn click to openAddSessionsPanel."""
+def test_bind_static_event_listeners_no_add_sessions_btn() -> None:
+ """bindStaticEventListeners must NOT bind #add-sessions-btn (button was removed in Issue 5)."""
match = re.search(
r"function bindStaticEventListeners\s*\(\s*\)\s*\{(.*?)\n\}",
_JS,
@@ -4457,8 +4449,9 @@ def test_bind_static_event_listeners_binds_add_sessions_btn() -> None:
)
assert match, "bindStaticEventListeners function not found"
body = match.group(1)
- assert "add-sessions-btn" in body, (
- "bindStaticEventListeners must bind #add-sessions-btn click to openAddSessionsPanel"
+ assert "add-sessions-btn" not in body, (
+ "bindStaticEventListeners must not bind #add-sessions-btn — "
+ "the button was removed; Manage View is reached via the dropdown"
)
@@ -4488,3 +4481,151 @@ def test_tile_options_btn_css_not_absolute() -> None:
".tile-options-btn must not use position:absolute — "
"it should be an inline flex item inside tile-header to prevent device badge overlap"
)
+
+
+# ============================================================
+# UX Overhaul — 9 refinements from live testing
+# ============================================================
+
+
+# — Issue 1: Sidebar dropdown "New View" click-dismiss race ——————————————
+
+
+def test_sidebar_click_outside_has_new_view_input_guard() -> None:
+ """Sidebar click-outside handler must guard against new-view input dismiss race.
+
+ Race condition: clicking '+ New View' in the sidebar triggers the click-outside
+ handler before the input appears. Guard: check for .view-dropdown__new-input
+ presence and return early if found.
+ """
+ match = re.search(
+ r"// Click-outside closes the sidebar view dropdown\s*\n\s*"
+ r"document\.addEventListener\('click',\s*function\(e\)\s*\{(.*?)\}\s*\);",
+ _JS,
+ re.DOTALL,
+ )
+ assert match, "Click-outside handler for sidebar view dropdown not found"
+ body = match.group(1)
+ assert ".view-dropdown__new-input" in body, (
+ "Sidebar click-outside handler must guard: "
+ "if (sidebarDropdown.querySelector('.view-dropdown__new-input')) return; "
+ "— prevents race where the new-view input is dismissed immediately"
+ )
+
+
+# — Issue 4: Dropdown structure — "Manage [ViewName]…" ————————————————
+
+
+def test_render_view_dropdown_has_manage_view_item_for_user_view() -> None:
+ """renderViewDropdown must include 'Manage [ViewName]...' action for user views."""
+ match = re.search(
+ r"function renderViewDropdown\s*\(\s*\)\s*\{(.*?)(?=\nfunction |\n// )",
+ _JS,
+ re.DOTALL,
+ )
+ assert match, "renderViewDropdown function not found"
+ body = match.group(1)
+ assert "manage-view" in body, (
+ "renderViewDropdown must include a 'Manage [ViewName]...' action "
+ "with data-action='manage-view' for user views"
+ )
+
+
+def test_render_sidebar_dropdown_has_manage_view_item_for_user_view() -> None:
+ """renderSidebarViewDropdown must include 'Manage [ViewName]...' action for user views."""
+ match = re.search(
+ r"function renderSidebarViewDropdown\s*\(\s*\)\s*\{(.*?)(?=\nfunction |\n// )",
+ _JS,
+ re.DOTALL,
+ )
+ assert match, "renderSidebarViewDropdown function not found"
+ body = match.group(1)
+ assert "manage-view" in body, (
+ "renderSidebarViewDropdown must include a 'Manage [ViewName]...' action "
+ "with data-action='manage-view' for user views"
+ )
+
+
+# — Issue 5: No #add-sessions-btn + updateAddSessionsButton removed ————————
+
+
+def test_domcontentloaded_no_update_add_sessions_button_call() -> None:
+ """DOMContentLoaded handler must NOT call updateAddSessionsButton (it was removed)."""
+ assert "updateAddSessionsButton" not in _JS, (
+ "updateAddSessionsButton must be completely removed from app.js — "
+ "the #add-sessions-btn header button was replaced by the dropdown manage-view item"
+ )
+
+
+# — Issue 6: Manage View settings tab ——————————————————————————————————
+
+
+def test_views_settings_tab_has_manage_button_per_row() -> None:
+ """renderViewsSettingsTab must include a 'Manage' button per view row."""
+ match = re.search(
+ r"function renderViewsSettingsTab\s*\(\s*\)\s*\{(.*?)(?=\nfunction |\n// )",
+ _JS,
+ re.DOTALL,
+ )
+ assert match, "renderViewsSettingsTab function not found"
+ body = match.group(1)
+ assert "manage-view" in body.lower() or "Manage" in body, (
+ "renderViewsSettingsTab must include a 'Manage' button per view row "
+ "that opens the Manage View panel for that view"
+ )
+
+
+def test_views_settings_tab_has_new_view_button_at_bottom() -> None:
+ """renderViewsSettingsTab must include a '+ New View' button at the bottom."""
+ match = re.search(
+ r"function renderViewsSettingsTab\s*\(\s*\)\s*\{(.*?)(?=\nfunction |\n// )",
+ _JS,
+ re.DOTALL,
+ )
+ assert match, "renderViewsSettingsTab function not found"
+ body = match.group(1)
+ assert "New View" in body or "new-view" in body, (
+ "renderViewsSettingsTab must include a '+ New View' button at the bottom"
+ )
+
+
+def test_views_settings_tab_no_inline_rename_on_name_click() -> None:
+ """renderViewsSettingsTab must NOT have click-to-rename on the name span.
+
+ Rename now lives in the Manage View panel, not in the settings tab.
+ """
+ match = re.search(
+ r"function renderViewsSettingsTab\s*\(\s*\)\s*\{(.*?)(?=\nfunction |\n// )",
+ _JS,
+ re.DOTALL,
+ )
+ assert match, "renderViewsSettingsTab function not found"
+ body = match.group(1)
+ # The old inline rename code clicked on views-settings-name span to show an input
+ assert "views-settings-rename-input" not in body, (
+ "renderViewsSettingsTab must not have inline rename via views-settings-rename-input — "
+ "rename now lives in the Manage View panel"
+ )
+
+
+# — Issue 7: Dropdown label fix on page reload —————————————————————————
+
+
+def test_domcontentloaded_calls_render_view_dropdown_after_restore() -> None:
+ """DOMContentLoaded must call renderViewDropdown() after restoreState() completes.
+
+ Bug: restoreState() sets _activeView but the dropdown label stays as 'All Sessions'.
+ Fix: call renderViewDropdown() after restoreState() so the label reflects _activeView.
+ """
+ # Find the DOMContentLoaded handler section (from DOMContentLoaded to end of file)
+ idx = _JS.find("document.addEventListener('DOMContentLoaded'")
+ assert idx >= 0, "DOMContentLoaded handler not found"
+ handler_section = _JS[idx:]
+ # restoreState must appear before renderViewDropdown in the handler
+ restore_idx = handler_section.find("restoreState()")
+ assert restore_idx >= 0, "restoreState call not found in DOMContentLoaded section"
+ after_restore = handler_section[restore_idx:]
+ assert "renderViewDropdown" in after_restore, (
+ "DOMContentLoaded must call renderViewDropdown() after restoreState() so the "
+ "dropdown label correctly reflects _activeView on page reload"
+ )