feat: add New Session settings tab with template textarea
This commit is contained in:
@@ -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 ────────────────────────────────────────────────────────
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
============================================================ */
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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}"
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user