feat: add New Session settings tab with template textarea

This commit is contained in:
Brian Krabach
2026-03-30 01:03:54 -07:00
parent 2e86692c3c
commit 5f81b87348
7 changed files with 890 additions and 102 deletions
+24
View File
@@ -141,6 +141,7 @@ const DISPLAY_DEFAULTS = {
bellSound: false,
notificationPermission: 'default',
};
const NEW_SESSION_DEFAULT_TEMPLATE = 'tmux new-session -d -s {name}';
// ─── DOM helpers ──────────────────────────────────────────────────────────────
function $(id) {
@@ -1126,6 +1127,12 @@ function openSettings() {
if (autoOpenEl) {
autoOpenEl.checked = ss && ss.auto_open !== undefined ? !!ss.auto_open : true;
}
// New Session tab - populate template textarea
const templateEl = $('setting-template');
if (templateEl) {
templateEl.value = (ss && ss.new_session_template) || NEW_SESSION_DEFAULT_TEMPLATE;
}
});
}
@@ -1390,6 +1397,23 @@ function bindStaticEventListeners() {
console.error('Notification.requestPermission() failed:', err);
});
});
// New Session tab — template textarea with 500ms debounce
var _templateDebounceTimer;
on($('setting-template'), 'input', function() {
clearTimeout(_templateDebounceTimer);
var val = this.value;
_templateDebounceTimer = setTimeout(function() {
patchServerSetting('new_session_template', val);
}, 500);
});
// New Session tab — reset button restores default template
on($('setting-template-reset'), 'click', function() {
var el = $('setting-template');
if (el) el.value = NEW_SESSION_DEFAULT_TEMPLATE;
patchServerSetting('new_session_template', NEW_SESSION_DEFAULT_TEMPLATE);
});
}
// ─── Test-only helpers ────────────────────────────────────────────────────────
+8 -1
View File
@@ -152,7 +152,14 @@
</div>
</div>
</div>
<div class="settings-panel hidden" data-tab="new-session"></div>
<div class="settings-panel hidden" data-tab="new-session">
<div class="settings-field settings-field--column">
<label class="settings-label" for="setting-template">Command template</label>
<textarea id="setting-template" class="settings-textarea" rows="3" placeholder="tmux new-session -d -s {name}"></textarea>
<span class="settings-helper">{name} is replaced with the session name</span>
<button id="setting-template-reset" class="settings-action-btn">Reset to default</button>
</div>
</div>
</div>
</div>
</dialog>
+28
View File
@@ -1091,6 +1091,34 @@ body {
cursor: pointer;
}
/* ============================================================
New Session tab controls
============================================================ */
.settings-textarea {
width: 100%;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 4px;
color: var(--text);
font-family: var(--font-mono);
font-size: 13px;
padding: 10px;
resize: vertical;
min-height: 60px;
outline: none;
}
.settings-textarea:focus {
border-color: var(--accent);
}
.settings-helper {
font-size: 12px;
color: var(--text-muted);
font-style: italic;
}
/* ============================================================
Notifications tab controls
============================================================ */
+255
View File
@@ -2127,4 +2127,259 @@ test('ANSI_COLORS uses xterm.js default GTK/Tango palette', () => {
assert.strictEqual(app.ansi256Color(14), '#34e2e2', 'color 14 must be xterm.js default: #34e2e2');
});
// --- New Session tab ---
test('HTML index.html has setting-template textarea with correct attributes', () => {
const source = fs.readFileSync(
new URL('../index.html', import.meta.url), 'utf8'
);
assert.ok(source.includes('id="setting-template"'), 'must have setting-template textarea');
assert.ok(source.includes('settings-textarea'), 'must have settings-textarea class');
assert.ok(source.includes('rows="3"'), 'must have rows=3');
assert.ok(source.includes('placeholder="tmux new-session -d -s {name}"'), 'must have correct placeholder');
});
test('HTML index.html has setting-template-reset button', () => {
const source = fs.readFileSync(
new URL('../index.html', import.meta.url), 'utf8'
);
assert.ok(source.includes('id="setting-template-reset"'), 'must have setting-template-reset button');
assert.ok(source.includes('settings-action-btn'), 'must use settings-action-btn class on reset button');
});
test('HTML index.html has settings-helper text for template', () => {
const source = fs.readFileSync(
new URL('../index.html', import.meta.url), 'utf8'
);
assert.ok(source.includes('settings-helper'), 'must have settings-helper class');
assert.ok(source.includes('{name} is replaced with the session name'), 'must have helper text');
});
test('CSS style.css has .settings-textarea class', () => {
const source = fs.readFileSync(
new URL('../style.css', import.meta.url), 'utf8'
);
assert.ok(source.includes('.settings-textarea'), 'must have .settings-textarea CSS class');
});
test('CSS style.css has .settings-helper class', () => {
const source = fs.readFileSync(
new URL('../style.css', import.meta.url), 'utf8'
);
assert.ok(source.includes('.settings-helper'), 'must have .settings-helper CSS class');
});
test('openSettings populates setting-template textarea from server settings', async () => {
// Reset _currentSessions to empty to avoid createTextNode calls in openSettings callback
app._setCurrentSessions([]);
const elements = {};
const origFetch = globalThis.fetch;
globalThis.fetch = async (url) => {
if (url === '/api/settings') {
return {
ok: true,
json: async () => ({ new_session_template: 'my-custom-template' }),
};
}
return { ok: true, json: async () => ({}) };
};
const origGetById = globalThis.document.getElementById;
globalThis.document.getElementById = (id) => {
if (!elements[id]) {
elements[id] = {
value: '',
checked: false,
innerHTML: '',
disabled: false,
appendChild: () => {},
querySelectorAll: () => [],
classList: { add: () => {}, remove: () => {} },
showModal: () => {},
close: () => {},
addEventListener: () => {},
};
}
return elements[id];
};
const origQSA = globalThis.document.querySelectorAll;
globalThis.document.querySelectorAll = () => [];
const origCreateTextNode = globalThis.document.createTextNode;
globalThis.document.createTextNode = (text) => ({ nodeType: 3, textContent: text });
app.openSettings();
// Flush microtask queue so all Promises in loadServerSettings chain resolve
// before yielding to macrotask queue (where other tests could start).
// Each await Promise.resolve() flushes one microtask hop; 10 covers nested chains.
for (let i = 0; i < 10; i++) await Promise.resolve();
assert.strictEqual(
elements['setting-template'] && elements['setting-template'].value,
'my-custom-template',
'textarea should be populated with new_session_template from server settings',
);
globalThis.fetch = origFetch;
globalThis.document.getElementById = origGetById;
globalThis.document.querySelectorAll = origQSA;
globalThis.document.createTextNode = origCreateTextNode;
});
test('openSettings uses default template when new_session_template not in server settings', async () => {
// Reset _currentSessions to empty to avoid createTextNode calls in openSettings callback
app._setCurrentSessions([]);
const elements = {};
const origFetch = globalThis.fetch;
globalThis.fetch = async (url) => {
if (url === '/api/settings') {
return {
ok: true,
json: async () => ({}),
};
}
return { ok: true, json: async () => ({}) };
};
const origGetById = globalThis.document.getElementById;
globalThis.document.getElementById = (id) => {
if (!elements[id]) {
elements[id] = {
value: '',
checked: false,
innerHTML: '',
disabled: false,
appendChild: () => {},
querySelectorAll: () => [],
classList: { add: () => {}, remove: () => {} },
showModal: () => {},
close: () => {},
addEventListener: () => {},
};
}
return elements[id];
};
const origQSA = globalThis.document.querySelectorAll;
globalThis.document.querySelectorAll = () => [];
const origCreateTextNode = globalThis.document.createTextNode;
globalThis.document.createTextNode = (text) => ({ nodeType: 3, textContent: text });
app.openSettings();
// Flush microtask queue so all Promises in loadServerSettings chain resolve
// before yielding to macrotask queue (where other tests could start).
for (let i = 0; i < 10; i++) await Promise.resolve();
assert.strictEqual(
elements['setting-template'] && elements['setting-template'].value,
'tmux new-session -d -s {name}',
'textarea should use default when new_session_template not set',
);
globalThis.fetch = origFetch;
globalThis.document.getElementById = origGetById;
globalThis.document.querySelectorAll = origQSA;
globalThis.document.createTextNode = origCreateTextNode;
});
test('bindStaticEventListeners binds input on setting-template', () => {
const eventsBound = {};
const origGetById = globalThis.document.getElementById;
const origDocAddListener = globalThis.document.addEventListener;
globalThis.document.getElementById = (id) => {
const el = {
_events: {},
addEventListener: (ev, fn) => { el._events[ev] = fn; },
value: '',
querySelectorAll: () => [],
};
eventsBound[id] = el;
return el;
};
globalThis.document.addEventListener = () => {};
app.bindStaticEventListeners();
assert.ok(
eventsBound['setting-template'] && 'input' in eventsBound['setting-template']._events,
'#setting-template should have an input listener',
);
globalThis.document.getElementById = origGetById;
globalThis.document.addEventListener = origDocAddListener;
});
test('bindStaticEventListeners binds click on setting-template-reset', () => {
const eventsBound = {};
const origGetById = globalThis.document.getElementById;
const origDocAddListener = globalThis.document.addEventListener;
globalThis.document.getElementById = (id) => {
const el = {
_events: {},
addEventListener: (ev, fn) => { el._events[ev] = fn; },
value: '',
querySelectorAll: () => [],
};
eventsBound[id] = el;
return el;
};
globalThis.document.addEventListener = () => {};
app.bindStaticEventListeners();
assert.ok(
eventsBound['setting-template-reset'] && 'click' in eventsBound['setting-template-reset']._events,
'#setting-template-reset should have a click listener',
);
globalThis.document.getElementById = origGetById;
globalThis.document.addEventListener = origDocAddListener;
});
test('setting-template-reset click resets textarea to default value', () => {
const elements = {};
const origFetch = globalThis.fetch;
globalThis.fetch = async () => ({ ok: true, json: async () => ({}) });
const origGetById = globalThis.document.getElementById;
const origDocAddListener = globalThis.document.addEventListener;
globalThis.document.getElementById = (id) => {
if (!elements[id]) {
elements[id] = {
_events: {},
addEventListener: (ev, fn) => { elements[id]._events[ev] = fn; },
value: 'custom-value',
querySelectorAll: () => [],
};
}
return elements[id];
};
globalThis.document.addEventListener = () => {};
app.bindStaticEventListeners();
// Simulate reset button click
if (elements['setting-template-reset']) {
elements['setting-template-reset']._events.click();
}
assert.strictEqual(
elements['setting-template'] && elements['setting-template'].value,
'tmux new-session -d -s {name}',
'textarea should be reset to default value on reset button click',
);
globalThis.fetch = origFetch;
globalThis.document.getElementById = origGetById;
globalThis.document.addEventListener = origDocAddListener;
});
test('app.js source uses 500ms debounce for template input and references new_session_template', () => {
const source = fs.readFileSync(
new URL('../app.js', import.meta.url), 'utf8'
);
assert.ok(source.includes('500'), 'must have 500ms debounce timeout');
assert.ok(source.includes('new_session_template'), 'must reference new_session_template setting key');
});
+265 -24
View File
@@ -404,12 +404,16 @@ def test_css_sidebar_item_body_pre():
assert "left: 0" in block
assert "right: 0" in block
assert "font-size: 10px" in block
assert "line-height: 1.0" in block, "sidebar-item-body pre must use line-height: 1.0 (xterm.js default)"
assert "line-height: 1.0" in block, (
"sidebar-item-body pre must use line-height: 1.0 (xterm.js default)"
)
assert "#c9d1d9" in block, "sidebar-item-body pre must match xterm.js foreground"
assert "white-space: pre" in block
assert "padding: 0 8px 6px" in block
# Explicit xterm.js font family (not design token variable)
assert "'SF Mono'" in block or "SF Mono" in block, "sidebar-item-body pre must use explicit xterm.js font family"
assert "'SF Mono'" in block or "SF Mono" in block, (
"sidebar-item-body pre must use explicit xterm.js font family"
)
def test_css_sidebar_empty():
@@ -649,8 +653,12 @@ def test_preview_popover_pre_matches_xterm_typography():
""".preview-popover pre must match xterm.js terminal: 14px, line-height 1.0, explicit font stack."""
css = read_css()
block = _extract_rule_block(css, ".preview-popover pre {")
assert "font-size: 14px" in block, ".preview-popover pre must use 14px (xterm.js default)"
assert "line-height: 1.0" in block, ".preview-popover pre must use line-height: 1.0 (xterm.js default)"
assert "font-size: 14px" in block, (
".preview-popover pre must use 14px (xterm.js default)"
)
assert "line-height: 1.0" in block, (
".preview-popover pre must use line-height: 1.0 (xterm.js default)"
)
assert "'SF Mono'" in block or "SF Mono" in block, (
".preview-popover pre must use explicit xterm.js font family"
)
@@ -669,7 +677,9 @@ def test_tile_body_pre_has_xterm_line_height():
""".tile-body pre must use line-height: 1.0 to match xterm.js terminal."""
css = read_css()
block = _extract_rule_block(css, ".tile-body pre {")
assert "line-height: 1.0" in block, ".tile-body pre must use line-height: 1.0 (xterm.js default)"
assert "line-height: 1.0" in block, (
".tile-body pre must use line-height: 1.0 (xterm.js default)"
)
def test_tile_body_pre_has_explicit_font_family():
@@ -694,7 +704,9 @@ def test_sidebar_item_body_pre_has_xterm_line_height():
""".sidebar-item-body pre must use line-height: 1.0 to match xterm.js terminal."""
css = read_css()
block = _extract_rule_block(css, ".sidebar-item-body pre {")
assert "line-height: 1.0" in block, ".sidebar-item-body pre must use line-height: 1.0 (xterm.js default)"
assert "line-height: 1.0" in block, (
".sidebar-item-body pre must use line-height: 1.0 (xterm.js default)"
)
def test_sidebar_item_body_pre_has_explicit_font_family():
@@ -710,7 +722,9 @@ def test_no_gradient_fade_on_previews():
"""Gradient fade overlays must be removed — they obscure ANSI colors at the bottom."""
css = read_css()
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"
)
# ============================================================
@@ -750,7 +764,9 @@ def test_css_settings_backdrop_exists():
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"
assert "blur" in block or "backdrop-filter" in block, (
".settings-backdrop must use blur"
)
def test_css_settings_dialog_exists():
@@ -767,7 +783,9 @@ def test_css_settings_dialog_exists():
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"
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"
@@ -810,7 +828,9 @@ def test_css_settings_content_exists():
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)"
assert "overflow-y: auto" in block, (
".settings-content must be scrollable (overflow-y: auto)"
)
def test_css_settings_field_exists():
@@ -819,8 +839,12 @@ def test_css_settings_field_exists():
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"
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():
@@ -833,24 +857,36 @@ def test_css_settings_select_exists():
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"
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"
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"
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"
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"
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"
)
# ─── Sessions tab CSS (task-1-sessions-tab) ───────────────────────────────────
@@ -864,6 +900,7 @@ def test_css_settings_field_column() -> None:
)
# Find the rule and check flex-direction
import re
match = re.search(
r"\.settings-field--column\s*\{([^}]*)\}",
css,
@@ -908,6 +945,7 @@ def test_css_settings_checkbox() -> None:
def test_css_settings_notification_status_exists() -> None:
""".settings-notification-status must exist with flex column, align-items flex-end."""
import re
css = read_css()
assert ".settings-notification-status" in css, (
".settings-notification-status class must be defined in style.css"
@@ -930,6 +968,7 @@ def test_css_settings_notification_status_exists() -> None:
def test_css_settings_status_text_exists() -> None:
""".settings-status-text must exist with 12px font-size and text-muted color."""
import re
css = read_css()
assert ".settings-status-text" in css, (
".settings-status-text class must be defined in style.css"
@@ -953,6 +992,7 @@ def test_css_settings_status_text_exists() -> None:
def test_css_settings_action_btn_exists() -> None:
""".settings-action-btn must exist with background, border, 12px font-size."""
import re
css = read_css()
assert ".settings-action-btn" in css, (
".settings-action-btn class must be defined in style.css"
@@ -967,17 +1007,14 @@ def test_css_settings_action_btn_exists() -> None:
assert "font-size: 12px" in body or "font-size:12px" in body, (
".settings-action-btn must set font-size: 12px"
)
assert "border" in body, (
".settings-action-btn must have border property"
)
assert "background" in body, (
".settings-action-btn must have background property"
)
assert "border" in body, ".settings-action-btn must have border property"
assert "background" in body, ".settings-action-btn must have background property"
def test_css_settings_action_btn_hover_exists() -> None:
""".settings-action-btn:hover must exist with border-color accent."""
import re
css = read_css()
assert ".settings-action-btn:hover" in css, (
".settings-action-btn:hover must be defined in style.css"
@@ -997,6 +1034,7 @@ def test_css_settings_action_btn_hover_exists() -> None:
def test_css_settings_action_btn_disabled_opacity() -> None:
""".settings-action-btn:disabled must have opacity 0.5."""
import re
css = read_css()
assert ".settings-action-btn:disabled" in css, (
".settings-action-btn:disabled must be defined in style.css"
@@ -1011,3 +1049,206 @@ def test_css_settings_action_btn_disabled_opacity() -> None:
assert "opacity: 0.5" in body or "opacity:0.5" in body, (
".settings-action-btn:disabled must set opacity: 0.5"
)
# ============================================================
# New Session tab CSS (task-3-new-session-tab)
# ============================================================
def test_css_settings_textarea_exists() -> None:
""".settings-textarea must be defined in style.css."""
css = read_css()
assert ".settings-textarea" in css, (
".settings-textarea class must be defined in style.css"
)
def test_css_settings_textarea_full_width() -> None:
""".settings-textarea must have width: 100%."""
import re
css = read_css()
match = re.search(
r"\.settings-textarea\s*\{([^}]*)\}",
css,
re.DOTALL,
)
assert match, ".settings-textarea rule not found"
body = match.group(1)
assert "width: 100%" in body or "width:100%" in body, (
".settings-textarea must set width: 100%"
)
def test_css_settings_textarea_background() -> None:
""".settings-textarea must use var(--bg-secondary) background."""
import re
css = read_css()
match = re.search(
r"\.settings-textarea\s*\{([^}]*)\}",
css,
re.DOTALL,
)
assert match, ".settings-textarea rule not found"
body = match.group(1)
assert "var(--bg-secondary)" in body, (
".settings-textarea must use var(--bg-secondary) background"
)
def test_css_settings_textarea_border_and_radius() -> None:
""".settings-textarea must have border and border-radius: 4px."""
import re
css = read_css()
match = re.search(
r"\.settings-textarea\s*\{([^}]*)\}",
css,
re.DOTALL,
)
assert match, ".settings-textarea rule not found"
body = match.group(1)
assert "border:" in body or "border :" in body, (
".settings-textarea must have a border property"
)
assert "border-radius: 4px" in body or "border-radius:4px" in body, (
".settings-textarea must have border-radius: 4px"
)
def test_css_settings_textarea_font_mono_13px() -> None:
""".settings-textarea must use font-family var(--font-mono) and font-size 13px."""
import re
css = read_css()
match = re.search(
r"\.settings-textarea\s*\{([^}]*)\}",
css,
re.DOTALL,
)
assert match, ".settings-textarea rule not found"
body = match.group(1)
assert "var(--font-mono)" in body, (
".settings-textarea must use var(--font-mono) font-family"
)
assert "font-size: 13px" in body or "font-size:13px" in body, (
".settings-textarea must use font-size: 13px"
)
def test_css_settings_textarea_padding_and_resize() -> None:
""".settings-textarea must have padding: 10px and resize: vertical."""
import re
css = read_css()
match = re.search(
r"\.settings-textarea\s*\{([^}]*)\}",
css,
re.DOTALL,
)
assert match, ".settings-textarea rule not found"
body = match.group(1)
assert "padding: 10px" in body or "padding:10px" in body, (
".settings-textarea must have padding: 10px"
)
assert "resize: vertical" in body or "resize:vertical" in body, (
".settings-textarea must have resize: vertical"
)
def test_css_settings_textarea_min_height() -> None:
""".settings-textarea must have min-height: 60px."""
import re
css = read_css()
match = re.search(
r"\.settings-textarea\s*\{([^}]*)\}",
css,
re.DOTALL,
)
assert match, ".settings-textarea rule not found"
body = match.group(1)
assert "min-height: 60px" in body or "min-height:60px" in body, (
".settings-textarea must have min-height: 60px"
)
def test_css_settings_textarea_focus_accent_border() -> None:
""".settings-textarea:focus must use border-color: var(--accent)."""
import re
css = read_css()
assert ".settings-textarea:focus" in css, (
".settings-textarea:focus rule must be defined in style.css"
)
match = re.search(
r"\.settings-textarea:focus\s*\{([^}]*)\}",
css,
re.DOTALL,
)
assert match, ".settings-textarea:focus rule not found"
body = match.group(1)
assert "var(--accent)" in body, (
".settings-textarea:focus must use var(--accent) border-color"
)
def test_css_settings_helper_exists() -> None:
""".settings-helper must be defined in style.css."""
css = read_css()
assert ".settings-helper" in css, (
".settings-helper class must be defined in style.css"
)
def test_css_settings_helper_font_size() -> None:
""".settings-helper must have font-size: 12px."""
import re
css = read_css()
match = re.search(
r"\.settings-helper\s*\{([^}]*)\}",
css,
re.DOTALL,
)
assert match, ".settings-helper rule not found"
body = match.group(1)
assert "font-size: 12px" in body or "font-size:12px" in body, (
".settings-helper must have font-size: 12px"
)
def test_css_settings_helper_text_muted_color() -> None:
""".settings-helper must use var(--text-muted) color."""
import re
css = read_css()
match = re.search(
r"\.settings-helper\s*\{([^}]*)\}",
css,
re.DOTALL,
)
assert match, ".settings-helper rule not found"
body = match.group(1)
assert "var(--text-muted)" in body, (
".settings-helper must use var(--text-muted) color"
)
def test_css_settings_helper_italic() -> None:
""".settings-helper must have font-style: italic."""
import re
css = read_css()
match = re.search(
r"\.settings-helper\s*\{([^}]*)\}",
css,
re.DOTALL,
)
assert match, ".settings-helper rule not found"
body = match.group(1)
assert "font-style: italic" in body or "font-style:italic" in body, (
".settings-helper must have font-style: italic"
)
+158 -16
View File
@@ -589,7 +589,9 @@ def test_html_sessions_panel_has_default_session_select() -> None:
soup = _SOUP
dialog = soup.find(id="settings-dialog")
assert dialog is not None, "Missing #settings-dialog"
sessions_panel = dialog.find(class_="settings-panel", attrs={"data-tab": "sessions"})
sessions_panel = dialog.find(
class_="settings-panel", attrs={"data-tab": "sessions"}
)
assert sessions_panel is not None, "Missing sessions settings-panel"
el = sessions_panel.find(id="setting-default-session")
assert el is not None, "Missing #setting-default-session inside sessions panel"
@@ -603,7 +605,9 @@ def test_html_sessions_panel_has_sort_order_select() -> None:
soup = _SOUP
dialog = soup.find(id="settings-dialog")
assert dialog is not None, "Missing #settings-dialog"
sessions_panel = dialog.find(class_="settings-panel", attrs={"data-tab": "sessions"})
sessions_panel = dialog.find(
class_="settings-panel", attrs={"data-tab": "sessions"}
)
assert sessions_panel is not None, "Missing sessions settings-panel"
el = sessions_panel.find(id="setting-sort-order")
assert el is not None, "Missing #setting-sort-order inside sessions panel"
@@ -621,7 +625,9 @@ def test_html_sessions_panel_has_hidden_sessions_container() -> None:
soup = _SOUP
dialog = soup.find(id="settings-dialog")
assert dialog is not None, "Missing #settings-dialog"
sessions_panel = dialog.find(class_="settings-panel", attrs={"data-tab": "sessions"})
sessions_panel = dialog.find(
class_="settings-panel", attrs={"data-tab": "sessions"}
)
assert sessions_panel is not None, "Missing sessions settings-panel"
el = sessions_panel.find(id="setting-hidden-sessions")
assert el is not None, "Missing #setting-hidden-sessions inside sessions panel"
@@ -632,7 +638,9 @@ def test_html_sessions_panel_has_window_size_largest_checkbox() -> None:
soup = _SOUP
dialog = soup.find(id="settings-dialog")
assert dialog is not None, "Missing #settings-dialog"
sessions_panel = dialog.find(class_="settings-panel", attrs={"data-tab": "sessions"})
sessions_panel = dialog.find(
class_="settings-panel", attrs={"data-tab": "sessions"}
)
assert sessions_panel is not None, "Missing sessions settings-panel"
el = sessions_panel.find(id="setting-window-size-largest")
assert el is not None, "Missing #setting-window-size-largest inside sessions panel"
@@ -649,13 +657,13 @@ def test_html_sessions_panel_has_auto_open_checkbox_default_checked() -> None:
soup = _SOUP
dialog = soup.find(id="settings-dialog")
assert dialog is not None, "Missing #settings-dialog"
sessions_panel = dialog.find(class_="settings-panel", attrs={"data-tab": "sessions"})
sessions_panel = dialog.find(
class_="settings-panel", attrs={"data-tab": "sessions"}
)
assert sessions_panel is not None, "Missing sessions settings-panel"
el = sessions_panel.find(id="setting-auto-open")
assert el is not None, "Missing #setting-auto-open inside sessions panel"
assert el.name == "input", (
f"#setting-auto-open must be an <input>, got: {el.name}"
)
assert el.name == "input", f"#setting-auto-open must be an <input>, got: {el.name}"
assert el.get("type") == "checkbox", (
f"#setting-auto-open must be type='checkbox', got: {el.get('type')}"
)
@@ -674,13 +682,13 @@ def test_html_notifications_panel_has_bell_sound_checkbox() -> None:
soup = _SOUP
dialog = soup.find(id="settings-dialog")
assert dialog is not None, "Missing #settings-dialog"
notif_panel = dialog.find(class_="settings-panel", attrs={"data-tab": "notifications"})
notif_panel = dialog.find(
class_="settings-panel", attrs={"data-tab": "notifications"}
)
assert notif_panel is not None, "Missing notifications settings-panel"
el = notif_panel.find(id="setting-bell-sound")
assert el is not None, "Missing #setting-bell-sound inside notifications panel"
assert el.name == "input", (
f"#setting-bell-sound must be an <input>, got: {el.name}"
)
assert el.name == "input", f"#setting-bell-sound must be an <input>, got: {el.name}"
assert el.get("type") == "checkbox", (
f"#setting-bell-sound must be type='checkbox', got: {el.get('type')}"
)
@@ -695,10 +703,14 @@ def test_html_notifications_panel_has_notification_status_text() -> None:
soup = _SOUP
dialog = soup.find(id="settings-dialog")
assert dialog is not None, "Missing #settings-dialog"
notif_panel = dialog.find(class_="settings-panel", attrs={"data-tab": "notifications"})
notif_panel = dialog.find(
class_="settings-panel", attrs={"data-tab": "notifications"}
)
assert notif_panel is not None, "Missing notifications settings-panel"
el = notif_panel.find(id="notification-status-text")
assert el is not None, "Missing #notification-status-text inside notifications panel"
assert el is not None, (
"Missing #notification-status-text inside notifications panel"
)
classes = el.get("class") or []
assert "settings-status-text" in classes, (
f"#notification-status-text must have class 'settings-status-text', has: {classes}"
@@ -710,11 +722,141 @@ def test_html_notifications_panel_has_request_btn() -> None:
soup = _SOUP
dialog = soup.find(id="settings-dialog")
assert dialog is not None, "Missing #settings-dialog"
notif_panel = dialog.find(class_="settings-panel", attrs={"data-tab": "notifications"})
notif_panel = dialog.find(
class_="settings-panel", attrs={"data-tab": "notifications"}
)
assert notif_panel is not None, "Missing notifications settings-panel"
el = notif_panel.find(id="notification-request-btn")
assert el is not None, "Missing #notification-request-btn inside notifications panel"
assert el is not None, (
"Missing #notification-request-btn inside notifications panel"
)
classes = el.get("class") or []
assert "settings-action-btn" in classes, (
f"#notification-request-btn must have class 'settings-action-btn', has: {classes}"
)
# ============================================================
# New Session tab (task-3-new-session-tab)
# ============================================================
def test_html_new_session_panel_exists() -> None:
"""New Session settings panel must exist with data-tab='new-session'."""
soup = _SOUP
dialog = soup.find(id="settings-dialog")
assert dialog is not None, "Missing #settings-dialog"
new_session_panel = dialog.find(
class_="settings-panel", attrs={"data-tab": "new-session"}
)
assert new_session_panel is not None, "Missing new-session settings-panel"
def test_html_new_session_panel_has_settings_field_column() -> None:
"""New Session panel must contain a .settings-field--column div."""
soup = _SOUP
dialog = soup.find(id="settings-dialog")
assert dialog is not None, "Missing #settings-dialog"
new_session_panel = dialog.find(
class_="settings-panel", attrs={"data-tab": "new-session"}
)
assert new_session_panel is not None, "Missing new-session settings-panel"
field = new_session_panel.find(class_="settings-field--column")
assert field is not None, "Missing .settings-field--column inside new-session panel"
def test_html_new_session_panel_has_template_label() -> None:
"""New Session panel must have a label for #setting-template with text 'Command template'."""
soup = _SOUP
dialog = soup.find(id="settings-dialog")
assert dialog is not None, "Missing #settings-dialog"
new_session_panel = dialog.find(
class_="settings-panel", attrs={"data-tab": "new-session"}
)
assert new_session_panel is not None, "Missing new-session settings-panel"
label = new_session_panel.find("label", attrs={"for": "setting-template"})
assert label is not None, (
"Missing <label for='setting-template'> inside new-session panel"
)
label_text = label.get_text(strip=True)
assert "Command template" in label_text, (
f"Label for #setting-template must contain 'Command template', got: {label_text!r}"
)
def test_html_new_session_panel_has_template_textarea() -> None:
"""New Session panel must contain #setting-template textarea with class settings-textarea."""
soup = _SOUP
dialog = soup.find(id="settings-dialog")
assert dialog is not None, "Missing #settings-dialog"
new_session_panel = dialog.find(
class_="settings-panel", attrs={"data-tab": "new-session"}
)
assert new_session_panel is not None, "Missing new-session settings-panel"
textarea = new_session_panel.find("textarea", id="setting-template")
assert textarea is not None, (
"Missing <textarea id='setting-template'> inside new-session panel"
)
classes = textarea.get("class") or []
assert "settings-textarea" in classes, (
f"#setting-template must have class 'settings-textarea', has: {classes}"
)
def test_html_new_session_template_textarea_rows() -> None:
"""#setting-template textarea must have rows=3."""
soup = _SOUP
textarea = soup.find("textarea", id="setting-template")
assert textarea is not None, "Missing #setting-template textarea"
rows = textarea.get("rows")
assert rows == "3", f"#setting-template must have rows='3', got: {rows!r}"
def test_html_new_session_template_textarea_placeholder() -> None:
"""#setting-template textarea must have placeholder 'tmux new-session -d -s {name}'."""
soup = _SOUP
textarea = soup.find("textarea", id="setting-template")
assert textarea is not None, "Missing #setting-template textarea"
placeholder = textarea.get("placeholder")
assert placeholder == "tmux new-session -d -s {name}", (
f"#setting-template placeholder must be 'tmux new-session -d -s {{name}}', got: {placeholder!r}"
)
def test_html_new_session_panel_has_helper_text() -> None:
"""New Session panel must contain a .settings-helper span with '{name} is replaced...' text."""
soup = _SOUP
dialog = soup.find(id="settings-dialog")
assert dialog is not None, "Missing #settings-dialog"
new_session_panel = dialog.find(
class_="settings-panel", attrs={"data-tab": "new-session"}
)
assert new_session_panel is not None, "Missing new-session settings-panel"
helper = new_session_panel.find(class_="settings-helper")
assert helper is not None, "Missing .settings-helper inside new-session panel"
helper_text = helper.get_text(strip=True)
assert "{name}" in helper_text, (
f".settings-helper must contain '{{name}}', got: {helper_text!r}"
)
assert "replaced" in helper_text.lower() or "session name" in helper_text.lower(), (
f".settings-helper must describe what {{name}} is replaced with, got: {helper_text!r}"
)
def test_html_new_session_panel_has_reset_button() -> None:
"""New Session panel must contain #setting-template-reset button with class settings-action-btn."""
soup = _SOUP
dialog = soup.find(id="settings-dialog")
assert dialog is not None, "Missing #settings-dialog"
new_session_panel = dialog.find(
class_="settings-panel", attrs={"data-tab": "new-session"}
)
assert new_session_panel is not None, "Missing new-session settings-panel"
reset_btn = new_session_panel.find(id="setting-template-reset")
assert reset_btn is not None, (
"Missing #setting-template-reset inside new-session panel"
)
classes = reset_btn.get("class") or []
assert "settings-action-btn" in classes, (
f"#setting-template-reset must have class 'settings-action-btn', has: {classes}"
)
+152 -61
View File
@@ -11,6 +11,7 @@ _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, (
@@ -34,9 +35,7 @@ def test_no_palette_filtered_sessions_variable() -> None:
def test_no_palette_open_variable() -> None:
"""_paletteOpen variable must be removed."""
assert "_paletteOpen" not in _JS, (
"_paletteOpen must be removed from app.js"
)
assert "_paletteOpen" not in _JS, "_paletteOpen must be removed from app.js"
def test_no_palette_input_listener_variable() -> None:
@@ -48,6 +47,7 @@ def test_no_palette_input_listener_variable() -> None:
# ── Palette functions must be removed ────────────────────────────────────────
def test_no_render_palette_list_function() -> None:
"""renderPaletteList function must be removed."""
assert "renderPaletteList" not in _JS, (
@@ -64,23 +64,17 @@ def test_no_highlight_palette_item_function() -> None:
def test_no_open_palette_function() -> None:
"""openPalette function must be removed."""
assert "openPalette" not in _JS, (
"openPalette must be removed from app.js"
)
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"
)
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"
)
assert "onPaletteInput" not in _JS, "onPaletteInput must be removed from app.js"
def test_no_handle_palette_keydown_function() -> None:
@@ -92,6 +86,7 @@ def test_no_handle_palette_keydown_function() -> None:
# ── handleGlobalKeydown must be simplified ───────────────────────────────────
def test_handle_global_keydown_exists() -> None:
"""handleGlobalKeydown function must exist."""
assert "function handleGlobalKeydown" in _JS, (
@@ -123,9 +118,7 @@ def test_handle_global_keydown_no_open_palette_call() -> None:
)
assert match, "handleGlobalKeydown function not found"
body = match.group(1)
assert "openPalette" not in body, (
"handleGlobalKeydown must not call openPalette"
)
assert "openPalette" not in body, "handleGlobalKeydown must not call openPalette"
def test_handle_global_keydown_handles_escape_in_fullscreen() -> None:
@@ -144,6 +137,7 @@ def test_handle_global_keydown_handles_escape_in_fullscreen() -> None:
# ── 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(
@@ -174,6 +168,7 @@ def test_bind_static_event_listeners_no_palette_backdrop() -> None:
# ── 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, (
@@ -204,20 +199,17 @@ def test_no_get_palette_selected_index_helper() -> None:
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"
)
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"
)
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(
@@ -255,9 +247,7 @@ def test_exports_no_open_palette() -> None:
)
assert match, "module.exports block not found"
exports = match.group(1)
assert "openPalette" not in exports, (
"module.exports must not export openPalette"
)
assert "openPalette" not in exports, "module.exports must not export openPalette"
def test_exports_no_close_palette() -> None:
@@ -269,9 +259,7 @@ def test_exports_no_close_palette() -> None:
)
assert match, "module.exports block not found"
exports = match.group(1)
assert "closePalette" not in exports, (
"module.exports must not export closePalette"
)
assert "closePalette" not in exports, "module.exports must not export closePalette"
def test_exports_no_on_palette_input() -> None:
@@ -332,6 +320,7 @@ def test_exports_still_has_bind_static_event_listeners() -> None:
# ── Settings state variables ─────────────────────────────────────────────────
def test_settings_open_state_variable_exists() -> None:
"""_settingsOpen state variable must be declared."""
assert "_settingsOpen" in _JS, "_settingsOpen must be declared in app.js"
@@ -377,11 +366,14 @@ def test_display_defaults_has_bell_sound() -> None:
def test_display_defaults_has_notification_permission() -> None:
"""DISPLAY_DEFAULTS must contain notificationPermission: 'default'."""
assert "notificationPermission" in _JS, "DISPLAY_DEFAULTS must include notificationPermission"
assert "notificationPermission" in _JS, (
"DISPLAY_DEFAULTS must include notificationPermission"
)
# ── Settings functions must exist ─────────────────────────────────────────────
def test_load_display_settings_function_exists() -> None:
"""loadDisplaySettings function must exist."""
assert "function loadDisplaySettings" in _JS, (
@@ -415,6 +407,7 @@ def test_switch_settings_tab_function_exists() -> None:
# ── loadDisplaySettings implementation ───────────────────────────────────────
def test_load_display_settings_reads_from_localstorage() -> None:
"""loadDisplaySettings must read from localStorage using DISPLAY_SETTINGS_KEY."""
match = re.search(
@@ -439,7 +432,9 @@ def test_load_display_settings_uses_object_assign() -> None:
)
assert match, "loadDisplaySettings function not found"
body = match.group(1)
assert "Object.assign" in body, "loadDisplaySettings must use Object.assign to merge with defaults"
assert "Object.assign" in body, (
"loadDisplaySettings must use Object.assign to merge with defaults"
)
def test_load_display_settings_returns_defaults_on_error() -> None:
@@ -458,6 +453,7 @@ def test_load_display_settings_returns_defaults_on_error() -> None:
# ── saveDisplaySettings implementation ───────────────────────────────────────
def test_save_display_settings_writes_to_localstorage() -> None:
"""saveDisplaySettings must write to localStorage."""
match = re.search(
@@ -487,6 +483,7 @@ def test_save_display_settings_catches_errors() -> None:
# ── openSettings implementation ───────────────────────────────────────────────
def test_open_settings_sets_settings_open_true() -> None:
"""openSettings must set _settingsOpen = true."""
match = re.search(
@@ -535,13 +532,20 @@ def test_open_settings_loads_form_controls() -> None:
)
assert match, "openSettings function not found"
body = match.group(1)
assert "setting-font-size" in body, "openSettings must set setting-font-size form control"
assert "setting-hover-delay" in body, "openSettings must set setting-hover-delay form control"
assert "setting-grid-columns" in body, "openSettings must set setting-grid-columns form control"
assert "setting-font-size" in body, (
"openSettings must set setting-font-size form control"
)
assert "setting-hover-delay" in body, (
"openSettings must set setting-hover-delay form control"
)
assert "setting-grid-columns" in body, (
"openSettings must set setting-grid-columns form control"
)
# ── closeSettings implementation ──────────────────────────────────────────────
def test_close_settings_sets_settings_open_false() -> None:
"""closeSettings must set _settingsOpen = false."""
match = re.search(
@@ -583,6 +587,7 @@ def test_close_settings_adds_hidden_to_backdrop() -> None:
# ── switchSettingsTab implementation ──────────────────────────────────────────
def test_switch_settings_tab_has_tab_name_param() -> None:
"""switchSettingsTab must accept tabName parameter."""
assert "function switchSettingsTab" in _JS, "switchSettingsTab must be defined"
@@ -635,6 +640,7 @@ def test_switch_settings_tab_toggles_panel_hidden() -> None:
# ── handleGlobalKeydown settings integration ─────────────────────────────────
def test_handle_global_keydown_checks_settings_open() -> None:
"""handleGlobalKeydown must check _settingsOpen."""
match = re.search(
@@ -644,9 +650,7 @@ def test_handle_global_keydown_checks_settings_open() -> None:
)
assert match, "handleGlobalKeydown function not found"
body = match.group(1)
assert "_settingsOpen" in body, (
"handleGlobalKeydown must check _settingsOpen"
)
assert "_settingsOpen" in body, "handleGlobalKeydown must check _settingsOpen"
def test_handle_global_keydown_calls_close_settings_on_escape() -> None:
@@ -658,9 +662,7 @@ def test_handle_global_keydown_calls_close_settings_on_escape() -> None:
)
assert match, "handleGlobalKeydown function not found"
body = match.group(1)
assert "closeSettings" in body, (
"handleGlobalKeydown must call closeSettings"
)
assert "closeSettings" in body, "handleGlobalKeydown must call closeSettings"
def test_handle_global_keydown_opens_settings_on_comma() -> None:
@@ -691,9 +693,11 @@ def test_handle_global_keydown_comma_guards_input_elements() -> None:
body = match.group(1)
# Must guard against inputs
has_input_guard = (
"INPUT" in body or "input" in body.lower() or
"textarea" in body.lower() or "select" in body.lower() or
"tagName" in body
"INPUT" in body
or "input" in body.lower()
or "textarea" in body.lower()
or "select" in body.lower()
or "tagName" in body
)
assert has_input_guard, (
"handleGlobalKeydown comma shortcut must guard against input/textarea/select"
@@ -702,6 +706,7 @@ def test_handle_global_keydown_comma_guards_input_elements() -> None:
# ── bindStaticEventListeners settings wiring ─────────────────────────────────
def test_bind_static_event_listeners_binds_settings_btn() -> None:
"""bindStaticEventListeners must bind settings-btn click to openSettings."""
match = re.search(
@@ -753,9 +758,7 @@ def test_bind_static_event_listeners_binds_dialog_cancel() -> None:
)
assert match, "bindStaticEventListeners function not found"
body = match.group(1)
assert "cancel" in body, (
"bindStaticEventListeners must bind dialog cancel event"
)
assert "cancel" in body, "bindStaticEventListeners must bind dialog cancel event"
def test_bind_static_event_listeners_binds_settings_tabs() -> None:
@@ -777,6 +780,7 @@ def test_bind_static_event_listeners_binds_settings_tabs() -> None:
# ── module.exports for new settings functions ────────────────────────────────
def test_exports_load_display_settings() -> None:
"""module.exports must export loadDisplaySettings."""
match = re.search(
@@ -814,9 +818,7 @@ def test_exports_open_settings() -> None:
)
assert match, "module.exports block not found"
exports = match.group(1)
assert "openSettings" in exports, (
"module.exports must export openSettings"
)
assert "openSettings" in exports, "module.exports must export openSettings"
def test_exports_close_settings() -> None:
@@ -828,9 +830,7 @@ def test_exports_close_settings() -> None:
)
assert match, "module.exports block not found"
exports = match.group(1)
assert "closeSettings" in exports, (
"module.exports must export closeSettings"
)
assert "closeSettings" in exports, "module.exports must export closeSettings"
def test_exports_switch_settings_tab() -> None:
@@ -849,6 +849,7 @@ def test_exports_switch_settings_tab() -> None:
# ── Display tab wiring (task-8) ────────────────────────────────────────────────
def test_apply_display_settings_function_exists() -> None:
"""applyDisplaySettings function must exist."""
assert "function applyDisplaySettings" in _JS, (
@@ -896,9 +897,7 @@ def test_apply_display_settings_handles_auto_grid_columns() -> None:
)
assert match, "applyDisplaySettings function not found"
body = match.group(1)
assert "auto" in body, (
"applyDisplaySettings must handle 'auto' gridColumns"
)
assert "auto" in body, "applyDisplaySettings must handle 'auto' gridColumns"
assert "session-grid" in body or "gridTemplateColumns" in body, (
"applyDisplaySettings must update session-grid or gridTemplateColumns"
)
@@ -1120,6 +1119,7 @@ def test_exports_on_display_setting_change() -> None:
# ─── Server settings functions (task-1-sessions-tab) ─────────────────────────
def test_load_server_settings_function_exists() -> None:
"""loadServerSettings function must exist."""
assert "function loadServerSettings" in _JS, (
@@ -1196,9 +1196,7 @@ def test_open_settings_calls_load_server_settings() -> None:
)
assert match, "openSettings function not found"
body = match.group(1)
assert "loadServerSettings" in body, (
"openSettings must call loadServerSettings"
)
assert "loadServerSettings" in body, "openSettings must call loadServerSettings"
def test_bind_static_event_listeners_binds_default_session_change() -> None:
@@ -1257,7 +1255,9 @@ def test_bind_static_event_listeners_binds_auto_open_change() -> None:
)
def test_bind_static_event_listeners_uses_delegated_handler_for_hidden_sessions() -> None:
def test_bind_static_event_listeners_uses_delegated_handler_for_hidden_sessions() -> (
None
):
"""bindStaticEventListeners must use delegated change handler on #setting-hidden-sessions."""
match = re.search(
r"function bindStaticEventListeners\s*\(\s*\)\s*\{(.*?)\n\}",
@@ -1301,6 +1301,7 @@ def test_exports_patch_server_setting() -> None:
# ─── Notifications tab (task-2-notifications-tab) ─────────────────────────────
def test_open_settings_populates_bell_sound() -> None:
"""openSettings must set setting-bell-sound checkbox from loadDisplaySettings().bellSound."""
match = re.search(
@@ -1313,9 +1314,7 @@ def test_open_settings_populates_bell_sound() -> None:
assert "setting-bell-sound" in body, (
"openSettings must reference setting-bell-sound to set bell sound checkbox"
)
assert "bellSound" in body, (
"openSettings must read bellSound from display settings"
)
assert "bellSound" in body, "openSettings must read bellSound from display settings"
def test_open_settings_updates_notification_status_text() -> None:
@@ -1429,6 +1428,98 @@ def test_update_notification_ui_has_null_guard() -> None:
)
assert match, "_updateNotificationUI function not found"
body = match.group(1)
assert "null" in body or "non-null" in body or "!statusEl" in body or "!reqBtn" in body, (
assert (
"null" in body or "non-null" in body or "!statusEl" in body or "!reqBtn" in body
), (
"_updateNotificationUI must include a null guard (null check) or JSDoc non-null annotation"
)
# ─── New Session tab (task-3-new-session-tab) ─────────────────────────────────
def test_new_session_default_template_constant_exists() -> None:
"""NEW_SESSION_DEFAULT_TEMPLATE constant must be declared in app.js."""
assert "NEW_SESSION_DEFAULT_TEMPLATE" in _JS, (
"NEW_SESSION_DEFAULT_TEMPLATE constant must be declared in app.js"
)
def test_new_session_default_template_value() -> None:
"""NEW_SESSION_DEFAULT_TEMPLATE must equal 'tmux new-session -d -s {name}'."""
assert "tmux new-session -d -s {name}" in _JS, (
"NEW_SESSION_DEFAULT_TEMPLATE must be 'tmux new-session -d -s {name}'"
)
def test_open_settings_populates_template_textarea() -> None:
"""openSettings must populate #setting-template textarea from server settings."""
match = re.search(
r"function openSettings\s*\(\s*\)\s*\{(.*?)(?=\nfunction |\n// )",
_JS,
re.DOTALL,
)
assert match, "openSettings function not found"
body = match.group(1)
assert "setting-template" in body, (
"openSettings must reference setting-template textarea"
)
assert "new_session_template" in body, (
"openSettings must populate template from ss.new_session_template"
)
assert "NEW_SESSION_DEFAULT_TEMPLATE" in body, (
"openSettings must fall back to NEW_SESSION_DEFAULT_TEMPLATE"
)
def test_bind_static_event_listeners_binds_template_input_with_debounce() -> None:
"""bindStaticEventListeners must bind input on #setting-template with 500ms debounce."""
match = re.search(
r"function bindStaticEventListeners\s*\(\s*\)\s*\{(.*?)\n\}",
_JS,
re.DOTALL,
)
assert match, "bindStaticEventListeners function not found"
body = match.group(1)
assert "setting-template" in body, (
"bindStaticEventListeners must bind setting-template input event"
)
assert "500" in body, (
"bindStaticEventListeners template input handler must use 500ms debounce"
)
assert "patchServerSetting" in body, (
"bindStaticEventListeners template input handler must call patchServerSetting"
)
assert "new_session_template" in body, (
"bindStaticEventListeners template input handler must pass 'new_session_template' key"
)
def test_bind_static_event_listeners_binds_template_reset_button() -> None:
"""bindStaticEventListeners must bind click on #setting-template-reset."""
match = re.search(
r"function bindStaticEventListeners\s*\(\s*\)\s*\{(.*?)\n\}",
_JS,
re.DOTALL,
)
assert match, "bindStaticEventListeners function not found"
body = match.group(1)
assert "setting-template-reset" in body, (
"bindStaticEventListeners must bind setting-template-reset click event"
)
def test_bind_static_event_listeners_reset_patches_server() -> None:
"""bindStaticEventListeners reset handler must call patchServerSetting with default template."""
match = re.search(
r"function bindStaticEventListeners\s*\(\s*\)\s*\{(.*?)\n\}",
_JS,
re.DOTALL,
)
assert match, "bindStaticEventListeners function not found"
body = match.group(1)
# The reset button section must patch with the default template
assert "setting-template-reset" in body, "setting-template-reset must be referenced"
assert "NEW_SESSION_DEFAULT_TEMPLATE" in body, (
"bindStaticEventListeners reset handler must use NEW_SESSION_DEFAULT_TEMPLATE"
)