Files
hermes-webui/tests/test_3850_settings_search.py
T

309 lines
14 KiB
Python

"""Regression tests for searchable settings feature.
The Settings panel must have a search input and filtering capability to allow
users to find settings across all tabs without having to click through each section.
Issue: #3850 (Add search input at top of Settings panel)
"""
from pathlib import Path
import re
INDEX_HTML = (Path(__file__).parent.parent / "static" / "index.html").read_text(encoding="utf-8")
PANELS_JS = (Path(__file__).parent.parent / "static" / "panels.js").read_text(encoding="utf-8")
I18N_JS = (Path(__file__).parent.parent / "static" / "i18n.js").read_text(encoding="utf-8")
STYLE_CSS = (Path(__file__).parent.parent / "static" / "style.css").read_text(encoding="utf-8")
class TestSettingsSearch:
"""Search input and filtering must be present and functional."""
def test_index_html_has_search_input(self):
"""index.html must contain the search input element with correct id."""
assert 'id="settingsSearch"' in INDEX_HTML, (
"index.html must contain a search input with id='settingsSearch'"
)
assert 'filterSettings(this.value)' in INDEX_HTML, (
"index.html search input must have oninput='filterSettings(this.value)'"
)
assert 'data-i18n-placeholder="settings_search_placeholder"' in INDEX_HTML, (
"index.html search input must have data-i18n-placeholder attribute"
)
def test_index_html_has_search_results_dropdown(self):
"""index.html must contain the search results dropdown element."""
assert 'id="settingsSearchResults"' in INDEX_HTML, (
"index.html must contain a results dropdown with id='settingsSearchResults'"
)
assert 'class="settings-search-results"' in INDEX_HTML, (
"index.html results dropdown must have class='settings-search-results'"
)
def test_panels_js_has_build_settings_index_function(self):
"""panels.js must contain _buildSettingsIndex function."""
assert "function _buildSettingsIndex()" in PANELS_JS, (
"panels.js must contain _buildSettingsIndex() function"
)
body = PANELS_JS[PANELS_JS.find("function _buildSettingsIndex()"):]
assert "settingsPaneConversation" in body, (
"_buildSettingsIndex must reference settingsPaneConversation pane"
)
assert "searchBlob" in body, (
"_buildSettingsIndex must store a searchBlob for settings fields"
)
assert "titleText" in body, (
"_buildSettingsIndex must store title bucket text"
)
assert "valueText" in body, (
"_buildSettingsIndex must store value bucket text"
)
assert "descriptionText" in body, (
"_buildSettingsIndex must store description bucket text"
)
def test_panels_js_has_filter_settings_function(self):
"""panels.js must contain filterSettings function."""
assert "function filterSettings(query)" in PANELS_JS, (
"panels.js must contain filterSettings(query) function"
)
body = PANELS_JS[PANELS_JS.find("function filterSettings(query)"):]
assert "_scoreSettingsSearchMatch" in body, (
"filterSettings must call _scoreSettingsSearchMatch for ranking"
)
assert "esc(m.label)" in body, (
"filterSettings must keep rendering the visible label text"
)
assert ".sort(" in body, (
"filterSettings must order matches deterministically"
)
def test_panels_js_has_navigate_to_field_function(self):
"""panels.js must contain _navigateToSettingsField function."""
assert "function _navigateToSettingsField(entry)" in PANELS_JS, (
"panels.js must contain _navigateToSettingsField(entry) function"
)
assert "switchSettingsSection" in PANELS_JS[PANELS_JS.find("function _navigateToSettingsField"):], (
"_navigateToSettingsField must call switchSettingsSection"
)
def test_panels_js_has_highlight_settings_field_function(self):
"""panels.js must contain _highlightSettingsField function."""
assert "function _highlightSettingsField(el)" in PANELS_JS, (
"panels.js must contain _highlightSettingsField(el) function"
)
assert "settings-field-highlight" in PANELS_JS[PANELS_JS.find("function _highlightSettingsField"):], (
"_highlightSettingsField must toggle 'settings-field-highlight' class"
)
def test_panels_js_resets_index_on_panel_open(self):
"""panels.js _beginSettingsPanelSession must reset the settings index."""
idx = PANELS_JS.find("function _beginSettingsPanelSession()")
assert idx >= 0, "_beginSettingsPanelSession function not found"
body = PANELS_JS[idx:idx + 300]
assert "_settingsIndex = null" in body, (
"_beginSettingsPanelSession must reset _settingsIndex to null"
)
def test_i18n_js_has_search_placeholder_key(self):
"""i18n.js must contain settings_search_placeholder key."""
assert "settings_search_placeholder:" in I18N_JS, (
"i18n.js must contain settings_search_placeholder key"
)
assert "Search settings" in I18N_JS, (
"settings_search_placeholder must have an appropriate translation"
)
def test_i18n_js_has_no_results_key(self):
"""i18n.js must contain settings_search_no_results key."""
assert "settings_search_no_results:" in I18N_JS, (
"i18n.js must contain settings_search_no_results key"
)
assert "No settings found" in I18N_JS, (
"settings_search_no_results must have an appropriate translation"
)
def test_style_css_has_search_results_style(self):
"""style.css must contain styles for the search results dropdown."""
assert ".settings-search-results" in STYLE_CSS, (
"style.css must contain .settings-search-results class"
)
assert ".settings-search-result" in STYLE_CSS, (
"style.css must contain .settings-search-result class for individual items"
)
def test_style_css_has_field_highlight_style(self):
"""style.css must contain styles for field highlighting."""
assert ".settings-field-highlight" in STYLE_CSS, (
"style.css must contain .settings-field-highlight class"
)
assert "settings-field-pulse" in STYLE_CSS, (
"style.css must contain settings-field-pulse animation"
)
def test_style_css_has_search_positioning(self):
"""style.css must position search results absolutely within the menu."""
assert "position:relative" in STYLE_CSS and ".settings-search" in STYLE_CSS, (
"style.css must make .settings-search relative positioned"
)
assert "position:absolute" in STYLE_CSS and ".settings-search-results" in STYLE_CSS, (
"style.css must make .settings-search-results absolutely positioned"
)
def test_settings_menu_layout_ownership_contract(self):
"""Settings search should be anchored in the menu while scrolling is owned by the button list."""
assert 'class="settings-menu-items"' in INDEX_HTML, (
"index.html must keep the section buttons inside .settings-menu-items"
)
menu_match = re.search(
r"(^|\n)\s*#settingsMenu\s*\{[^}]*\}",
STYLE_CSS,
re.MULTILINE,
)
assert menu_match is not None, "style.css must have a #settingsMenu rule"
menu_rules = menu_match.group(0)
assert "overflow: visible" in menu_rules, (
"settings menu must not own vertical clipping overflow"
)
items_match = re.search(
r"(^|\n)\s*#settingsMenu\s+\.settings-menu-items\s*\{[^}]*\}",
STYLE_CSS,
re.MULTILINE,
)
assert items_match is not None, "style.css must have a .settings-menu-items rule"
items_rules = items_match.group(0)
assert "overflow-y: auto" in items_rules, (
"settings menu items wrapper must own vertical scrolling"
)
def test_panels_js_handles_providers_pane(self):
"""panels.js must handle the Providers pane in index building."""
idx = PANELS_JS.find("function _buildSettingsIndex()")
assert idx >= 0, "_buildSettingsIndex not found"
body = PANELS_JS[idx:idx + 2000]
assert "settingsPaneProviders" in body, (
"_buildSettingsIndex must handle Providers pane"
)
def test_panels_js_handles_plugins_pane(self):
"""panels.js must handle the Plugins pane in index building."""
idx = PANELS_JS.find("function _buildSettingsIndex()")
assert idx >= 0, "_buildSettingsIndex not found"
body = PANELS_JS[idx:idx + 2000]
assert "settingsPanePlugins" in body, (
"_buildSettingsIndex must handle Plugins pane"
)
def test_settings_index_includes_provider_cards(self):
"""Providers pane entries must index provider cards and API key fields."""
idx = PANELS_JS.find("function _buildSettingsIndex()")
assert idx >= 0, "_buildSettingsIndex not found"
body = PANELS_JS[idx:idx + 3500]
assert "pane.querySelectorAll('.provider-card')" in body, (
"_buildSettingsIndex must scan provider cards so Providers search is not empty"
)
assert "card.querySelectorAll('.provider-card-field')" in body, (
"_buildSettingsIndex must index provider card fields like API key controls"
)
def test_settings_index_includes_plugin_cards(self):
"""Plugins pane entries must index plugin cards by plugin name."""
idx = PANELS_JS.find("function _buildSettingsIndex()")
assert idx >= 0, "_buildSettingsIndex not found"
end = PANELS_JS.find("function _resolveSettingsField(entry)", idx)
body = PANELS_JS[idx:end]
assert "if (sectionKey === 'plugins')" in body, (
"_buildSettingsIndex should include a plugins section branch"
)
assert "pane.querySelectorAll('.plugin-card')" in body, (
"_buildSettingsIndex must scan plugin cards so Plugins search is not empty"
)
def test_resolve_settings_field_rehydrates_provider_plugin_cards(self):
"""Provider and plugin search entries must survive pane re-renders."""
idx = PANELS_JS.find("function _resolveSettingsField(entry)")
assert idx >= 0, "_resolveSettingsField not found"
body = PANELS_JS[idx:idx + 2200]
assert "entry.cardName && (entry.sectionKey === 'providers' || entry.sectionKey === 'plugins')" in body, (
"_resolveSettingsField must re-find provider/plugin cards by name after lazy pane re-renders"
)
assert "card.querySelectorAll('.provider-card-field')" in body, (
"_resolveSettingsField must be able to re-find provider card fields by label"
)
def test_filter_settings_caps_results(self):
"""filterSettings must cap results at 12 items."""
idx = PANELS_JS.find("function filterSettings(query)")
assert idx >= 0, "filterSettings function not found"
body = PANELS_JS[idx:idx + 2000]
assert ".slice(0, 12)" in body, (
"filterSettings must cap results at 12 items with .slice(0, 12)"
)
def test_filter_settings_uses_escaping(self):
"""filterSettings must escape HTML in rendered text with esc()."""
idx = PANELS_JS.find("function filterSettings(query)")
assert idx >= 0, "filterSettings function not found"
body = PANELS_JS[idx:idx + 2000]
assert "esc(" in body, (
"filterSettings must use esc() for HTML escaping of all rendered text"
)
class TestSettingsSearchReviewFixes:
"""Regression coverage for the #4340 review fixes (Codex+Opus)."""
def test_index_covers_toggle_label_shapes(self):
"""The field index must catch the common toggle shape
<label><input><span data-i18n='...'></span></label>, not just
label[data-i18n]. Otherwise most checkbox settings are unsearchable."""
idx = PANELS_JS.find("function _buildSettingsIndex")
assert idx >= 0, "_buildSettingsIndex not found"
body = PANELS_JS[idx:idx + 2600]
assert "field.querySelector(" in body, (
"field index query should run a querySelector against field"
)
assert "label[data-i18n]" in body, (
"field index query must include 'label [data-i18n]' (span-in-label) "
"and plain 'label' so toggle settings are searchable"
)
def test_resolver_finds_data_i18n_anywhere(self):
"""_resolveSettingsField must resolve a [data-i18n] node anywhere (not
only on the <label>) back to its .settings-field."""
idx = PANELS_JS.find("function _resolveSettingsField")
assert idx >= 0, "_resolveSettingsField not found"
body = PANELS_JS[idx:idx + 2200]
assert "[data-i18n=" in body and "CSS.escape(entry.i18nKey)" in body, (
"_resolveSettingsField must query any [data-i18n] node, not "
"label[data-i18n] only"
)
assert "label[data-i18n=" not in body, (
"_resolveSettingsField must NOT restrict the lookup to label[data-i18n]"
)
def test_panel_session_invalidates_stale_search(self):
"""_beginSettingsPanelSession must bump the search seq and clear the
input + results so a stale in-flight render from a prior session can't
paint into the dropdown."""
idx = PANELS_JS.find("function _beginSettingsPanelSession")
assert idx >= 0, "_beginSettingsPanelSession not found"
body = PANELS_JS[idx:idx + 900]
assert "++_settingsSearchSeq" in body, (
"_beginSettingsPanelSession must bump _settingsSearchSeq to "
"invalidate in-flight searches"
)
assert "settingsSearch" in body and "value = ''" in body, (
"_beginSettingsPanelSession must clear the search input"
)
def test_dismiss_handler_invalidates_inflight_build(self):
"""The outside-click dismiss must also invalidate an in-flight first
build so it can't resurrect the dismissed dropdown."""
idx = PANELS_JS.find("_settingsSearchDismissListenerRegistered = true")
assert idx >= 0, "dismiss listener registration not found"
body = PANELS_JS[idx:idx + 500]
assert "++_settingsSearchSeq" in body, (
"the outside-click dismiss handler must bump _settingsSearchSeq"
)