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
This commit is contained in:
parent
66556faef7
commit
a76b5eb79f
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user