feat: add Sessions settings tab with server-side persistence
This commit is contained in:
@@ -132,6 +132,7 @@ var _previewSessionName = null; // track by NAME, not DOM element
|
||||
|
||||
// ─── Settings state ───────────────────────────────────────────────────────────
|
||||
let _settingsOpen = false;
|
||||
let _serverSettings = null;
|
||||
const DISPLAY_SETTINGS_KEY = 'muxplex.display';
|
||||
const DISPLAY_DEFAULTS = {
|
||||
fontSize: 14,
|
||||
@@ -909,6 +910,42 @@ function _setViewingSession(name) {
|
||||
_viewingSession = name;
|
||||
}
|
||||
|
||||
// ─── Server settings ─────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Load server settings from GET /api/settings and cache in _serverSettings.
|
||||
* Always resolves — errors are logged as warnings.
|
||||
* @returns {Promise<object>}
|
||||
*/
|
||||
async function loadServerSettings() {
|
||||
try {
|
||||
const res = await api('GET', '/api/settings');
|
||||
_serverSettings = await res.json();
|
||||
} catch (err) {
|
||||
console.warn('[loadServerSettings] failed:', err);
|
||||
if (!_serverSettings) _serverSettings = {};
|
||||
}
|
||||
return _serverSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a PATCH to /api/settings with a single key/value update.
|
||||
* Shows a toast on success or failure.
|
||||
* @param {string} key
|
||||
* @param {*} value
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function patchServerSetting(key, value) {
|
||||
try {
|
||||
await api('PATCH', '/api/settings', { [key]: value });
|
||||
_serverSettings = Object.assign({}, _serverSettings, { [key]: value });
|
||||
showToast('Setting saved');
|
||||
} catch (err) {
|
||||
showToast('Failed to save setting');
|
||||
console.warn('[patchServerSetting] failed:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Settings dialog ──────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -1000,6 +1037,61 @@ function openSettings() {
|
||||
if (hoverDelayEl) hoverDelayEl.value = String(settings.hoverPreviewDelay);
|
||||
const gridColumnsEl = $('setting-grid-columns');
|
||||
if (gridColumnsEl) gridColumnsEl.value = String(settings.gridColumns);
|
||||
|
||||
// Populate Sessions tab from server settings
|
||||
loadServerSettings().then(function(ss) {
|
||||
// Default session dropdown
|
||||
const defaultSessionEl = $('setting-default-session');
|
||||
if (defaultSessionEl) {
|
||||
// Rebuild options from current sessions
|
||||
defaultSessionEl.innerHTML = '<option value="">(none)</option>';
|
||||
(_currentSessions || []).forEach(function(s) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = s.name || '';
|
||||
opt.textContent = s.name || '';
|
||||
if (ss && ss.default_session === s.name) opt.selected = true;
|
||||
defaultSessionEl.appendChild(opt);
|
||||
});
|
||||
}
|
||||
|
||||
// Sort order
|
||||
const sortOrderEl = $('setting-sort-order');
|
||||
if (sortOrderEl && ss && ss.sort_order) {
|
||||
sortOrderEl.value = ss.sort_order;
|
||||
}
|
||||
|
||||
// Hidden sessions checkboxes
|
||||
const hiddenSessionsEl = $('setting-hidden-sessions');
|
||||
if (hiddenSessionsEl) {
|
||||
hiddenSessionsEl.innerHTML = '';
|
||||
const hiddenList = (ss && ss.hidden_sessions) || [];
|
||||
(_currentSessions || []).forEach(function(s) {
|
||||
const name = s.name || '';
|
||||
const item = document.createElement('label');
|
||||
item.className = 'settings-checkbox-item';
|
||||
const cb = document.createElement('input');
|
||||
cb.type = 'checkbox';
|
||||
cb.className = 'settings-checkbox';
|
||||
cb.value = name;
|
||||
cb.checked = hiddenList.includes(name);
|
||||
item.appendChild(cb);
|
||||
item.appendChild(document.createTextNode(' ' + name));
|
||||
hiddenSessionsEl.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
// Window size largest
|
||||
const windowSizeEl = $('setting-window-size-largest');
|
||||
if (windowSizeEl) {
|
||||
windowSizeEl.checked = !!(ss && ss.window_size_largest);
|
||||
}
|
||||
|
||||
// Auto-open
|
||||
const autoOpenEl = $('setting-auto-open');
|
||||
if (autoOpenEl) {
|
||||
autoOpenEl.checked = ss && ss.auto_open !== undefined ? !!ss.auto_open : true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1205,6 +1297,38 @@ function bindStaticEventListeners() {
|
||||
on($('setting-font-size'), 'change', onDisplaySettingChange);
|
||||
on($('setting-hover-delay'), 'change', onDisplaySettingChange);
|
||||
on($('setting-grid-columns'), 'change', onDisplaySettingChange);
|
||||
|
||||
// Sessions settings — bind change events for server-side persistence
|
||||
on($('setting-default-session'), 'change', function() {
|
||||
var el = $('setting-default-session');
|
||||
if (el) patchServerSetting('default_session', el.value);
|
||||
});
|
||||
on($('setting-sort-order'), 'change', function() {
|
||||
var el = $('setting-sort-order');
|
||||
if (el) patchServerSetting('sort_order', el.value);
|
||||
});
|
||||
on($('setting-window-size-largest'), 'change', function() {
|
||||
var el = $('setting-window-size-largest');
|
||||
if (el) patchServerSetting('window_size_largest', el.checked);
|
||||
});
|
||||
on($('setting-auto-open'), 'change', function() {
|
||||
var el = $('setting-auto-open');
|
||||
if (el) patchServerSetting('auto_open', el.checked);
|
||||
});
|
||||
|
||||
// Hidden sessions — delegated handler on container (checkboxes are dynamic)
|
||||
var hiddenSessionsContainer = $('setting-hidden-sessions');
|
||||
if (hiddenSessionsContainer) {
|
||||
hiddenSessionsContainer.addEventListener('change', function(e) {
|
||||
var cb = e.target.closest('input[type="checkbox"]');
|
||||
if (!cb) return;
|
||||
var hidden = [];
|
||||
hiddenSessionsContainer.querySelectorAll('input[type="checkbox"]').forEach(function(c) {
|
||||
if (c.checked) hidden.push(c.value);
|
||||
});
|
||||
patchServerSetting('hidden_sessions', hidden);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Test-only helpers ────────────────────────────────────────────────────────
|
||||
@@ -1291,6 +1415,9 @@ if (typeof module !== 'undefined' && module.exports) {
|
||||
openSettings,
|
||||
closeSettings,
|
||||
switchSettingsTab,
|
||||
// Server settings
|
||||
loadServerSettings,
|
||||
patchServerSetting,
|
||||
// Test-only helpers
|
||||
_setCurrentSessions,
|
||||
_setViewMode,
|
||||
|
||||
@@ -111,7 +111,34 @@
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-panel hidden" data-tab="sessions"></div>
|
||||
<div class="settings-panel hidden" data-tab="sessions">
|
||||
<div class="settings-field">
|
||||
<label class="settings-label" for="setting-default-session">Default Session</label>
|
||||
<select id="setting-default-session" class="settings-select">
|
||||
<option value="">(none)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label class="settings-label" for="setting-sort-order">Sort Order</label>
|
||||
<select id="setting-sort-order" class="settings-select">
|
||||
<option value="manual">Manual</option>
|
||||
<option value="alphabetical">Alphabetical</option>
|
||||
<option value="recent">Recent</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="settings-field settings-field--column">
|
||||
<label class="settings-label">Hidden Sessions</label>
|
||||
<div id="setting-hidden-sessions" class="settings-checkbox-list"></div>
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label class="settings-label" for="setting-window-size-largest">Auto-set window size (largest)</label>
|
||||
<input type="checkbox" id="setting-window-size-largest" class="settings-checkbox" />
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<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>
|
||||
<div class="settings-panel hidden" data-tab="notifications"></div>
|
||||
<div class="settings-panel hidden" data-tab="new-session"></div>
|
||||
</div>
|
||||
|
||||
@@ -1059,6 +1059,38 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Sessions tab controls
|
||||
============================================================ */
|
||||
|
||||
.settings-field--column {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.settings-checkbox-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.settings-checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.settings-checkbox {
|
||||
accent-color: var(--accent);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Responsive overlay sidebar at <960px
|
||||
============================================================ */
|
||||
|
||||
@@ -851,3 +851,50 @@ def test_css_settings_mobile_tabs_horizontal():
|
||||
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"
|
||||
|
||||
|
||||
# ─── Sessions tab CSS (task-1-sessions-tab) ───────────────────────────────────
|
||||
|
||||
|
||||
def test_css_settings_field_column() -> None:
|
||||
""".settings-field--column must set flex-direction: column."""
|
||||
css = read_css()
|
||||
assert ".settings-field--column" in css, (
|
||||
".settings-field--column class must be defined in style.css"
|
||||
)
|
||||
# Find the rule and check flex-direction
|
||||
import re
|
||||
match = re.search(
|
||||
r"\.settings-field--column\s*\{([^}]*)\}",
|
||||
css,
|
||||
re.DOTALL,
|
||||
)
|
||||
assert match, ".settings-field--column rule not found"
|
||||
body = match.group(1)
|
||||
assert "flex-direction" in body and "column" in body, (
|
||||
".settings-field--column must set flex-direction: column"
|
||||
)
|
||||
|
||||
|
||||
def test_css_settings_checkbox_list() -> None:
|
||||
""".settings-checkbox-list class must be defined."""
|
||||
css = read_css()
|
||||
assert ".settings-checkbox-list" in css, (
|
||||
".settings-checkbox-list class must be defined in style.css"
|
||||
)
|
||||
|
||||
|
||||
def test_css_settings_checkbox_item() -> None:
|
||||
""".settings-checkbox-item class must be defined."""
|
||||
css = read_css()
|
||||
assert ".settings-checkbox-item" in css, (
|
||||
".settings-checkbox-item class must be defined in style.css"
|
||||
)
|
||||
|
||||
|
||||
def test_css_settings_checkbox() -> None:
|
||||
""".settings-checkbox class must be defined."""
|
||||
css = read_css()
|
||||
assert ".settings-checkbox" in css, (
|
||||
".settings-checkbox class must be defined in style.css"
|
||||
)
|
||||
|
||||
@@ -577,3 +577,88 @@ def test_html_settings_tab_panel_data_tab_alignment() -> None:
|
||||
f"Tab buttons {missing_panels} have no matching settings-panel[data-tab=...]. "
|
||||
f"Panel data-tab values found: {panel_values}"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Sessions tab (task-1-sessions-tab)
|
||||
# ============================================================
|
||||
|
||||
|
||||
def test_html_sessions_panel_has_default_session_select() -> None:
|
||||
"""Sessions panel must contain a #setting-default-session select."""
|
||||
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"})
|
||||
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"
|
||||
assert el.name == "select", (
|
||||
f"#setting-default-session must be a <select>, got: {el.name}"
|
||||
)
|
||||
|
||||
|
||||
def test_html_sessions_panel_has_sort_order_select() -> None:
|
||||
"""Sessions panel must contain a #setting-sort-order select with manual/alphabetical/recent options."""
|
||||
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"})
|
||||
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"
|
||||
assert el.name == "select", (
|
||||
f"#setting-sort-order must be a <select>, got: {el.name}"
|
||||
)
|
||||
options = el.find_all("option")
|
||||
values = [o.get("value") for o in options]
|
||||
for v in ("manual", "alphabetical", "recent"):
|
||||
assert v in values, f"#setting-sort-order missing option value='{v}'"
|
||||
|
||||
|
||||
def test_html_sessions_panel_has_hidden_sessions_container() -> None:
|
||||
"""Sessions panel must contain a #setting-hidden-sessions container for checkboxes."""
|
||||
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"})
|
||||
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"
|
||||
|
||||
|
||||
def test_html_sessions_panel_has_window_size_largest_checkbox() -> None:
|
||||
"""Sessions panel must contain a #setting-window-size-largest checkbox."""
|
||||
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"})
|
||||
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"
|
||||
assert el.name == "input", (
|
||||
f"#setting-window-size-largest must be an <input>, got: {el.name}"
|
||||
)
|
||||
assert el.get("type") == "checkbox", (
|
||||
f"#setting-window-size-largest must be type='checkbox', got: {el.get('type')}"
|
||||
)
|
||||
|
||||
|
||||
def test_html_sessions_panel_has_auto_open_checkbox_default_checked() -> None:
|
||||
"""Sessions panel must contain a #setting-auto-open checkbox with default checked."""
|
||||
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"})
|
||||
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.get("type") == "checkbox", (
|
||||
f"#setting-auto-open must be type='checkbox', got: {el.get('type')}"
|
||||
)
|
||||
assert el.get("checked") is not None, (
|
||||
"#setting-auto-open must be checked by default"
|
||||
)
|
||||
|
||||
@@ -1116,3 +1116,184 @@ def test_exports_on_display_setting_change() -> None:
|
||||
assert "onDisplaySettingChange" in exports, (
|
||||
"module.exports must export onDisplaySettingChange"
|
||||
)
|
||||
|
||||
|
||||
# ─── Server settings functions (task-1-sessions-tab) ─────────────────────────
|
||||
|
||||
def test_load_server_settings_function_exists() -> None:
|
||||
"""loadServerSettings function must exist."""
|
||||
assert "function loadServerSettings" in _JS, (
|
||||
"loadServerSettings must be defined in app.js"
|
||||
)
|
||||
|
||||
|
||||
def test_load_server_settings_fetches_api_settings() -> None:
|
||||
"""loadServerSettings must fetch GET /api/settings."""
|
||||
match = re.search(
|
||||
r"async function loadServerSettings\s*\(\s*\)\s*\{(.*?)(?=\nasync function |\nfunction |\n// )",
|
||||
_JS,
|
||||
re.DOTALL,
|
||||
)
|
||||
assert match, "loadServerSettings function not found"
|
||||
body = match.group(1)
|
||||
assert "/api/settings" in body, "loadServerSettings must fetch /api/settings"
|
||||
assert "GET" in body, "loadServerSettings must use GET method"
|
||||
|
||||
|
||||
def test_load_server_settings_caches_in_server_settings() -> None:
|
||||
"""loadServerSettings must cache result in _serverSettings."""
|
||||
assert "_serverSettings" in _JS, "_serverSettings variable must exist in app.js"
|
||||
match = re.search(
|
||||
r"async function loadServerSettings\s*\(\s*\)\s*\{(.*?)(?=\nasync function |\nfunction |\n// )",
|
||||
_JS,
|
||||
re.DOTALL,
|
||||
)
|
||||
assert match, "loadServerSettings function not found"
|
||||
body = match.group(1)
|
||||
assert "_serverSettings" in body, (
|
||||
"loadServerSettings must cache the result in _serverSettings"
|
||||
)
|
||||
|
||||
|
||||
def test_patch_server_setting_function_exists() -> None:
|
||||
"""patchServerSetting function must exist."""
|
||||
assert "function patchServerSetting" in _JS, (
|
||||
"patchServerSetting must be defined in app.js"
|
||||
)
|
||||
|
||||
|
||||
def test_patch_server_setting_sends_patch_request() -> None:
|
||||
"""patchServerSetting must send PATCH to /api/settings."""
|
||||
match = re.search(
|
||||
r"async function patchServerSetting\s*\(.*?\)\s*\{(.*?)(?=\nasync function |\nfunction |\n// )",
|
||||
_JS,
|
||||
re.DOTALL,
|
||||
)
|
||||
assert match, "patchServerSetting function not found"
|
||||
body = match.group(1)
|
||||
assert "/api/settings" in body, "patchServerSetting must send to /api/settings"
|
||||
assert "PATCH" in body, "patchServerSetting must use PATCH method"
|
||||
|
||||
|
||||
def test_patch_server_setting_shows_toast() -> None:
|
||||
"""patchServerSetting must call showToast."""
|
||||
match = re.search(
|
||||
r"async function patchServerSetting\s*\(.*?\)\s*\{(.*?)(?=\nasync function |\nfunction |\n// )",
|
||||
_JS,
|
||||
re.DOTALL,
|
||||
)
|
||||
assert match, "patchServerSetting function not found"
|
||||
body = match.group(1)
|
||||
assert "showToast" in body, "patchServerSetting must call showToast"
|
||||
|
||||
|
||||
def test_open_settings_calls_load_server_settings() -> None:
|
||||
"""openSettings must call loadServerSettings to populate sessions tab."""
|
||||
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 "loadServerSettings" in body, (
|
||||
"openSettings must call loadServerSettings"
|
||||
)
|
||||
|
||||
|
||||
def test_bind_static_event_listeners_binds_default_session_change() -> None:
|
||||
"""bindStaticEventListeners must bind change on setting-default-session."""
|
||||
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-default-session" in body, (
|
||||
"bindStaticEventListeners must bind setting-default-session change"
|
||||
)
|
||||
|
||||
|
||||
def test_bind_static_event_listeners_binds_sort_order_change() -> None:
|
||||
"""bindStaticEventListeners must bind change on setting-sort-order."""
|
||||
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-sort-order" in body, (
|
||||
"bindStaticEventListeners must bind setting-sort-order change"
|
||||
)
|
||||
|
||||
|
||||
def test_bind_static_event_listeners_binds_window_size_largest_change() -> None:
|
||||
"""bindStaticEventListeners must bind change on setting-window-size-largest."""
|
||||
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-window-size-largest" in body, (
|
||||
"bindStaticEventListeners must bind setting-window-size-largest change"
|
||||
)
|
||||
|
||||
|
||||
def test_bind_static_event_listeners_binds_auto_open_change() -> None:
|
||||
"""bindStaticEventListeners must bind change on setting-auto-open."""
|
||||
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-auto-open" in body, (
|
||||
"bindStaticEventListeners must bind setting-auto-open change"
|
||||
)
|
||||
|
||||
|
||||
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\}",
|
||||
_JS,
|
||||
re.DOTALL,
|
||||
)
|
||||
assert match, "bindStaticEventListeners function not found"
|
||||
body = match.group(1)
|
||||
assert "setting-hidden-sessions" in body, (
|
||||
"bindStaticEventListeners must use delegated handler on setting-hidden-sessions"
|
||||
)
|
||||
|
||||
|
||||
def test_exports_load_server_settings() -> None:
|
||||
"""module.exports must export loadServerSettings."""
|
||||
match = re.search(
|
||||
r"module\.exports\s*=\s*\{(.*?)\};",
|
||||
_JS,
|
||||
re.DOTALL,
|
||||
)
|
||||
assert match, "module.exports block not found"
|
||||
exports = match.group(1)
|
||||
assert "loadServerSettings" in exports, (
|
||||
"module.exports must export loadServerSettings"
|
||||
)
|
||||
|
||||
|
||||
def test_exports_patch_server_setting() -> None:
|
||||
"""module.exports must export patchServerSetting."""
|
||||
match = re.search(
|
||||
r"module\.exports\s*=\s*\{(.*?)\};",
|
||||
_JS,
|
||||
re.DOTALL,
|
||||
)
|
||||
assert match, "module.exports block not found"
|
||||
exports = match.group(1)
|
||||
assert "patchServerSetting" in exports, (
|
||||
"module.exports must export patchServerSetting"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user