Commit c167efca authored by Ian Hickson's avatar Ian Hickson

Minor widget_tester refactoring and docs (#3472)

This reorders some classes so that this file makes more sense, and adds
a bunch of docs. It also makes the following changes:

* Move allElements from Instrumentation to TestWidgets. (Instrumentation
  is going away.)

* Remove findElements.

* Rename byElement to byElementPredicate

* Rename byPredicate to byWidgetPredicate

* Implement _WidgetPredicateFinder so that byWidgetPredicate has good
  messages

* Fix one use of byElementPredicate to use byWidgetPredicate.
parent 31d199b0
...@@ -15,8 +15,7 @@ void main() { ...@@ -15,8 +15,7 @@ void main() {
tester.pump(); // see https://github.com/flutter/flutter/issues/1865 tester.pump(); // see https://github.com/flutter/flutter/issues/1865
tester.pump(); // triggers a frame tester.pump(); // triggers a frame
Finder navigationMenu = find.byElement((Element element) { Finder navigationMenu = find.byWidgetPredicate((Widget widget) {
Widget widget = element.widget;
if (widget is Tooltip) if (widget is Tooltip)
return widget.message == 'Open navigation menu'; return widget.message == 'Open navigation menu';
return false; return false;
......
...@@ -14,14 +14,14 @@ import '../lib/gallery/item.dart' as material_gallery; ...@@ -14,14 +14,14 @@ import '../lib/gallery/item.dart' as material_gallery;
const List<String> demoCategories = const <String>['Demos', 'Components', 'Style']; const List<String> demoCategories = const <String>['Demos', 'Components', 'Style'];
Finder findGalleryItemByRouteName(WidgetTester tester, String routeName) { Finder findGalleryItemByRouteName(WidgetTester tester, String routeName) {
return find.byPredicate((Widget widget) { return find.byWidgetPredicate((Widget widget) {
return widget is material_gallery.GalleryItem return widget is material_gallery.GalleryItem
&& widget.routeName == routeName; && widget.routeName == routeName;
}); });
} }
Finder byTooltip(WidgetTester tester, String message) { Finder byTooltip(WidgetTester tester, String message) {
return find.byPredicate((Widget widget) { return find.byWidgetPredicate((Widget widget) {
return widget is Tooltip && widget.message == message; return widget is Tooltip && widget.message == message;
}); });
} }
......
...@@ -10,8 +10,9 @@ import 'package:test/test.dart'; ...@@ -10,8 +10,9 @@ import 'package:test/test.dart';
import 'test_widgets.dart'; import 'test_widgets.dart';
void checkTree(WidgetTester tester, List<BoxDecoration> expectedDecorations) { void checkTree(WidgetTester tester, List<BoxDecoration> expectedDecorations) {
MultiChildRenderObjectElement element = MultiChildRenderObjectElement element = tester.elementOf(find.byElementPredicate(
tester.elementOf(find.byElement((Element element) => element is MultiChildRenderObjectElement)); (Element element) => element is MultiChildRenderObjectElement
));
expect(element, isNotNull); expect(element, isNotNull);
expect(element.renderObject is RenderStack, isTrue); expect(element.renderObject is RenderStack, isTrue);
RenderStack renderObject = element.renderObject; RenderStack renderObject = element.renderObject;
......
...@@ -19,8 +19,9 @@ class TestParentData { ...@@ -19,8 +19,9 @@ class TestParentData {
} }
void checkTree(WidgetTester tester, List<TestParentData> expectedParentData) { void checkTree(WidgetTester tester, List<TestParentData> expectedParentData) {
MultiChildRenderObjectElement element = MultiChildRenderObjectElement element = tester.elementOf(
tester.elementOf(find.byElement((Element element) => element is MultiChildRenderObjectElement)); find.byElementPredicate((Element element) => element is MultiChildRenderObjectElement)
);
expect(element, isNotNull); expect(element, isNotNull);
expect(element.renderObject is RenderStack, isTrue); expect(element.renderObject is RenderStack, isTrue);
RenderStack renderObject = element.renderObject; RenderStack renderObject = element.renderObject;
......
...@@ -14,8 +14,9 @@ void main() { ...@@ -14,8 +14,9 @@ void main() {
testWidgets((WidgetTester tester) { testWidgets((WidgetTester tester) {
void checkTree(BoxDecoration expectedDecoration) { void checkTree(BoxDecoration expectedDecoration) {
SingleChildRenderObjectElement element = SingleChildRenderObjectElement element = tester.elementOf(
tester.elementOf(find.byElement((Element element) => element is SingleChildRenderObjectElement)); find.byElementPredicate((Element element) => element is SingleChildRenderObjectElement)
);
expect(element, isNotNull); expect(element, isNotNull);
expect(element.renderObject is RenderDecoratedBox, isTrue); expect(element.renderObject is RenderDecoratedBox, isTrue);
RenderDecoratedBox renderObject = element.renderObject; RenderDecoratedBox renderObject = element.renderObject;
......
...@@ -2,8 +2,6 @@ ...@@ -2,8 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:collection';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
...@@ -63,20 +61,6 @@ class Instrumentation { ...@@ -63,20 +61,6 @@ class Instrumentation {
return null; return null;
} }
/// Returns all elements ordered in a depth-first traversal fashion.
///
/// The returned iterable is lazy. It does not walk the entire element tree
/// immediately, but rather a chunk at a time as the iteration progresses
/// using [Iterator.moveNext].
Iterable<Element> get allElements {
return new _DepthFirstChildIterable(binding.renderViewElement);
}
/// Returns all elements that satisfy [predicate].
Iterable<Element> findElements(bool predicate(Element element)) {
return allElements.where(predicate);
}
/// Returns the first element that corresponds to a widget with the /// Returns the first element that corresponds to a widget with the
/// given [Key], or null if there is no such element. /// given [Key], or null if there is no such element.
Element findElementByKey(Key key) { Element findElementByKey(Key key) {
...@@ -248,42 +232,3 @@ class Instrumentation { ...@@ -248,42 +232,3 @@ class Instrumentation {
return result; return result;
} }
} }
class _DepthFirstChildIterable extends IterableBase<Element> {
_DepthFirstChildIterable(this.rootElement);
Element rootElement;
@override
Iterator<Element> get iterator => new _DepthFirstChildIterator(rootElement);
}
class _DepthFirstChildIterator implements Iterator<Element> {
_DepthFirstChildIterator(Element rootElement)
: _stack = _reverseChildrenOf(rootElement).toList();
Element _current;
final List<Element> _stack;
@override
Element get current => _current;
@override
bool moveNext() {
if (_stack.isEmpty)
return false;
_current = _stack.removeLast();
// Stack children in reverse order to traverse first branch first
_stack.addAll(_reverseChildrenOf(_current));
return true;
}
static Iterable<Element> _reverseChildrenOf(Element element) {
List<Element> children = <Element>[];
element.visitChildren(children.add);
return children.reversed;
}
}
...@@ -2,6 +2,9 @@ ...@@ -2,6 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:collection';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:quiver/testing/async.dart'; import 'package:quiver/testing/async.dart';
...@@ -10,6 +13,12 @@ import 'package:test/test.dart'; ...@@ -10,6 +13,12 @@ import 'package:test/test.dart';
import 'binding.dart'; import 'binding.dart';
import 'test_pointer.dart'; import 'test_pointer.dart';
/// Signature for [CommonFinders.byPredicate].
typedef bool WidgetPredicate(Widget widget);
/// Signature for [CommonFinders.byElement].
typedef bool ElementPredicate(Element element);
/// Runs the [callback] inside the Flutter test environment. /// Runs the [callback] inside the Flutter test environment.
/// ///
/// Use this function for testing custom [StatelessWidget]s and /// Use this function for testing custom [StatelessWidget]s and
...@@ -30,26 +39,6 @@ void testWidgets(void callback(WidgetTester widgetTester)) { ...@@ -30,26 +39,6 @@ void testWidgets(void callback(WidgetTester widgetTester)) {
}); });
} }
/// A convenient accessor to frequently used finders.
///
/// Examples:
///
/// tester.tap(find.text('Save'));
/// tester.widget(find.byType(MyWidget));
/// tester.stateOf(find.byConfig(config));
/// tester.getSize(find.byKey(new ValueKey('save-button')));
const CommonFinders find = const CommonFinders._();
/// Asserts that [finder] locates a widget in the test element tree.
///
/// Example:
///
/// expect(tester, hasWidget(find.text('Save')));
Matcher hasWidget(Finder finder) => new _HasWidgetMatcher(finder);
/// Opposite of [hasWidget].
Matcher doesNotHaveWidget(Finder finder) => new _DoesNotHaveWidgetMatcher(finder);
/// Class that programmatically interacts with widgets and the test environment. /// Class that programmatically interacts with widgets and the test environment.
class WidgetTester { class WidgetTester {
WidgetTester._(this.elementTreeTester); WidgetTester._(this.elementTreeTester);
...@@ -57,6 +46,8 @@ class WidgetTester { ...@@ -57,6 +46,8 @@ class WidgetTester {
/// Exposes the [Element] tree created from widgets. /// Exposes the [Element] tree created from widgets.
final ElementTreeTester elementTreeTester; final ElementTreeTester elementTreeTester;
/// The binding instance that the widget tester is using when it
/// needs a binding (e.g. for event dispatch).
Widgeteer get binding => elementTreeTester.binding; Widgeteer get binding => elementTreeTester.binding;
/// Renders the UI from the given [widget]. /// Renders the UI from the given [widget].
...@@ -114,8 +105,7 @@ class WidgetTester { ...@@ -114,8 +105,7 @@ class WidgetTester {
/// All widgets currently live on the UI returned in a depth-first traversal /// All widgets currently live on the UI returned in a depth-first traversal
/// order. /// order.
Iterable<Widget> get widgets { Iterable<Widget> get widgets {
return this.elementTreeTester.allElements return this.allElements.map((Element element) => element.widget);
.map((Element element) => element.widget);
} }
/// Finds the first widget, searching in the depth-first traversal order. /// Finds the first widget, searching in the depth-first traversal order.
...@@ -233,16 +223,82 @@ class WidgetTester { ...@@ -233,16 +223,82 @@ class WidgetTester {
Element element = finder.findFirst(this); Element element = finder.findFirst(this);
return elementTreeTester.getBottomRight(element); return elementTreeTester.getBottomRight(element);
} }
/// Returns all elements ordered in a depth-first traversal fashion.
///
/// The returned iterable is lazy. It does not walk the entire element tree
/// immediately, but rather a chunk at a time as the iteration progresses
/// using [Iterator.moveNext].
Iterable<Element> get allElements {
return new _DepthFirstChildIterable(binding.renderViewElement);
}
}
class _DepthFirstChildIterable extends IterableBase<Element> {
_DepthFirstChildIterable(this.rootElement);
Element rootElement;
@override
Iterator<Element> get iterator => new _DepthFirstChildIterator(rootElement);
}
class _DepthFirstChildIterator implements Iterator<Element> {
_DepthFirstChildIterator(Element rootElement)
: _stack = _reverseChildrenOf(rootElement).toList();
Element _current;
final List<Element> _stack;
@override
Element get current => _current;
@override
bool moveNext() {
if (_stack.isEmpty)
return false;
_current = _stack.removeLast();
// Stack children in reverse order to traverse first branch first
_stack.addAll(_reverseChildrenOf(_current));
return true;
}
static Iterable<Element> _reverseChildrenOf(Element element) {
final List<Element> children = <Element>[];
element.visitChildren(children.add);
return children.reversed;
}
} }
/// A convenient accessor to frequently used finders.
///
/// Examples:
///
/// tester.tap(find.text('Save'));
/// tester.widget(find.byType(MyWidget));
/// tester.stateOf(find.byConfig(config));
/// tester.getSize(find.byKey(new ValueKey('save-button')));
const CommonFinders find = const CommonFinders._();
/// Provides lightweight syntax for getting frequently used widget [Finder]s. /// Provides lightweight syntax for getting frequently used widget [Finder]s.
///
/// This class is instantiated once, as [find].
class CommonFinders { class CommonFinders {
const CommonFinders._(); const CommonFinders._();
/// Finds [Text] widgets containing string equal to [text]. /// Finds [Text] widgets containing string equal to the `text`
/// argument.
///
/// Example:
///
/// expect(tester, hasWidget(find.text('Back')));
Finder text(String text) => new _TextFinder(text); Finder text(String text) => new _TextFinder(text);
/// Looks for widgets that contain [Text] with [text] in it. /// Looks for widgets that contain a [Text] descendant with `text`
/// in it.
/// ///
/// Example: /// Example:
/// ///
...@@ -255,32 +311,67 @@ class CommonFinders { ...@@ -255,32 +311,67 @@ class CommonFinders {
/// tester.tap(find.widgetWithText(Button, 'Update')); /// tester.tap(find.widgetWithText(Button, 'Update'));
Finder widgetWithText(Type widgetType, String text) => new _WidgetWithTextFinder(widgetType, text); Finder widgetWithText(Type widgetType, String text) => new _WidgetWithTextFinder(widgetType, text);
/// Finds widgets by [key]. /// Finds widgets by searching for one with a particular [Key].
///
/// Example:
///
/// expect(tester, hasWidget(find.byKey(backKey)));
Finder byKey(Key key) => new _KeyFinder(key); Finder byKey(Key key) => new _KeyFinder(key);
/// Finds widgets by [type]. /// Finds widgets by searching for widgehts with a particular type.
///
/// The `type` argument must be a subclass of [Widget].
///
/// Example:
///
/// expect(tester, hasWidget(find.byType(IconButton)));
Finder byType(Type type) => new _TypeFinder(type); Finder byType(Type type) => new _TypeFinder(type);
/// Finds widgets equal to [config]. /// Finds widgets whose current widget is the instance given by the
/// argument.
///
/// Example:
///
/// // Suppose you have a button created like this:
/// Widget myButton = new Button(
/// child: new Text('Update')
/// );
///
/// // You can find and tap on it like this:
/// tester.tap(find.byConfig(myButton));
Finder byConfig(Widget config) => new _ConfigFinder(config); Finder byConfig(Widget config) => new _ConfigFinder(config);
/// Finds widgets using a [predicate]. /// Finds widgets using a widget predicate.
Finder byPredicate(WidgetPredicate predicate) { ///
return new _ElementFinder((Element element) => predicate(element.widget)); /// Example:
} ///
/// expect(tester, hasWidget(find.byWidgetPredicate(
/// (Widget widget) => widget is Tooltip && widget.message == 'Back'
/// )));
Finder byWidgetPredicate(WidgetPredicate predicate) => new _WidgetPredicateFinder(predicate);
/// Finds widgets using an element [predicate]. /// Finds widgets using an element predicate.
Finder byElement(ElementPredicate predicate) => new _ElementFinder(predicate); ///
/// Example:
///
/// expect(tester, hasWidget(find.byWidgetPredicate(
/// (Element element) => element is SingleChildRenderObjectElement
/// )));
Finder byElementPredicate(ElementPredicate predicate) => new _ElementPredicateFinder(predicate);
} }
/// Finds [Element]s inside the element tree. /// Finds [Element]s inside the element tree.
abstract class Finder { abstract class Finder {
/// Returns all the elements that match this finder's pattern,
/// using the given tester to determine which element tree to look at.
Iterable<Element> find(WidgetTester tester); Iterable<Element> find(WidgetTester tester);
/// Describes what the finder is looking for. The description should be such /// Describes what the finder is looking for. The description should be
/// that [toString] reads as a descriptive English sentence. /// a brief English noun phrase describing the finder's pattern.
String get description; String get description;
/// Returns the first value returned from [find], unless no value is found,
/// in which case it throws an [ElementNotFoundError].
Element findFirst(WidgetTester tester) { Element findFirst(WidgetTester tester) {
Iterable<Element> results = find(tester); Iterable<Element> results = find(tester);
return results.isNotEmpty return results.isNotEmpty
...@@ -316,7 +407,7 @@ class _TextFinder extends Finder { ...@@ -316,7 +407,7 @@ class _TextFinder extends Finder {
@override @override
Iterable<Element> find(WidgetTester tester) { Iterable<Element> find(WidgetTester tester) {
return tester.elementTreeTester.findElements((Element element) { return tester.allElements.where((Element element) {
if (element.widget is! Text) if (element.widget is! Text)
return false; return false;
Text textWidget = element.widget; Text textWidget = element.widget;
...@@ -336,7 +427,7 @@ class _WidgetWithTextFinder extends Finder { ...@@ -336,7 +427,7 @@ class _WidgetWithTextFinder extends Finder {
@override @override
Iterable<Element> find(WidgetTester tester) { Iterable<Element> find(WidgetTester tester) {
return tester.elementTreeTester.allElements return tester.allElements
.map((Element textElement) { .map((Element textElement) {
if (textElement.widget is! Text) if (textElement.widget is! Text)
return null; return null;
...@@ -370,7 +461,9 @@ class _KeyFinder extends Finder { ...@@ -370,7 +461,9 @@ class _KeyFinder extends Finder {
@override @override
Iterable<Element> find(WidgetTester tester) { Iterable<Element> find(WidgetTester tester) {
return tester.elementTreeTester.findElements((Element element) => element.widget.key == key); return tester.allElements.where((Element element) {
return element.widget.key == key;
});
} }
} }
...@@ -384,7 +477,7 @@ class _TypeFinder extends Finder { ...@@ -384,7 +477,7 @@ class _TypeFinder extends Finder {
@override @override
Iterable<Element> find(WidgetTester tester) { Iterable<Element> find(WidgetTester tester) {
return tester.elementTreeTester.allElements.where((Element element) { return tester.allElements.where((Element element) {
return element.widget.runtimeType == widgetType; return element.widget.runtimeType == widgetType;
}); });
} }
...@@ -400,29 +493,49 @@ class _ConfigFinder extends Finder { ...@@ -400,29 +493,49 @@ class _ConfigFinder extends Finder {
@override @override
Iterable<Element> find(WidgetTester tester) { Iterable<Element> find(WidgetTester tester) {
return tester.elementTreeTester.allElements.where((Element element) { return tester.allElements.where((Element element) {
return element.widget == config; return element.widget == config;
}); });
} }
} }
typedef bool WidgetPredicate(Widget element); class _WidgetPredicateFinder extends Finder {
typedef bool ElementPredicate(Element element); _WidgetPredicateFinder(this.predicate);
class _ElementFinder extends Finder { final WidgetPredicate predicate;
_ElementFinder(this.predicate);
@override
String get description => 'widget predicate ($predicate)';
@override
Iterable<Element> find(WidgetTester tester) {
return tester.allElements.where((Element element) {
return predicate(element.widget);
});
}
}
class _ElementPredicateFinder extends Finder {
_ElementPredicateFinder(this.predicate);
final ElementPredicate predicate; final ElementPredicate predicate;
@override @override
String get description => 'element satisfying given predicate ($predicate)'; String get description => 'element predicate ($predicate)';
@override @override
Iterable<Element> find(WidgetTester tester) { Iterable<Element> find(WidgetTester tester) {
return tester.elementTreeTester.allElements.where(predicate); return tester.allElements.where(predicate);
} }
} }
/// Asserts that [finder] locates a widget in the test element tree.
///
/// Example:
///
/// expect(tester, hasWidget(find.text('Save')));
Matcher hasWidget(Finder finder) => new _HasWidgetMatcher(finder);
class _HasWidgetMatcher extends Matcher { class _HasWidgetMatcher extends Matcher {
const _HasWidgetMatcher(this.finder); const _HasWidgetMatcher(this.finder);
...@@ -445,6 +558,14 @@ class _HasWidgetMatcher extends Matcher { ...@@ -445,6 +558,14 @@ class _HasWidgetMatcher extends Matcher {
} }
} }
/// Asserts that [finder] does not locate a widget in the test element tree.
/// Opposite of [hasWidget].
///
/// Example:
///
/// expect(tester, doesNotHaveWidget(find.text('Save')));
Matcher doesNotHaveWidget(Finder finder) => new _DoesNotHaveWidgetMatcher(finder);
class _DoesNotHaveWidgetMatcher extends Matcher { class _DoesNotHaveWidgetMatcher extends Matcher {
const _DoesNotHaveWidgetMatcher(this.finder); const _DoesNotHaveWidgetMatcher(this.finder);
......
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