4
4
5
5
import 'dart:js_util' as js_util;
6
6
7
+ import 'package:meta/meta.dart' ;
7
8
import 'package:test/bootstrap/browser.dart' ;
8
9
import 'package:test/test.dart' ;
9
10
import 'package:ui/src/engine.dart' ;
@@ -2757,6 +2758,7 @@ void _testClickDebouncer({required PointerBinding Function() getBinding}) {
2757
2758
String description,
2758
2759
Future <void > Function () body, {
2759
2760
Object ? skip,
2761
+ @doNotSubmit bool solo = false ,
2760
2762
}) {
2761
2763
test (
2762
2764
description,
@@ -2768,6 +2770,7 @@ void _testClickDebouncer({required PointerBinding Function() getBinding}) {
2768
2770
EngineSemantics .instance.semanticsEnabled = false ;
2769
2771
},
2770
2772
skip: skip,
2773
+ solo: solo, // ignore: invalid_use_of_do_not_submit_member
2771
2774
);
2772
2775
}
2773
2776
@@ -3053,6 +3056,67 @@ void _testClickDebouncer({required PointerBinding Function() getBinding}) {
3053
3056
// TODO(yjbanov): https://github.com/flutter/flutter/issues/142991.
3054
3057
}, skip: ui_web.browser.operatingSystem == ui_web.OperatingSystem .windows);
3055
3058
3059
+ // Regression test for https://github.com/flutter/flutter/issues/147050
3060
+ //
3061
+ // This test emulates a long-press. Start with a "pointerdown" followed by no
3062
+ // activity long enough that the debounce timer expires and the state of the
3063
+ // ClickDebouncer is reset back to idle. Then a "pointerup" arrives seemingly
3064
+ // standalone. Since no gesture is being debounced, the debouncer simply
3065
+ // forwards it to the framework. However, "pointerup" will be immediately
3066
+ // followed by a "click". Since we sent the "pointerdown" and "pointerup" to
3067
+ // the framework already, the framework registered a tap. Forwarding the
3068
+ // "click" would lead to a double-tap. This was the bug.
3069
+ testWithSemantics ('Dedupes click if pointer up happened recently without debouncing' , () async {
3070
+ expect (EnginePlatformDispatcher .instance.semanticsEnabled, true );
3071
+ expect (PointerBinding .clickDebouncer.isDebouncing, false );
3072
+
3073
+ final DomElement testElement = createDomElement ('flt-semantics' );
3074
+ testElement.setAttribute ('flt-tappable' , '' );
3075
+ view.dom.semanticsHost.appendChild (testElement);
3076
+
3077
+ // Begin a long-press with a "pointerdown".
3078
+ testElement.dispatchEvent (context.primaryDown ());
3079
+
3080
+ // Expire the timer causing the debouncer to reset itself.
3081
+ await Future <void >.delayed (const Duration (milliseconds: 250 ));
3082
+ expect (
3083
+ reason: '"pointerdown" should be flushed when the timer expires.' ,
3084
+ pointerPackets,
3085
+ < ui.PointerChange > [ui.PointerChange .add, ui.PointerChange .down],
3086
+ );
3087
+ pointerPackets.clear ();
3088
+
3089
+ // Send a "pointerup" while the debouncer is not debouncing anything.
3090
+ testElement.dispatchEvent (context.primaryUp ());
3091
+
3092
+ // A standalone "pointerup" should not start debouncing anything.
3093
+ expect (PointerBinding .clickDebouncer.isDebouncing, isFalse);
3094
+ expect (
3095
+ reason: 'The "pointerup" should be forwarded to the framework immediately' ,
3096
+ pointerPackets,
3097
+ < ui.PointerChange > [ui.PointerChange .up],
3098
+ );
3099
+
3100
+ // Use a delay that's short enough for the click to be deduped.
3101
+ await Future <void >.delayed (const Duration (milliseconds: 10 ));
3102
+
3103
+ final DomEvent click = createDomMouseEvent (
3104
+ 'click' ,
3105
+ < Object ? , Object ? > {
3106
+ 'clientX' : testElement.getBoundingClientRect ().x,
3107
+ 'clientY' : testElement.getBoundingClientRect ().y,
3108
+ }
3109
+ );
3110
+ PointerBinding .clickDebouncer.onClick (click, 42 , true );
3111
+
3112
+ expect (
3113
+ reason: 'Because the DOM click event was deduped.' ,
3114
+ semanticsActions,
3115
+ isEmpty,
3116
+ );
3117
+ // TODO(yjbanov): https://github.com/flutter/flutter/issues/142991.
3118
+ }, skip: ui_web.browser.operatingSystem == ui_web.OperatingSystem .windows);
3119
+
3056
3120
testWithSemantics ('Forwards click if enough time passed after the last flushed pointerup' , () async {
3057
3121
expect (EnginePlatformDispatcher .instance.semanticsEnabled, true );
3058
3122
expect (PointerBinding .clickDebouncer.isDebouncing, false );
0 commit comments