double_tap_test.dart 20.2 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
Ian Hickson's avatar
Ian Hickson committed
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'package:fake_async/fake_async.dart';
6 7
import 'package:flutter/gestures.dart';
import 'package:flutter_test/flutter_test.dart';
8

9 10
import 'gesture_tester.dart';

11
class TestGestureArenaMember extends GestureArenaMember {
12
  @override
13
  void acceptGesture(int key) {
14 15
    accepted = true;
  }
16 17

  @override
18
  void rejectGesture(int key) {
19 20
    rejected = true;
  }
21

22 23
  bool accepted = false;
  bool rejected = false;
24 25 26
}

void main() {
27 28
  TestWidgetsFlutterBinding.ensureInitialized();

29 30 31 32
  late DoubleTapGestureRecognizer tap;
  bool doubleTapRecognized = false;
  TapDownDetails? doubleTapDownDetails;
  bool doubleTapCanceled = false;
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58

  setUp(() {
    tap = DoubleTapGestureRecognizer();

    doubleTapRecognized = false;
    tap.onDoubleTap = () {
      expect(doubleTapRecognized, isFalse);
      doubleTapRecognized = true;
    };

    doubleTapDownDetails = null;
    tap.onDoubleTapDown = (TapDownDetails details) {
      expect(doubleTapDownDetails, isNull);
      doubleTapDownDetails = details;
    };

    doubleTapCanceled = false;
    tap.onDoubleTapCancel = () {
      expect(doubleTapCanceled, isFalse);
      doubleTapCanceled = true;
    };
  });

  tearDown(() {
    tap.dispose();
  });
59 60

  // Down/up pair 1: normal tap sequence
61
  const PointerDownEvent down1 = PointerDownEvent(
62
    pointer: 1,
63
    position: Offset(10.0, 10.0),
64 65
  );

66
  const PointerUpEvent up1 = PointerUpEvent(
67
    pointer: 1,
68
    position: Offset(11.0, 9.0),
69 70 71
  );

  // Down/up pair 2: normal tap sequence close to pair 1
72
  const PointerDownEvent down2 = PointerDownEvent(
73
    pointer: 2,
74
    position: Offset(12.0, 12.0),
75 76
  );

77
  const PointerUpEvent up2 = PointerUpEvent(
78
    pointer: 2,
79
    position: Offset(13.0, 11.0),
80 81 82
  );

  // Down/up pair 3: normal tap sequence far away from pair 1
83
  const PointerDownEvent down3 = PointerDownEvent(
84
    pointer: 3,
85
    position: Offset(130.0, 130.0),
86 87
  );

88
  const PointerUpEvent up3 = PointerUpEvent(
89
    pointer: 3,
90
    position: Offset(131.0, 129.0),
91 92 93
  );

  // Down/move/up sequence 4: intervening motion
94
  const PointerDownEvent down4 = PointerDownEvent(
95
    pointer: 4,
96
    position: Offset(10.0, 10.0),
97 98
  );

99
  const PointerMoveEvent move4 = PointerMoveEvent(
100
    pointer: 4,
101
    position: Offset(25.0, 25.0),
102 103
  );

104
  const PointerUpEvent up4 = PointerUpEvent(
105
    pointer: 4,
106
    position: Offset(25.0, 25.0),
107 108
  );

109
  // Down/up pair 5: normal tap sequence identical to pair 1
110
  const PointerDownEvent down5 = PointerDownEvent(
111
    pointer: 5,
112
    position: Offset(10.0, 10.0),
113 114
  );

115
  const PointerUpEvent up5 = PointerUpEvent(
116
    pointer: 5,
117
    position: Offset(11.0, 9.0),
118 119
  );

120 121 122 123 124 125 126 127 128 129 130 131
  // Down/up pair 6: normal tap sequence close to pair 1 but on secondary button
  const PointerDownEvent down6 = PointerDownEvent(
    pointer: 6,
    position: Offset(10.0, 10.0),
    buttons: kSecondaryMouseButton,
  );

  const PointerUpEvent up6 = PointerUpEvent(
    pointer: 6,
    position: Offset(11.0, 9.0),
  );

132
  testGesture('Should recognize double tap', (GestureTester tester) {
133
    tap.addPointer(down1);
134 135 136
    tester.closeArena(1);
    tester.route(down1);
    tester.route(up1);
137
    GestureBinding.instance.gestureArena.sweep(1);
138
    expect(doubleTapDownDetails, isNull);
139

140
    tester.async.elapse(const Duration(milliseconds: 100));
141
    tap.addPointer(down2);
142
    tester.closeArena(2);
143
    expect(doubleTapDownDetails, isNotNull);
144 145
    expect(doubleTapDownDetails!.globalPosition, down2.position);
    expect(doubleTapDownDetails!.localPosition, down2.localPosition);
146
    tester.route(down2);
147 148
    expect(doubleTapRecognized, isFalse);

149
    tester.route(up2);
150
    expect(doubleTapRecognized, isTrue);
151
    GestureBinding.instance.gestureArena.sweep(2);
152
    expect(doubleTapCanceled, isFalse);
153 154
  });

155
  testGesture('Inter-tap distance cancels double tap', (GestureTester tester) {
156
    tap.addPointer(down1);
157 158 159
    tester.closeArena(1);
    tester.route(down1);
    tester.route(up1);
160
    GestureBinding.instance.gestureArena.sweep(1);
161 162

    tap.addPointer(down3);
163 164 165
    tester.closeArena(3);
    tester.route(down3);
    tester.route(up3);
166
    GestureBinding.instance.gestureArena.sweep(3);
167

168 169 170
    expect(doubleTapRecognized, isFalse);
    expect(doubleTapDownDetails, isNull);
    expect(doubleTapCanceled, isFalse);
171 172
  });

173
  testGesture('Intra-tap distance cancels double tap', (GestureTester tester) {
174
    tap.addPointer(down4);
175 176
    tester.closeArena(4);
    tester.route(down4);
177

178 179
    tester.route(move4);
    tester.route(up4);
180
    GestureBinding.instance.gestureArena.sweep(4);
181 182

    tap.addPointer(down1);
183 184 185
    tester.closeArena(1);
    tester.route(down2);
    tester.route(up1);
186
    GestureBinding.instance.gestureArena.sweep(1);
187

188 189 190
    expect(doubleTapRecognized, isFalse);
    expect(doubleTapDownDetails, isNull);
    expect(doubleTapCanceled, isFalse);
191 192
  });

193 194 195 196 197
  testGesture('Inter-tap delay cancels double tap', (GestureTester tester) {
    tap.addPointer(down1);
    tester.closeArena(1);
    tester.route(down1);
    tester.route(up1);
198
    GestureBinding.instance.gestureArena.sweep(1);
199

200
    tester.async.elapse(const Duration(milliseconds: 5000));
201 202 203 204
    tap.addPointer(down2);
    tester.closeArena(2);
    tester.route(down2);
    tester.route(up2);
205
    GestureBinding.instance.gestureArena.sweep(2);
206

207 208 209
    expect(doubleTapRecognized, isFalse);
    expect(doubleTapDownDetails, isNull);
    expect(doubleTapCanceled, isFalse);
210 211
  });

212 213 214 215 216
  testGesture('Inter-tap delay resets double tap, allowing third tap to be a double-tap', (GestureTester tester) {
    tap.addPointer(down1);
    tester.closeArena(1);
    tester.route(down1);
    tester.route(up1);
217
    GestureBinding.instance.gestureArena.sweep(1);
218

219
    tester.async.elapse(const Duration(milliseconds: 5000));
220 221 222 223
    tap.addPointer(down2);
    tester.closeArena(2);
    tester.route(down2);
    tester.route(up2);
224
    GestureBinding.instance.gestureArena.sweep(2);
225
    expect(doubleTapDownDetails, isNull);
226

227
    tester.async.elapse(const Duration(milliseconds: 100));
228 229 230 231
    tap.addPointer(down5);
    tester.closeArena(5);
    tester.route(down5);
    expect(doubleTapRecognized, isFalse);
232
    expect(doubleTapDownDetails, isNotNull);
233 234
    expect(doubleTapDownDetails!.globalPosition, down5.position);
    expect(doubleTapDownDetails!.localPosition, down5.localPosition);
235

236 237
    tester.route(up5);
    expect(doubleTapRecognized, isTrue);
238
    GestureBinding.instance.gestureArena.sweep(5);
239
    expect(doubleTapCanceled, isFalse);
240 241
  });

242 243 244 245
  testGesture('Intra-tap delay does not cancel double tap', (GestureTester tester) {
    tap.addPointer(down1);
    tester.closeArena(1);
    tester.route(down1);
246
    tester.async.elapse(const Duration(milliseconds: 1000));
247
    tester.route(up1);
248
    GestureBinding.instance.gestureArena.sweep(1);
249
    expect(doubleTapDownDetails, isNull);
250

251 252 253 254
    tap.addPointer(down2);
    tester.closeArena(2);
    tester.route(down2);
    expect(doubleTapRecognized, isFalse);
255
    expect(doubleTapDownDetails, isNotNull);
256 257
    expect(doubleTapDownDetails!.globalPosition, down2.position);
    expect(doubleTapDownDetails!.localPosition, down2.localPosition);
258

259 260
    tester.route(up2);
    expect(doubleTapRecognized, isTrue);
261
    GestureBinding.instance.gestureArena.sweep(2);
262
    expect(doubleTapCanceled, isFalse);
263 264
  });

265
  testGesture('Should not recognize two overlapping taps', (GestureTester tester) {
266
    tap.addPointer(down1);
267 268
    tester.closeArena(1);
    tester.route(down1);
269 270

    tap.addPointer(down2);
271 272
    tester.closeArena(2);
    tester.route(down1);
273

274
    tester.route(up1);
275
    GestureBinding.instance.gestureArena.sweep(1);
276

277
    tester.route(up2);
278
    GestureBinding.instance.gestureArena.sweep(2);
279

280 281 282
    expect(doubleTapRecognized, isFalse);
    expect(doubleTapDownDetails, isNull);
    expect(doubleTapCanceled, isFalse);
283 284
  });

285
  testGesture('Should recognize one tap of group followed by second tap', (GestureTester tester) {
286
    tap.addPointer(down1);
287 288
    tester.closeArena(1);
    tester.route(down1);
289 290

    tap.addPointer(down2);
291 292
    tester.closeArena(2);
    tester.route(down1);
293

294
    tester.route(up1);
295
    GestureBinding.instance.gestureArena.sweep(1);
296

297
    tester.route(up2);
298
    GestureBinding.instance.gestureArena.sweep(2);
299
    expect(doubleTapDownDetails, isNull);
300

301
    tester.async.elapse(const Duration(milliseconds: 100));
302
    tap.addPointer(down1);
303 304
    tester.closeArena(1);
    tester.route(down1);
305
    expect(doubleTapRecognized, isFalse);
306
    expect(doubleTapDownDetails, isNotNull);
307 308
    expect(doubleTapDownDetails!.globalPosition, down1.position);
    expect(doubleTapDownDetails!.localPosition, down1.localPosition);
309

310
    tester.route(up1);
311
    expect(doubleTapRecognized, isTrue);
312
    GestureBinding.instance.gestureArena.sweep(1);
313
    expect(doubleTapCanceled, isFalse);
314 315
  });

316
  testGesture('Should cancel on arena reject during first tap', (GestureTester tester) {
317
    tap.addPointer(down1);
318
    final TestGestureArenaMember member = TestGestureArenaMember();
319
    final GestureArenaEntry entry = GestureBinding.instance.gestureArena.add(1, member);
320 321
    tester.closeArena(1);
    tester.route(down1);
322

323
    tester.route(up1);
324
    entry.resolve(GestureDisposition.accepted);
325
    expect(member.accepted, isTrue);
326
    GestureBinding.instance.gestureArena.sweep(1);
327 328

    tap.addPointer(down2);
329 330 331
    tester.closeArena(2);
    tester.route(down2);
    tester.route(up2);
332
    GestureBinding.instance.gestureArena.sweep(2);
333

334 335 336
    expect(doubleTapRecognized, isFalse);
    expect(doubleTapDownDetails, isNull);
    expect(doubleTapCanceled, isFalse);
337 338
  });

339
  testGesture('Should cancel on arena reject between taps', (GestureTester tester) {
340
    tap.addPointer(down1);
341
    final TestGestureArenaMember member = TestGestureArenaMember();
342
    final GestureArenaEntry entry = GestureBinding.instance.gestureArena.add(1, member);
343 344 345
    tester.closeArena(1);
    tester.route(down1);
    tester.route(up1);
346
    GestureBinding.instance.gestureArena.sweep(1);
347 348

    entry.resolve(GestureDisposition.accepted);
349
    expect(member.accepted, isTrue);
350 351

    tap.addPointer(down2);
352 353 354
    tester.closeArena(2);
    tester.route(down2);
    tester.route(up2);
355
    GestureBinding.instance.gestureArena.sweep(2);
356

357 358 359
    expect(doubleTapRecognized, isFalse);
    expect(doubleTapDownDetails, isNull);
    expect(doubleTapCanceled, isFalse);
360 361
  });

362
  testGesture('Should cancel on arena reject during last tap', (GestureTester tester) {
363
    tap.addPointer(down1);
364
    final TestGestureArenaMember member = TestGestureArenaMember();
365
    final GestureArenaEntry entry = GestureBinding.instance.gestureArena.add(1, member);
366 367 368
    tester.closeArena(1);
    tester.route(down1);
    tester.route(up1);
369
    GestureBinding.instance.gestureArena.sweep(1);
370
    expect(doubleTapDownDetails, isNull);
371

372
    tester.async.elapse(const Duration(milliseconds: 100));
373
    tap.addPointer(down2);
374 375
    tester.closeArena(2);
    tester.route(down2);
376
    expect(doubleTapDownDetails, isNotNull);
377 378
    expect(doubleTapDownDetails!.globalPosition, down2.position);
    expect(doubleTapDownDetails!.localPosition, down2.localPosition);
379
    expect(doubleTapCanceled, isFalse);
380 381

    entry.resolve(GestureDisposition.accepted);
382
    expect(member.accepted, isTrue);
383
    expect(doubleTapCanceled, isTrue);
384

385
    tester.route(up2);
386
    GestureBinding.instance.gestureArena.sweep(2);
387 388 389
    expect(doubleTapRecognized, isFalse);
  });

390
  testGesture('Passive gesture should trigger on double tap cancel', (GestureTester tester) {
391
    FakeAsync().run((FakeAsync async) {
392
      tap.addPointer(down1);
393
      final TestGestureArenaMember member = TestGestureArenaMember();
394
      GestureBinding.instance.gestureArena.add(1, member);
395 396 397
      tester.closeArena(1);
      tester.route(down1);
      tester.route(up1);
398
      GestureBinding.instance.gestureArena.sweep(1);
399 400

      expect(member.accepted, isFalse);
Ian Hickson's avatar
Ian Hickson committed
401
      async.elapse(const Duration(milliseconds: 5000));
402 403
      expect(member.accepted, isTrue);

404 405 406 407
      expect(doubleTapRecognized, isFalse);
      expect(doubleTapDownDetails, isNull);
      expect(doubleTapCanceled, isFalse);
    });
408 409
  });

410 411 412 413 414
  testGesture('Should not recognize two over-rapid taps', (GestureTester tester) {
    tap.addPointer(down1);
    tester.closeArena(1);
    tester.route(down1);
    tester.route(up1);
415
    GestureBinding.instance.gestureArena.sweep(1);
416 417 418 419 420 421

    tester.async.elapse(const Duration(milliseconds: 10));
    tap.addPointer(down2);
    tester.closeArena(2);
    tester.route(down2);
    tester.route(up2);
422
    GestureBinding.instance.gestureArena.sweep(2);
423

424 425 426
    expect(doubleTapRecognized, isFalse);
    expect(doubleTapDownDetails, isNull);
    expect(doubleTapCanceled, isFalse);
427 428 429 430 431 432 433
  });

  testGesture('Over-rapid taps resets double tap, allowing third tap to be a double-tap', (GestureTester tester) {
    tap.addPointer(down1);
    tester.closeArena(1);
    tester.route(down1);
    tester.route(up1);
434
    GestureBinding.instance.gestureArena.sweep(1);
435 436 437 438 439 440

    tester.async.elapse(const Duration(milliseconds: 10));
    tap.addPointer(down2);
    tester.closeArena(2);
    tester.route(down2);
    tester.route(up2);
441
    GestureBinding.instance.gestureArena.sweep(2);
442
    expect(doubleTapDownDetails, isNull);
443 444 445 446 447 448

    tester.async.elapse(const Duration(milliseconds: 100));
    tap.addPointer(down5);
    tester.closeArena(5);
    tester.route(down5);
    expect(doubleTapRecognized, isFalse);
449
    expect(doubleTapDownDetails, isNotNull);
450 451
    expect(doubleTapDownDetails!.globalPosition, down5.position);
    expect(doubleTapDownDetails!.localPosition, down5.localPosition);
452 453 454

    tester.route(up5);
    expect(doubleTapRecognized, isTrue);
455
    GestureBinding.instance.gestureArena.sweep(5);
456
    expect(doubleTapCanceled, isFalse);
457
  });
458 459 460 461 462 463 464 465 466 467 468 469 470 471

  group('Enforce consistent-button restriction:', () {
    testGesture('Button change should interrupt existing sequence', (GestureTester tester) {
      // Down1 -> down6 (different button from 1) -> down2 (same button as 1)
      // Down1 and down2 could've been a double tap, but is interrupted by down 6.

      const Duration interval = Duration(milliseconds: 100);
      assert(interval * 2 < kDoubleTapTimeout);
      assert(interval > kDoubleTapMinTime);

      tap.addPointer(down1);
      tester.closeArena(1);
      tester.route(down1);
      tester.route(up1);
472
      GestureBinding.instance.gestureArena.sweep(1);
473 474 475 476 477 478 479

      tester.async.elapse(interval);

      tap.addPointer(down6);
      tester.closeArena(6);
      tester.route(down6);
      tester.route(up6);
480
      GestureBinding.instance.gestureArena.sweep(6);
481 482 483 484 485 486 487 488

      tester.async.elapse(interval);
      expect(doubleTapRecognized, isFalse);

      tap.addPointer(down2);
      tester.closeArena(2);
      tester.route(down2);
      tester.route(up2);
489
      GestureBinding.instance.gestureArena.sweep(2);
490 491

      expect(doubleTapRecognized, isFalse);
492 493
      expect(doubleTapDownDetails, isNull);
      expect(doubleTapCanceled, isFalse);
494 495 496 497 498 499 500 501 502 503 504 505 506
    });

    testGesture('Button change should start a valid sequence', (GestureTester tester) {
      // Down6 -> down1 (different button from 6) -> down2 (same button as 1)

      const Duration interval = Duration(milliseconds: 100);
      assert(interval * 2 < kDoubleTapTimeout);
      assert(interval > kDoubleTapMinTime);

      tap.addPointer(down6);
      tester.closeArena(6);
      tester.route(down6);
      tester.route(up6);
507
      GestureBinding.instance.gestureArena.sweep(6);
508 509 510 511 512 513 514

      tester.async.elapse(interval);

      tap.addPointer(down1);
      tester.closeArena(1);
      tester.route(down1);
      tester.route(up1);
515
      GestureBinding.instance.gestureArena.sweep(1);
516 517

      expect(doubleTapRecognized, isFalse);
518
      expect(doubleTapDownDetails, isNull);
519 520 521 522 523
      tester.async.elapse(interval);

      tap.addPointer(down2);
      tester.closeArena(2);
      tester.route(down2);
524
      expect(doubleTapDownDetails, isNotNull);
525 526
      expect(doubleTapDownDetails!.globalPosition, down2.position);
      expect(doubleTapDownDetails!.localPosition, down2.localPosition);
527
      tester.route(up2);
528
      GestureBinding.instance.gestureArena.sweep(2);
529 530

      expect(doubleTapRecognized, isTrue);
531
      expect(doubleTapCanceled, isFalse);
532 533 534 535 536 537 538 539 540 541 542 543
    });
  });

  group('Recognizers listening on different buttons do not form competition:', () {
    // This test is assisted by tap recognizers. If a tap gesture has
    // no competing recognizers, a pointer down event triggers its onTapDown
    // immediately; if there are competitors, onTapDown is triggered after a
    // timeout.
    // The following tests make sure that double tap recognizers do not form
    // competition with a tap gesture recognizer listening on a different button.

    final List<String> recognized = <String>[];
544 545 546
    late TapGestureRecognizer tapPrimary;
    late TapGestureRecognizer tapSecondary;
    late DoubleTapGestureRecognizer doubleTap;
547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568
    setUp(() {
      tapPrimary = TapGestureRecognizer()
        ..onTapDown = (TapDownDetails details) {
          recognized.add('tapPrimary');
        };
      tapSecondary = TapGestureRecognizer()
        ..onSecondaryTapDown = (TapDownDetails details) {
          recognized.add('tapSecondary');
        };
      doubleTap = DoubleTapGestureRecognizer()
        ..onDoubleTap = () {
          recognized.add('doubleTap');
        };
    });

    tearDown(() {
      recognized.clear();
      tapPrimary.dispose();
      tapSecondary.dispose();
      doubleTap.dispose();
    });

Chris Bracken's avatar
Chris Bracken committed
569
    testGesture('A primary double tap recognizer does not form competition with a secondary tap recognizer', (GestureTester tester) {
570 571 572 573 574 575 576 577
      doubleTap.addPointer(down6);
      tapSecondary.addPointer(down6);
      tester.closeArena(down6.pointer);

      tester.route(down6);
      expect(recognized, <String>['tapSecondary']);
    });

Chris Bracken's avatar
Chris Bracken committed
578
    testGesture('A primary double tap recognizer forms competition with a primary tap recognizer', (GestureTester tester) {
579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613
      doubleTap.addPointer(down1);
      tapPrimary.addPointer(down1);
      tester.closeArena(down1.pointer);

      tester.route(down1);
      expect(recognized, <String>[]);

      tester.async.elapse(const Duration(milliseconds: 300));
      expect(recognized, <String>['tapPrimary']);
    });
  });

  testGesture('A secondary double tap should not trigger primary', (GestureTester tester) {
    final List<String> recognized = <String>[];
    final DoubleTapGestureRecognizer doubleTap = DoubleTapGestureRecognizer()
      ..onDoubleTap = () {
        recognized.add('primary');
      };

    // Down/up pair 7: normal tap sequence close to pair 6
    const PointerDownEvent down7 = PointerDownEvent(
      pointer: 7,
      position: Offset(10.0, 10.0),
      buttons: kSecondaryMouseButton,
    );

    const PointerUpEvent up7 = PointerUpEvent(
      pointer: 7,
      position: Offset(11.0, 9.0),
    );

    doubleTap.addPointer(down6);
    tester.closeArena(6);
    tester.route(down6);
    tester.route(up6);
614
    GestureBinding.instance.gestureArena.sweep(6);
615 616 617 618 619 620 621 622 623 624 625

    tester.async.elapse(const Duration(milliseconds: 100));
    doubleTap.addPointer(down7);
    tester.closeArena(7);
    tester.route(down7);
    tester.route(up7);
    expect(recognized, <String>[]);

    recognized.clear();
    doubleTap.dispose();
  });
626 627 628 629 630 631 632 633 634 635 636 637 638 639 640

  // Regression test for https://github.com/flutter/flutter/issues/73667
  testGesture('Unfinished DoubleTap does not prevent competing Tap', (GestureTester tester) {
    int tapCount = 0;
    final DoubleTapGestureRecognizer doubleTap = DoubleTapGestureRecognizer()
      ..onDoubleTap = () {};
    final TapGestureRecognizer tap = TapGestureRecognizer()
      ..onTap = () => tapCount++;

    // Open a arena with 2 members and holding.
    doubleTap.addPointer(down1);
    tap.addPointer(down1);
    tester.closeArena(1);
    tester.route(down1);
    tester.route(up1);
641
    GestureBinding.instance.gestureArena.sweep(1);
642 643 644 645 646 647 648 649 650 651

    // Open a new arena with only one TapGestureRecognizer.
    tester.async.elapse(const Duration(milliseconds: 100));
    tap.addPointer(down2);
    tester.closeArena(2);
    tester.route(down2);
    final PointerMoveEvent move2 = PointerMoveEvent(pointer: 2, position: down2.position);
    tester.route(move2);
    tester.route(up2);
    expect(tapCount, 1); // The second tap will win immediately.
652
    GestureBinding.instance.gestureArena.sweep(2);
653 654 655 656 657 658 659 660

    // Finish the previous gesture arena.
    tester.async.elapse(const Duration(milliseconds: 300));
    expect(tapCount, 1); // The first tap should not trigger onTap callback though it wins the arena.

    tap.dispose();
    doubleTap.dispose();
  });
661
}