Files
readest/apps/readest-app/.claude/memory/bug-patterns.md
T
Huang Xin d609de58f0 fix(reader): preserve position when toggling scrolled mode, closes #3987 (#3996)
The paginator's scrolled-mode scroll handler is debounced 250 ms, so
#anchor and #primaryIndex can lag behind the user's actual viewport.
Toggling out of scrolled mode within that window made
render() → scrollToAnchor(#anchor) restore the stale anchor, reverting
the position to a previously visible section.

Update foliate-js to flush the pending scroll state before flow leaves
'scrolled', and add regression coverage for the multi-section toggle path.
2026-04-29 09:32:54 +02:00

7.8 KiB

Bug Fixing Patterns & Strategies

Common Root Cause Categories

1. Overly Broad CSS Selectors

Pattern: A CSS rule targets too many elements, causing unintended visual side effects. Examples:

  • hr { mix-blend-mode: multiply } applied to ALL hr elements instead of only decorative ones (#3086)
  • p img { mix-blend-mode } applied to block images, not just inline (#3112)
  • svg, img { height: auto; width: auto } overrode explicit HTML width/height attributes (#3274)
  • Background-color override applied unconditionally instead of only when user enabled color override (#3316)

Fix Strategy: Narrow selectors with class qualifiers (.background-img, .has-text-siblings) or attribute pseudo-selectors (:where(:not([width]))). Check if the rule should be conditional on a user setting.

2. Conditional vs Unconditional Style Overrides

Pattern: CSS rules meant for "Override Book Color/Layout" mode are placed in the always-active stylesheet. Examples:

  • Calibre .calibre { color: unset } was in getLayoutStyles() instead of getColorStyles() (#3448)
  • Image background-color override applied without checking overrideColor flag (#3316, #3377)

Fix Strategy: Move rules to the correct conditional block: getColorStyles() for color overrides, getLayoutStyles() for layout overrides. Check the overrideColor/overrideLayout flags.

3. Missing EPUB Stylesheet Transformations

Pattern: EPUB stylesheets contain CSS that conflicts with app functionality. Examples:

  • user-select: none prevents text selection (#3370) -> regex replace in transformStylesheet()
  • font-family: serif/sans-serif on body bypasses user font (#3334) -> detect and unset
  • Hardcoded Calibre backgrounds persist in dark mode (#3448) -> unset in color override

Fix Strategy: Add regex-based transformation passes in transformStylesheet() in style.ts.

4. Stale State / Refs Not Reset

Pattern: A useRef or state variable is set once and never properly reset, blocking re-entry. Examples:

  • TTS ttsOnRef prevented restarting TTS from a new location (#3292)
  • initializedRef in AnnotationRangeEditor prevented handle position updates (#3286)
  • view.tts not nulled on shutdown prevented clean TTS restart (#3400)
  • TTS safety timeout fired after pause, advancing to next sentence (#3244)

Fix Strategy: Check all refs/guards in the affected flow. Ensure cleanup in shutdown/unmount. Remove overly aggressive guards that prevent re-entry.

5. Platform API Differences

Pattern: A Web API behaves differently or is unavailable on certain platforms. Examples:

  • navigator.getGamepads() returns null on older Android WebView (#3245)
  • CompressionStream unavailable on some Android versions (#3255)
  • btoa() throws on non-ASCII characters (#3436)
  • View Transitions API unsupported in WebKitGTK/Linux (#3417)
  • document.startViewTransition() crashes on Linux

Fix Strategy: Always check API availability before use. Add fallback paths. Use feature detection, not platform detection when possible.

6. Safe Area Inset Issues

Pattern: UI elements overlap system bars (status bar, navigation bar, notch) on mobile. Examples:

  • Zoom controls behind status bar (#3426)
  • Android navigation bar overlap (#3466)
  • iPad sidebar insets incorrect (#3395)
  • Reader page layout jump after system UI change (#3469)

Fix Strategy: Use gridInsets and statusBarHeight from useSafeAreaInsets. Use env(safe-area-inset-*) CSS functions. Call onUpdateInsets() after system UI visibility changes. See docs/safe-area-insets.md.

7. Z-Index Layering Issues

Pattern: Interactive elements rendered behind other layers, becoming unclickable. Examples:

  • Navigation buttons invisible on mobile (#3201) -> added z-10
  • Annotation nav bar too prominent (#3386) -> reduced from z-30 to z-10
  • Page nav buttons behind TTS control (#3184)

Fix Strategy: Check z-index ordering. Use minimum necessary z-index. Reference the z-index hierarchy in the codebase.

8. Event Handling Race Conditions

Pattern: Timing issues between pointer events, native menus, and React state updates. Examples:

  • macOS context menu steals pointer event loop (#3324) -> 100ms setTimeout delay
  • Traffic light buttons flicker due to timeout race (#3488, #3129)
  • Android tool buttons unresponsive due to premature re-selection (#3225)

Fix Strategy: Add small delays before native menu calls. Check event state machine consistency. Remove premature re-triggers on Android.

9. foliate-js Rendering Issues

Pattern: Bugs in the lower-level EPUB renderer (paginator.js, epub.js). Examples:

  • Image size not constrained in double-page mode (#3432)
  • Background not shown in scrolled mode (#3344)
  • Section content cached incorrectly after mode switch (#3242, #3206)
  • Swipe sensitivity too low for non-animated paging (#3310)

Fix Strategy: Check both columnize() and scrolled() code paths in paginator.js. Verify CSS variables (--available-width, --available-height) are computed correctly. Test in both paginated and scrolled modes.

10. Progress/Navigation Calculation Errors

Pattern: Page counts, progress percentages, or position tracking are wrong. Examples:

  • Progress shows 99.9% at last page (#3383) -> boundary condition
  • Pages left shows estimated instead of physical count (#3213, #3200)
  • FB2 subsection progress wrong (#3136) -> nested structure not handled
  • TOC auto-scrolls on expand (#3124) -> scroll-into-view triggered too broadly

Fix Strategy: Use physical view.renderer.page/view.renderer.pages instead of estimated section metadata. Check boundary conditions (0-indexed vs 1-indexed, inclusive vs exclusive).

11. Debounced State Stale on User-Initiated Layout Change

Pattern: A scroll/resize handler is debounced for performance, but during the debounce window any code path that re-runs layout based on saved state (e.g. #anchor, #primaryIndex) sees stale values. Example:

  • Scrolled-mode toggle reverted to previous chapter (#3987): the paginator's scroll handler is debounced 250 ms, so toggling flow=scrolled → flow=paginated within that window made render() → scrollToAnchor(#anchor) restore the anchor from before the user scrolled into the next section. Both #anchor and #primaryIndex were stale together, sending the position back.

Fix Strategy: When an external trigger forces a re-render (here, setAttribute('flow', ...)), flush the debounced state synchronously before changing the layout. In paginator.js this means overriding setAttribute and calling #detectPrimaryView() + #getVisibleRange() while this.scrolled is still true.

12. Multiview Paginator Side Effects

Pattern: The multiview paginator (e925e9d+) loads adjacent sections in background. Events from these loads can interfere with user interactions on the primary section. Examples:

  • load event from adjacent section triggers docLoadHandler which re-adds ALL annotations, overwriting drag edits
  • Multiple overlayers with duplicate SVG <clipPath> IDs cause url(#id) to resolve to wrong element
  • MagnifierLoupe destroying/recreating body clone on every drag tick triggers ResizeObserver → expand → redraw

Fix Strategy: Scope event handlers to the loaded section's index. Use unique IDs for SVG elements across overlayer instances. Minimize iframe DOM mutations during drag operations.

Debugging Workflow

  1. Identify the category from the issue description
  2. Check style.ts first for any CSS-related visual bugs
  3. Check foliate-js for rendering/layout bugs
  4. Check platform-specific code for mobile/desktop differences
  5. Write a failing test before implementing the fix
  6. Test in both paginated and scrolled modes for layout changes
  7. Test on multiple platforms for any UI change
  8. Run pnpm build-check before submitting