mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
fix(desktop): kill boot overlay fade-race in e2e screenshots
Screenshots were catching the CONNECTING overlay and onboarding Preparing loading bar mid-transition because the wait helpers fire on text content while visual state lags behind. Fix at the source via reduced motion: - playwright.config.ts: emulate prefers-reduced-motion: reduce - styles.css: blanket reduced-motion rule kills all CSS animations/transitions - gateway-connecting-overlay.tsx: skip JS setTimeout exit choreography (text-out 360ms + hold 300ms + overlay fade 520ms) — jump straight to gone - decode-text.tsx: skip scramble interval, render resolved text immediately
This commit is contained in:
parent
2eb320a7bf
commit
0fd12ca11b
4 changed files with 65 additions and 3 deletions
|
|
@ -38,6 +38,14 @@ export default defineConfig({
|
|||
use: {
|
||||
screenshot: 'on',
|
||||
trace: { mode: 'on', screenshots: true, snapshots: true, sources: true },
|
||||
// Emulate prefers-reduced-motion: reduce so all CSS transitions and
|
||||
// animations resolve instantly. This prevents boot/connecting overlays
|
||||
// from being mid-fade when a screenshot fires, and skips JS-driven exit
|
||||
// choreography in components that check matchMedia (onboarding, connecting
|
||||
// overlay, DecodeText). Without this, screenshots capture the loading bar
|
||||
// or overlay at a transient opacity because the text-content check fires
|
||||
// before the visual transition finishes.
|
||||
reducedMotion: 'reduce',
|
||||
},
|
||||
expect: {
|
||||
toHaveScreenshot: {
|
||||
|
|
|
|||
|
|
@ -35,11 +35,20 @@ function forcedPreview(): boolean {
|
|||
}
|
||||
}
|
||||
|
||||
function prefersReducedMotion(): boolean {
|
||||
return typeof window !== 'undefined' && Boolean(window.matchMedia?.('(prefers-reduced-motion: reduce)').matches)
|
||||
}
|
||||
|
||||
export function GatewayConnectingOverlay() {
|
||||
const gatewayState = useStore($gatewayState)
|
||||
const boot = useStore($desktopBoot)
|
||||
const gatewaySwitching = useStore($gatewaySwitching)
|
||||
const [previewing] = useState(forcedPreview)
|
||||
const reduce = prefersReducedMotion()
|
||||
// Under reduced motion, skip the multi-phase exit choreography (text-out →
|
||||
// hold → overlay fade) and jump straight to gone so the overlay unmounts
|
||||
// the instant the gateway opens. E2E screenshots rely on this to avoid
|
||||
// catching the overlay mid-fade.
|
||||
const [phase, setPhase] = useState<Phase>('live')
|
||||
// Once cold boot has completed once, never resurrect the fullscreen overlay
|
||||
// — soft gateway switches keep the shell and reskeleton the sidebar instead.
|
||||
|
|
@ -74,16 +83,32 @@ export function GatewayConnectingOverlay() {
|
|||
return
|
||||
}
|
||||
|
||||
if (previewing) {
|
||||
if(reduce) {
|
||||
// Under reduced motion, skip straight to gone — no text-out, no hold,
|
||||
// no overlay fade. The overlay unmounts immediately.
|
||||
setPhase('gone')
|
||||
}
|
||||
|
||||
if (previewing) {
|
||||
// Under reduced motion, skip straight to gone — no text-out, no hold,
|
||||
// no overlay fade. The overlay unmounts immediately.
|
||||
if(reduce) {
|
||||
setPhase('gone')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const id = window.setTimeout(() => setPhase('text-out'), PREVIEW_CONNECT_MS)
|
||||
|
||||
return () => window.clearTimeout(id)
|
||||
}
|
||||
|
||||
if (gatewayState === 'open' && shownRef.current) {
|
||||
setPhase('text-out')
|
||||
// Under reduced motion, skip straight to gone — no text-out, no hold,
|
||||
// no overlay fade. The overlay unmounts immediately.
|
||||
setPhase(reduce ? 'gone' : 'text-out')
|
||||
}
|
||||
}, [phase, previewing, gatewayState])
|
||||
}, [phase, previewing, gatewayState, reduce])
|
||||
|
||||
// Advance the exit choreography: text-out -> overlay-out -> gone.
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -23,6 +23,10 @@ export const DECODE_SCRAMBLE_CHARS = '/\\|-_=+<>~:*'
|
|||
const TICK_MS = 45
|
||||
const HOLD_TICKS = 16
|
||||
|
||||
function prefersReducedMotion(): boolean {
|
||||
return typeof window !== 'undefined' && Boolean(window.matchMedia?.('(prefers-reduced-motion: reduce)').matches)
|
||||
}
|
||||
|
||||
function scrambled(tail: string, resolvedCount: number): string {
|
||||
return Array.from(tail, (ch, i) =>
|
||||
ch === ' ' || i < resolvedCount ? ch : DECODE_SCRAMBLE_CHARS[(Math.random() * DECODE_SCRAMBLE_CHARS.length) | 0]
|
||||
|
|
@ -62,6 +66,15 @@ export function DecodeText({
|
|||
return
|
||||
}
|
||||
|
||||
// Under reduced motion, skip the scramble interval and render the fully
|
||||
// resolved text immediately. The cursor blink (CSS animation) is also
|
||||
// killed by the blanket reduced-motion CSS rule.
|
||||
if (prefersReducedMotion()) {
|
||||
setTail(tailText)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let resolved = 0
|
||||
let hold = 0
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,22 @@
|
|||
@import '@vscode/codicons/dist/codicon.css';
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
/* Blanket reduced-motion override: kill ALL CSS animations and transitions
|
||||
when the user (or the E2E test harness via prefers-reduced-motion) requests
|
||||
it. Per-component @media rules below handle specific cases; this catches
|
||||
everything else so overlays and loading bars resolve instantly instead of
|
||||
being caught mid-fade by a screenshot. */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Sidebar sections: tall viewports give each its own scroller; compact ones
|
||||
(this variant) flatten everything into one shared scroll. See ChatSidebar. */
|
||||
@custom-variant compact (@media (max-height: 768px));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue