2026-04-22 05:54:06 +00:00
|
|
|
"""Tests for #835 — refresh button in Tasks / Scheduled Jobs panel."""
|
|
|
|
|
import os
|
|
|
|
|
import re
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_SRC = os.path.join(os.path.dirname(__file__), "..")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _read(name):
|
|
|
|
|
return open(os.path.join(_SRC, name), encoding="utf-8").read()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestCronRefreshButtonHtml:
|
|
|
|
|
"""index.html must expose a refresh button in the Tasks panel header."""
|
|
|
|
|
|
|
|
|
|
def test_refresh_button_present(self):
|
|
|
|
|
html = _read("static/index.html")
|
|
|
|
|
assert 'id="cronRefreshBtn"' in html, (
|
|
|
|
|
"Tasks panel must have a #cronRefreshBtn element"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_refresh_button_has_accessibility_labels(self):
|
fix(ux): polish CSS tooltips + clear native title + extend coverage
Stage 311 maintainer-side enhancements on top of @jasonjcwu's PR #1782,
addressing browser-verified issues + extending coverage to high-traffic
icon buttons:
(1) Clear native title when custom data-tooltip is present (the core bug fix):
- static/i18n.js: when data-i18n-title runs against an element that has
data-tooltip, sync data-tooltip AND removeAttribute('title'). Without
this, the slow ~1.5s native browser tooltip co-fires alongside the
fast custom CSS tooltip — exactly the bug #1775 reports.
- static/ui.js _applyDashboardStatus: same treatment for the dashboard
rail/mobile buttons (was setting btn.title=warning unconditionally).
- static/boot.js: added _setButtonTooltip() helper, replaced 6 direct
.title assignments (workspace toggle/collapse/clear, voice dictate,
voice mode active/inactive) with calls through the helper.
(2) Extend coverage to high-traffic icon buttons in static/index.html:
- Composer area (side tooltip): btnAttach, btnMic, btnVoiceMode,
btnWorkspacePanelToggle, btnSend.
- Workspace panel header (bottom tooltip): btnCollapseWorkspacePanel,
btnUpDir, btnNewFile, btnNewFolder, btnRefreshPanel, btnClearPreview.
- All 11 buttons gain has-tooltip[--bottom] class and data-tooltip,
lose their native title=. Total covered surfaces: rail (12), sidebar
nav-tabs (12), panel-head (31), composer/workspace icons (11) = 66.
(3) CSS polish (browser-verified visible improvement):
- z-index 60 → 1500/1501 so the tooltip clears all sidebar/panel
stacking contexts. Earlier verification showed the tooltip overlapping
the Filter conversations search input.
- background: var(--bg-strong, ...) → var(--surface) (solid #1A1A2E
instead of falling back via undefined cascade).
- color: var(--text, var(--accent-text)) → var(--text) (solid warm white
#FFF8DC instead of gold which clashed at body-text size).
- border: var(--accent-bg-strong) → var(--border) (#2A2A45 solid
instead of gold at 0.15 alpha — the old border was barely visible
and the arrow ::before triangle was invisible).
- shadow: 4px/0.45 alpha → 6px/0.55 alpha + 0 0 0 1px ring fallback.
- Added 150ms hover-onset delay (matches Cygnus's spec in #1775); 0s
dismissal-delay so quick mouse-aways don't leave the tooltip behind.
- Fixed has-tooltip--bottom arrow direction: was pointing down (wrong),
now points up at the trigger (border-color order corrected).
- Bumped offsets: side tooltip 10px → 12px (clearance from icon edge),
bottom tooltip 8px → 10px.
(4) Test fixes (the 2 CI failures):
- tests/test_cron_refresh_button_835.py: assertion accepts either
title= or data-tooltip= per #1775 (was hardcoded title=).
- tests/test_mobile_layout.py::test_profiles_sidebar_tab_present:
regex tolerant to additional utility classes (has-tooltip).
(5) Regression tests added to tests/test_css_tooltips.py:
- test_native_title_cleared_when_custom_tooltip_present: pins the
removeAttribute('title') call so we don't regress to dual tooltips.
- test_native_title_path_preserved_for_non_tooltip_elements: pins the
el.title fallback for elements without data-tooltip.
Browser-verified: all 72 has-tooltip elements have zero native title at
runtime (was 94 with native, 2 stuck via dashboard JS path).
Co-authored-by: Jason Wu <jasonjcwu@users.noreply.github.com>
2026-05-07 04:00:40 +00:00
|
|
|
"""Icon-only buttons need aria-label + a hover tooltip so screen readers
|
|
|
|
|
and sighted users both have an affordance. Accept either the native
|
|
|
|
|
`title=` attribute or the custom `data-tooltip=` attribute introduced
|
|
|
|
|
in #1775 (faster ~120ms display vs the native ~1.5s delay)."""
|
2026-04-22 05:54:06 +00:00
|
|
|
html = _read("static/index.html")
|
|
|
|
|
m = re.search(r'<button[^>]*id="cronRefreshBtn"[^>]*>', html)
|
|
|
|
|
assert m, "cronRefreshBtn tag not found"
|
|
|
|
|
tag = m.group(0)
|
|
|
|
|
assert 'aria-label=' in tag, (
|
|
|
|
|
"#cronRefreshBtn is icon-only and must have aria-label"
|
|
|
|
|
)
|
fix(ux): polish CSS tooltips + clear native title + extend coverage
Stage 311 maintainer-side enhancements on top of @jasonjcwu's PR #1782,
addressing browser-verified issues + extending coverage to high-traffic
icon buttons:
(1) Clear native title when custom data-tooltip is present (the core bug fix):
- static/i18n.js: when data-i18n-title runs against an element that has
data-tooltip, sync data-tooltip AND removeAttribute('title'). Without
this, the slow ~1.5s native browser tooltip co-fires alongside the
fast custom CSS tooltip — exactly the bug #1775 reports.
- static/ui.js _applyDashboardStatus: same treatment for the dashboard
rail/mobile buttons (was setting btn.title=warning unconditionally).
- static/boot.js: added _setButtonTooltip() helper, replaced 6 direct
.title assignments (workspace toggle/collapse/clear, voice dictate,
voice mode active/inactive) with calls through the helper.
(2) Extend coverage to high-traffic icon buttons in static/index.html:
- Composer area (side tooltip): btnAttach, btnMic, btnVoiceMode,
btnWorkspacePanelToggle, btnSend.
- Workspace panel header (bottom tooltip): btnCollapseWorkspacePanel,
btnUpDir, btnNewFile, btnNewFolder, btnRefreshPanel, btnClearPreview.
- All 11 buttons gain has-tooltip[--bottom] class and data-tooltip,
lose their native title=. Total covered surfaces: rail (12), sidebar
nav-tabs (12), panel-head (31), composer/workspace icons (11) = 66.
(3) CSS polish (browser-verified visible improvement):
- z-index 60 → 1500/1501 so the tooltip clears all sidebar/panel
stacking contexts. Earlier verification showed the tooltip overlapping
the Filter conversations search input.
- background: var(--bg-strong, ...) → var(--surface) (solid #1A1A2E
instead of falling back via undefined cascade).
- color: var(--text, var(--accent-text)) → var(--text) (solid warm white
#FFF8DC instead of gold which clashed at body-text size).
- border: var(--accent-bg-strong) → var(--border) (#2A2A45 solid
instead of gold at 0.15 alpha — the old border was barely visible
and the arrow ::before triangle was invisible).
- shadow: 4px/0.45 alpha → 6px/0.55 alpha + 0 0 0 1px ring fallback.
- Added 150ms hover-onset delay (matches Cygnus's spec in #1775); 0s
dismissal-delay so quick mouse-aways don't leave the tooltip behind.
- Fixed has-tooltip--bottom arrow direction: was pointing down (wrong),
now points up at the trigger (border-color order corrected).
- Bumped offsets: side tooltip 10px → 12px (clearance from icon edge),
bottom tooltip 8px → 10px.
(4) Test fixes (the 2 CI failures):
- tests/test_cron_refresh_button_835.py: assertion accepts either
title= or data-tooltip= per #1775 (was hardcoded title=).
- tests/test_mobile_layout.py::test_profiles_sidebar_tab_present:
regex tolerant to additional utility classes (has-tooltip).
(5) Regression tests added to tests/test_css_tooltips.py:
- test_native_title_cleared_when_custom_tooltip_present: pins the
removeAttribute('title') call so we don't regress to dual tooltips.
- test_native_title_path_preserved_for_non_tooltip_elements: pins the
el.title fallback for elements without data-tooltip.
Browser-verified: all 72 has-tooltip elements have zero native title at
runtime (was 94 with native, 2 stuck via dashboard JS path).
Co-authored-by: Jason Wu <jasonjcwu@users.noreply.github.com>
2026-05-07 04:00:40 +00:00
|
|
|
assert 'title=' in tag or 'data-tooltip=' in tag, (
|
|
|
|
|
"#cronRefreshBtn should have a hover tooltip "
|
|
|
|
|
"(native title= or custom data-tooltip= per #1775)"
|
2026-04-22 05:54:06 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_refresh_button_calls_load_crons_with_animate(self):
|
|
|
|
|
html = _read("static/index.html")
|
|
|
|
|
m = re.search(r'<button[^>]*id="cronRefreshBtn"[^>]*>', html)
|
|
|
|
|
assert m
|
|
|
|
|
tag = m.group(0)
|
|
|
|
|
assert 'loadCrons(true)' in tag, (
|
|
|
|
|
"#cronRefreshBtn must call loadCrons(true) to enable the dim-while-fetching animation"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_refresh_button_sits_next_to_new_job_button(self):
|
|
|
|
|
"""Refresh button should appear in the same header row as the New Job
|
|
|
|
|
button so the header layout stays tight."""
|
|
|
|
|
html = _read("static/index.html")
|
|
|
|
|
ref_pos = html.find('id="cronRefreshBtn"')
|
refactor(ui): three-column layout with left rail + main-view migration (#899)
refactor(ui): three-column layout with left rail + main-view migration (#899)
Unifies the shell into a three-column layout (rail + sidebar + main) matching the
hermes-desktop reference, and migrates every per-item detail/edit surface into a
shared main-view canvas with consistent headers, empty states, and action buttons.
Changes:
- New desktop-only left rail (48px) with 8 nav tabs (chat/tasks/skills/memory/workspaces/profiles/todos/settings)
- Persistent app titlebar (replaces per-chat topbar), active conversation title shown
- All panel detail/create/edit views migrated to #mainSkills, #mainTasks, #mainSettings, #mainWorkspaces, #mainProfiles, #mainMemory
- Settings moved out of modal into main-view page; ESC closes it
- YAML frontmatter rendered in collapsible <details> block in skill detail
- Toasts repositioned from bottom-center to top-right with theme-aware success/error/warning/info variants
- Composer workspace chip split into two-button group: files-icon toggles file panel, label opens workspace picker
- .settings-menu → .side-menu / .side-menu-item (generalised, shared by memory and settings panels)
- i18n: ~25 new keys across en/ru/es/de/zh/zh-Hant for all new form labels, placeholders, and empty states
- Mobile: hamburger in titlebar, slide-in sidebar; box-shadow removed from sidebar
- New regression test: tests/test_settings_navigation_and_detail_refresh.py (9 tests)
Co-authored-by: Aron Prins <pwf.aron@gmail.com>
2026-04-24 16:05:25 +00:00
|
|
|
newjob_pos = html.find('openCronCreate()')
|
2026-04-22 05:54:06 +00:00
|
|
|
assert ref_pos != -1 and newjob_pos != -1
|
|
|
|
|
# Must be close enough to be in the same header row (single SVG-inline
|
|
|
|
|
# button can be around 500 chars by itself due to inline styles/attrs).
|
|
|
|
|
assert abs(ref_pos - newjob_pos) < 1000, (
|
|
|
|
|
"Refresh button and New Job button should be in the same header row"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestLoadCronsAnimateFlag:
|
|
|
|
|
"""panels.js loadCrons() must accept an optional animate flag that dims
|
|
|
|
|
the refresh button while fetching."""
|
|
|
|
|
|
|
|
|
|
def test_load_crons_accepts_animate_param(self):
|
|
|
|
|
js = _read("static/panels.js")
|
|
|
|
|
assert re.search(r'async function loadCrons\s*\(\s*animate\s*\)', js), (
|
|
|
|
|
"loadCrons must accept an `animate` parameter"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_load_crons_restores_button_in_finally(self):
|
|
|
|
|
"""The opacity/disabled restore MUST be in a finally block so a
|
|
|
|
|
throwing fetch doesn't leave the button stuck at 0.5 / disabled."""
|
|
|
|
|
js = _read("static/panels.js")
|
|
|
|
|
m = re.search(r'async function loadCrons\(.*?\n\}', js, re.DOTALL)
|
|
|
|
|
assert m, "loadCrons body not found"
|
|
|
|
|
fn = m.group(0)
|
|
|
|
|
assert 'finally' in fn, (
|
|
|
|
|
"loadCrons must restore the refresh button's opacity/disabled state "
|
|
|
|
|
"in a finally block so errors during fetch don't leave the button stuck"
|
|
|
|
|
)
|
|
|
|
|
# The restore block sets opacity='' (not '1') so CSS cascade wins
|
|
|
|
|
assert "opacity = ''" in fn or "opacity=''" in fn, (
|
|
|
|
|
"restore must use opacity='' to clear the inline override"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestCronCreatedEventListener:
|
|
|
|
|
"""A global `hermes:cron_created` listener must be registered so
|
|
|
|
|
future chat paths can trigger the cron list refresh."""
|
|
|
|
|
|
|
|
|
|
def test_listener_registered_at_module_scope(self):
|
|
|
|
|
js = _read("static/panels.js")
|
|
|
|
|
assert re.search(
|
|
|
|
|
r"addEventListener\(\s*['\"]hermes:cron_created['\"]",
|
|
|
|
|
js,
|
|
|
|
|
), (
|
|
|
|
|
"panels.js must register a window-level 'hermes:cron_created' event listener"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_listener_triggers_load_crons(self):
|
|
|
|
|
js = _read("static/panels.js")
|
|
|
|
|
m = re.search(
|
|
|
|
|
r"addEventListener\(\s*['\"]hermes:cron_created['\"].*?\}\s*\)",
|
|
|
|
|
js,
|
|
|
|
|
re.DOTALL,
|
|
|
|
|
)
|
|
|
|
|
assert m, "hermes:cron_created listener body not found"
|
|
|
|
|
body = m.group(0)
|
|
|
|
|
assert 'loadCrons' in body, (
|
|
|
|
|
"hermes:cron_created listener must call loadCrons() to refresh the list"
|
|
|
|
|
)
|