feat: reorganize settings dialog — add Multi-Device tab, enable/disable toggle, device name as page title
- Add multi_device_enabled: false to DEFAULT_SETTINGS in settings.py - Add fifth 'Multi-Device' tab (data-tab='devices') to settings dialog sidebar - Move #setting-device-name, #setting-remote-instances, #add-remote-instance-btn, #setting-view-mode, and #setting-view-scope out of Display/Sessions tabs into the new dedicated devices panel - Add #setting-multi-device-enabled checkbox at top of Multi-Device tab; when unchecked, all other fields in the tab are greyed out and disabled - Gate buildSources() remote instances behind multi_device_enabled (smart default: treated as enabled if remote_instances is non-empty for backward compat) - Wire device name → document.title: updates title on openSettings() and on every keystroke in the device name input field - Add _updateMultiDeviceFieldsState() helper to enable/disable fields - Add #multi-device-fields CSS transition for smooth opacity animation - Add 19 new tests (settings, HTML, JS, CSS) covering all new behavior
This commit is contained in:
+60
-11
@@ -1380,17 +1380,23 @@ function buildSources(settings) {
|
||||
var sources = [
|
||||
{ url: '', name: localName, type: 'local', status: 'authenticated', backoffMs: 2000 },
|
||||
];
|
||||
// Only add remote sources when multi-device is enabled.
|
||||
// Smart default: treat as enabled if remote_instances is non-empty (backward compat).
|
||||
var remotes = (settings && settings.remote_instances) || [];
|
||||
for (var i = 0; i < remotes.length; i++) {
|
||||
var r = remotes[i];
|
||||
if (r && r.url) {
|
||||
sources.push({
|
||||
url: r.url.replace(/\/+$/, ''),
|
||||
name: r.name || r.url,
|
||||
type: 'remote',
|
||||
status: 'authenticated',
|
||||
backoffMs: 2000,
|
||||
});
|
||||
var multiDeviceEnabled = (settings && settings.multi_device_enabled) ||
|
||||
(remotes.length > 0);
|
||||
if (multiDeviceEnabled) {
|
||||
for (var i = 0; i < remotes.length; i++) {
|
||||
var r = remotes[i];
|
||||
if (r && r.url) {
|
||||
sources.push({
|
||||
url: r.url.replace(/\/+$/, ''),
|
||||
name: r.name || r.url,
|
||||
type: 'remote',
|
||||
status: 'authenticated',
|
||||
backoffMs: 2000,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return sources;
|
||||
@@ -1449,6 +1455,25 @@ function _saveRemoteInstances() {
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Multi-Device helper ──────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Enable or disable all Multi-Device tab fields (except the enable toggle itself).
|
||||
* When disabled, the fields container gets opacity: 0.5 and inputs/selects/buttons
|
||||
* are disabled so users cannot interact with them.
|
||||
* @param {boolean} enabled
|
||||
*/
|
||||
function _updateMultiDeviceFieldsState(enabled) {
|
||||
var fieldsContainer = $('multi-device-fields');
|
||||
if (!fieldsContainer) return;
|
||||
var controls = fieldsContainer.querySelectorAll('input, select, button');
|
||||
controls.forEach(function(ctrl) {
|
||||
ctrl.disabled = !enabled;
|
||||
});
|
||||
fieldsContainer.style.opacity = enabled ? '' : '0.5';
|
||||
}
|
||||
|
||||
|
||||
// ─── Settings dialog ──────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -1766,6 +1791,20 @@ function openSettings() {
|
||||
deviceNameEl.value = (ss && ss.device_name) || '';
|
||||
}
|
||||
|
||||
// Update document.title from device_name setting
|
||||
if (ss && ss.device_name) {
|
||||
document.title = ss.device_name;
|
||||
}
|
||||
|
||||
// Multi-device enabled checkbox (with smart default: checked if remote_instances non-empty)
|
||||
const multiDeviceEnabledEl = $('setting-multi-device-enabled');
|
||||
if (multiDeviceEnabledEl) {
|
||||
var remoteList = (ss && ss.remote_instances) || [];
|
||||
multiDeviceEnabledEl.checked = !!(ss && ss.multi_device_enabled) ||
|
||||
remoteList.length > 0;
|
||||
_updateMultiDeviceFieldsState(multiDeviceEnabledEl.checked);
|
||||
}
|
||||
|
||||
// Remote instances
|
||||
const remoteInstancesEl = $('setting-remote-instances');
|
||||
if (remoteInstancesEl) {
|
||||
@@ -2298,11 +2337,21 @@ function bindStaticEventListeners() {
|
||||
patchServerSetting('new_session_template', NEW_SESSION_DEFAULT_TEMPLATE);
|
||||
});
|
||||
|
||||
// Sessions tab — device name with 500ms debounce
|
||||
// Multi-Device tab — enable/disable toggle
|
||||
on($('setting-multi-device-enabled'), 'change', function() {
|
||||
var enabled = this.checked;
|
||||
_updateMultiDeviceFieldsState(enabled);
|
||||
patchServerSetting('multi_device_enabled', enabled).then(function() {
|
||||
_sources = buildSources(_serverSettings);
|
||||
});
|
||||
});
|
||||
|
||||
// Multi-Device tab — device name with 500ms debounce; updates document.title immediately
|
||||
var _deviceNameDebounceTimer;
|
||||
on($('setting-device-name'), 'input', function() {
|
||||
clearTimeout(_deviceNameDebounceTimer);
|
||||
var val = this.value;
|
||||
if (val) document.title = val;
|
||||
_deviceNameDebounceTimer = setTimeout(function() {
|
||||
patchServerSetting('device_name', val).then(function() {
|
||||
_sources = buildSources(_serverSettings);
|
||||
|
||||
+33
-24
@@ -90,6 +90,7 @@
|
||||
<button class="settings-tab" data-tab="sessions">Sessions</button>
|
||||
<button class="settings-tab" data-tab="notifications">Notifications</button>
|
||||
<button class="settings-tab" data-tab="new-session">Commands</button>
|
||||
<button class="settings-tab" data-tab="devices">Multi-Device</button>
|
||||
</nav>
|
||||
<div class="settings-content">
|
||||
<div class="settings-panel" data-tab="display">
|
||||
@@ -123,21 +124,6 @@
|
||||
<option value="4">4</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label class="settings-label" for="setting-view-mode">View Mode</label>
|
||||
<select id="setting-view-mode" class="settings-select">
|
||||
<option value="flat">Flat</option>
|
||||
<option value="grouped">Grouped</option>
|
||||
<option value="filtered">Filtered</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label class="settings-label" for="setting-view-scope">View Scope</label>
|
||||
<select id="setting-view-scope" class="settings-select">
|
||||
<option value="local">Local</option>
|
||||
<option value="server">Server</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-panel hidden" data-tab="sessions">
|
||||
<div class="settings-field">
|
||||
@@ -166,15 +152,6 @@
|
||||
<label class="settings-label" for="setting-auto-open">Auto-open created sessions</label>
|
||||
<input type="checkbox" id="setting-auto-open" class="settings-checkbox" checked />
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label class="settings-label" for="setting-device-name">Device Name</label>
|
||||
<input type="text" id="setting-device-name" class="settings-input" placeholder="This device" />
|
||||
</div>
|
||||
<div class="settings-field settings-field--column">
|
||||
<label class="settings-label">Remote Instances</label>
|
||||
<div id="setting-remote-instances" class="settings-remote-list"></div>
|
||||
<button id="add-remote-instance-btn" class="settings-action-btn">+ Add Remote</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-panel hidden" data-tab="notifications">
|
||||
<div class="settings-field">
|
||||
@@ -203,6 +180,38 @@
|
||||
<button id="setting-delete-template-reset" class="settings-action-btn">Reset to default</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-panel hidden" data-tab="devices">
|
||||
<div class="settings-field">
|
||||
<label class="settings-label" for="setting-multi-device-enabled">Enable multi-device support</label>
|
||||
<input type="checkbox" id="setting-multi-device-enabled" class="settings-checkbox" />
|
||||
</div>
|
||||
<div id="multi-device-fields">
|
||||
<div class="settings-field">
|
||||
<label class="settings-label" for="setting-device-name">Device Name</label>
|
||||
<input type="text" id="setting-device-name" class="settings-input" placeholder="This device" />
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label class="settings-label" for="setting-view-mode">View Mode</label>
|
||||
<select id="setting-view-mode" class="settings-select">
|
||||
<option value="flat">Flat</option>
|
||||
<option value="grouped">Grouped</option>
|
||||
<option value="filtered">Filtered</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label class="settings-label" for="setting-view-scope">View Scope</label>
|
||||
<select id="setting-view-scope" class="settings-select">
|
||||
<option value="local">Local</option>
|
||||
<option value="server">Server</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="settings-field settings-field--column">
|
||||
<label class="settings-label">Remote Instances</label>
|
||||
<div id="setting-remote-instances" class="settings-remote-list"></div>
|
||||
<button id="add-remote-instance-btn" class="settings-action-btn">+ Add Remote</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
@@ -1653,3 +1653,8 @@ body {
|
||||
color: #ef4444;
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
|
||||
/* Multi-Device tab — smooth enable/disable opacity transition */
|
||||
#multi-device-fields {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ DEFAULT_SETTINGS: dict = {
|
||||
"remote_instances": [],
|
||||
"device_name": "",
|
||||
"delete_session_template": "tmux kill-session -t {name}",
|
||||
"multi_device_enabled": False,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1987,3 +1987,23 @@ def test_fit_view_pre_has_top_zero() -> None:
|
||||
assert "top: 0" in block or "top:0" in block, (
|
||||
".session-grid--fit .tile-body pre must have top: 0 to fill full tile body height"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Multi-Device tab CSS (settings UI reorganization)
|
||||
# ============================================================
|
||||
|
||||
|
||||
def test_css_multi_device_fields_transition() -> None:
|
||||
"""#multi-device-fields must have a CSS transition for smooth enable/disable animation."""
|
||||
css = read_css()
|
||||
assert "#multi-device-fields" in css, (
|
||||
"Missing #multi-device-fields CSS rule — needed for smooth enable/disable opacity transition"
|
||||
)
|
||||
match = re.search(r"#multi-device-fields\s*\{([^}]*)\}", css, re.DOTALL)
|
||||
assert match, "#multi-device-fields CSS rule not found"
|
||||
body = match.group(1)
|
||||
assert "transition" in body, (
|
||||
"#multi-device-fields must have a transition property for smooth enable/disable animation"
|
||||
)
|
||||
assert "opacity" in body, "#multi-device-fields transition must include opacity"
|
||||
|
||||
@@ -434,7 +434,7 @@ def test_html_settings_dialog() -> None:
|
||||
|
||||
|
||||
def test_html_settings_tabs() -> None:
|
||||
"""settings-dialog must contain 4 tab buttons with correct data-tab values."""
|
||||
"""settings-dialog must contain 5 tab buttons with correct data-tab values."""
|
||||
soup = _SOUP
|
||||
dialog = soup.find(id="settings-dialog")
|
||||
assert dialog is not None, "Missing #settings-dialog"
|
||||
@@ -442,7 +442,7 @@ def test_html_settings_tabs() -> None:
|
||||
assert tabs_container is not None, (
|
||||
"Missing nav.settings-tabs inside #settings-dialog"
|
||||
)
|
||||
expected_tabs = ["display", "sessions", "notifications", "new-session"]
|
||||
expected_tabs = ["display", "sessions", "notifications", "new-session", "devices"]
|
||||
for tab_value in expected_tabs:
|
||||
tab = tabs_container.find("button", attrs={"data-tab": tab_value})
|
||||
assert tab is not None, (
|
||||
@@ -540,8 +540,8 @@ def test_html_settings_panels_use_data_tab() -> None:
|
||||
dialog = soup.find(id="settings-dialog")
|
||||
assert dialog is not None, "Missing #settings-dialog"
|
||||
panels = dialog.find_all(class_="settings-panel")
|
||||
assert len(panels) == 4, (
|
||||
f"Expected 4 .settings-panel elements, found: {len(panels)}"
|
||||
assert len(panels) == 5, (
|
||||
f"Expected 5 .settings-panel elements, found: {len(panels)}"
|
||||
)
|
||||
for panel in panels:
|
||||
assert panel.get("data-tab") is not None, (
|
||||
@@ -1070,44 +1070,44 @@ def test_html_settings_close_btn_exists() -> None:
|
||||
|
||||
|
||||
def test_html_sessions_tab_device_name_input() -> None:
|
||||
"""Sessions tab must contain a #setting-device-name text input for the device name."""
|
||||
"""Multi-Device tab must contain a #setting-device-name text input for the device name."""
|
||||
soup = _SOUP
|
||||
el = soup.find(id="setting-device-name")
|
||||
assert el is not None, "Missing element with id='setting-device-name'"
|
||||
# Must be inside the sessions panel
|
||||
sessions_panel = soup.find("div", attrs={"data-tab": "sessions"})
|
||||
assert sessions_panel is not None, "Missing sessions panel (data-tab='sessions')"
|
||||
assert sessions_panel.find(id="setting-device-name") is not None, (
|
||||
"#setting-device-name must be inside the sessions settings panel"
|
||||
# Must be inside the devices panel (not sessions)
|
||||
devices_panel = soup.find("div", attrs={"data-tab": "devices"})
|
||||
assert devices_panel is not None, "Missing devices panel (data-tab='devices')"
|
||||
assert devices_panel.find(id="setting-device-name") is not None, (
|
||||
"#setting-device-name must be inside the devices (Multi-Device) settings panel"
|
||||
)
|
||||
|
||||
|
||||
def test_html_sessions_tab_remote_instances_container() -> None:
|
||||
"""Sessions tab must contain a #setting-remote-instances container for remote instance rows."""
|
||||
"""Multi-Device tab must contain a #setting-remote-instances container for remote instance rows."""
|
||||
soup = _SOUP
|
||||
el = soup.find(id="setting-remote-instances")
|
||||
assert el is not None, "Missing element with id='setting-remote-instances'"
|
||||
# Must be inside the sessions panel
|
||||
sessions_panel = soup.find("div", attrs={"data-tab": "sessions"})
|
||||
assert sessions_panel is not None, "Missing sessions panel (data-tab='sessions')"
|
||||
assert sessions_panel.find(id="setting-remote-instances") is not None, (
|
||||
"#setting-remote-instances must be inside the sessions settings panel"
|
||||
# Must be inside the devices panel
|
||||
devices_panel = soup.find("div", attrs={"data-tab": "devices"})
|
||||
assert devices_panel is not None, "Missing devices panel (data-tab='devices')"
|
||||
assert devices_panel.find(id="setting-remote-instances") is not None, (
|
||||
"#setting-remote-instances must be inside the devices (Multi-Device) settings panel"
|
||||
)
|
||||
|
||||
|
||||
def test_html_sessions_tab_add_remote_instance_btn() -> None:
|
||||
"""Sessions tab must contain an #add-remote-instance-btn button to add remote instances."""
|
||||
"""Multi-Device tab must contain an #add-remote-instance-btn button to add remote instances."""
|
||||
soup = _SOUP
|
||||
el = soup.find(id="add-remote-instance-btn")
|
||||
assert el is not None, "Missing element with id='add-remote-instance-btn'"
|
||||
assert el.name == "button", (
|
||||
f"#add-remote-instance-btn must be a <button>, got: {el.name}"
|
||||
)
|
||||
# Must be inside the sessions panel
|
||||
sessions_panel = soup.find("div", attrs={"data-tab": "sessions"})
|
||||
assert sessions_panel is not None, "Missing sessions panel (data-tab='sessions')"
|
||||
assert sessions_panel.find(id="add-remote-instance-btn") is not None, (
|
||||
"#add-remote-instance-btn must be inside the sessions settings panel"
|
||||
# Must be inside the devices panel
|
||||
devices_panel = soup.find("div", attrs={"data-tab": "devices"})
|
||||
assert devices_panel is not None, "Missing devices panel (data-tab='devices')"
|
||||
assert devices_panel.find(id="add-remote-instance-btn") is not None, (
|
||||
"#add-remote-instance-btn must be inside the devices (Multi-Device) settings panel"
|
||||
)
|
||||
|
||||
|
||||
@@ -1206,10 +1206,134 @@ def test_html_view_mode_button_exists() -> None:
|
||||
"""#view-mode-btn must exist in the overview header for cycling view modes."""
|
||||
soup = _SOUP
|
||||
btn = soup.find(id="view-mode-btn")
|
||||
assert btn is not None, "Missing element with id='view-mode-btn' (view mode toggle button)"
|
||||
assert btn is not None, (
|
||||
"Missing element with id='view-mode-btn' (view mode toggle button)"
|
||||
)
|
||||
# Must be inside the overview header area
|
||||
overview = soup.find(id="view-overview")
|
||||
assert overview is not None, "Missing #view-overview"
|
||||
assert overview.find(id="view-mode-btn") is not None, (
|
||||
"#view-mode-btn must be inside #view-overview header"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Multi-Device tab (settings UI reorganization)
|
||||
# ============================================================
|
||||
|
||||
|
||||
def test_html_devices_tab_button_exists() -> None:
|
||||
"""Settings dialog must contain a tab button with data-tab='devices' labeled 'Multi-Device'."""
|
||||
soup = _SOUP
|
||||
dialog = soup.find(id="settings-dialog")
|
||||
assert dialog is not None, "Missing #settings-dialog"
|
||||
tabs_container = dialog.find("nav", class_="settings-tabs")
|
||||
assert tabs_container is not None, "Missing nav.settings-tabs"
|
||||
tab = tabs_container.find("button", attrs={"data-tab": "devices"})
|
||||
assert tab is not None, (
|
||||
"Missing tab button with data-tab='devices' in settings-tabs"
|
||||
)
|
||||
tab_classes = tab.get("class") or []
|
||||
assert "settings-tab" in tab_classes, (
|
||||
f"devices tab button must have class 'settings-tab', has: {tab_classes}"
|
||||
)
|
||||
|
||||
|
||||
def test_html_devices_panel_exists() -> None:
|
||||
"""A settings-panel with data-tab='devices' must exist inside #settings-dialog."""
|
||||
soup = _SOUP
|
||||
dialog = soup.find(id="settings-dialog")
|
||||
assert dialog is not None, "Missing #settings-dialog"
|
||||
panel = dialog.find(class_="settings-panel", attrs={"data-tab": "devices"})
|
||||
assert panel is not None, (
|
||||
"Missing settings-panel[data-tab='devices'] (Multi-Device tab panel)"
|
||||
)
|
||||
|
||||
|
||||
def test_html_devices_panel_has_enable_checkbox() -> None:
|
||||
"""Multi-Device tab panel must contain #setting-multi-device-enabled checkbox."""
|
||||
soup = _SOUP
|
||||
devices_panel = soup.find("div", attrs={"data-tab": "devices"})
|
||||
assert devices_panel is not None, "Missing devices panel (data-tab='devices')"
|
||||
el = devices_panel.find(id="setting-multi-device-enabled")
|
||||
assert el is not None, "Missing #setting-multi-device-enabled inside devices panel"
|
||||
assert el.name == "input", (
|
||||
f"#setting-multi-device-enabled must be an <input>, got: {el.name}"
|
||||
)
|
||||
assert el.get("type") == "checkbox", (
|
||||
f"#setting-multi-device-enabled must be type='checkbox', got: {el.get('type')}"
|
||||
)
|
||||
|
||||
|
||||
def test_html_devices_panel_has_device_name() -> None:
|
||||
"""Multi-Device tab panel must contain #setting-device-name text input."""
|
||||
soup = _SOUP
|
||||
devices_panel = soup.find("div", attrs={"data-tab": "devices"})
|
||||
assert devices_panel is not None, "Missing devices panel (data-tab='devices')"
|
||||
el = devices_panel.find(id="setting-device-name")
|
||||
assert el is not None, "Missing #setting-device-name inside devices panel"
|
||||
|
||||
|
||||
def test_html_devices_panel_has_remote_instances() -> None:
|
||||
"""Multi-Device tab panel must contain #setting-remote-instances container."""
|
||||
soup = _SOUP
|
||||
devices_panel = soup.find("div", attrs={"data-tab": "devices"})
|
||||
assert devices_panel is not None, "Missing devices panel (data-tab='devices')"
|
||||
el = devices_panel.find(id="setting-remote-instances")
|
||||
assert el is not None, "Missing #setting-remote-instances inside devices panel"
|
||||
|
||||
|
||||
def test_html_devices_panel_has_add_remote_btn() -> None:
|
||||
"""Multi-Device tab panel must contain #add-remote-instance-btn button."""
|
||||
soup = _SOUP
|
||||
devices_panel = soup.find("div", attrs={"data-tab": "devices"})
|
||||
assert devices_panel is not None, "Missing devices panel (data-tab='devices')"
|
||||
btn = devices_panel.find(id="add-remote-instance-btn")
|
||||
assert btn is not None, "Missing #add-remote-instance-btn inside devices panel"
|
||||
assert btn.name == "button", (
|
||||
f"#add-remote-instance-btn must be a <button>, got: {btn.name}"
|
||||
)
|
||||
|
||||
|
||||
def test_html_devices_panel_has_view_mode() -> None:
|
||||
"""Multi-Device tab panel must contain #setting-view-mode select."""
|
||||
soup = _SOUP
|
||||
devices_panel = soup.find("div", attrs={"data-tab": "devices"})
|
||||
assert devices_panel is not None, "Missing devices panel (data-tab='devices')"
|
||||
el = devices_panel.find(id="setting-view-mode")
|
||||
assert el is not None, "Missing #setting-view-mode inside devices panel"
|
||||
assert el.name == "select", f"#setting-view-mode must be a <select>, got: {el.name}"
|
||||
|
||||
|
||||
def test_html_devices_panel_has_view_scope() -> None:
|
||||
"""Multi-Device tab panel must contain #setting-view-scope select."""
|
||||
soup = _SOUP
|
||||
devices_panel = soup.find("div", attrs={"data-tab": "devices"})
|
||||
assert devices_panel is not None, "Missing devices panel (data-tab='devices')"
|
||||
el = devices_panel.find(id="setting-view-scope")
|
||||
assert el is not None, "Missing #setting-view-scope inside devices panel"
|
||||
assert el.name == "select", (
|
||||
f"#setting-view-scope must be a <select>, got: {el.name}"
|
||||
)
|
||||
|
||||
|
||||
def test_html_display_panel_no_view_mode() -> None:
|
||||
"""Display panel must NOT contain #setting-view-mode (moved to Multi-Device tab)."""
|
||||
soup = _SOUP
|
||||
display_panel = soup.find("div", attrs={"data-tab": "display"})
|
||||
assert display_panel is not None, "Missing display panel"
|
||||
el = display_panel.find(id="setting-view-mode")
|
||||
assert el is None, (
|
||||
"#setting-view-mode must NOT be in the display panel (moved to Multi-Device tab)"
|
||||
)
|
||||
|
||||
|
||||
def test_html_display_panel_no_view_scope() -> None:
|
||||
"""Display panel must NOT contain #setting-view-scope (moved to Multi-Device tab)."""
|
||||
soup = _SOUP
|
||||
display_panel = soup.find("div", attrs={"data-tab": "display"})
|
||||
assert display_panel is not None, "Missing display panel"
|
||||
el = display_panel.find(id="setting-view-scope")
|
||||
assert el is None, (
|
||||
"#setting-view-scope must NOT be in the display panel (moved to Multi-Device tab)"
|
||||
)
|
||||
|
||||
@@ -2444,3 +2444,81 @@ def test_build_remote_instance_row_name_input_aria_label() -> None:
|
||||
assert "Remote instance display name" in body, (
|
||||
"nameInput must have aria-label='Remote instance display name' for screen-reader accessibility"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Multi-Device tab — settings UI reorganization
|
||||
# ============================================================
|
||||
|
||||
|
||||
def test_open_settings_populates_multi_device_enabled() -> None:
|
||||
"""openSettings must populate #setting-multi-device-enabled from server settings."""
|
||||
match = re.search(
|
||||
r"function openSettings\s*\(\s*\)\s*\{(.*?)(?=\nfunction |\n// )",
|
||||
_JS,
|
||||
re.DOTALL,
|
||||
)
|
||||
assert match, "openSettings function not found in app.js"
|
||||
body = match.group(1)
|
||||
assert "setting-multi-device-enabled" in body, (
|
||||
"openSettings must populate #setting-multi-device-enabled checkbox"
|
||||
)
|
||||
assert "multi_device_enabled" in body, (
|
||||
"openSettings must reference multi_device_enabled from server settings"
|
||||
)
|
||||
|
||||
|
||||
def test_open_settings_updates_document_title() -> None:
|
||||
"""openSettings must update document.title from the device_name setting."""
|
||||
match = re.search(
|
||||
r"function openSettings\s*\(\s*\)\s*\{(.*?)(?=\nfunction |\n// )",
|
||||
_JS,
|
||||
re.DOTALL,
|
||||
)
|
||||
assert match, "openSettings function not found in app.js"
|
||||
body = match.group(1)
|
||||
assert "document.title" in body, (
|
||||
"openSettings must update document.title from device_name"
|
||||
)
|
||||
|
||||
|
||||
def test_bind_static_event_listeners_binds_multi_device_enabled() -> None:
|
||||
"""bindStaticEventListeners must bind change on #setting-multi-device-enabled."""
|
||||
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-multi-device-enabled" in body, (
|
||||
"bindStaticEventListeners must bind #setting-multi-device-enabled change event"
|
||||
)
|
||||
|
||||
|
||||
def test_build_sources_checks_multi_device_enabled() -> None:
|
||||
"""buildSources must check multi_device_enabled before adding remote sources."""
|
||||
match = re.search(
|
||||
r"function buildSources\s*\(.*?\)\s*\{(.*?)(?=\nfunction |\n// )",
|
||||
_JS,
|
||||
re.DOTALL,
|
||||
)
|
||||
assert match, "buildSources function not found in app.js"
|
||||
body = match.group(1)
|
||||
assert "multi_device_enabled" in body, (
|
||||
"buildSources must check multi_device_enabled to gate remote sources"
|
||||
)
|
||||
|
||||
|
||||
def test_device_name_change_updates_document_title() -> None:
|
||||
"""device name change handler in bindStaticEventListeners must update document.title."""
|
||||
match = re.search(
|
||||
r"function bindStaticEventListeners\s*\(\s*\)\s*\{(.*?)\n\}",
|
||||
_JS,
|
||||
re.DOTALL,
|
||||
)
|
||||
assert match, "bindStaticEventListeners function not found"
|
||||
body = match.group(1)
|
||||
assert "document.title" in body, (
|
||||
"bindStaticEventListeners device-name handler must update document.title"
|
||||
)
|
||||
|
||||
@@ -270,3 +270,39 @@ def test_delete_session_template_patchable():
|
||||
# Verify it was persisted
|
||||
loaded = load_settings()
|
||||
assert loaded["delete_session_template"] == custom
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Multi-device enabled flag (task: settings UI reorganization)
|
||||
# ============================================================
|
||||
|
||||
|
||||
def test_defaults_include_multi_device_enabled():
|
||||
"""DEFAULT_SETTINGS must include 'multi_device_enabled' key initialised to False."""
|
||||
assert "multi_device_enabled" in DEFAULT_SETTINGS, (
|
||||
"DEFAULT_SETTINGS must include 'multi_device_enabled'"
|
||||
)
|
||||
assert DEFAULT_SETTINGS["multi_device_enabled"] is False, (
|
||||
f"multi_device_enabled default must be False, got: {DEFAULT_SETTINGS['multi_device_enabled']!r}"
|
||||
)
|
||||
|
||||
|
||||
def test_load_returns_multi_device_enabled_default():
|
||||
"""load_settings() must return multi_device_enabled with default value of False."""
|
||||
result = load_settings()
|
||||
assert "multi_device_enabled" in result, (
|
||||
"load_settings() must include 'multi_device_enabled'"
|
||||
)
|
||||
assert result["multi_device_enabled"] is False, (
|
||||
f"load_settings() multi_device_enabled must default to False, got: {result['multi_device_enabled']!r}"
|
||||
)
|
||||
|
||||
|
||||
def test_multi_device_enabled_patchable():
|
||||
"""patch_settings() must accept and persist multi_device_enabled."""
|
||||
result = patch_settings({"multi_device_enabled": True})
|
||||
assert result["multi_device_enabled"] is True, (
|
||||
f"patch_settings() must accept multi_device_enabled=True, got: {result['multi_device_enabled']!r}"
|
||||
)
|
||||
loaded = load_settings()
|
||||
assert loaded["multi_device_enabled"] is True
|
||||
|
||||
Reference in New Issue
Block a user