From a76b5eb79fae960254498eb89d9fd41b4e89ac00 Mon Sep 17 00:00:00 2001 From: "Ming Lyu (CareF)" Date: Fri, 10 Jul 2020 17:03:12 -0400 Subject: [PATCH] Add support in WidgetTester for an array of inputs (#60796) * Add input event array support * Add a tap test * remove unused import * remove extra assert --- packages/flutter_test/lib/src/binding.dart | 25 +++++- packages/flutter_test/lib/src/controller.dart | 21 +++++ .../flutter_test/lib/src/test_pointer.dart | 27 ++++++ .../flutter_test/lib/src/widget_tester.dart | 84 +++++++++++++++++++ .../flutter_test/test/widget_tester_test.dart | 68 +++++++++++++++ 5 files changed, 224 insertions(+), 1 deletion(-) diff --git a/packages/flutter_test/lib/src/binding.dart b/packages/flutter_test/lib/src/binding.dart index 2372683eb8..a47fdf1728 100644 --- a/packages/flutter_test/lib/src/binding.dart +++ b/packages/flutter_test/lib/src/binding.dart @@ -256,6 +256,17 @@ abstract class TestWidgetsFlutterBinding extends BindingBase // See AutomatedTestWidgetsFlutterBinding.addTime for an actual implementation. void addTime(Duration duration); + /// Delay for `duration` of time. + /// + /// In the automated test environment ([AutomatedTestWidgetsFlutterBinding], + /// typically used in `flutter test`), this advances the fake [clock] for the + /// period and also increases timeout (see [addTime]). + /// + /// In the live test environemnt ([LiveTestWidgetsFlutterBinding], typically + /// used for `flutter run` and for [e2e](https://pub.dev/packages/e2e)), it is + /// equivalent as [Future.delayed]. + Future delayed(Duration duration); + /// The value to set [debugCheckIntrinsicSizes] to while tests are running. /// /// This can be used to enable additional checks. For example, @@ -1109,6 +1120,14 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { _timeout += duration; } + @override + Future delayed(Duration duration) { + assert(_currentFakeAsync != null); + addTime(duration); + _currentFakeAsync.elapse(duration); + return Future.value(); + } + @override Future runTest( Future testBody(), @@ -1201,7 +1220,6 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { _timeoutStopwatch = null; _timeout = null; } - } /// Available policies for how a [LiveTestWidgetsFlutterBinding] should paint @@ -1354,6 +1372,11 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { // See runTest(). } + @override + Future delayed(Duration duration) { + return Future.delayed(duration); + } + @override void scheduleFrame() { if (framePolicy == LiveTestWidgetsFlutterBindingFramePolicy.benchmark) diff --git a/packages/flutter_test/lib/src/controller.dart b/packages/flutter_test/lib/src/controller.dart index f719f51fc0..0df3f45e6c 100644 --- a/packages/flutter_test/lib/src/controller.dart +++ b/packages/flutter_test/lib/src/controller.dart @@ -400,6 +400,20 @@ abstract class WidgetController { }); } + /// A simulator of how the framework handles a series of [PointerEvent]s + /// received from the Flutter engine. + /// + /// The [PointerEventRecord.timeDelay] is used as the time delay of the events + /// injection relative to the starting point of the method call. + /// + /// Returns a list of the difference between [PointerEventRecord.timeDelay] + /// and the real delay time when the [PointerEventRecord.events] are processed. + /// The closer these values are to zero the more faithful it is to the + /// `records`. + /// + /// See [PointerEventRecord]. + Future> handlePointerEventRecord(List records); + /// Called to indicate that time should advance. /// /// This is invoked by [flingFrom], for instance, so that the sequence of @@ -679,4 +693,11 @@ class LiveWidgetController extends WidgetController { binding.scheduleFrame(); await binding.endOfFrame; } + + @override + Future> handlePointerEventRecord(List records) { + // TODO(CareF): This will be implemented after we decide what should be the + // correct pumping strategy. + throw UnimplementedError; + } } diff --git a/packages/flutter_test/lib/src/test_pointer.dart b/packages/flutter_test/lib/src/test_pointer.dart index f24ebf1d73..c1d259cc3e 100644 --- a/packages/flutter_test/lib/src/test_pointer.dart +++ b/packages/flutter_test/lib/src/test_pointer.dart @@ -437,3 +437,30 @@ class TestGesture { }); } } + +/// A record of input [PointerEvent] list with the timeStamp of when it is +/// injected. +/// +/// The [timeDelay] is used to indicate the time when the event packet should +/// be sent. +/// +/// This is a simulation of how the framework is receiving input events from +/// the engine. See [GestureBinding] and [PointerDataPacket]. +class PointerEventRecord { + /// Creates a pack of [PointerEvent]s. + PointerEventRecord(this.timeDelay, this.events); + + /// The time delay of when the event record should be sent. + /// + /// This value is used as the time delay relative to the start of + /// [WidgetTester.handlePointerEventRecord] call. + final Duration timeDelay; + + /// The event list of the record. + /// + /// This can be considered as a simulation of the events expanded from the + /// [PointerDataPacket]. + /// + /// See [PointerEventConverter.expand]. + final List events; +} diff --git a/packages/flutter_test/lib/src/widget_tester.dart b/packages/flutter_test/lib/src/widget_tester.dart index a570497802..b0cc8327f7 100644 --- a/packages/flutter_test/lib/src/widget_tester.dart +++ b/packages/flutter_test/lib/src/widget_tester.dart @@ -25,6 +25,7 @@ import 'finders.dart'; import 'matchers.dart'; import 'test_async_utils.dart'; import 'test_compat.dart'; +import 'test_pointer.dart'; import 'test_text_input.dart'; /// Keep users from needing multiple imports to test semantics. @@ -463,6 +464,89 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker }); } + @override + Future> handlePointerEventRecord(List records) { + assert(records != null); + assert(records.isNotEmpty); + return TestAsyncUtils.guard>(() async { + // hitTestHistory is an equivalence of _hitTests in [GestureBinding] + final Map hitTestHistory = {}; + final List handleTimeStampDiff = []; + DateTime startTime; + for (final PointerEventRecord record in records) { + final DateTime now = binding.clock.now(); + startTime ??= now; + // So that the first event is promised to receive a zero timeDiff + final Duration timeDiff = record.timeDelay - now.difference(startTime); + if (timeDiff.isNegative) { + // Flush all past events + handleTimeStampDiff.add(timeDiff); + for (final PointerEvent event in record.events) { + _handlePointerEvent(event, hitTestHistory); + } + } else { + // TODO(CareF): reconsider the pumping strategy after + // https://github.com/flutter/flutter/issues/60739 is fixed + await binding.pump(); + await binding.delayed(timeDiff); + handleTimeStampDiff.add( + record.timeDelay - binding.clock.now().difference(startTime), + ); + for (final PointerEvent event in record.events) { + _handlePointerEvent(event, hitTestHistory); + } + } + } + await binding.pump(); + // This makes sure that a gesture is completed, with no more pointers + // active. + assert(hitTestHistory.isEmpty); + return handleTimeStampDiff; + }); + } + + // This is a parallel implementation of [GestureBinding._handlePointerEvent] + // to make compatible with test bindings. + void _handlePointerEvent( + PointerEvent event, + Map _hitTests + ) { + HitTestResult hitTestResult; + if (event is PointerDownEvent || event is PointerSignalEvent) { + assert(!_hitTests.containsKey(event.pointer)); + hitTestResult = HitTestResult(); + binding.hitTest(hitTestResult, event.position); + if (event is PointerDownEvent) { + _hitTests[event.pointer] = hitTestResult; + } + assert(() { + if (debugPrintHitTestResults) + debugPrint('$event: $hitTestResult'); + return true; + }()); + } else if (event is PointerUpEvent || event is PointerCancelEvent) { + hitTestResult = _hitTests.remove(event.pointer); + } else if (event.down) { + // Because events that occur with the pointer down (like + // PointerMoveEvents) should be dispatched to the same place that their + // initial PointerDownEvent was, we want to re-use the path we found when + // the pointer went down, rather than do hit detection each time we get + // such an event. + hitTestResult = _hitTests[event.pointer]; + } + assert(() { + if (debugPrintMouseHoverEvents && event is PointerHoverEvent) + debugPrint('$event'); + return true; + }()); + if (hitTestResult != null || + event is PointerHoverEvent || + event is PointerAddedEvent || + event is PointerRemovedEvent) { + binding.dispatchEvent(event, hitTestResult, source: TestBindingEventSource.test); + } + } + /// Triggers a frame after `duration` amount of time. /// /// This makes the framework act as if the application had janked (missed diff --git a/packages/flutter_test/test/widget_tester_test.dart b/packages/flutter_test/test/widget_tester_test.dart index 936de47ac6..ecc237a320 100644 --- a/packages/flutter_test/test/widget_tester_test.dart +++ b/packages/flutter_test/test/widget_tester_test.dart @@ -8,6 +8,7 @@ import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; @@ -640,6 +641,73 @@ void main() { expect(await tester.pumpAndSettle(const Duration(milliseconds: 300)), 5); // 0, 300, 600, 900, 1200ms }); + testWidgets('Input event array', (WidgetTester tester) async { + final List logs = []; + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Listener( + onPointerDown: (PointerDownEvent event) => logs.add('down ${event.buttons}'), + onPointerMove: (PointerMoveEvent event) => logs.add('move ${event.buttons}'), + onPointerUp: (PointerUpEvent event) => logs.add('up ${event.buttons}'), + child: const Text('test'), + ), + ), + ); + + final Offset location = tester.getCenter(find.text('test')); + final List records = [ + PointerEventRecord(Duration.zero, [ + // Typically PointerAddedEvent is not used in testers, but for records + // captured on a device it is usually what start a gesture. + PointerAddedEvent( + timeStamp: Duration.zero, + position: location, + ), + PointerDownEvent( + timeStamp: Duration.zero, + position: location, + buttons: kSecondaryMouseButton, + pointer: 1, + ), + ]), + ...[ + for (Duration t = const Duration(milliseconds: 5); + t < const Duration(milliseconds: 80); + t += const Duration(milliseconds: 16)) + PointerEventRecord(t, [ + PointerMoveEvent( + timeStamp: t - const Duration(milliseconds: 1), + position: location, + buttons: kSecondaryMouseButton, + pointer: 1, + ) + ]) + ], + PointerEventRecord(const Duration(milliseconds: 80), [ + PointerUpEvent( + timeStamp: const Duration(milliseconds: 79), + position: location, + buttons: kSecondaryMouseButton, + pointer: 1, + ) + ]) + ]; + final List timeDiffs = await tester.handlePointerEventRecord(records); + expect(timeDiffs.length, records.length); + for (final Duration diff in timeDiffs) { + expect(diff, Duration.zero); + } + + const String b = '$kSecondaryMouseButton'; + expect(logs.first, 'down $b'); + for (int i = 1; i < logs.length - 1; i++) { + expect(logs[i], 'move $b'); + } + expect(logs.last, 'up $b'); + }); + group('runAsync', () { testWidgets('works with no async calls', (WidgetTester tester) async { String value;