clean up & refactor
This commit is contained in:
1 parent
13fa0de891
commit
ad88c92155
5 files changed
+51
-52
No files matched your search
+28
-27
@@ -111,6 +111,10 @@ const SESSION_SWIPE_DURATION_MS = 500;
|
||||
const SESSION_SWIPE_REFLOW_LEAD_MS = 220;
|
||||
const SESSION_REFLOW_TIMEOUT_MS = 420;
|
||||
const SESSION_LIST_FLIP_TIMEOUT_MS = 460;
|
||||
const SESSION_LONG_PRESS_DELAY_MS = 400;
|
||||
const SESSION_ARCHIVE_SWIPE_THRESHOLD_PX = 128;
|
||||
const SESSION_DELETE_SWIPE_THRESHOLD_PX = 128;
|
||||
const SESSION_SWIPE_CANCEL_RATIO = 0.75;
|
||||
|
||||
function _formatSessionModelWithGateway(s){
|
||||
if(!s||!s.model)return'';
|
||||
@@ -1517,7 +1521,25 @@ function _playSessionRowsReflowFromPositions(before, timeoutMs, prefersReducedMo
|
||||
}
|
||||
|
||||
function _sessionPrefersReducedMotion(){
|
||||
return Boolean(window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches);
|
||||
try{
|
||||
return Boolean(window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches);
|
||||
}catch(_){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function _makeSessionSwipeAffordance(side, icon, label){
|
||||
const affordance=document.createElement('div');
|
||||
affordance.className='session-swipe-affordance session-swipe-affordance-'+side;
|
||||
affordance.setAttribute('aria-hidden','true');
|
||||
const badge=document.createElement('span');
|
||||
badge.className='session-swipe-badge';
|
||||
badge.innerHTML=li(icon,18);
|
||||
const text=document.createElement('span');
|
||||
text.className='session-swipe-label';
|
||||
text.textContent=label;
|
||||
affordance.append(badge,text);
|
||||
return affordance;
|
||||
}
|
||||
const SESSION_VIRTUAL_ROW_HEIGHT = 52;
|
||||
const SESSION_VIRTUAL_BUFFER_ROWS = 12;
|
||||
@@ -3616,6 +3638,7 @@ function renderSessionListFromCache(){
|
||||
el.oncontextmenu=(e)=>{
|
||||
if(readOnly) return;
|
||||
e.preventDefault();
|
||||
if(e.pointerType==='touch'||e.pointerType==='pen') return;
|
||||
e.stopPropagation();
|
||||
clearTimeout(_tapTimer);
|
||||
_tapTimer=null;
|
||||
@@ -3624,22 +3647,9 @@ function renderSessionListFromCache(){
|
||||
_openSessionActionMenu(s, actions||el);
|
||||
};
|
||||
|
||||
const _makeSessionSwipeAffordance=(side,icon,label)=>{
|
||||
const affordance=document.createElement('div');
|
||||
affordance.className='session-swipe-affordance session-swipe-affordance-'+side;
|
||||
affordance.setAttribute('aria-hidden','true');
|
||||
const badge=document.createElement('span');
|
||||
badge.className='session-swipe-badge';
|
||||
badge.innerHTML=li(icon,18);
|
||||
const text=document.createElement('span');
|
||||
text.className='session-swipe-label';
|
||||
text.textContent=label;
|
||||
affordance.append(badge,text);
|
||||
return affordance;
|
||||
};
|
||||
if(!readOnly){
|
||||
el.append(
|
||||
_makeSessionSwipeAffordance('right',s.archived?'undo':'archive',s.archived?'Restore':t('session_batch_archive')),
|
||||
_makeSessionSwipeAffordance('right',s.archived?'undo':'archive',s.archived?t('session_restore'):t('session_batch_archive')),
|
||||
_makeSessionSwipeAffordance('left','trash-2',t('session_batch_delete')),
|
||||
);
|
||||
}
|
||||
@@ -3663,10 +3673,6 @@ function renderSessionListFromCache(){
|
||||
let _pointerX=0;
|
||||
let _pointerY=0;
|
||||
let _gesturePointerType='';
|
||||
const _longPressDelay=400;
|
||||
const _archiveSwipeActionThreshold=128;
|
||||
const _deleteSwipeActionThreshold=128;
|
||||
const _swipeCancelRatio=0.75;
|
||||
const _clearLongPressTimer=()=>{
|
||||
if(_longPressTimer){clearTimeout(_longPressTimer);_longPressTimer=null;}
|
||||
if(!_longPressMenuOpened) el.classList.remove('long-pressing');
|
||||
@@ -3695,7 +3701,7 @@ function renderSessionListFromCache(){
|
||||
_tapTimer=null;
|
||||
_lastTapTime=0;
|
||||
_openSessionActionMenu(s, el);
|
||||
},_longPressDelay);
|
||||
},SESSION_LONG_PRESS_DELAY_MS);
|
||||
};
|
||||
const _isSessionSwipeTarget=()=>{
|
||||
return _gesturePointerType!=='mouse'&&!readOnly&&!_renamingSid&&!_sessionSelectMode;
|
||||
@@ -3769,9 +3775,9 @@ function renderSessionListFromCache(){
|
||||
};
|
||||
const _handleSessionSwipe=(signedDx,signedDy)=>{
|
||||
if(_gestureState==='committed'||!_isSessionSwipeTarget()) return false;
|
||||
const actionThreshold=signedDx>0?_archiveSwipeActionThreshold:_deleteSwipeActionThreshold;
|
||||
const actionThreshold=signedDx>0?SESSION_ARCHIVE_SWIPE_THRESHOLD_PX:SESSION_DELETE_SWIPE_THRESHOLD_PX;
|
||||
if(Math.abs(signedDx)<actionThreshold) return false;
|
||||
if(Math.abs(signedDy)>Math.abs(signedDx)*_swipeCancelRatio) return false;
|
||||
if(Math.abs(signedDy)>Math.abs(signedDx)*SESSION_SWIPE_CANCEL_RATIO) return false;
|
||||
_gestureState='committed';
|
||||
_clearLongPressTimer();
|
||||
clearTimeout(_tapTimer);
|
||||
@@ -3918,11 +3924,6 @@ function renderSessionListFromCache(){
|
||||
if (_loadingSessionId && _loadingSessionId !== s.session_id) return;
|
||||
startRename();
|
||||
};
|
||||
el.oncontextmenu=(e)=>{
|
||||
if(e.pointerType==='touch'||e.pointerType==='pen'){
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
el.addEventListener('touchstart',(e)=>{
|
||||
if(_isSessionActionTarget(e.target)) return;
|
||||
const touch=e.changedTouches&&e.changedTouches[0];
|
||||
|
||||
@@ -74,6 +74,15 @@ os.environ['HERMES_BASE_HOME'] = str(TEST_STATE_DIR)
|
||||
# tests that read/write config.yaml stay inside the isolated test home.
|
||||
os.environ['HERMES_CONFIG_PATH'] = str(TEST_STATE_DIR / 'config.yaml')
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _isolate_hermes_config_path():
|
||||
"""Keep profile/.env side effects from leaking the live config path across tests."""
|
||||
isolated_config_path = str(TEST_STATE_DIR / 'config.yaml')
|
||||
os.environ['HERMES_CONFIG_PATH'] = isolated_config_path
|
||||
yield
|
||||
os.environ['HERMES_CONFIG_PATH'] = isolated_config_path
|
||||
|
||||
# ── Server script: always relative to repo root ───────────────────────────
|
||||
SERVER_SCRIPT = REPO_ROOT / 'server.py'
|
||||
if not SERVER_SCRIPT.exists():
|
||||
|
||||
@@ -55,14 +55,16 @@ def test_active_session_idle_reconcile_clears_stale_busy_and_inflight_state():
|
||||
def test_session_list_payload_reconciles_active_idle_state_before_optimistic_merge_and_render():
|
||||
body = _function_body(SESSIONS_SRC, "function _applySessionListPayload(")
|
||||
|
||||
reconcile_pos = body.find("_reconcileActiveSessionIdleStateFromList(sessData.sessions||[])")
|
||||
filter_pos = body.find("const serverSessions=_optimisticallyRemovedSessionIds.size")
|
||||
reconcile_pos = body.find("_reconcileActiveSessionIdleStateFromList(serverSessions)")
|
||||
merge_pos = body.find("_allSessions = _mergeOptimisticFirstTurnSessions")
|
||||
render_pos = body.find("renderSessionListFromCache()")
|
||||
|
||||
assert filter_pos != -1, "payload application must filter optimistic removals before row reconciliation"
|
||||
assert reconcile_pos != -1, "active-session idle reconciliation must run for refreshed rows"
|
||||
assert merge_pos != -1, "session rows must still be applied from /api/sessions"
|
||||
assert render_pos != -1, "payload application must still render from cache"
|
||||
assert reconcile_pos < merge_pos < render_pos, (
|
||||
assert filter_pos < reconcile_pos < merge_pos < render_pos, (
|
||||
"local S.busy/INFLIGHT state must be reconciled against raw server rows "
|
||||
"before optimistic merging can re-label a stale active session as streaming"
|
||||
)
|
||||
|
||||
@@ -120,7 +120,7 @@ def test_plain_mouse_hover_does_not_mark_session_row_dragging():
|
||||
"""Pointermove fires during ordinary hover; drag styling must require an active press."""
|
||||
assert "let _gestureState='idle';" in SESSIONS_JS
|
||||
assert "_gestureState='pressing';" in SESSIONS_JS
|
||||
assert "if(_gestureState==='idle') return;" in SESSIONS_JS
|
||||
assert "if(_gestureState==='idle') return false;" in SESSIONS_JS
|
||||
assert "_gestureState='idle';" in SESSIONS_JS
|
||||
assert ".session-item.dragging:hover" in STYLE_CSS
|
||||
|
||||
|
||||
@@ -24,25 +24,17 @@ def test_session_menu_uses_viewport_height_not_fixed_scroll_cap():
|
||||
assert "max-height:320px" not in session_menu
|
||||
|
||||
|
||||
def test_session_menu_has_subtle_open_animation():
|
||||
def test_session_menu_respects_motion_preferences():
|
||||
session_menu = STYLE_CSS[STYLE_CSS.find(".session-action-menu{"):STYLE_CSS.find(".session-action-menu.open")]
|
||||
assert "will-change:opacity,transform" in session_menu
|
||||
assert "transform-origin:top right" in session_menu
|
||||
assert "function _playSessionActionMenuEntrance(menu){" in SESSIONS_JS
|
||||
assert "typeof menu.animate==='function'" in SESSIONS_JS
|
||||
assert "{opacity:0, transform:'translate3d(0,-4px,0) scale(.985)'}" in SESSIONS_JS
|
||||
assert "{duration:450, easing:'cubic-bezier(.2,.8,.2,1)'}" in SESSIONS_JS
|
||||
assert "menu.classList.add('open-animated')" in SESSIONS_JS
|
||||
assert ".session-action-menu.open-animated{animation:session-menu-in .45s cubic-bezier(.2,.8,.2,1);}" in STYLE_CSS
|
||||
assert "@keyframes session-menu-in" in STYLE_CSS
|
||||
assert "@media (prefers-reduced-motion:reduce)" in STYLE_CSS
|
||||
assert ".session-action-menu{animation:none;will-change:auto;}" in STYLE_CSS
|
||||
assert ".session-item,.session-item.dragging,.session-item.session-reflowing,.session-item.swipe-committed,.session-item.swipe-removing{transition:none;}" in STYLE_CSS
|
||||
assert ".session-item.long-pressing,.session-item.session-swipe-returning{animation:none;}" in STYLE_CSS
|
||||
|
||||
|
||||
def test_mobile_session_menu_opens_from_long_press_and_hides_dots():
|
||||
assert "_longPressDelay=400" in SESSIONS_JS
|
||||
assert "const SESSION_LONG_PRESS_DELAY_MS = 400;" in SESSIONS_JS
|
||||
assert "el.classList.add('long-pressing')" in SESSIONS_JS
|
||||
assert "if(!_longPressMenuOpened) el.classList.remove('long-pressing')" in SESSIONS_JS
|
||||
assert "row.classList.remove('menu-open','long-pressing')" in SESSIONS_JS
|
||||
@@ -58,6 +50,10 @@ def test_mobile_session_menu_opens_from_long_press_and_hides_dots():
|
||||
|
||||
|
||||
def test_open_session_menu_consumes_next_row_activation():
|
||||
context_menu = _sessions_block("el.oncontextmenu=(e)=>{", "// Use release events")
|
||||
assert SESSIONS_JS.count("el.oncontextmenu=(e)=>{") == 1
|
||||
assert "if(e.pointerType==='touch'||e.pointerType==='pen') return;" in context_menu
|
||||
assert "_openSessionActionMenu(s, actions||el);" in context_menu
|
||||
assert "if(_sessionActionMenu&&!_sessionActionMenu.contains(target)){" in SESSIONS_JS
|
||||
assert "closeSessionActionMenu();" in SESSIONS_JS
|
||||
assert "e.stopPropagation();" in SESSIONS_JS
|
||||
@@ -84,14 +80,12 @@ def test_open_session_menu_consumes_next_row_activation():
|
||||
def test_session_swipes_route_archive_restore_and_delete():
|
||||
assert "_gesturePointerType!=='mouse'" in SESSIONS_JS
|
||||
assert "_swipeTracking=true" in SESSIONS_JS
|
||||
assert "_archiveSwipeActionThreshold=128" in SESSIONS_JS
|
||||
assert "_deleteSwipeActionThreshold=128" in SESSIONS_JS
|
||||
assert "const SESSION_ARCHIVE_SWIPE_THRESHOLD_PX = 128;" in SESSIONS_JS
|
||||
assert "const SESSION_DELETE_SWIPE_THRESHOLD_PX = 128;" in SESSIONS_JS
|
||||
assert "const SESSION_SWIPE_DURATION_MS = 500;" in SESSIONS_JS
|
||||
assert "const SESSION_SWIPE_REFLOW_LEAD_MS = 220;" in SESSIONS_JS
|
||||
assert "const committedSwipeDuration=_sessionPrefersReducedMotion()?0:SESSION_SWIPE_DURATION_MS;" in SESSIONS_JS
|
||||
assert "const committedSwipeReflowDelay=Math.max(0,committedSwipeDuration-SESSION_SWIPE_REFLOW_LEAD_MS);" in SESSIONS_JS
|
||||
swipe_block = _sessions_block("const _handleSessionSwipe=(signedDx,signedDy)=>{", "const _commitSessionSwipe=()=>{")
|
||||
assert "const actionThreshold=signedDx>0?_archiveSwipeActionThreshold:_deleteSwipeActionThreshold;" in SESSIONS_JS
|
||||
assert "const actionThreshold=signedDx>0?SESSION_ARCHIVE_SWIPE_THRESHOLD_PX:SESSION_DELETE_SWIPE_THRESHOLD_PX;" in SESSIONS_JS
|
||||
assert "if(Math.abs(signedDx)<actionThreshold) return false;" in SESSIONS_JS
|
||||
assert "const _updateSessionGesture=(clientX,clientY)=>{" in SESSIONS_JS
|
||||
assert "if(_isSessionSwipeTarget()&&(_swipeTracking||dx>dy)) _paintSessionSwipe(signedDx)" in SESSIONS_JS
|
||||
@@ -116,8 +110,6 @@ def test_session_swipes_route_archive_restore_and_delete():
|
||||
assert delete_branch.find("deleteSession(s.session_id,async()=>{") < delete_branch.find("_completeSessionSwipePaint(signedDx);")
|
||||
assert "showToast('Imported sessions cannot be deleted here.',3000);" in SESSIONS_JS
|
||||
assert "let _gestureState='idle';" in SESSIONS_JS
|
||||
assert "const _commitSessionSwipe=()=>{" in SESSIONS_JS
|
||||
assert "if(_gestureState==='committed'){" in SESSIONS_JS
|
||||
assert SESSIONS_JS.count("if(e.pointerType==='touch') return;") >= 3
|
||||
assert "if(_gesturePointerType==='mouse'&&_gestureState!=='idle') _clearPointerDragState();" in SESSIONS_JS
|
||||
|
||||
@@ -136,10 +128,8 @@ def test_session_swipe_paint_uses_transform_only_exit():
|
||||
assert "requestAnimationFrame(()=>requestAnimationFrame(_clearSessionSwipePaint))" in SESSIONS_JS
|
||||
assert "el.classList.remove('swiping-right','swiping-left','swipe-committed','swipe-removing')" in clear
|
||||
assert ".session-item.swipe-committed,\n .session-item.swipe-removing{transition:" in STYLE_CSS
|
||||
assert "transition:background .15s,color .15s,transform .5s cubic-bezier(.22,.61,.36,1),box-shadow .15s ease" in STYLE_CSS
|
||||
assert "transform:translate3d(calc(-1 * var(--session-swipe-offset,0px)),0,0)" in STYLE_CSS
|
||||
assert "transform:scale(var(--session-swipe-icon-scale,1))" in STYLE_CSS
|
||||
assert "min-width:36px" not in STYLE_CSS[STYLE_CSS.find(".session-swipe-affordance{"):STYLE_CSS.find(".session-swipe-affordance-right{")]
|
||||
swipe_start = STYLE_CSS.find(".session-item.swipe-removing{")
|
||||
swipe_end = STYLE_CSS.find("}", swipe_start)
|
||||
assert swipe_start >= 0 and swipe_end > swipe_start
|
||||
@@ -160,7 +150,6 @@ def test_session_removal_reflows_surviving_rows_smoothly():
|
||||
assert "const delta=oldTop-row.getBoundingClientRect().top;" in helper
|
||||
assert "const movingRows=[];" in helper
|
||||
assert "list.getBoundingClientRect();" in helper
|
||||
assert helper.count("getBoundingClientRect()") == 2
|
||||
assert "row.style.transition='none';" in helper
|
||||
assert "row.classList.add('session-reflowing')" in helper
|
||||
assert "requestAnimationFrame(()=>requestAnimationFrame(()=>{" in helper
|
||||
@@ -177,8 +166,6 @@ def test_session_removal_reflows_surviving_rows_smoothly():
|
||||
assert "if(_showArchived&&!_sessionPrefersReducedMotion()) _sessionSwipeReturnOffsets.set(session.session_id,'0px');" in SESSIONS_JS
|
||||
assert "const swipeReturnOffset=_sessionSwipeReturnOffsets.get(s.session_id);" in SESSIONS_JS
|
||||
assert "el.classList.add('session-swipe-returning');" in SESSIONS_JS
|
||||
assert "@keyframes sessionSwipeReturn" in STYLE_CSS
|
||||
assert ".session-item.session-swipe-returning{animation:sessionSwipeReturn .5s cubic-bezier(.22,.61,.36,1) both;will-change:transform,opacity;}" in STYLE_CSS
|
||||
delete_start = SESSIONS_JS.find("async function deleteSession(sid, beforeDelete=null){")
|
||||
delete_end = SESSIONS_JS.find("// ── Project helpers", delete_start)
|
||||
assert delete_start >= 0 and delete_end > delete_start
|
||||
|
||||
Reference in new issue
Block a user