@@ -11,11 +11,13 @@ import android.widget.AbsListView
1111import android.widget.ListAdapter
1212import androidx.core.view.ScrollingView
1313import io.sentry.Breadcrumb
14+ import io.sentry.ILogger
1415import io.sentry.IScope
1516import io.sentry.IScopes
1617import io.sentry.PropagationContext
1718import io.sentry.Scope
1819import io.sentry.ScopeCallback
20+ import io.sentry.SentryLevel
1921import io.sentry.SentryLevel.INFO
2022import io.sentry.android.core.SentryAndroidOptions
2123import kotlin.test.Test
@@ -28,6 +30,7 @@ import org.mockito.kotlin.doAnswer
2830import org.mockito.kotlin.inOrder
2931import org.mockito.kotlin.mock
3032import org.mockito.kotlin.never
33+ import org.mockito.kotlin.times
3134import org.mockito.kotlin.verify
3235import org.mockito.kotlin.verifyNoMoreInteractions
3336import org.mockito.kotlin.whenever
@@ -229,6 +232,50 @@ class SentryGestureListenerScrollTest {
229232 verify(fixture.scope).propagationContext = any()
230233 }
231234
235+ @Test
236+ fun `logs error message only once per gesture when no scroll target is found` () {
237+ val mockLogger = mock<ILogger >()
238+ fixture.options.setLogger(mockLogger)
239+
240+ // Create a setup where no scrollable view is found
241+ // Use regular View which doesn't implement ScrollingView/AbsListView/ScrollView
242+ fixture.target =
243+ mockView<View >(
244+ event = fixture.firstEvent,
245+ touchWithinBounds = true ,
246+ context = fixture.context,
247+ )
248+ fixture.window.mockDecorView<ViewGroup >(event = fixture.firstEvent) {
249+ whenever(it.childCount).thenReturn(1 )
250+ whenever(it.getChildAt(0 )).thenReturn(fixture.target)
251+ }
252+
253+ fixture.resources.mockForTarget(fixture.target, " test_view" )
254+ whenever(fixture.context.resources).thenReturn(fixture.resources)
255+ whenever(fixture.target.context).thenReturn(fixture.context)
256+ whenever(fixture.activity.window).thenReturn(fixture.window)
257+ doAnswer { (it.arguments[0 ] as ScopeCallback ).run (fixture.scope) }
258+ .whenever(fixture.scopes)
259+ .configureScope(any())
260+ doAnswer {
261+ (it.arguments[0 ] as Scope .IWithPropagationContext ).accept(fixture.propagationContext)
262+ fixture.propagationContext
263+ }
264+ .whenever(fixture.scope)
265+ .withPropagationContext(any())
266+
267+ val sut = SentryGestureListener (fixture.activity, fixture.scopes, fixture.options)
268+
269+ sut.onDown(fixture.firstEvent)
270+ // Multiple onScroll calls during the same gesture - should only log once
271+ fixture.eventsInBetween.forEach { sut.onScroll(fixture.firstEvent, it, 10f , 0f ) }
272+ sut.onUp(fixture.endEvent)
273+
274+ // Verify that the error message is logged only once during the entire gesture
275+ verify(mockLogger, times(1 ))
276+ .log(SentryLevel .DEBUG , " Unable to find scroll target. No breadcrumb captured." )
277+ }
278+
232279 internal class ScrollableView : View (mock()), ScrollingView {
233280 override fun computeVerticalScrollOffset (): Int = 0
234281
0 commit comments