Some cleanup of the test framework (#4001)
* Add a "build" phase to EnginePhase for completeness. * Ignore events from the device during test execution. * More dartdocs * Slightly more helpful messages about Timers in verifyInvariants. * Add widgetList, elementList, stateList, renderObjectList. * Send test events asynchronously for consistency with other APIs. * Fix a test that was depending on test events being synchronous (or rather, scheduled in a microtask that came before the microtask for the completer of the future that the tap() function returned).
This commit is contained in:
parent
0dafe1a480
commit
d2c8c82f4b
@ -27,7 +27,7 @@ void main() {
|
||||
home: new Material(
|
||||
child: new Align(
|
||||
alignment: FractionalOffset.topCenter,
|
||||
child:button
|
||||
child: button
|
||||
)
|
||||
)
|
||||
)
|
||||
@ -39,13 +39,16 @@ void main() {
|
||||
|
||||
// We should have two copies of item 5, one in the menu and one in the
|
||||
// button itself.
|
||||
expect(find.text('5').evaluate().length, 2);
|
||||
expect(tester.elementList(find.text('5')), hasLength(2));
|
||||
|
||||
// We should only have one copy of item 19, which is in the button itself.
|
||||
// The copy in the menu shouldn't be in the tree because it's off-screen.
|
||||
expect(find.text('19').evaluate().length, 1);
|
||||
expect(tester.elementList(find.text('19')), hasLength(1));
|
||||
|
||||
expect(value, 4);
|
||||
await tester.tap(find.byConfig(button));
|
||||
expect(value, 4);
|
||||
await tester.idle(); // this waits for the route's completer to complete, which calls handleChanged
|
||||
|
||||
// Ideally this would be 4 because the menu would be overscrolled to the
|
||||
// correct position, but currently we just reposition the menu so that it
|
||||
|
@ -20,16 +20,51 @@ import 'package:vector_math/vector_math_64.dart';
|
||||
import 'test_async_utils.dart';
|
||||
import 'stack_manipulation.dart';
|
||||
|
||||
/// Enumeration of possible phases to reach in
|
||||
/// [WidgetTester.pumpWidget] and [TestWidgetsFlutterBinding.pump].
|
||||
// TODO(ianh): Merge with identical code in the rendering test code.
|
||||
/// Phases that can be reached by [WidgetTester.pumpWidget] and
|
||||
/// [TestWidgetsFlutterBinding.pump].
|
||||
// TODO(ianh): Merge with near-identical code in the rendering test code.
|
||||
enum EnginePhase {
|
||||
/// The build phase in the widgets library. See [BuildOwner.buildDirtyElements].
|
||||
build,
|
||||
|
||||
/// The layout phase in the rendering library. See [PipelineOwner.flushLayout].
|
||||
layout,
|
||||
|
||||
/// The compositing bits update phase in the rendering library. See
|
||||
/// [PipelineOwner.flushCompositingBits].
|
||||
compositingBits,
|
||||
|
||||
/// The paint phase in the rendering library. See [PipelineOwner.flushPaint].
|
||||
paint,
|
||||
|
||||
/// The compositing phase in the rendering library. See
|
||||
/// [RenderView.compositeFrame]. This is the phase in which data is sent to
|
||||
/// the GPU. If semantics are not enabled, then this is the last phase.
|
||||
composite,
|
||||
|
||||
/// The semantics building phase in the rendering library. See
|
||||
/// [PipelineOwner.flushSemantics].
|
||||
flushSemantics,
|
||||
sendSemanticsTree
|
||||
|
||||
/// The final phase in the rendering library, wherein semantics information is
|
||||
/// sent to the embedder. See [SemanticsNode.sendSemanticsTree].
|
||||
sendSemanticsTree,
|
||||
}
|
||||
|
||||
/// Parts of the system that can generate pointer events that reach the test
|
||||
/// binding.
|
||||
///
|
||||
/// This is used to identify how to handle events in the
|
||||
/// [LiveTestWidgetsFlutterBinding]. See
|
||||
/// [TestWidgetsFlutterBinding.dispatchEvent].
|
||||
enum TestBindingEventSource {
|
||||
/// The pointer event came from the test framework itself, e.g. from a
|
||||
/// [TestGesture] created by [WidgetTester.startGesture].
|
||||
test,
|
||||
|
||||
/// The pointer event came from the system, presumably as a result of the user
|
||||
/// interactive directly with the device while the test was running.
|
||||
device,
|
||||
}
|
||||
|
||||
const Size _kTestViewportSize = const Size(800.0, 600.0);
|
||||
@ -74,6 +109,7 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
|
||||
super.initInstances();
|
||||
}
|
||||
|
||||
/// Whether there is currently a test executing.
|
||||
bool get inTest;
|
||||
|
||||
/// The default test timeout for tests when using this binding.
|
||||
@ -112,6 +148,14 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
|
||||
return new Future<Null>.value();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispatchEvent(PointerEvent event, HitTestResult result, {
|
||||
TestBindingEventSource source: TestBindingEventSource.device
|
||||
}) {
|
||||
assert(source == TestBindingEventSource.test);
|
||||
super.dispatchEvent(event, result);
|
||||
}
|
||||
|
||||
/// Returns the exception most recently caught by the Flutter framework.
|
||||
///
|
||||
/// Call this if you expect an exception during a test. If an exception is
|
||||
@ -385,6 +429,8 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
|
||||
void beginFrame() {
|
||||
assert(inTest);
|
||||
buildOwner.buildDirtyElements();
|
||||
if (_phase == EnginePhase.build)
|
||||
return;
|
||||
assert(renderView != null);
|
||||
pipelineOwner.flushLayout();
|
||||
if (_phase == EnginePhase.layout)
|
||||
@ -439,11 +485,11 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
|
||||
void _verifyInvariants() {
|
||||
super._verifyInvariants();
|
||||
assert(() {
|
||||
'A Timer is still running even after the widget tree was disposed.';
|
||||
'A periodic Timer is still running even after the widget tree was disposed.';
|
||||
return _fakeAsync.periodicTimerCount == 0;
|
||||
});
|
||||
assert(() {
|
||||
'A Timer is still running even after the widget tree was disposed.';
|
||||
'A Timer is still pending even after the widget tree was disposed.';
|
||||
return _fakeAsync.nonPeriodicTimerCount == 0;
|
||||
});
|
||||
assert(_fakeAsync.microtaskCount == 0); // Shouldn't be possible.
|
||||
@ -534,6 +580,18 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispatchEvent(PointerEvent event, HitTestResult result, {
|
||||
TestBindingEventSource source: TestBindingEventSource.device
|
||||
}) {
|
||||
if (source == TestBindingEventSource.test) {
|
||||
super.dispatchEvent(event, result, source: source);
|
||||
return;
|
||||
}
|
||||
// we eat all device events for now
|
||||
// TODO(ianh): do something useful with device events
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Null> pump([ Duration duration, EnginePhase newPhase = EnginePhase.sendSemanticsTree ]) {
|
||||
assert(newPhase == EnginePhase.sendSemanticsTree);
|
||||
|
@ -32,6 +32,7 @@ class WidgetController {
|
||||
return finder.evaluate().isNotEmpty;
|
||||
}
|
||||
|
||||
|
||||
/// All widgets currently in the widget tree (lazy pre-order traversal).
|
||||
///
|
||||
/// Can contain duplicates, since widgets can be used in multiple
|
||||
@ -46,6 +47,9 @@ class WidgetController {
|
||||
///
|
||||
/// Throws a [StateError] if `finder` is empty or matches more than
|
||||
/// one widget.
|
||||
///
|
||||
/// * Use [firstWidget] if you expect to match several widgets but only want the first.
|
||||
/// * Use [widgetList] if you expect to match several widgets and want all of them.
|
||||
Widget/*=T*/ widget/*<T extends Widget>*/(Finder finder) {
|
||||
TestAsyncUtils.guardSync();
|
||||
return finder.evaluate().single.widget;
|
||||
@ -55,11 +59,23 @@ class WidgetController {
|
||||
/// traversal of the widget tree.
|
||||
///
|
||||
/// Throws a [StateError] if `finder` is empty.
|
||||
///
|
||||
/// * Use [widget] if you only expect to match one widget.
|
||||
Widget/*=T*/ firstWidget/*<T extends Widget>*/(Finder finder) {
|
||||
TestAsyncUtils.guardSync();
|
||||
return finder.evaluate().first.widget;
|
||||
}
|
||||
|
||||
/// The matching widgets in the widget tree.
|
||||
///
|
||||
/// * Use [widget] if you only expect to match one widget.
|
||||
/// * Use [firstWidget] if you expect to match several but only want the first.
|
||||
Iterable<Widget/*=T*/> widgetList/*<T extends Widget>*/(Finder finder) {
|
||||
TestAsyncUtils.guardSync();
|
||||
return finder.evaluate().map((Element element) => element.widget);
|
||||
}
|
||||
|
||||
|
||||
/// All elements currently in the widget tree (lazy pre-order traversal).
|
||||
///
|
||||
/// The returned iterable is lazy. It does not walk the entire widget tree
|
||||
@ -74,6 +90,9 @@ class WidgetController {
|
||||
///
|
||||
/// Throws a [StateError] if `finder` is empty or matches more than
|
||||
/// one element.
|
||||
///
|
||||
/// * Use [firstElement] if you expect to match several elements but only want the first.
|
||||
/// * Use [elementList] if you expect to match several elements and want all of them.
|
||||
Element/*=T*/ element/*<T extends Element>*/(Finder finder) {
|
||||
TestAsyncUtils.guardSync();
|
||||
return finder.evaluate().single;
|
||||
@ -83,11 +102,23 @@ class WidgetController {
|
||||
/// traversal of the widget tree.
|
||||
///
|
||||
/// Throws a [StateError] if `finder` is empty.
|
||||
///
|
||||
/// * Use [element] if you only expect to match one element.
|
||||
Element/*=T*/ firstElement/*<T extends Element>*/(Finder finder) {
|
||||
TestAsyncUtils.guardSync();
|
||||
return finder.evaluate().first;
|
||||
}
|
||||
|
||||
/// The matching elements in the widget tree.
|
||||
///
|
||||
/// * Use [element] if you only expect to match one element.
|
||||
/// * Use [firstElement] if you expect to match several but only want the first.
|
||||
Iterable<Element/*=T*/> elementList/*<T extends Element>*/(Finder finder) {
|
||||
TestAsyncUtils.guardSync();
|
||||
return finder.evaluate();
|
||||
}
|
||||
|
||||
|
||||
/// All states currently in the widget tree (lazy pre-order traversal).
|
||||
///
|
||||
/// The returned iterable is lazy. It does not walk the entire widget tree
|
||||
@ -104,6 +135,9 @@ class WidgetController {
|
||||
///
|
||||
/// Throws a [StateError] if `finder` is empty, matches more than
|
||||
/// one state, or matches a widget that has no state.
|
||||
///
|
||||
/// * Use [firstState] if you expect to match several states but only want the first.
|
||||
/// * Use [stateList] if you expect to match several states and want all of them.
|
||||
State/*=T*/ state/*<T extends State>*/(Finder finder) {
|
||||
TestAsyncUtils.guardSync();
|
||||
return _stateOf/*<T>*/(finder.evaluate().single, finder);
|
||||
@ -114,11 +148,25 @@ class WidgetController {
|
||||
///
|
||||
/// Throws a [StateError] if `finder` is empty or if the first
|
||||
/// matching widget has no state.
|
||||
///
|
||||
/// * Use [state] if you only expect to match one state.
|
||||
State/*=T*/ firstState/*<T extends State>*/(Finder finder) {
|
||||
TestAsyncUtils.guardSync();
|
||||
return _stateOf/*<T>*/(finder.evaluate().first, finder);
|
||||
}
|
||||
|
||||
/// The matching states in the widget tree.
|
||||
///
|
||||
/// Throws a [StateError] if any of the elements in `finder` match a widget
|
||||
/// that has no state.
|
||||
///
|
||||
/// * Use [state] if you only expect to match one state.
|
||||
/// * Use [firstState] if you expect to match several but only want the first.
|
||||
Iterable<State/*=T*/> stateList/*<T extends State>*/(Finder finder) {
|
||||
TestAsyncUtils.guardSync();
|
||||
return finder.evaluate().map((Element element) => _stateOf/*<T>*/(element, finder));
|
||||
}
|
||||
|
||||
State/*=T*/ _stateOf/*<T extends State>*/(Element element, Finder finder) {
|
||||
TestAsyncUtils.guardSync();
|
||||
if (element is StatefulElement)
|
||||
@ -126,6 +174,7 @@ class WidgetController {
|
||||
throw new StateError('Widget of type ${element.widget.runtimeType}, with ${finder.description}, is not a StatefulWidget.');
|
||||
}
|
||||
|
||||
|
||||
/// Render objects of all the widgets currently in the widget tree
|
||||
/// (lazy pre-order traversal).
|
||||
///
|
||||
@ -143,6 +192,9 @@ class WidgetController {
|
||||
///
|
||||
/// Throws a [StateError] if `finder` is empty or matches more than
|
||||
/// one widget (even if they all have the same render object).
|
||||
///
|
||||
/// * Use [firstRenderObject] if you expect to match several render objects but only want the first.
|
||||
/// * Use [renderObjectList] if you expect to match several render objects and want all of them.
|
||||
RenderObject/*=T*/ renderObject/*<T extends RenderObject>*/(Finder finder) {
|
||||
TestAsyncUtils.guardSync();
|
||||
return finder.evaluate().single.renderObject;
|
||||
@ -152,11 +204,22 @@ class WidgetController {
|
||||
/// depth-first pre-order traversal of the widget tree.
|
||||
///
|
||||
/// Throws a [StateError] if `finder` is empty.
|
||||
///
|
||||
/// * Use [renderObject] if you only expect to match one render object.
|
||||
RenderObject/*=T*/ firstRenderObject/*<T extends RenderObject>*/(Finder finder) {
|
||||
TestAsyncUtils.guardSync();
|
||||
return finder.evaluate().first.renderObject;
|
||||
}
|
||||
|
||||
/// The render objects of the matching widgets in the widget tree.
|
||||
///
|
||||
/// * Use [renderObject] if you only expect to match one render object.
|
||||
/// * Use [firstRenderObject] if you expect to match several but only want the first.
|
||||
Iterable<RenderObject/*=T*/> renderObjectList/*<T extends RenderObject>*/(Finder finder) {
|
||||
TestAsyncUtils.guardSync();
|
||||
return finder.evaluate().map((Element element) => element.renderObject);
|
||||
}
|
||||
|
||||
|
||||
/// Returns a list of all the [Layer] objects in the rendering.
|
||||
List<Layer> get layers => _walkLayers(binding.renderView.layer).toList();
|
||||
@ -214,13 +277,13 @@ class WidgetController {
|
||||
const int kMoveCount = 50; // Needs to be >= kHistorySize, see _LeastSquaresVelocityTrackerStrategy
|
||||
final double timeStampDelta = 1000.0 * offset.distance / (kMoveCount * velocity);
|
||||
double timeStamp = 0.0;
|
||||
await _dispatchEvent(p.down(startLocation, timeStamp: new Duration(milliseconds: timeStamp.round())), result);
|
||||
await sendEventToBinding(p.down(startLocation, timeStamp: new Duration(milliseconds: timeStamp.round())), result);
|
||||
for (int i = 0; i <= kMoveCount; i++) {
|
||||
final Point location = startLocation + Offset.lerp(Offset.zero, offset, i / kMoveCount);
|
||||
await _dispatchEvent(p.move(location, timeStamp: new Duration(milliseconds: timeStamp.round())), result);
|
||||
await sendEventToBinding(p.move(location, timeStamp: new Duration(milliseconds: timeStamp.round())), result);
|
||||
timeStamp += timeStampDelta;
|
||||
}
|
||||
await _dispatchEvent(p.up(timeStamp: new Duration(milliseconds: timeStamp.round())), result);
|
||||
await sendEventToBinding(p.up(timeStamp: new Duration(milliseconds: timeStamp.round())), result);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
@ -248,7 +311,7 @@ class WidgetController {
|
||||
/// Begins a gesture at a particular point, and returns the
|
||||
/// [TestGesture] object which you can use to continue the gesture.
|
||||
Future<TestGesture> startGesture(Point downLocation, { int pointer: 1 }) {
|
||||
return TestGesture.down(downLocation, pointer: pointer, dispatcher: _dispatchEvent);
|
||||
return TestGesture.down(downLocation, pointer: pointer, dispatcher: sendEventToBinding);
|
||||
}
|
||||
|
||||
HitTestResult _hitTest(Point location) {
|
||||
@ -257,9 +320,12 @@ class WidgetController {
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<Null> _dispatchEvent(PointerEvent event, HitTestResult result) {
|
||||
binding.dispatchEvent(event, result);
|
||||
return new Future<Null>.value();
|
||||
/// Forwards the given pointer event to the binding.
|
||||
Future<Null> sendEventToBinding(PointerEvent event, HitTestResult result) {
|
||||
return TestAsyncUtils.guard(() async {
|
||||
binding.dispatchEvent(event, result);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
@ -4,6 +4,7 @@
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:test/test.dart' as test_package;
|
||||
|
||||
@ -156,6 +157,14 @@ class WidgetTester extends WidgetController {
|
||||
return TestAsyncUtils.guard(() => binding.pump(duration, phase));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Null> sendEventToBinding(PointerEvent event, HitTestResult result) {
|
||||
return TestAsyncUtils.guard(() async {
|
||||
binding.dispatchEvent(event, result, source: TestBindingEventSource.test);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns the exception most recently caught by the Flutter framework.
|
||||
///
|
||||
/// See [TestWidgetsFlutterBinding.takeException] for details.
|
||||
|
Loading…
x
Reference in New Issue
Block a user