feat: add settings dialog HTML structure and CSS
This commit is contained in:
@@ -20,7 +20,11 @@
|
|||||||
<div id="view-overview" class="view view--active">
|
<div id="view-overview" class="view view--active">
|
||||||
<header class="app-header">
|
<header class="app-header">
|
||||||
<h1 class="app-wordmark"><img src="/wordmark-on-dark.svg" alt="muxplex" height="24" /></h1>
|
<h1 class="app-wordmark"><img src="/wordmark-on-dark.svg" alt="muxplex" height="24" /></h1>
|
||||||
<span id="connection-status"></span>
|
<div class="header-actions">
|
||||||
|
<button id="new-session-btn" class="header-btn" aria-label="New session">+</button>
|
||||||
|
<button id="settings-btn" class="header-btn" aria-label="Settings">⚙</button>
|
||||||
|
<span id="connection-status"></span>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div id="session-grid" class="session-grid" role="list"></div>
|
<div id="session-grid" class="session-grid" role="list"></div>
|
||||||
<div id="empty-state" class="empty-state hidden">No active tmux sessions</div>
|
<div id="empty-state" class="empty-state hidden">No active tmux sessions</div>
|
||||||
@@ -32,6 +36,7 @@
|
|||||||
<button id="back-btn" class="back-btn" aria-label="Back">←</button>
|
<button id="back-btn" class="back-btn" aria-label="Back">←</button>
|
||||||
<button id="sidebar-toggle-btn" class="sidebar-toggle-btn" aria-label="Toggle session list">☰</button>
|
<button id="sidebar-toggle-btn" class="sidebar-toggle-btn" aria-label="Toggle session list">☰</button>
|
||||||
<span id="expanded-session-name" class="expanded-session-name"></span>
|
<span id="expanded-session-name" class="expanded-session-name"></span>
|
||||||
|
<button id="settings-btn-expanded" class="header-btn" aria-label="Settings">⚙</button>
|
||||||
</header>
|
</header>
|
||||||
<div class="view-body">
|
<div class="view-body">
|
||||||
<div id="session-sidebar" class="session-sidebar">
|
<div id="session-sidebar" class="session-sidebar">
|
||||||
@@ -64,6 +69,55 @@
|
|||||||
<!-- ── Toast notification ─────────────────────────────────────────────────── -->
|
<!-- ── Toast notification ─────────────────────────────────────────────────── -->
|
||||||
<div id="toast" class="toast hidden" role="status" aria-live="polite" aria-atomic="true"></div>
|
<div id="toast" class="toast hidden" role="status" aria-live="polite" aria-atomic="true"></div>
|
||||||
|
|
||||||
|
<!-- ── Settings ─────────────────────────────────────────────────────────────────────── -->
|
||||||
|
<div id="settings-backdrop" class="settings-backdrop hidden"></div>
|
||||||
|
<dialog id="settings-dialog" class="settings-dialog">
|
||||||
|
<div class="settings-layout">
|
||||||
|
<nav class="settings-tabs">
|
||||||
|
<button class="settings-tab settings-tab--active" data-tab="display">Display</button>
|
||||||
|
<button class="settings-tab" data-tab="sessions">Sessions</button>
|
||||||
|
<button class="settings-tab" data-tab="notifications">Notifications</button>
|
||||||
|
<button class="settings-tab" data-tab="new-session">New Session</button>
|
||||||
|
</nav>
|
||||||
|
<div class="settings-content">
|
||||||
|
<div class="settings-panel" data-panel="display">
|
||||||
|
<div class="settings-field">
|
||||||
|
<label class="settings-label" for="setting-font-size">Font Size</label>
|
||||||
|
<select id="setting-font-size" class="settings-select">
|
||||||
|
<option value="11">11</option>
|
||||||
|
<option value="12">12</option>
|
||||||
|
<option value="13">13</option>
|
||||||
|
<option value="14" selected>14</option>
|
||||||
|
<option value="16">16</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="settings-field">
|
||||||
|
<label class="settings-label" for="setting-hover-delay">Hover Delay</label>
|
||||||
|
<select id="setting-hover-delay" class="settings-select">
|
||||||
|
<option value="0">Off</option>
|
||||||
|
<option value="1000">1000ms</option>
|
||||||
|
<option value="1500" selected>1500ms</option>
|
||||||
|
<option value="2000">2000ms</option>
|
||||||
|
<option value="3000">3000ms</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="settings-field">
|
||||||
|
<label class="settings-label" for="setting-grid-columns">Grid Columns</label>
|
||||||
|
<select id="setting-grid-columns" class="settings-select">
|
||||||
|
<option value="auto" selected>Auto</option>
|
||||||
|
<option value="2">2</option>
|
||||||
|
<option value="3">3</option>
|
||||||
|
<option value="4">4</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-panel hidden" data-panel="sessions"></div>
|
||||||
|
<div class="settings-panel hidden" data-panel="notifications"></div>
|
||||||
|
<div class="settings-panel hidden" data-panel="new-session"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
<!-- ── Scripts ─────────────────────────────────────────────────────────────── -->
|
<!-- ── Scripts ─────────────────────────────────────────────────────────────── -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js"></script>
|
||||||
|
|||||||
@@ -873,6 +873,192 @@ body {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
Header actions + settings buttons
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-btn {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
background: none;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--text);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-btn:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
Settings dialog
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
.settings-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
z-index: 299;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-dialog {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 600px;
|
||||||
|
height: 480px;
|
||||||
|
background: var(--bg-surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
z-index: 300;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-dialog::backdrop {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-layout {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-tabs {
|
||||||
|
width: 140px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border-right: 1px solid var(--border);
|
||||||
|
padding: 8px 0;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-tab {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-left: 3px solid transparent;
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
padding: 10px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-tab:hover {
|
||||||
|
color: var(--text);
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-tab--active {
|
||||||
|
color: var(--accent);
|
||||||
|
border-left-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-panel-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text);
|
||||||
|
margin: 0 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-select {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
Settings dialog — mobile (bottom sheet at ≤599px)
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
@media (max-width: 599px) {
|
||||||
|
.settings-dialog {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: auto;
|
||||||
|
transform: none;
|
||||||
|
width: 100%;
|
||||||
|
height: 85vh;
|
||||||
|
border-radius: 12px 12px 0 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-layout {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-tabs {
|
||||||
|
width: auto;
|
||||||
|
flex-direction: row;
|
||||||
|
overflow-x: auto;
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
padding: 0;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-tab {
|
||||||
|
border-left: none;
|
||||||
|
border-bottom: 3px solid transparent;
|
||||||
|
white-space: nowrap;
|
||||||
|
padding: 10px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-tab--active {
|
||||||
|
border-bottom-color: var(--accent);
|
||||||
|
border-left-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
Responsive overlay sidebar at <960px
|
Responsive overlay sidebar at <960px
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -711,3 +711,143 @@ def test_no_gradient_fade_on_previews():
|
|||||||
css = read_css()
|
css = read_css()
|
||||||
assert ".tile-body::before" not in css, "tile-body::before gradient must be removed"
|
assert ".tile-body::before" not in css, "tile-body::before gradient must be removed"
|
||||||
assert ".sidebar-item-body::before" not in css, "sidebar-item-body::before gradient must be removed"
|
assert ".sidebar-item-body::before" not in css, "sidebar-item-body::before gradient must be removed"
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Settings modal CSS (task-6)
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
|
||||||
|
def test_css_header_actions_exists():
|
||||||
|
""".header-actions must exist with flex layout and gap."""
|
||||||
|
css = read_css()
|
||||||
|
assert ".header-actions" in css, "Missing .header-actions CSS rule"
|
||||||
|
block = _extract_rule_block(css, ".header-actions {")
|
||||||
|
assert "display: flex" in block, ".header-actions must use display: flex"
|
||||||
|
assert "gap:" in block and "8px" in block, ".header-actions must have gap: 8px"
|
||||||
|
|
||||||
|
|
||||||
|
def test_css_header_btn_exists():
|
||||||
|
""".header-btn must exist with 32x32 size, border, cursor."""
|
||||||
|
css = read_css()
|
||||||
|
assert ".header-btn" in css, "Missing .header-btn CSS rule"
|
||||||
|
block = _extract_rule_block(css, ".header-btn {")
|
||||||
|
assert "width: 32px" in block, ".header-btn must be 32px wide"
|
||||||
|
assert "height: 32px" in block, ".header-btn must be 32px tall"
|
||||||
|
assert "border:" in block or "border :" in block, ".header-btn must have border"
|
||||||
|
assert "cursor: pointer" in block, ".header-btn must have cursor: pointer"
|
||||||
|
|
||||||
|
|
||||||
|
def test_css_header_btn_hover():
|
||||||
|
""".header-btn:hover must exist."""
|
||||||
|
css = read_css()
|
||||||
|
assert ".header-btn:hover" in css, "Missing .header-btn:hover CSS rule"
|
||||||
|
|
||||||
|
|
||||||
|
def test_css_settings_backdrop_exists():
|
||||||
|
""".settings-backdrop must exist with position: fixed and blur."""
|
||||||
|
css = read_css()
|
||||||
|
assert ".settings-backdrop" in css, "Missing .settings-backdrop CSS rule"
|
||||||
|
block = _extract_rule_block(css, ".settings-backdrop {")
|
||||||
|
assert "position: fixed" in block, ".settings-backdrop must use position: fixed"
|
||||||
|
assert "blur" in block or "backdrop-filter" in block, ".settings-backdrop must use blur"
|
||||||
|
|
||||||
|
|
||||||
|
def test_css_settings_dialog_exists():
|
||||||
|
""".settings-dialog must exist with correct dimensions and z-index."""
|
||||||
|
css = read_css()
|
||||||
|
assert ".settings-dialog" in css, "Missing .settings-dialog CSS rule"
|
||||||
|
block = _extract_rule_block(css, ".settings-dialog {")
|
||||||
|
assert "600px" in block, ".settings-dialog must be 600px wide"
|
||||||
|
assert "480px" in block, ".settings-dialog must be 480px tall"
|
||||||
|
assert "z-index: 300" in block, ".settings-dialog must have z-index: 300"
|
||||||
|
assert "border-radius:" in block, ".settings-dialog must have border-radius"
|
||||||
|
|
||||||
|
|
||||||
|
def test_css_settings_dialog_backdrop_transparent():
|
||||||
|
""".settings-dialog::backdrop must be transparent."""
|
||||||
|
css = read_css()
|
||||||
|
assert ".settings-dialog::backdrop" in css, "Missing .settings-dialog::backdrop CSS rule"
|
||||||
|
block = _extract_rule_block(css, ".settings-dialog::backdrop {")
|
||||||
|
assert "transparent" in block, ".settings-dialog::backdrop must be transparent"
|
||||||
|
|
||||||
|
|
||||||
|
def test_css_settings_layout_flex():
|
||||||
|
""".settings-layout must use flex layout."""
|
||||||
|
css = read_css()
|
||||||
|
assert ".settings-layout" in css, "Missing .settings-layout CSS rule"
|
||||||
|
block = _extract_rule_block(css, ".settings-layout {")
|
||||||
|
assert "display: flex" in block, ".settings-layout must use display: flex"
|
||||||
|
|
||||||
|
|
||||||
|
def test_css_settings_tabs_exists():
|
||||||
|
""".settings-tabs must exist with 140px width sidebar."""
|
||||||
|
css = read_css()
|
||||||
|
assert ".settings-tabs" in css, "Missing .settings-tabs CSS rule"
|
||||||
|
block = _extract_rule_block(css, ".settings-tabs {")
|
||||||
|
assert "140px" in block, ".settings-tabs must have 140px width"
|
||||||
|
|
||||||
|
|
||||||
|
def test_css_settings_tab_exists():
|
||||||
|
""".settings-tab must exist as text buttons with left border indicator."""
|
||||||
|
css = read_css()
|
||||||
|
assert ".settings-tab" in css, "Missing .settings-tab CSS rule"
|
||||||
|
block = _extract_rule_block(css, ".settings-tab {")
|
||||||
|
assert "border-left:" in block, ".settings-tab must have border-left indicator"
|
||||||
|
assert "cursor: pointer" in block, ".settings-tab must have cursor: pointer"
|
||||||
|
|
||||||
|
|
||||||
|
def test_css_settings_tab_active():
|
||||||
|
""".settings-tab--active must exist with accent color."""
|
||||||
|
css = read_css()
|
||||||
|
assert ".settings-tab--active" in css, "Missing .settings-tab--active CSS rule"
|
||||||
|
block = _extract_rule_block(css, ".settings-tab--active {")
|
||||||
|
assert "var(--accent)" in block, ".settings-tab--active must use var(--accent)"
|
||||||
|
|
||||||
|
|
||||||
|
def test_css_settings_content_exists():
|
||||||
|
""".settings-content must exist as scrollable container."""
|
||||||
|
css = read_css()
|
||||||
|
assert ".settings-content" in css, "Missing .settings-content CSS rule"
|
||||||
|
block = _extract_rule_block(css, ".settings-content {")
|
||||||
|
assert "overflow-y: auto" in block, ".settings-content must be scrollable (overflow-y: auto)"
|
||||||
|
|
||||||
|
|
||||||
|
def test_css_settings_field_exists():
|
||||||
|
""".settings-field must exist as flex row between."""
|
||||||
|
css = read_css()
|
||||||
|
assert ".settings-field" in css, "Missing .settings-field CSS rule"
|
||||||
|
block = _extract_rule_block(css, ".settings-field {")
|
||||||
|
assert "display: flex" in block, ".settings-field must use display: flex"
|
||||||
|
assert "flex-direction: row" in block, ".settings-field must use flex-direction: row"
|
||||||
|
assert "justify-content: space-between" in block, ".settings-field must use justify-content: space-between"
|
||||||
|
|
||||||
|
|
||||||
|
def test_css_settings_select_exists():
|
||||||
|
""".settings-select must exist as styled select."""
|
||||||
|
css = read_css()
|
||||||
|
assert ".settings-select" in css, "Missing .settings-select CSS rule"
|
||||||
|
assert ".settings-select:focus" in css, "Missing .settings-select:focus CSS rule"
|
||||||
|
|
||||||
|
|
||||||
|
def test_css_settings_mobile_media_query():
|
||||||
|
"""@media (max-width: 599px) block must exist for settings dialog mobile styles."""
|
||||||
|
css = read_css()
|
||||||
|
assert "@media (max-width: 599px)" in css, "Missing @media (max-width: 599px) for settings mobile"
|
||||||
|
media_block = _extract_media_block(css, "@media (max-width: 599px)")
|
||||||
|
assert ".settings-dialog" in media_block, ".settings-dialog mobile styles must be in 599px media block"
|
||||||
|
# Bottom sheet: 100% width
|
||||||
|
block = _extract_rule_block(media_block, ".settings-dialog {")
|
||||||
|
assert "width: 100%" in block, ".settings-dialog must be 100% wide on mobile"
|
||||||
|
assert "85vh" in block, ".settings-dialog must be 85vh tall on mobile"
|
||||||
|
assert "bottom: 0" in block or "bottom:0" in block, ".settings-dialog must be bottom-anchored on mobile"
|
||||||
|
|
||||||
|
|
||||||
|
def test_css_settings_mobile_tabs_horizontal():
|
||||||
|
"""Inside mobile media query, settings tabs must become horizontal scrolling row."""
|
||||||
|
css = read_css()
|
||||||
|
media_block = _extract_media_block(css, "@media (max-width: 599px)")
|
||||||
|
assert ".settings-tabs" in media_block, ".settings-tabs must have mobile styles in 599px media block"
|
||||||
|
tabs_block = _extract_rule_block(media_block, ".settings-tabs {")
|
||||||
|
assert "flex-direction: row" in tabs_block, ".settings-tabs must become horizontal on mobile"
|
||||||
|
assert "overflow-x: auto" in tabs_block, ".settings-tabs must scroll horizontally on mobile"
|
||||||
|
|||||||
@@ -316,3 +316,185 @@ def test_login_html_has_error_display() -> None:
|
|||||||
assert "error" in text, (
|
assert "error" in text, (
|
||||||
"login.html must include an error display element (text 'error' not found)"
|
"login.html must include an error display element (text 'error' not found)"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Settings modal (task-6)
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
|
||||||
|
def test_html_header_actions_div() -> None:
|
||||||
|
"""overview header must have .header-actions div containing #new-session-btn, #settings-btn, and #connection-status."""
|
||||||
|
soup = _SOUP
|
||||||
|
view_overview = soup.find(id="view-overview")
|
||||||
|
assert view_overview is not None, "Missing #view-overview"
|
||||||
|
header = view_overview.find("header", class_="app-header")
|
||||||
|
assert header is not None, "Missing header.app-header inside #view-overview"
|
||||||
|
header_actions = header.find(class_="header-actions")
|
||||||
|
assert header_actions is not None, "Missing .header-actions inside app-header"
|
||||||
|
|
||||||
|
|
||||||
|
def test_html_new_session_btn() -> None:
|
||||||
|
"""#new-session-btn must exist in .header-actions with class header-btn."""
|
||||||
|
soup = _SOUP
|
||||||
|
btn = soup.find(id="new-session-btn")
|
||||||
|
assert btn is not None, "Missing #new-session-btn"
|
||||||
|
classes = btn.get("class") or []
|
||||||
|
assert "header-btn" in classes, f"#new-session-btn must have class 'header-btn', has: {classes}"
|
||||||
|
# Must be inside header-actions
|
||||||
|
header_actions = soup.find(class_="header-actions")
|
||||||
|
assert header_actions is not None, "Missing .header-actions"
|
||||||
|
assert header_actions.find(id="new-session-btn") is not None, (
|
||||||
|
"#new-session-btn must be inside .header-actions"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_html_settings_btn() -> None:
|
||||||
|
"""#settings-btn must exist in .header-actions with class header-btn."""
|
||||||
|
soup = _SOUP
|
||||||
|
btn = soup.find(id="settings-btn")
|
||||||
|
assert btn is not None, "Missing #settings-btn"
|
||||||
|
classes = btn.get("class") or []
|
||||||
|
assert "header-btn" in classes, f"#settings-btn must have class 'header-btn', has: {classes}"
|
||||||
|
# Must be inside header-actions
|
||||||
|
header_actions = soup.find(class_="header-actions")
|
||||||
|
assert header_actions is not None, "Missing .header-actions"
|
||||||
|
assert header_actions.find(id="settings-btn") is not None, (
|
||||||
|
"#settings-btn must be inside .header-actions"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_html_settings_btn_expanded() -> None:
|
||||||
|
"""#settings-btn-expanded must exist in expanded-header after #expanded-session-name."""
|
||||||
|
soup = _SOUP
|
||||||
|
btn = soup.find(id="settings-btn-expanded")
|
||||||
|
assert btn is not None, "Missing #settings-btn-expanded"
|
||||||
|
classes = btn.get("class") or []
|
||||||
|
assert "header-btn" in classes, f"#settings-btn-expanded must have class 'header-btn', has: {classes}"
|
||||||
|
# Must be inside expanded-header
|
||||||
|
header = soup.find("header", class_="expanded-header")
|
||||||
|
assert header is not None, "Missing header.expanded-header"
|
||||||
|
assert header.find(id="settings-btn-expanded") is not None, (
|
||||||
|
"#settings-btn-expanded must be inside header.expanded-header"
|
||||||
|
)
|
||||||
|
# Must appear after #expanded-session-name
|
||||||
|
header_children_ids = [
|
||||||
|
el.get("id") for el in header.children if isinstance(el, Tag)
|
||||||
|
]
|
||||||
|
header_children_ids = [i for i in header_children_ids if i]
|
||||||
|
assert "expanded-session-name" in header_children_ids, "#expanded-session-name must be in expanded-header"
|
||||||
|
assert "settings-btn-expanded" in header_children_ids, "#settings-btn-expanded must be in expanded-header"
|
||||||
|
name_idx = header_children_ids.index("expanded-session-name")
|
||||||
|
settings_idx = header_children_ids.index("settings-btn-expanded")
|
||||||
|
assert name_idx < settings_idx, (
|
||||||
|
f"#settings-btn-expanded must come after #expanded-session-name, got indices {name_idx}, {settings_idx}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_html_settings_backdrop() -> None:
|
||||||
|
"""#settings-backdrop must exist with class settings-backdrop and hidden."""
|
||||||
|
soup = _SOUP
|
||||||
|
el = soup.find(id="settings-backdrop")
|
||||||
|
assert el is not None, "Missing #settings-backdrop"
|
||||||
|
classes = el.get("class") or []
|
||||||
|
assert "settings-backdrop" in classes, f"#settings-backdrop must have class 'settings-backdrop', has: {classes}"
|
||||||
|
assert "hidden" in classes, f"#settings-backdrop must have class 'hidden', has: {classes}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_html_settings_dialog() -> None:
|
||||||
|
"""#settings-dialog must be a <dialog> with class settings-dialog."""
|
||||||
|
soup = _SOUP
|
||||||
|
el = soup.find(id="settings-dialog")
|
||||||
|
assert el is not None, "Missing #settings-dialog"
|
||||||
|
assert el.name == "dialog", f"#settings-dialog must be a <dialog> element, got: {el.name}"
|
||||||
|
classes = el.get("class") or []
|
||||||
|
assert "settings-dialog" in classes, f"#settings-dialog must have class 'settings-dialog', has: {classes}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_html_settings_tabs() -> None:
|
||||||
|
"""settings-dialog must contain 4 tab buttons with correct data-tab values."""
|
||||||
|
soup = _SOUP
|
||||||
|
dialog = soup.find(id="settings-dialog")
|
||||||
|
assert dialog is not None, "Missing #settings-dialog"
|
||||||
|
tabs_container = dialog.find("nav", class_="settings-tabs")
|
||||||
|
assert tabs_container is not None, "Missing nav.settings-tabs inside #settings-dialog"
|
||||||
|
expected_tabs = ["display", "sessions", "notifications", "new-session"]
|
||||||
|
for tab_value in expected_tabs:
|
||||||
|
tab = tabs_container.find("button", attrs={"data-tab": tab_value})
|
||||||
|
assert tab is not None, f"Missing tab button with data-tab='{tab_value}' in settings-tabs"
|
||||||
|
# Display tab must be active by default
|
||||||
|
display_tab = tabs_container.find("button", attrs={"data-tab": "display"})
|
||||||
|
assert display_tab is not None
|
||||||
|
display_classes = display_tab.get("class") or []
|
||||||
|
assert "settings-tab--active" in display_classes, (
|
||||||
|
f"Display tab must have class 'settings-tab--active', has: {display_classes}"
|
||||||
|
)
|
||||||
|
# All tabs must have settings-tab class
|
||||||
|
for tab_value in expected_tabs:
|
||||||
|
tab = tabs_container.find("button", attrs={"data-tab": tab_value})
|
||||||
|
tab_classes = tab.get("class") or []
|
||||||
|
assert "settings-tab" in tab_classes, (
|
||||||
|
f"Tab data-tab='{tab_value}' must have class 'settings-tab', has: {tab_classes}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_html_settings_display_panel_controls() -> None:
|
||||||
|
"""Display panel must have font-size, hover-delay, and grid-columns selects."""
|
||||||
|
soup = _SOUP
|
||||||
|
dialog = soup.find(id="settings-dialog")
|
||||||
|
assert dialog is not None, "Missing #settings-dialog"
|
||||||
|
for ctrl_id in ("setting-font-size", "setting-hover-delay", "setting-grid-columns"):
|
||||||
|
el = dialog.find(id=ctrl_id)
|
||||||
|
assert el is not None, f"Missing #{ctrl_id} inside #settings-dialog"
|
||||||
|
assert el.name == "select", f"#{ctrl_id} must be a <select> element, got: {el.name}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_html_settings_font_size_options() -> None:
|
||||||
|
"""setting-font-size select must have options 11, 12, 13, 14 (selected), 16."""
|
||||||
|
soup = _SOUP
|
||||||
|
select = soup.find(id="setting-font-size")
|
||||||
|
assert select is not None, "Missing #setting-font-size"
|
||||||
|
options = select.find_all("option")
|
||||||
|
values = [o.get("value") for o in options]
|
||||||
|
for v in ("11", "12", "13", "14", "16"):
|
||||||
|
assert v in values, f"#setting-font-size missing option value='{v}'"
|
||||||
|
# 14 must be selected
|
||||||
|
selected_opt = select.find("option", attrs={"selected": True})
|
||||||
|
assert selected_opt is not None, "setting-font-size must have a selected option"
|
||||||
|
assert selected_opt.get("value") == "14", (
|
||||||
|
f"setting-font-size default selection must be 14, got: {selected_opt.get('value')}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_html_settings_hover_delay_options() -> None:
|
||||||
|
"""setting-hover-delay select must have Off (0), 1000, 1500 (selected), 2000, 3000."""
|
||||||
|
soup = _SOUP
|
||||||
|
select = soup.find(id="setting-hover-delay")
|
||||||
|
assert select is not None, "Missing #setting-hover-delay"
|
||||||
|
options = select.find_all("option")
|
||||||
|
values = [o.get("value") for o in options]
|
||||||
|
for v in ("0", "1000", "1500", "2000", "3000"):
|
||||||
|
assert v in values, f"#setting-hover-delay missing option value='{v}'"
|
||||||
|
# 1500 must be selected
|
||||||
|
selected_opt = select.find("option", attrs={"selected": True})
|
||||||
|
assert selected_opt is not None, "setting-hover-delay must have a selected option"
|
||||||
|
assert selected_opt.get("value") == "1500", (
|
||||||
|
f"setting-hover-delay default selection must be 1500, got: {selected_opt.get('value')}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_html_settings_grid_columns_options() -> None:
|
||||||
|
"""setting-grid-columns select must have auto (selected), 2, 3, 4."""
|
||||||
|
soup = _SOUP
|
||||||
|
select = soup.find(id="setting-grid-columns")
|
||||||
|
assert select is not None, "Missing #setting-grid-columns"
|
||||||
|
options = select.find_all("option")
|
||||||
|
values = [o.get("value") for o in options]
|
||||||
|
for v in ("auto", "2", "3", "4"):
|
||||||
|
assert v in values, f"#setting-grid-columns missing option value='{v}'"
|
||||||
|
# auto must be selected
|
||||||
|
selected_opt = select.find("option", attrs={"selected": True})
|
||||||
|
assert selected_opt is not None, "setting-grid-columns must have a selected option"
|
||||||
|
assert selected_opt.get("value") == "auto", (
|
||||||
|
f"setting-grid-columns default selection must be auto, got: {selected_opt.get('value')}"
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user