Skip to content

Commit 664f126

Browse files
romtsnclaude
andcommitted
test(android): Add unit tests for SentryGestureDetector
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 41beaed commit 664f126

File tree

1 file changed

+272
-0
lines changed

1 file changed

+272
-0
lines changed
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
package io.sentry.android.core.internal.gestures
2+
3+
import android.os.SystemClock
4+
import android.view.GestureDetector
5+
import android.view.MotionEvent
6+
import android.view.ViewConfiguration
7+
import androidx.test.core.app.ApplicationProvider
8+
import androidx.test.ext.junit.runners.AndroidJUnit4
9+
import kotlin.test.Test
10+
import org.junit.runner.RunWith
11+
import org.mockito.kotlin.any
12+
import org.mockito.kotlin.anyOrNull
13+
import org.mockito.kotlin.eq
14+
import org.mockito.kotlin.mock
15+
import org.mockito.kotlin.never
16+
import org.mockito.kotlin.verify
17+
18+
@RunWith(AndroidJUnit4::class)
19+
class SentryGestureDetectorTest {
20+
21+
class Fixture {
22+
val listener = mock<GestureDetector.OnGestureListener>()
23+
val context = ApplicationProvider.getApplicationContext<android.content.Context>()
24+
val touchSlop = ViewConfiguration.get(context).scaledTouchSlop
25+
26+
fun getSut(): SentryGestureDetector {
27+
return SentryGestureDetector(context, listener)
28+
}
29+
}
30+
31+
private val fixture = Fixture()
32+
33+
@Test
34+
fun `tap - DOWN followed by UP within touch slop fires onSingleTapUp`() {
35+
val sut = fixture.getSut()
36+
val downTime = SystemClock.uptimeMillis()
37+
38+
val down = MotionEvent.obtain(downTime, downTime, MotionEvent.ACTION_DOWN, 100f, 100f, 0)
39+
val up = MotionEvent.obtain(downTime, downTime + 50, MotionEvent.ACTION_UP, 100f, 100f, 0)
40+
41+
sut.onTouchEvent(down)
42+
sut.onTouchEvent(up)
43+
44+
verify(fixture.listener).onDown(down)
45+
verify(fixture.listener).onSingleTapUp(up)
46+
verify(fixture.listener, never()).onScroll(any(), any(), any(), any())
47+
verify(fixture.listener, never()).onFling(anyOrNull(), any(), any(), any())
48+
49+
down.recycle()
50+
up.recycle()
51+
}
52+
53+
@Test
54+
fun `no tap - DOWN followed by MOVE beyond slop and UP does not fire onSingleTapUp`() {
55+
val sut = fixture.getSut()
56+
val downTime = SystemClock.uptimeMillis()
57+
val beyondSlop = fixture.touchSlop + 10f
58+
59+
val down = MotionEvent.obtain(downTime, downTime, MotionEvent.ACTION_DOWN, 100f, 100f, 0)
60+
val move =
61+
MotionEvent.obtain(
62+
downTime,
63+
downTime + 16,
64+
MotionEvent.ACTION_MOVE,
65+
100f + beyondSlop,
66+
100f,
67+
0,
68+
)
69+
val up =
70+
MotionEvent.obtain(downTime, downTime + 50, MotionEvent.ACTION_UP, 100f + beyondSlop, 100f, 0)
71+
72+
sut.onTouchEvent(down)
73+
sut.onTouchEvent(move)
74+
sut.onTouchEvent(up)
75+
76+
verify(fixture.listener, never()).onSingleTapUp(any())
77+
78+
down.recycle()
79+
move.recycle()
80+
up.recycle()
81+
}
82+
83+
@Test
84+
fun `scroll - DOWN followed by MOVE beyond slop fires onScroll with correct deltas`() {
85+
val sut = fixture.getSut()
86+
val downTime = SystemClock.uptimeMillis()
87+
val beyondSlop = fixture.touchSlop + 10f
88+
89+
val down = MotionEvent.obtain(downTime, downTime, MotionEvent.ACTION_DOWN, 100f, 200f, 0)
90+
val move =
91+
MotionEvent.obtain(
92+
downTime,
93+
downTime + 16,
94+
MotionEvent.ACTION_MOVE,
95+
100f + beyondSlop,
96+
200f,
97+
0,
98+
)
99+
100+
sut.onTouchEvent(down)
101+
sut.onTouchEvent(move)
102+
103+
// scrollX = lastX - currentX = 100 - (100 + beyondSlop) = -beyondSlop
104+
verify(fixture.listener).onScroll(anyOrNull(), eq(move), eq(-beyondSlop), eq(0f))
105+
106+
down.recycle()
107+
move.recycle()
108+
}
109+
110+
@Test
111+
fun `fling - fast swipe fires onFling`() {
112+
val sut = fixture.getSut()
113+
val downTime = SystemClock.uptimeMillis()
114+
val beyondSlop = fixture.touchSlop + 10f
115+
116+
val down = MotionEvent.obtain(downTime, downTime, MotionEvent.ACTION_DOWN, 100f, 100f, 0)
117+
// Move far and fast (large distance in short time = high velocity)
118+
val move =
119+
MotionEvent.obtain(
120+
downTime,
121+
downTime + 10,
122+
MotionEvent.ACTION_MOVE,
123+
100f + beyondSlop,
124+
100f,
125+
0,
126+
)
127+
val up = MotionEvent.obtain(downTime, downTime + 20, MotionEvent.ACTION_UP, 500f, 100f, 0)
128+
129+
sut.onTouchEvent(down)
130+
sut.onTouchEvent(move)
131+
sut.onTouchEvent(up)
132+
133+
verify(fixture.listener).onFling(anyOrNull(), eq(up), any(), any())
134+
135+
down.recycle()
136+
move.recycle()
137+
up.recycle()
138+
}
139+
140+
@Test
141+
fun `slow release - DOWN MOVE and slow UP does not fire onFling`() {
142+
val sut = fixture.getSut()
143+
val downTime = SystemClock.uptimeMillis()
144+
val beyondSlop = fixture.touchSlop + 1f
145+
146+
val down = MotionEvent.obtain(downTime, downTime, MotionEvent.ACTION_DOWN, 100f, 100f, 0)
147+
// Move just beyond slop
148+
val move =
149+
MotionEvent.obtain(
150+
downTime,
151+
downTime + 100,
152+
MotionEvent.ACTION_MOVE,
153+
100f + beyondSlop,
154+
100f,
155+
0,
156+
)
157+
// Stay at the same position for a long time to ensure near-zero velocity
158+
val moveStill =
159+
MotionEvent.obtain(
160+
downTime,
161+
downTime + 10000,
162+
MotionEvent.ACTION_MOVE,
163+
100f + beyondSlop,
164+
100f,
165+
0,
166+
)
167+
val up =
168+
MotionEvent.obtain(
169+
downTime,
170+
downTime + 10001,
171+
MotionEvent.ACTION_UP,
172+
100f + beyondSlop,
173+
100f,
174+
0,
175+
)
176+
177+
sut.onTouchEvent(down)
178+
sut.onTouchEvent(move)
179+
sut.onTouchEvent(moveStill)
180+
sut.onTouchEvent(up)
181+
182+
verify(fixture.listener, never()).onFling(anyOrNull(), any(), any(), any())
183+
184+
down.recycle()
185+
move.recycle()
186+
moveStill.recycle()
187+
up.recycle()
188+
}
189+
190+
@Test
191+
fun `cancel - DOWN followed by CANCEL does not fire tap or fling callbacks`() {
192+
val sut = fixture.getSut()
193+
val downTime = SystemClock.uptimeMillis()
194+
195+
val down = MotionEvent.obtain(downTime, downTime, MotionEvent.ACTION_DOWN, 100f, 100f, 0)
196+
val cancel =
197+
MotionEvent.obtain(downTime, downTime + 50, MotionEvent.ACTION_CANCEL, 100f, 100f, 0)
198+
199+
sut.onTouchEvent(down)
200+
sut.onTouchEvent(cancel)
201+
202+
verify(fixture.listener).onDown(down)
203+
verify(fixture.listener, never()).onSingleTapUp(any())
204+
verify(fixture.listener, never()).onScroll(any(), any(), any(), any())
205+
verify(fixture.listener, never()).onFling(anyOrNull(), any(), any(), any())
206+
207+
down.recycle()
208+
cancel.recycle()
209+
}
210+
211+
@Test
212+
fun `sequential gestures - state resets between tap and scroll`() {
213+
val sut = fixture.getSut()
214+
val beyondSlop = fixture.touchSlop + 10f
215+
216+
// First gesture: tap
217+
var downTime = SystemClock.uptimeMillis()
218+
val down1 = MotionEvent.obtain(downTime, downTime, MotionEvent.ACTION_DOWN, 100f, 100f, 0)
219+
val up1 = MotionEvent.obtain(downTime, downTime + 50, MotionEvent.ACTION_UP, 100f, 100f, 0)
220+
221+
sut.onTouchEvent(down1)
222+
sut.onTouchEvent(up1)
223+
verify(fixture.listener).onSingleTapUp(up1)
224+
225+
// Second gesture: scroll
226+
downTime = SystemClock.uptimeMillis()
227+
val down2 = MotionEvent.obtain(downTime, downTime, MotionEvent.ACTION_DOWN, 200f, 200f, 0)
228+
val move2 =
229+
MotionEvent.obtain(
230+
downTime,
231+
downTime + 16,
232+
MotionEvent.ACTION_MOVE,
233+
200f + beyondSlop,
234+
200f,
235+
0,
236+
)
237+
val up2 =
238+
MotionEvent.obtain(
239+
downTime,
240+
downTime + 5000,
241+
MotionEvent.ACTION_UP,
242+
200f + beyondSlop,
243+
200f,
244+
0,
245+
)
246+
247+
sut.onTouchEvent(down2)
248+
sut.onTouchEvent(move2)
249+
sut.onTouchEvent(up2)
250+
251+
verify(fixture.listener).onScroll(anyOrNull(), eq(move2), any(), any())
252+
// onSingleTapUp should NOT have been called again for the second gesture
253+
verify(fixture.listener, never()).onSingleTapUp(up2)
254+
255+
// Third gesture: another tap to verify clean reset
256+
downTime = SystemClock.uptimeMillis()
257+
val down3 = MotionEvent.obtain(downTime, downTime, MotionEvent.ACTION_DOWN, 300f, 300f, 0)
258+
val up3 = MotionEvent.obtain(downTime, downTime + 50, MotionEvent.ACTION_UP, 300f, 300f, 0)
259+
260+
sut.onTouchEvent(down3)
261+
sut.onTouchEvent(up3)
262+
verify(fixture.listener).onSingleTapUp(up3)
263+
264+
down1.recycle()
265+
up1.recycle()
266+
down2.recycle()
267+
move2.recycle()
268+
up2.recycle()
269+
down3.recycle()
270+
up3.recycle()
271+
}
272+
}

0 commit comments

Comments
 (0)