Commit d2c8c82f authored by Ian Hickson's avatar Ian Hickson

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).
parent 0dafe1a4
......@@ -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.
......
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