@@ -24,4 +24,84 @@ final class SingleTapGestureTestCase: GestureTestCase {
2424
2525 await fulfillment ( of: [ setupExpectation] , timeout: 10 )
2626 }
27+
28+ /// Regression test for the SwiftUI `Map.onMapTapGesture` firing when a user taps
29+ /// a `MapViewAnnotation`. ViewAnnotations must be transparent to drag-like gestures
30+ /// (pan/pinch/rotate) but opaque to single taps.
31+ func testSingleTapOnViewAnnotationDoesNotPropagateToMap( ) async throws {
32+ mapView. mapboxMap. loadStyle ( . standard)
33+
34+ let setupExpectation = expectation ( description: " Map setup " )
35+ didBecomeIdle = { [ weak self] mapView in
36+ guard let self else { return }
37+ // Prevent re-entry on subsequent idle events.
38+ didBecomeIdle = nil
39+
40+ let annotationView = UIView ( frame: CGRect ( x: 0 , y: 0 , width: 80 , height: 80 ) )
41+ annotationView. backgroundColor = . red
42+ let annotation = ViewAnnotation ( coordinate: camera. center, view: annotationView)
43+ annotation. allowOverlap = true
44+
45+ let visibleExpectation = expectation ( description: " View annotation became visible " )
46+ annotation. onVisibilityChanged = { visible in
47+ if visible { visibleExpectation. fulfill ( ) }
48+ }
49+ mapView. viewAnnotations. add ( annotation)
50+ wait ( for: [ visibleExpectation] , timeout: 5 )
51+
52+ let onAnnotationTap = expectation (
53+ description: " map tap must NOT fire when the tap targets a view annotation "
54+ )
55+ onAnnotationTap. isInverted = true
56+ let offMapTap = expectation (
57+ description: " map tap must fire when the tap lands outside the view annotation "
58+ )
59+ var didTapAnnotation = false
60+ mapView. mapboxMap. addInteraction ( TapInteraction { _ in
61+ if didTapAnnotation {
62+ offMapTap. fulfill ( )
63+ } else {
64+ onAnnotationTap. fulfill ( )
65+ }
66+ return true
67+ } )
68+
69+ try ! eventGenerator. fingerTap ( . rightIndex, at: annotationView)
70+ wait ( for: [ onAnnotationTap] , timeout: 2 )
71+
72+ didTapAnnotation = true
73+ // Tap well clear of the 80x80 annotation centered on the screen.
74+ try ! eventGenerator. fingerTap ( . rightIndex, at: OffsetLocation ( location: annotationView, x: 120 , y: 0 ) )
75+ wait ( for: [ offMapTap] , timeout: 5 )
76+
77+ setupExpectation. fulfill ( )
78+ }
79+
80+ await fulfillment ( of: [ setupExpectation] , timeout: 30 )
81+ }
82+
83+ /// Sanity check: tapping the bare map (no annotation in the way) still fires `onMapTap`.
84+ /// Guards against an over-correction that would silence single taps entirely.
85+ func testSingleTapOnBareMapFiresOnMapTap( ) async throws {
86+ mapView. mapboxMap. loadStyle ( . standard)
87+
88+ let setupExpectation = expectation ( description: " Map setup " )
89+ didBecomeIdle = { [ weak self] mapView in
90+ guard let self else { return }
91+ didBecomeIdle = nil
92+
93+ let mapTapExpectation = expectation ( description: " map tap fires on bare map tap " )
94+ mapView. mapboxMap. addInteraction ( TapInteraction { _ in
95+ mapTapExpectation. fulfill ( )
96+ return true
97+ } )
98+
99+ try ! eventGenerator. fingerTap ( . rightIndex)
100+
101+ wait ( for: [ mapTapExpectation] , timeout: 5 )
102+ setupExpectation. fulfill ( )
103+ }
104+
105+ await fulfillment ( of: [ setupExpectation] , timeout: 30 )
106+ }
27107}
0 commit comments