* fix: include terminal backend identity in agent cache signature (#5937) (#6175) * fix(mobile): preserve extension panels in drawer sync * fix: close mobile sidebar drawer when opening main-view create forms The add-space/new-profile/new-skill/new-cron forms render in the main view, which on phones is covered by the full-screen sidebar drawer (z-index 200). Opening one from inside the drawer only focused the input (keyboard popped up) while the form stayed hidden underneath. - openWorkspaceCreate / openProfileCreate / openSkillCreate / openCronCreate now close the drawer after rendering the form, mirroring their *_Detail siblings. - move the fromRailClick drawer re-open in switchPanel() into the synchronous section: it used to run after await loadXxxPanel(), racing (and re-opening over) a drawer close issued from inside the drawer during the panel load window. Verified in Playwright (390x844): drawer closes and stays closed after plus-click, form visible with focus on the path input; desktop (1280x800) unchanged; 83 mobile + 26 workspace tests pass. * fix: close mobile sidebar after project quick-create plus The per-project '+' button (_attachProjectQuickCreateButton) created the new project conversation but never closed the mobile sidebar drawer, so on phones the tap looked like a no-op: the session was created, just hidden underneath the full-screen drawer (z-index 200). Close the drawer in the success path after the sidebar repaint, mirroring $('btnNewChat').onclick in boot.js and the #5409 close in _openSidebarSession. Failure path keeps the drawer open so the error toast stays visible for retry. Adds static regression tests pinning the guarded close call and its position after the repaint. * test: pin quick-create drawer close to the success path (try block) Greptile review note: the string-presence and ordering assertions alone would still pass if the guarded closeMobileSidebar call were moved into the later catch branch, silently reintroducing the mobile drawer bug. Add an explicit assertion that the close sits before the success-path catch(err), so a regression that moves it into the failure branch is caught. * docs(changelog): stamp batch1-p1 (#6921 #6933 #6671 #6175) --------- Co-authored-by: webtecnica <75556242+webtecnica@users.noreply.github.com> Co-authored-by: Rodrigo Gomes da Silva <rodrigo.smscom@gmail.com> Co-authored-by: silent-reader-cn <silent-reader-cn@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
280 lines
13 KiB
Python
280 lines
13 KiB
Python
"""Regression tests for the project chip UI fixes (issue #1085).
|
|
|
|
Two bugs:
|
|
|
|
1. The right-click context menu opened by `_showProjectContextMenu` was styled
|
|
with `background: var(--panel)`, but `--panel` is NOT defined anywhere in
|
|
style.css. CSS falls back to `transparent` for undefined variables, so the
|
|
menu appeared see-through and the session list bled through. The fix
|
|
replaces `var(--panel)` with `var(--surface)` — the same opaque variable
|
|
used by `.session-action-menu` and other floating popovers.
|
|
|
|
2. The `.project-create-input` (used for both rename and new-project creation)
|
|
had `width: 100px` hard-coded, so the field was always exactly 100px wide
|
|
regardless of the project name being edited. Fix: bound the field with
|
|
`min-width: 40px` / `max-width: 180px` and `width: auto`, plus a
|
|
`_resizeProjectInput()` JS helper that measures the current value with a
|
|
hidden span and sets the pixel width accordingly.
|
|
|
|
These are static-source tests — CSS/JS behaviour of a popover and an input
|
|
sizer can't be exercised faithfully without a browser, but the patterns
|
|
worth pinning are the variable names, the absence of the bad ones, and the
|
|
presence of the resize helper at both call sites.
|
|
"""
|
|
|
|
import pathlib
|
|
|
|
REPO = pathlib.Path(__file__).parent.parent
|
|
SESSIONS_JS = (REPO / "static" / "sessions.js").read_text(encoding="utf-8")
|
|
STYLE_CSS = (REPO / "static" / "style.css").read_text(encoding="utf-8")
|
|
|
|
|
|
# ── Bug 1: context menu background ────────────────────────────────────────────
|
|
|
|
|
|
class TestContextMenuBackground:
|
|
|
|
def test_panel_variable_not_defined_in_stylesheet(self):
|
|
"""`--panel` is not defined as a CSS custom property anywhere — so
|
|
any rule using `var(--panel)` falls back to `transparent`, which is
|
|
the actual root cause of the menu bleed-through. This test
|
|
documents that fact: if `--panel` is ever defined, the test will
|
|
need updating but the fix is still safer using `--surface`."""
|
|
# Match either ":root --panel:" or "--panel:" assignments; absence
|
|
# confirms the fallback-to-transparent failure mode.
|
|
assert "--panel:" not in STYLE_CSS, (
|
|
"If --panel is now defined, update this test, but the menu "
|
|
"should still use --surface for consistency with other popovers."
|
|
)
|
|
|
|
def test_context_menu_uses_surface_not_panel(self):
|
|
"""`_showProjectContextMenu` must set the menu background to
|
|
`var(--surface)`, not `var(--panel)`."""
|
|
# Locate the menu construction
|
|
idx = SESSIONS_JS.find("project-ctx-menu")
|
|
assert idx >= 0, "project-ctx-menu className not found in sessions.js"
|
|
# Look at the surrounding 800 chars where the cssText is set
|
|
window = SESSIONS_JS[idx: idx + 1200]
|
|
assert "background:var(--surface)" in window, (
|
|
"Project context menu must use background:var(--surface) for an "
|
|
"opaque surface — var(--panel) is undefined and falls back to "
|
|
"transparent."
|
|
)
|
|
assert "background:var(--panel)" not in window, (
|
|
"Project context menu still uses background:var(--panel) — "
|
|
"this CSS variable is not defined and renders transparent."
|
|
)
|
|
|
|
def test_session_action_menu_also_uses_surface_for_consistency(self):
|
|
"""Sanity check: the existing .session-action-menu (the analogous
|
|
right-click menu for session items) uses `var(--surface)` — so the
|
|
fix is consistent with the rest of the codebase."""
|
|
assert "session-action-menu" in STYLE_CSS
|
|
# Find the rule and confirm it uses --surface
|
|
idx = STYLE_CSS.find(".session-action-menu")
|
|
assert idx >= 0
|
|
rule = STYLE_CSS[idx: idx + 400]
|
|
assert "var(--surface)" in rule, (
|
|
".session-action-menu should use var(--surface) — kept here as "
|
|
"the canonical reference for opaque popover surfaces."
|
|
)
|
|
|
|
|
|
# ── Bug 2: project-create-input width ─────────────────────────────────────────
|
|
|
|
|
|
class TestProjectCreateInputWidth:
|
|
|
|
def test_no_hardcoded_100px_width(self):
|
|
"""The fixed `width: 100px` on .project-create-input is gone."""
|
|
idx = STYLE_CSS.find(".project-create-input{")
|
|
assert idx >= 0, ".project-create-input rule not found in style.css"
|
|
rule = STYLE_CSS[idx: idx + 400]
|
|
assert "width:100px" not in rule and "width: 100px" not in rule, (
|
|
"Fixed 100px width must be replaced with min-width/max-width/"
|
|
"width:auto so the input grows with its content."
|
|
)
|
|
|
|
def test_min_and_max_width_present(self):
|
|
"""Both min-width and max-width must be set on .project-create-input."""
|
|
idx = STYLE_CSS.find(".project-create-input{")
|
|
rule = STYLE_CSS[idx: idx + 400]
|
|
assert "min-width:40px" in rule, (
|
|
f"min-width:40px not found in .project-create-input rule: {rule}"
|
|
)
|
|
assert "max-width:180px" in rule, (
|
|
f"max-width:180px not found in .project-create-input rule: {rule}"
|
|
)
|
|
assert "width:auto" in rule, (
|
|
f"width:auto not found in .project-create-input rule: {rule}"
|
|
)
|
|
|
|
|
|
class TestResizeProjectInputHelper:
|
|
"""The `_resizeProjectInput` helper must exist and be wired into both
|
|
rename and create call sites."""
|
|
|
|
def test_resize_helper_defined(self):
|
|
assert "function _resizeProjectInput(" in SESSIONS_JS, (
|
|
"_resizeProjectInput helper not found in sessions.js"
|
|
)
|
|
|
|
def test_resize_helper_uses_hidden_span(self):
|
|
"""The standard pattern is to measure with a hidden absolute span
|
|
sharing the same font/padding as the input. Font and family are read
|
|
via getComputedStyle so the sizer stays calibrated if CSS changes."""
|
|
idx = SESSIONS_JS.find("function _resizeProjectInput(")
|
|
assert idx >= 0
|
|
body = SESSIONS_JS[idx: idx + 900]
|
|
assert "position:absolute" in body and "visibility:hidden" in body, (
|
|
"_resizeProjectInput should use a hidden absolute span to "
|
|
"measure the value's rendered width."
|
|
)
|
|
assert "getComputedStyle(inp)" in body, (
|
|
"_resizeProjectInput should use getComputedStyle to read font " "properties so the sizer stays calibrated if CSS changes."
|
|
)
|
|
assert "Math.min(180" in body, (
|
|
"max bound (180) not applied in _resizeProjectInput"
|
|
)
|
|
assert "Math.max(40" in body, (
|
|
"min bound (40) not applied in _resizeProjectInput"
|
|
)
|
|
|
|
def test_rename_calls_resize_helper(self):
|
|
"""`_startProjectRename` must call `_resizeProjectInput` once on
|
|
creation and again on every input event."""
|
|
idx = SESSIONS_JS.find("function _startProjectRename(")
|
|
assert idx >= 0
|
|
body = SESSIONS_JS[idx: idx + 1200]
|
|
assert "_resizeProjectInput(inp)" in body, (
|
|
"_startProjectRename must call _resizeProjectInput so the "
|
|
"input width matches the existing project name."
|
|
)
|
|
# Wired into the input event so it grows as the user types
|
|
assert "addEventListener('input'" in body and "_resizeProjectInput" in body, (
|
|
"_startProjectRename must wire input events to _resizeProjectInput"
|
|
)
|
|
|
|
def test_create_calls_resize_helper(self):
|
|
"""Same for `_startProjectCreate` (new-project entry field)."""
|
|
idx = SESSIONS_JS.find("function _startProjectCreate(")
|
|
assert idx >= 0
|
|
body = SESSIONS_JS[idx: idx + 1200]
|
|
assert "_resizeProjectInput(inp)" in body, (
|
|
"_startProjectCreate must call _resizeProjectInput on focus"
|
|
)
|
|
assert "addEventListener('input'" in body, (
|
|
"_startProjectCreate must wire input events to _resizeProjectInput"
|
|
)
|
|
|
|
|
|
class TestProjectChipLongPressTouch:
|
|
"""Mobile long-press to open the project context menu (#3760).
|
|
|
|
Project chips were deletable only via the right-click context menu, which has
|
|
no touch equivalent — so mobile users could never remove a project. A 500ms
|
|
long-press now opens the same menu.
|
|
"""
|
|
|
|
def _chip_touch_block(self):
|
|
# The chip touch handlers live just after the oncontextmenu wiring in the
|
|
# project-chip render loop.
|
|
idx = SESSIONS_JS.find("Touch long-press")
|
|
assert idx != -1, "project-chip long-press touch block not found"
|
|
return SESSIONS_JS[idx: idx + 2300]
|
|
|
|
def test_long_press_opens_project_context_menu(self):
|
|
block = self._chip_touch_block()
|
|
assert "addEventListener('touchstart'" in block
|
|
assert "setTimeout(" in block and "},500);" in block
|
|
assert "_showProjectContextMenu(" in block
|
|
# visual feedback + scroll-drift cancel, mirroring the session-item pattern
|
|
assert "long-pressing" in block
|
|
assert "addEventListener('touchmove'" in block
|
|
assert ">10" in block # >10px drift cancels the press
|
|
|
|
def test_long_press_suppresses_synthetic_click_and_filter_tap(self):
|
|
block = self._chip_touch_block()
|
|
# touchend must be non-passive so it can preventDefault the synthetic click
|
|
assert "addEventListener('touchend'" in block
|
|
assert "{passive:false}" in block
|
|
assert "e.preventDefault();e.stopPropagation();" in block
|
|
# the long-press handler cancels the pending single-tap filter timer
|
|
assert "clearTimeout(_pClickTimer)" in block
|
|
|
|
def test_touchstart_clears_inflight_timer_before_scheduling(self):
|
|
"""Regression: a second finger / stray touchstart must not orphan the
|
|
prior timer (which would then fire the menu after the gesture was
|
|
cancelled). touchstart clears any in-flight _lpTimer before scheduling,
|
|
and the timer body bails if the gesture was already consumed.
|
|
"""
|
|
block = self._chip_touch_block()
|
|
# clear-before-schedule at the top of touchstart
|
|
assert "if(_lpTimer){clearTimeout(_lpTimer);_lpTimer=null;}" in block, (
|
|
"touchstart must clear any in-flight long-press timer before scheduling "
|
|
"a new one (orphaned-timer fix)"
|
|
)
|
|
# stale-fire guard inside the timer body
|
|
assert "if(_lpHandled) return;" in block, (
|
|
"the long-press timer body must no-op if the gesture was already consumed"
|
|
)
|
|
|
|
def test_long_pressing_style_feedback_present(self):
|
|
assert ".project-chip.long-pressing" in STYLE_CSS
|
|
# Target the base .project-chip rule (the one carrying the layout props),
|
|
# not an unrelated theme override of the same selector.
|
|
base_idx = STYLE_CSS.find(".project-chip{font-size")
|
|
assert base_idx != -1, "base .project-chip rule not found"
|
|
chip_rule = STYLE_CSS[base_idx: STYLE_CSS.find("}", base_idx) + 1]
|
|
# touch tuning so the native callout/selection doesn't compete with the gesture
|
|
assert "touch-action:manipulation" in chip_rule
|
|
assert "user-select:none" in chip_rule
|
|
|
|
|
|
class TestQuickCreateMobileDrawer:
|
|
"""The project-chip "+" (quick-create) must close the mobile sidebar drawer.
|
|
|
|
On phones the sidebar is a full-screen drawer (`.sidebar.mobile-open`,
|
|
z-index 200) covering the main chat view. The quick-create button creates
|
|
the new project conversation but never closed the drawer, so on mobile the
|
|
tap *looked* like a no-op — the session was created, just hidden underneath.
|
|
Mirrors `$('btnNewChat').onclick` in boot.js and the #5409 close in
|
|
`_openSidebarSession`.
|
|
"""
|
|
|
|
def _quick_create_block(self):
|
|
idx = SESSIONS_JS.find("function _attachProjectQuickCreateButton(")
|
|
assert idx != -1, "project quick-create helper not found in sessions.js"
|
|
# Function body incl. the full onclick success path (~3K covers it).
|
|
return SESSIONS_JS[idx: idx + 3000]
|
|
|
|
def test_quick_create_success_path_closes_mobile_drawer(self):
|
|
block = self._quick_create_block()
|
|
assert "closeMobileSidebar" in block, (
|
|
"project quick-create + must close the mobile sidebar after "
|
|
"newSession so the new conversation is visible on phones"
|
|
)
|
|
# Guarded call, matching the _openSidebarSession (#5409) convention.
|
|
assert "typeof closeMobileSidebar==='function'" in block
|
|
|
|
def test_quick_create_close_runs_after_sidebar_repaint(self):
|
|
"""The close must sit in the success path after the sidebar repaint —
|
|
not before newSession (the drawer would reopen over the pending load)
|
|
and not in the catch branch (failure should keep the drawer open so the
|
|
user can see the toast and retry)."""
|
|
block = self._quick_create_block()
|
|
render_idx = block.find("renderSessionList({deferWhileInteracting:false})")
|
|
close_idx = block.find("closeMobileSidebar")
|
|
catch_idx = block.find("catch(err)")
|
|
assert render_idx != -1, "sidebar repaint call not found in quick-create handler"
|
|
assert close_idx != -1, "closeMobileSidebar not found in quick-create handler"
|
|
assert catch_idx != -1, "success-path try/catch not found in quick-create handler"
|
|
assert close_idx > render_idx, (
|
|
"closeMobileSidebar must run after the sidebar repaint in the success path"
|
|
)
|
|
assert close_idx < catch_idx, (
|
|
"closeMobileSidebar must stay in the success path (try block) — moving it "
|
|
"into the catch branch would keep the drawer open over the error toast"
|
|
)
|
|
|