Skip to content

test: NonScalingOverlay + ReactNativeZoomableView test suite (274 tests, 29 suites)#180

Draft
thomasttvo wants to merge 29 commits into
thomas/nonscaling-overlayfrom
thomas/zoomable-tests-v2
Draft

test: NonScalingOverlay + ReactNativeZoomableView test suite (274 tests, 29 suites)#180
thomasttvo wants to merge 29 commits into
thomas/nonscaling-overlayfrom
thomas/zoomable-tests-v2

Conversation

@thomasttvo
Copy link
Copy Markdown
Collaborator

Summary

Adds a Jest + React Testing Library test suite covering NonScalingOverlay, renderOverlay integration, and the full SPECS.md contract for ReactNativeZoomableView. 274 tests across 29 suites, all green locally.

Targets thomas/nonscaling-overlay (PR #178) — when #178 merges to master, this PR's base auto-updates and the tests come along.

Test coverage

  • 14 EC-NSO edge cases from PR Reanimated #151's review (translated from prose into regression tests).
  • 151 SPEC items from SPECS.md (with documented device-only and type-only gaps).
  • PR Reanimated #151 thread regressions: doubleTapZoomToCenter math (#3179084848), spurious-tap on cancel (#3179193006), settle double-fire (#3164939942), pinProps spread (#3107340687 / #3179480336), useLatestWorklet stale closure (#3179033549 / #3238350220), and others.

Suite breakdown

Suite Tests Surface
computeOverlayTransform.test.ts 9 Pure transform-math helper
NonScalingOverlay.test.tsx 9 Overlay component (RTL + reanimated mock)
renderOverlay.test.tsx 6 RNZV.renderOverlay integration
helper/__tests__/* (9 files) 51 Pure math helpers + 4 new extractions
RNZV.{props,imperativeHandle,callbacks}.test.tsx 57 Defaults, methods, callbacks (no gesture)
RNZV.{staticPin,feedback}.test.tsx + useLatestWorklet.test.ts 25 Static pin, debug, hook ref identity
treeShape.test.tsx + StaticPin.styling.test.tsx 17 Tree topology + StaticPin pointerEvents/styling
gestures/* (5 files) 82 Direct gesture-handler invocation via getByGestureTestId('canvas-gesture') (tap, double-tap, long-press, pinch, shift, multi-finger, pan-responder callbacks, intercept)

Test infrastructure (first commit)

  • Jest 29, @testing-library/react-native@^12.5.0, react-test-renderer@18.3.1, babel-jest, @types/jest.
  • jest.setup.ts wires react-native-reanimated/mock + react-native-gesture-handler/jestSetup.
  • Custom RNGH Gesture.Manual() mock with a withTestId registry (Path 2 from research) so gesture-driven tests can invoke onTouchesDown/Move/Up/Cancelled directly under reanimated/mock.
  • New CI step in .github/workflows/lint.yml: yarn test --ci --runInBand.

Pure-helper extractions (non-behavior-changing)

4 inline expressions in ReactNativeZoomableView.tsx extracted to src/helper/ for unit-testability — math is byte-equivalent to the inline original:

  • calcShiftDelta (from _calcOffsetShiftSinceLastGestureState)
  • applyPinchSensitivity (from _handlePinching resistance math)
  • clampZoom (pinch-frame clamp; publicZoomTo's reject-on-out-of-range was left untouched — different semantics)
  • shouldSkipShift (pan-gate predicate)

SPECS.md changes

  • New section ## NonScalingOverlay contract codifying the prop shape, 5-element transform formula, static-style rules, mount-order rules, and wrapperSize mirror semantics.

Known gaps (documented in phase5-specs-research.md §5)

  • Device-only items: GHRV mount requirement, FP-drift bypass of disablePanOnInitialZoom, native momentum absence, dual-version Reanimated matrix (Elliott's #3691782346).
  • Type-only SPECs covered by tsc --noEmit in CI (no expect-type/tsd dep added).
  • Mock-fidelity caveats: assertions are on JS-thread callback fires rather than intermediate SharedValue trajectories; withTiming/cancelAnimation/runOnUI are no-ops under the mock.

Test plan

  • Reviewer runs yarn test locally and confirms 29 suites / 274 tests pass.
  • CI's Run unit tests step passes on push.
  • Spot-check a few it('SPEC-NNN: …', …) cases to confirm SPEC IDs map to the items in SPECS.md.

🤖 Generated with Claude Code

thomasvo and others added 16 commits May 14, 2026 10:42
- devDeps: jest, babel-jest, @testing-library/react-native, react-test-renderer, @types/jest
- jest.setup.ts wires reanimated/mock + RNGH jestSetup
- package.json jest config: setupFiles + explicit transformIgnorePatterns
- `yarn jest --passWithNoTests` passes locally

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ests

- Pure helper covering EC-NSO-3/4/5/6 transform-math contracts
- Worklet directive preserved; useAnimatedStyle now delegates
- Unit tests cover 5-element shape invariant, centering math, zoom scaling,
  pan in rotated frame, negative/zero/large inputs

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Zero-dim guard (contentWidth/Height = 0 -> null)
- Rotation prop toggle does not change hook count
- Static styles: position:absolute, top:0, left:0, overflow:visible
- pointerEvents='none' prop
- Children pass-through
- Width/height/transform plumbing under reanimated mock

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-NSO-10–14

- Overlay branch gating by contentWidth/Height
- Mount order: overlay paints under StaticPin (sibling order)
- Overlay shares coordinate frame with GestureDetector (wrapper sibling)
- onLayout 0×0 doesn't overwrite wrapperSize state
- Identical onLayout dims dedup (no spurious re-renders)
- Adds testID="zoom-subject-wrapper" on wrapper View for RTL reachability

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…stGestureState

Pure-math helper for SPEC-027/028/109 unit tests. movementSensitivity=0
returns {dxShift:0, dyShift:0} (SPEC-028 silently disables panning).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…math

Pure-math helper for SPEC-095 unit tests. Formula
deltaGrowth * (1 - sensitivity*9/100) is identical at the call site.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pure-math helper for SPEC-020/023/097 unit tests. null/undefined bounds
mean unbounded on that side, matching the existing != null gate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pure-logic helper for SPEC-029/108 unit tests. Returns true when panEnabled
is false OR (disablePanOnInitialZoom && zoom === initialZoom).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Enables Phase C gesture-driven integration tests to acquire the
Gesture.Manual() instance via getByGestureTestId('canvas-gesture').

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Covers SPEC-001/004/008/009/010 (exports + peer deps),
SPEC-020/022/023/097/099-104 (getNextZoomStep cycle + clamp),
SPEC-027/028/109 (calcShiftDelta), SPEC-029/108 (shouldSkipShift),
SPEC-095 (applyPinchSensitivity), SPEC-096 (calcNewScaledOffsetForZoomCentering),
SPEC-010/133 (coordinateConversion), plus regression test for the
dy/dx swap previously seen in calcGestureTouchDistance (PR #151
thread #3076242724).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…4, 053-055, 065-080, 107, 137-149)

- Default prop values + initial zoom/offset
- 4 cancellation paths verified onZoomEnd does NOT fire
- zoomTo timing, zoomCenter math, return-value contract
- Programmatic methods bypass panEnabled
- onLayoutWorklet unwrapped payload, onTransformWorklet centering invariant
- movementSensibility legacy alias

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…7, 112-119, 047)

- Wrapper tree: GestureDetector + StaticPin siblings (not nested)
- StaticPin pinProps style merge (caller transform replaces anchor)
- pointerEvents defaults (wrapper box-none, icon none); pinProps override survives destructure-before-spread (threads #3107340687, #3179480336)
- Opacity 0->1 on icon onLayout, left/top from staticPinPosition

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…85, 117-122)

- Settle 100ms debounce + ε dedup + cancellation (threads #3164939942, #3179477073)
- Pin opacity gate, internal bottom-centre transform
- onStaticPinPositionMoveWorklet content-dim gating + payload math
- visualTouchFeedbackEnabled / debug conditional branches
- useLatestWorklet ref identity update (threads #3179033549, #3238350220)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…31-033, 037-041, 060-063, 092, 105-106, 123-130, 135)

Direct-gesture invocation via getByGestureTestId('canvas-gesture').
Acquires Gesture.Manual() and invokes onTouchesDown/Move/Up directly.
Covers PR #151 threads #3179084848 (doubleTapZoomToCenter math),
#3179033552 (singleTapTimeoutId cleanup), and #3179193006 cluster
(sentinel survival across recovery).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…SPEC-056-059, 081, 082)

- onPanResponderGrant/End/Terminate ordering across natural release, 3+ force-end, RNGH cancel
- gestureStarted true during gesture, reset to false AFTER end-callbacks (SPEC-082 clear-ordering)
- onPanResponderMoveWorklet intercept return-truthy short-circuits library (externallyHandled), preserves sentinel against spurious onSingleTap after intercepted drag

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-091, 093, 094, 098, 108, 110, 130, 150)

Direct-gesture invocation. Pinch/shift gesture classification, 3+ finger
force-end + recovery, tap-classification only on genuine release.
Covers PR #151 threads #3179193006 (no spurious tap on cancel) and
#3179193011 (firstTouch stability across finger lifts).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@cursor
Copy link
Copy Markdown

cursor Bot commented May 14, 2026

PR Summary

Medium Risk
Mostly adds test infrastructure and extensive Jest coverage, but also lightly refactors gesture math into new helpers and adds gesture/test IDs in ReactNativeZoomableView, which could subtly affect runtime behavior if not truly equivalent.

Overview
Adds a full Jest + React Testing Library setup (Babel preset, jest.setup.ts, Jest config/deps) and runs yarn test in CI.

Introduces a large test suite covering ReactNativeZoomableView contracts (props/defaults, imperative methods, callbacks, static pin behavior, overlay integration, and gesture classification via a custom RNGH Gesture.Manual() mock keyed by withTestId('canvas-gesture')).

Makes small production changes to support/enable this coverage: extracts pinch/shift math into new helper functions (applyPinchSensitivity, calcShiftDelta, clampZoom, shouldSkipShift), adds testID="zoom-subject-wrapper" on the wrapper View, and assigns a gesture test id via .withTestId('canvas-gesture'). Updates SPECS.md with a documented NonScalingOverlay contract (transform formula, style/mount-order rules, and wrapper-size mirroring semantics).

Reviewed by Cursor Bugbot for commit dd2672e. Configure here.

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit dd2672e. Configure here.

Comment thread jest.setup.ts
jest.spyOn(global.console, 'warn').mockImplementation((msg: unknown) => {
if (typeof msg === 'string' && msg.includes('Reanimated 2')) return;
// fall through other warnings
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jest.setup suppresses all console.warn, not just Reanimated

Medium Severity

The mockImplementation replaces console.warn entirely. When the message does NOT contain 'Reanimated 2', the function still returns undefined without calling the original implementation. The comment says "fall through other warnings" but there is no actual fall-through — all warnings are silently swallowed. This masks legitimate warnings emitted by the code under test, potentially hiding real issues like deprecation warnings or incorrect usage patterns.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit dd2672e. Configure here.

Comment thread SPECS.md
- The overlay is mounted as a **sibling** of `GestureDetector`'s zoom-transformed layer, not under it — both share the wrapper's coordinate frame.
- The overlay appears **before** `StaticPin` in source order, so RN paints the overlay underneath the pin (last sibling renders on top).
- When `contentWidth` or `contentHeight` is missing or zero, the overlay returns `null` — no markers render.
- `renderOverlay` is wired through automatically; the consumer never instantiates `NonScalingOverlay` directly when going through the prop.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SPECS.md documents internal implementation details for NonScalingOverlay

Low Severity

The NonScalingOverlay contract section includes implementation details that violate the SPECS.md authoring rules. Specifically: the "Transform formula" subsection documents the exact 5-element transform array structure with index positions (transform[3..4]), the "Mounting rules" subsection references internal tree topology relative to GestureDetector, and source ordering within ReactNativeZoomableView. These are implementation internals, not consumer-observable behavior.

Fix in Cursor Fix in Web

Triggered by learned rule: SPECS.md must document consumer-observable behavior only

Reviewed by Cursor Bugbot for commit dd2672e. Configure here.

@thomasttvo thomasttvo marked this pull request as draft May 14, 2026 19:09
thomasvo and others added 11 commits May 14, 2026 12:42
Probe demonstrates that RNZV's Gesture.Manual() callbacks can be driven
through RNGH's REAL builder + registry, without the per-file
jest.mock('react-native-gesture-handler', ...) used by the existing
gesture suites. Single-tap → onSingleTap fires after doubleTapDelay.

The only added mock is RN's renderer shim — minimum needed to bypass
the documented ReactNativeRenderer-dev load crash. Reanimated/mock and
RNGH/jestSetup are inherited from the global setup unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per phase E probe finding §6.1 — the narrow renderer-shim mock unblocks
real-RNGH integration tests without affecting existing mock-based tests.
Hoisting eliminates the per-file repetition for the scale-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the custom Proxy-based RNGH mock with the real
react-native-gesture-handler module (per phase E probe). Real
Gesture.Manual() builder + handlersRegistry + withTestId resolution
now exercise; touch-event dispatch remains direct-handler invocation
per phase E §6.5 (fireGestureHandler doesn't support Manual gestures
in RNGH 2.20.2).

Same SPEC IDs, same coverage — strictly more realistic plumbing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the custom Proxy-based RNGH mock with the real
react-native-gesture-handler module (per phase E probe). Real
Gesture.Manual() builder + handlersRegistry + withTestId resolution
now exercise; touch-event dispatch remains direct-handler invocation
per phase E §6.5 (fireGestureHandler doesn't support Manual gestures
in RNGH 2.20.2).

Same SPEC IDs, same coverage — strictly more realistic plumbing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the custom Proxy-based RNGH mock with the real
react-native-gesture-handler module (per phase E probe). Real
Gesture.Manual() builder + handlersRegistry + withTestId resolution
now exercise; touch-event dispatch remains direct-handler invocation
per phase E §6.5 (fireGestureHandler doesn't support Manual gestures
in RNGH 2.20.2).

Same SPEC IDs, same coverage — strictly more realistic plumbing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the custom Proxy-based RNGH mock with the real
react-native-gesture-handler module (per phase E probe). Real
Gesture.Manual() builder + handlersRegistry + withTestId resolution
now exercise; touch-event dispatch remains direct-handler invocation
per phase E §6.5 (fireGestureHandler doesn't support Manual gestures
in RNGH 2.20.2).

Same SPEC IDs, same coverage — strictly more realistic plumbing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the custom Proxy-based RNGH mock with the real
react-native-gesture-handler module (per phase E probe). Real
Gesture.Manual() builder + handlersRegistry + withTestId resolution
now exercise; touch-event dispatch remains direct-handler invocation
per phase E §6.5 (fireGestureHandler doesn't support Manual gestures
in RNGH 2.20.2).

Same SPEC IDs, same coverage — strictly more realistic plumbing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the custom Proxy-based RNGH mock with the real
react-native-gesture-handler module (per phase E probe). Real
Gesture.Manual() builder + handlersRegistry + withTestId resolution
now exercise; touch-event dispatch remains direct-handler invocation
per phase E §6.5 (fireGestureHandler doesn't support Manual gestures
in RNGH 2.20.2).

Same SPEC IDs, same coverage — strictly more realistic plumbing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the custom Proxy-based RNGH mock with the real
react-native-gesture-handler module (per phase E probe). Real
Gesture.Manual() builder + handlersRegistry + withTestId resolution
now exercise; touch-event dispatch remains direct-handler invocation
per phase E §6.5 (fireGestureHandler doesn't support Manual gestures
in RNGH 2.20.2).

Same SPEC IDs, same coverage — strictly more realistic plumbing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…est.tsx

Replaces the custom Proxy-based RNGH mock with the real
react-native-gesture-handler module (per phase E probe). Real
Gesture.Manual() builder + handlersRegistry + withTestId resolution
now exercise; touch-event dispatch remains direct-handler invocation
per phase E §6.5 (fireGestureHandler doesn't support Manual gestures
in RNGH 2.20.2).

Same SPEC IDs, same coverage — strictly more realistic plumbing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After scaling the Phase E probe across the gesture-test suite, document
the test-layer fidelity profile (real RNGH builder + registry, reanimated/mock,
direct-handler dispatch, renderer-shim stub) so future contributors know
what's exercised and what's stubbed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
thomasvo and others added 2 commits May 14, 2026 15:10
The previous refactor extracted calcShiftDelta, applyPinchSensitivity,
clampZoom, and shouldSkipShift from RNZV.tsx into src/helper/ for the
sole purpose of unit-testing them. Per user direction: do not create
new helpers just for testing. The semi-e2e scenario tests cover the
same math via gesture-driven assertions on rendered output + callback
payloads + SharedValue end-states; the new helpers are unnecessary.

Reverts:
- 2a13abc calcShiftDelta extraction
- 3ea1c10 applyPinchSensitivity extraction
- bebae3c clampZoom extraction
- 0129282 shouldSkipShift extraction

Also drops the 4 unit-test files that exercised those helpers
(getNextZoomStep, calcGestureCenterPoint, calcGestureTouchDistance,
calcNewScaledOffsetForZoomCentering, coordinateConversion, exports tests
remain — they test pre-existing public helpers).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… sheet transform

Multi-frame touch sequences via real RNGH + reanimated/mock. Each
scenario drives a realistic gesture and asserts on:
 - end-state SharedValues (zoom, offsetX, offsetY)
 - callback payloads (ZoomableViewEvent shape and values)
 - rendered Animated.View transform on the zoom layer
 - NonScalingOverlay markers translated per the overlay formula

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant