Unverified Commit a76b5eb7 authored by Ming Lyu (CareF)'s avatar Ming Lyu (CareF) Committed by GitHub

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
parent 66556fae
......@@ -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<void> 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<void> delayed(Duration duration) {
assert(_currentFakeAsync != null);
addTime(duration);
_currentFakeAsync.elapse(duration);
return Future<void>.value();
}
@override
Future<void> runTest(
Future<void> 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<void> delayed(Duration duration) {
return Future<void>.delayed(duration);
}
@override
void scheduleFrame() {
if (framePolicy == LiveTestWidgetsFlutterBindingFramePolicy.benchmark)
......
......@@ -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<List<Duration>> handlePointerEventRecord(List<PointerEventRecord> 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<List<Duration>> handlePointerEventRecord(List<PointerEventRecord> records) {
// TODO(CareF): This will be implemented after we decide what should be the
// correct pumping strategy.
throw UnimplementedError;
}
}
......@@ -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<PointerEvent> events;
}
......@@ -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<List<Duration>> handlePointerEventRecord(List<PointerEventRecord> records) {
assert(records != null);
assert(records.isNotEmpty);
return TestAsyncUtils.guard<List<Duration>>(() async {
// hitTestHistory is an equivalence of _hitTests in [GestureBinding]
final Map<int, HitTestResult> hitTestHistory = <int, HitTestResult>{};
final List<Duration> handleTimeStampDiff = <Duration>[];
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<int, HitTestResult> _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
......
......@@ -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<String> logs = <String>[];
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<PointerEventRecord> records = <PointerEventRecord>[
PointerEventRecord(Duration.zero, <PointerEvent>[
// 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,
),
]),
...<PointerEventRecord>[
for (Duration t = const Duration(milliseconds: 5);
t < const Duration(milliseconds: 80);
t += const Duration(milliseconds: 16))
PointerEventRecord(t, <PointerEvent>[
PointerMoveEvent(
timeStamp: t - const Duration(milliseconds: 1),
position: location,
buttons: kSecondaryMouseButton,
pointer: 1,
)
])
],
PointerEventRecord(const Duration(milliseconds: 80), <PointerEvent>[
PointerUpEvent(
timeStamp: const Duration(milliseconds: 79),
position: location,
buttons: kSecondaryMouseButton,
pointer: 1,
)
])
];
final List<Duration> 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;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment