Kate Lovett 9d96df2364
Modernize framework lints (#179089)
WIP

Commits separated as follows:
- Update lints in analysis_options files
- Run `dart fix --apply`
- Clean up leftover analysis issues 
- Run `dart format .` in the right places.

Local analysis and testing passes. Checking CI now.

Part of https://github.com/flutter/flutter/issues/178827
- Adoption of flutter_lints in examples/api coming in a separate change
(cc @loic-sharma)

## Pre-launch Checklist

- [ ] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [ ] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [ ] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [ ] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
2025-11-26 01:10:39 +00:00

434 lines
13 KiB
Dart

// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter_test/flutter_test.dart';
import 'gesture_tester.dart';
// Anything longer than [kDoubleTapTimeout] will reset the serial tap count.
final Duration kSerialTapDelay = kDoubleTapTimeout ~/ 2;
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
late List<String> events;
late SerialTapGestureRecognizer serial;
setUp(() {
events = <String>[];
serial = SerialTapGestureRecognizer()
..onSerialTapDown = (SerialTapDownDetails details) {
events.add('down#${details.count}');
}
..onSerialTapCancel = (SerialTapCancelDetails details) {
events.add('cancel#${details.count}');
}
..onSerialTapUp = (SerialTapUpDetails details) {
events.add('up#${details.count}');
};
addTearDown(serial.dispose);
});
// Down/up pair 1: normal tap sequence
const down1 = PointerDownEvent(pointer: 1, position: Offset(10.0, 10.0));
const cancel1 = PointerCancelEvent(pointer: 1);
const up1 = PointerUpEvent(pointer: 1, position: Offset(11.0, 9.0));
// Down/up pair 2: normal tap sequence close to pair 1
const down2 = PointerDownEvent(pointer: 2, position: Offset(12.0, 12.0));
const up2 = PointerUpEvent(pointer: 2, position: Offset(13.0, 11.0));
// Down/up pair 3: normal tap sequence close to pair 1
const down3 = PointerDownEvent(pointer: 3, position: Offset(12.0, 12.0));
const up3 = PointerUpEvent(pointer: 3, position: Offset(13.0, 11.0));
// Down/up pair 4: normal tap sequence far away from pair 1
const down4 = PointerDownEvent(pointer: 4, position: Offset(130.0, 130.0));
const up4 = PointerUpEvent(pointer: 4, position: Offset(131.0, 129.0));
// Down/move/up sequence 5: intervening motion
const down5 = PointerDownEvent(pointer: 5, position: Offset(10.0, 10.0));
const move5 = PointerMoveEvent(pointer: 5, position: Offset(25.0, 25.0));
const up5 = PointerUpEvent(pointer: 5, position: Offset(25.0, 25.0));
// Down/up pair 7: normal tap sequence close to pair 1 but on secondary button
const down6 = PointerDownEvent(
pointer: 6,
position: Offset(10.0, 10.0),
buttons: kSecondaryMouseButton,
);
const up6 = PointerUpEvent(pointer: 6, position: Offset(11.0, 9.0));
testGesture('Recognizes serial taps', (GestureTester tester) {
serial.addPointer(down1);
tester.closeArena(1);
tester.route(down1);
tester.route(up1);
GestureBinding.instance.gestureArena.sweep(1);
expect(events, <String>['down#1', 'up#1']);
events.clear();
tester.async.elapse(kSerialTapDelay);
serial.addPointer(down2);
tester.closeArena(2);
tester.route(down2);
tester.route(up2);
GestureBinding.instance.gestureArena.sweep(2);
expect(events, <String>['down#2', 'up#2']);
events.clear();
tester.async.elapse(kSerialTapDelay);
serial.addPointer(down3);
tester.closeArena(3);
tester.route(down3);
tester.route(up3);
GestureBinding.instance.gestureArena.sweep(3);
expect(events, <String>['down#3', 'up#3']);
});
// Because tap gesture will hold off on declaring victory.
testGesture('Wins over tap gesture below it in the tree', (GestureTester tester) {
var recognizedSingleTap = false;
var canceledSingleTap = false;
final singleTap = TapGestureRecognizer()
..onTap = () {
recognizedSingleTap = true;
}
..onTapCancel = () {
canceledSingleTap = true;
};
addTearDown(singleTap.dispose);
singleTap.addPointer(down1);
serial.addPointer(down1);
tester.closeArena(1);
tester.route(down1);
tester.async.elapse(kPressTimeout); // To register the possible single tap.
tester.route(up1);
GestureBinding.instance.gestureArena.sweep(1);
expect(events, <String>['down#1', 'up#1']);
expect(recognizedSingleTap, isFalse);
expect(canceledSingleTap, isTrue);
});
testGesture('Wins over tap gesture above it in the tree', (GestureTester tester) {
var recognizedSingleTap = false;
var canceledSingleTap = false;
final singleTap = TapGestureRecognizer()
..onTap = () {
recognizedSingleTap = true;
}
..onTapCancel = () {
canceledSingleTap = true;
};
addTearDown(singleTap.dispose);
serial.addPointer(down1);
singleTap.addPointer(down1);
tester.closeArena(1);
tester.route(down1);
tester.async.elapse(kPressTimeout); // To register the possible single tap.
tester.route(up1);
GestureBinding.instance.gestureArena.sweep(1);
expect(events, <String>['down#1', 'up#1']);
expect(recognizedSingleTap, isFalse);
expect(canceledSingleTap, isTrue);
});
testGesture('Loses to release gesture below it in the tree', (GestureTester tester) {
var recognizedRelease = false;
final release = ReleaseGestureRecognizer()
..onRelease = () {
recognizedRelease = true;
};
addTearDown(release.dispose);
release.addPointer(down1);
serial.addPointer(down1);
tester.closeArena(1);
tester.route(down1);
tester.route(up1);
GestureBinding.instance.gestureArena.sweep(1);
expect(events, <String>['down#1', 'cancel#1']);
expect(recognizedRelease, isTrue);
});
testGesture('Wins over release gesture above it in the tree', (GestureTester tester) {
var recognizedRelease = false;
final release = ReleaseGestureRecognizer()
..onRelease = () {
recognizedRelease = true;
};
addTearDown(release.dispose);
serial.addPointer(down1);
release.addPointer(down1);
tester.closeArena(1);
tester.route(down1);
tester.route(up1);
GestureBinding.instance.gestureArena.sweep(1);
expect(events, <String>['down#1', 'up#1']);
expect(recognizedRelease, isFalse);
});
testGesture('Fires cancel if competing recognizer declares victory', (GestureTester tester) {
final winner = WinningGestureRecognizer();
addTearDown(winner.dispose);
winner.addPointer(down1);
serial.addPointer(down1);
tester.closeArena(1);
tester.route(down1);
tester.route(up1);
GestureBinding.instance.gestureArena.sweep(1);
expect(events, <String>['down#1', 'cancel#1']);
});
testGesture('Wins over double-tap recognizer below it in the tree', (GestureTester tester) {
var recognizedDoubleTap = false;
final doubleTap = DoubleTapGestureRecognizer()
..onDoubleTap = () {
recognizedDoubleTap = true;
};
addTearDown(doubleTap.dispose);
doubleTap.addPointer(down1);
serial.addPointer(down1);
tester.closeArena(1);
tester.route(down1);
tester.route(up1);
GestureBinding.instance.gestureArena.sweep(1);
expect(events, <String>['down#1', 'up#1']);
expect(recognizedDoubleTap, isFalse);
events.clear();
tester.async.elapse(kSerialTapDelay);
doubleTap.addPointer(down2);
serial.addPointer(down2);
tester.closeArena(2);
tester.route(down2);
tester.route(up2);
GestureBinding.instance.gestureArena.sweep(2);
expect(events, <String>['down#2', 'up#2']);
expect(recognizedDoubleTap, isFalse);
events.clear();
tester.async.elapse(kSerialTapDelay);
serial.addPointer(down3);
tester.closeArena(3);
tester.route(down3);
tester.route(up3);
GestureBinding.instance.gestureArena.sweep(3);
expect(events, <String>['down#3', 'up#3']);
});
testGesture('Wins over double-tap recognizer above it in the tree', (GestureTester tester) {
var recognizedDoubleTap = false;
final doubleTap = DoubleTapGestureRecognizer()
..onDoubleTap = () {
recognizedDoubleTap = true;
};
addTearDown(doubleTap.dispose);
serial.addPointer(down1);
doubleTap.addPointer(down1);
tester.closeArena(1);
tester.route(down1);
tester.route(up1);
GestureBinding.instance.gestureArena.sweep(1);
expect(events, <String>['down#1', 'up#1']);
expect(recognizedDoubleTap, isFalse);
events.clear();
tester.async.elapse(kSerialTapDelay);
serial.addPointer(down2);
doubleTap.addPointer(down2);
tester.closeArena(2);
tester.route(down2);
tester.route(up2);
GestureBinding.instance.gestureArena.sweep(2);
expect(events, <String>['down#2', 'up#2']);
expect(recognizedDoubleTap, isFalse);
events.clear();
tester.async.elapse(kSerialTapDelay);
serial.addPointer(down3);
doubleTap.addPointer(down3);
tester.closeArena(3);
tester.route(down3);
tester.route(up3);
GestureBinding.instance.gestureArena.sweep(3);
expect(events, <String>['down#3', 'up#3']);
});
testGesture('Fires cancel and resets for PointerCancelEvent', (GestureTester tester) {
serial.addPointer(down1);
tester.closeArena(1);
tester.route(down1);
tester.route(cancel1);
GestureBinding.instance.gestureArena.sweep(1);
expect(events, <String>['down#1', 'cancel#1']);
events.clear();
tester.async.elapse(const Duration(milliseconds: 100));
serial.addPointer(down2);
tester.closeArena(2);
tester.route(down2);
tester.route(up2);
GestureBinding.instance.gestureArena.sweep(2);
expect(events, <String>['down#1', 'up#1']);
});
testGesture('Fires cancel and resets when pointer dragged past slop tolerance', (
GestureTester tester,
) {
serial.addPointer(down5);
tester.closeArena(5);
tester.route(down5);
tester.route(move5);
tester.route(up5);
GestureBinding.instance.gestureArena.sweep(5);
expect(events, <String>['down#1', 'cancel#1']);
events.clear();
tester.async.elapse(const Duration(milliseconds: 1000));
serial.addPointer(down1);
tester.closeArena(1);
tester.route(down1);
tester.route(up1);
GestureBinding.instance.gestureArena.sweep(1);
expect(events, <String>['down#1', 'up#1']);
});
testGesture('Resets if times out in between taps', (GestureTester tester) {
serial.addPointer(down1);
tester.closeArena(1);
tester.route(down1);
tester.route(up1);
GestureBinding.instance.gestureArena.sweep(1);
expect(events, <String>['down#1', 'up#1']);
events.clear();
tester.async.elapse(const Duration(milliseconds: 1000));
serial.addPointer(down2);
tester.closeArena(2);
tester.route(down2);
tester.route(up2);
GestureBinding.instance.gestureArena.sweep(2);
expect(events, <String>['down#1', 'up#1']);
});
testGesture('Resets if taps are far apart', (GestureTester tester) {
serial.addPointer(down1);
tester.closeArena(1);
tester.route(down1);
tester.route(up1);
GestureBinding.instance.gestureArena.sweep(1);
expect(events, <String>['down#1', 'up#1']);
events.clear();
tester.async.elapse(const Duration(milliseconds: 100));
serial.addPointer(down4);
tester.closeArena(4);
tester.route(down4);
tester.route(up4);
GestureBinding.instance.gestureArena.sweep(4);
expect(events, <String>['down#1', 'up#1']);
});
testGesture('Serial taps with different buttons will start a new tap sequence', (
GestureTester tester,
) {
serial.addPointer(down1);
tester.closeArena(1);
tester.route(down1);
tester.route(up1);
GestureBinding.instance.gestureArena.sweep(1);
expect(events, <String>['down#1', 'up#1']);
events.clear();
tester.async.elapse(const Duration(milliseconds: 1000));
serial.addPointer(down6);
tester.closeArena(6);
tester.route(down6);
tester.route(up6);
GestureBinding.instance.gestureArena.sweep(6);
expect(events, <String>['down#1', 'up#1']);
});
testGesture('Interleaving taps cancel first sequence and start second sequence', (
GestureTester tester,
) {
serial.addPointer(down1);
tester.closeArena(1);
tester.route(down1);
serial.addPointer(down2);
tester.closeArena(2);
tester.route(down2);
tester.route(up1);
GestureBinding.instance.gestureArena.sweep(1);
tester.route(up2);
GestureBinding.instance.gestureArena.sweep(2);
expect(events, <String>['down#1', 'cancel#1', 'down#1', 'up#1']);
});
testGesture('Is no-op if no callbacks are specified', (GestureTester tester) {
serial = SerialTapGestureRecognizer();
addTearDown(serial.dispose);
serial.addPointer(down1);
tester.closeArena(1);
tester.route(down1);
expect(serial.isTrackingPointer, isFalse);
tester.route(up1);
GestureBinding.instance.gestureArena.sweep(1);
expect(events, <String>[]);
});
testGesture('Works for non-primary button', (GestureTester tester) {
serial.addPointer(down6);
tester.closeArena(6);
tester.route(down6);
tester.route(up6);
GestureBinding.instance.gestureArena.sweep(6);
expect(events, <String>['down#1', 'up#1']);
});
}
class WinningGestureRecognizer extends PrimaryPointerGestureRecognizer {
@override
String get debugDescription => 'winner';
@override
void handlePrimaryPointer(PointerEvent event) {
resolve(GestureDisposition.accepted);
}
}
class ReleaseGestureRecognizer extends PrimaryPointerGestureRecognizer {
VoidCallback? onRelease;
@override
String get debugDescription => 'release';
@override
void handlePrimaryPointer(PointerEvent event) {
if (event is PointerUpEvent) {
resolve(GestureDisposition.accepted);
onRelease?.call();
}
}
}