diff --git a/packages/flutter_test/lib/src/binding.dart b/packages/flutter_test/lib/src/binding.dart index 3c74950cfa95a..c1ab7ce180ed0 100644 --- a/packages/flutter_test/lib/src/binding.dart +++ b/packages/flutter_test/lib/src/binding.dart @@ -494,9 +494,25 @@ abstract class TestWidgetsFlutterBinding extends BindingBase /// /// When [handlePointerEvent] is called directly, [pointerEventSource] /// is [TestBindingEventSource.device]. + /// + /// This means that pointer events triggered by the [WidgetController] (e.g. + /// via [WidgetController.tap]) will result in actual interactions with the + /// UI, but other pointer events such as those from physical taps will be + /// dropped. See also [shouldPropagateDevicePointerEvents] if this is + /// undesired. TestBindingEventSource get pointerEventSource => _pointerEventSource; TestBindingEventSource _pointerEventSource = TestBindingEventSource.device; + /// Whether pointer events from [TestBindingEventSource.device] will be + /// propagated to the framework, or dropped. + /// + /// Setting this can be useful to interact with the app in some other way + /// besides through the [WidgetController], such as with `adb shell input tap` + /// on Android. + /// + /// See also [pointerEventSource]. + bool shouldPropagateDevicePointerEvents = false; + /// Dispatch an event to the targets found by a hit test on its position, /// and remember its source as [pointerEventSource]. /// @@ -836,6 +852,7 @@ abstract class TestWidgetsFlutterBinding extends BindingBase final bool autoUpdateGoldensBeforeTest = autoUpdateGoldenFiles && !isBrowser; final TestExceptionReporter reportTestExceptionBeforeTest = reportTestException; final ErrorWidgetBuilder errorWidgetBuilderBeforeTest = ErrorWidget.builder; + final bool shouldPropagateDevicePointerEventsBeforeTest = shouldPropagateDevicePointerEvents; // run the test await testBody(); @@ -854,6 +871,7 @@ abstract class TestWidgetsFlutterBinding extends BindingBase _verifyAutoUpdateGoldensUnset(autoUpdateGoldensBeforeTest && !isBrowser); _verifyReportTestExceptionUnset(reportTestExceptionBeforeTest); _verifyErrorWidgetBuilderUnset(errorWidgetBuilderBeforeTest); + _verifyShouldPropagateDevicePointerEventsUnset(shouldPropagateDevicePointerEventsBeforeTest); _verifyInvariants(); } @@ -943,6 +961,21 @@ abstract class TestWidgetsFlutterBinding extends BindingBase }()); } + void _verifyShouldPropagateDevicePointerEventsUnset(bool valueBeforeTest) { + assert(() { + if (shouldPropagateDevicePointerEvents != valueBeforeTest) { + FlutterError.reportError(FlutterErrorDetails( + exception: FlutterError( + 'The value of shouldPropagateDevicePointerEvents was changed by the test.', + ), + stack: StackTrace.current, + library: 'Flutter test framework', + )); + } + return true; + }()); + } + /// Called by the [testWidgets] function after a test is executed. void postTest() { assert(inTest); @@ -1595,7 +1628,8 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { /// /// Normally, device events are silently dropped. However, if this property is /// set to a non-null value, then the events will be routed to its - /// [HitTestDispatcher.dispatchEvent] method instead. + /// [HitTestDispatcher.dispatchEvent] method instead, unless + /// [shouldPropagateDevicePointerEvents] is true. /// /// Events dispatched by [TestGesture] are not affected by this. HitTestDispatcher? deviceEventDispatcher; @@ -1630,6 +1664,10 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { super.handlePointerEvent(event); break; case TestBindingEventSource.device: + if (shouldPropagateDevicePointerEvents) { + super.handlePointerEvent(event); + break; + } if (deviceEventDispatcher != null) { // The pointer events received with this source has a global position // (see [handlePointerEventForSource]). Transform it to the local @@ -1651,6 +1689,10 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { break; case TestBindingEventSource.device: assert(hitTestResult != null || event is PointerAddedEvent || event is PointerRemovedEvent); + if (shouldPropagateDevicePointerEvents) { + super.dispatchEvent(event, hitTestResult); + break; + } assert(deviceEventDispatcher != null); if (hitTestResult != null) { deviceEventDispatcher!.dispatchEvent(event, hitTestResult); diff --git a/packages/flutter_test/lib/src/widget_tester.dart b/packages/flutter_test/lib/src/widget_tester.dart index 0b2e93f877e89..08e8ef91f4ba2 100644 --- a/packages/flutter_test/lib/src/widget_tester.dart +++ b/packages/flutter_test/lib/src/widget_tester.dart @@ -502,6 +502,10 @@ Future expectLater( /// /// For convenience, instances of this class (such as the one provided by /// `testWidgets`) can be used as the `vsync` for `AnimationController` objects. +/// +/// When the binding is [LiveTestWidgetsFlutterBinding], events from +/// [LiveTestWidgetsFlutterBinding.deviceEventDispatcher] will be handled in +/// [dispatchEvent]. class WidgetTester extends WidgetController implements HitTestDispatcher, TickerProvider { WidgetTester._(super.binding) { if (binding is LiveTestWidgetsFlutterBinding) { @@ -817,6 +821,10 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker } /// Handler for device events caught by the binding in live test mode. + /// + /// [PointerDownEvent]s received here will only print a diagnostic message + /// showing possible [Finder]s that can be used to interact with the widget at + /// the location of [result]. @override void dispatchEvent(PointerEvent event, HitTestResult result) { if (event is PointerDownEvent) { diff --git a/packages/flutter_test/test/live_binding_test.dart b/packages/flutter_test/test/live_binding_test.dart index ee656e5f5309e..c38bb0cce497d 100644 --- a/packages/flutter_test/test/live_binding_test.dart +++ b/packages/flutter_test/test/live_binding_test.dart @@ -8,7 +8,7 @@ import 'package:flutter_test/flutter_test.dart'; // This file is for testings that require a `LiveTestWidgetsFlutterBinding` void main() { - LiveTestWidgetsFlutterBinding(); + final LiveTestWidgetsFlutterBinding binding = LiveTestWidgetsFlutterBinding(); testWidgets('Input PointerAddedEvent', (WidgetTester tester) async { await tester.pumpWidget(const MaterialApp(home: Text('Test'))); await tester.pump(); @@ -99,4 +99,57 @@ void main() { await expectLater(tester.binding.reassembleApplication(), completes); }, timeout: const Timeout(Duration(seconds: 30))); + + testWidgets('shouldPropagateDevicePointerEvents can override events from ${TestBindingEventSource.device}', (WidgetTester tester) async { + binding.shouldPropagateDevicePointerEvents = true; + + await tester.pumpWidget(_ShowNumTaps()); + + final Offset position = tester.getCenter(find.text('0')); + + // Simulates a real device tap. + // + // `handlePointerEventForSource defaults to sending events using + // TestBindingEventSource.device. This will not be forwarded to the actual + // gesture handlers, unless `shouldPropagateDevicePointerEvents` is true. + binding.handlePointerEventForSource( + PointerDownEvent(position: position), + ); + binding.handlePointerEventForSource( + PointerUpEvent(position: position), + ); + + await tester.pump(); + + expect(find.text('1'), findsOneWidget); + + // Reset the value, otherwise the test will fail when it checks that this + // has not been changed as an invariant. + binding.shouldPropagateDevicePointerEvents = false; + }); +} + +/// A widget that shows the number of times it has been tapped. +class _ShowNumTaps extends StatefulWidget { + @override + _ShowNumTapsState createState() => _ShowNumTapsState(); +} + +class _ShowNumTapsState extends State<_ShowNumTaps> { + int _counter = 0; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + setState(() { + _counter++; + }); + }, + child: Directionality( + textDirection: TextDirection.ltr, + child: Text(_counter.toString()), + ), + ); + } }