// Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:quiver/testing/async.dart'; import 'package:test/test.dart'; import 'binding.dart'; import 'test_pointer.dart'; /// Runs the [callback] inside the Flutter test environment. /// /// Use this function for testing custom [StatelessWidget]s and /// [StatefulWidget]s. /// /// Example: /// /// test('MyWidget', () { /// testWidgets((WidgetTester tester) { /// tester.pumpWidget(new MyWidget()); /// tester.tap(find.text('Save')); /// expect(tester, hasWidget(find.text('Success'))); /// }); /// }); void testWidgets(void callback(WidgetTester widgetTester)) { testElementTree((ElementTreeTester elementTreeTester) { callback(new WidgetTester._(elementTreeTester)); }); } /// 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 WidgetTester { WidgetTester._(this.elementTreeTester); /// Exposes the [Element] tree created from widgets. final ElementTreeTester elementTreeTester; Widgeteer get binding => elementTreeTester.binding; /// Renders the UI from the given [widget]. /// /// See [ElementTreeTester.pumpWidget] for details. void pumpWidget(Widget widget, [ Duration duration, EnginePhase phase ]) { elementTreeTester.pumpWidget(widget, duration, phase); } /// Triggers a sequence of frames for [duration] amount of time. /// /// See [ElementTreeTester.pump] for details. void pump([ Duration duration, EnginePhase phase ]) { elementTreeTester.pump(duration, phase); } /// Changes the current locale. /// /// See [ElementTreeTester.setLocale] for details. void setLocale(String languageCode, String countryCode) { elementTreeTester.setLocale(languageCode, countryCode); } /// Sends an [event] at [result] location. void dispatchEvent(PointerEvent event, HitTestResult result) { binding.dispatchEvent(event, result); } /// Returns the exception most recently caught by the Flutter framework. /// /// See [ElementTreeTester.takeException] for details. dynamic takeException() { return elementTreeTester.takeException(); } /// Returns the fake implmentation of the event loop used by the test /// environment. /// /// Use it to travel into the future without actually waiting, control the /// flushing of microtasks and timers. FakeAsync get async => elementTreeTester.async; /// Runs all remaining microtasks, including those scheduled as a result of /// running them, until there are no more microtasks scheduled. /// /// Does not run timers. May result in an infinite loop or run out of memory /// if microtasks continue to recursively schedule new microtasks. void flushMicrotasks() { elementTreeTester.async.flushMicrotasks(); } /// Checks if the element identified by [finder] exists in the tree. bool exists(Finder finder) => finder.find(this).isNotEmpty; /// All widgets currently live on the UI returned in a depth-first traversal /// order. Iterable get widgets { return this.elementTreeTester.allElements .map((Element element) => element.widget); } /// Finds the first widget, searching in the depth-first traversal order. Widget widget(Finder finder) { return finder.findFirst(this).widget; } /// Finds the first state object, searching in the depth-first traversal order. State/*=T*/ stateOf/**/(Finder finder) { Element element = finder.findFirst(this); Widget widget = element.widget; if (widget is StatefulWidget) return (element as StatefulElement).state; throw new ElementNotFoundError( 'Widget of type ${widget.runtimeType} found by ${finder.description} does not correspond to a StatefulWidget' ); } /// Finds the [Element] corresponding to the first widget found by [finder], /// searching in the depth-first traversal order. Element elementOf(Finder finder) => finder.findFirst(this); /// Finds the [RenderObject] corresponding to the first widget found by /// [finder], searching in the depth-first traversal order. RenderObject renderObjectOf(Finder finder) => finder.findFirst(this).findRenderObject(); /// Emulates a tapping action at the center of the widget found by [finder]. void tap(Finder finder, { int pointer: 1 }) { tapAt(getCenter(finder), pointer: pointer); } /// Emulates a tapping action at the given [location]. /// /// See [ElementTreeTester.tapAt] for details. void tapAt(Point location, { int pointer: 1 }) { elementTreeTester.tapAt(location, pointer: pointer); } /// Scrolls by dragging the center of a widget found by [finder] by [offset]. void scroll(Finder finder, Offset offset, { int pointer: 1 }) { scrollAt(getCenter(finder), offset, pointer: pointer); } /// Scrolls by dragging the screen at [startLocation] by [offset]. /// /// See [ElementTreeTester.scrollAt] for details. void scrollAt(Point startLocation, Offset offset, { int pointer: 1 }) { elementTreeTester.scrollAt(startLocation, offset, pointer: pointer); } /// Attempts a fling gesture starting at the center of a widget found by /// [finder]. /// /// See also [flingFrom]. void fling(Finder finder, Offset offset, double velocity, { int pointer: 1 }) { flingFrom(getCenter(finder), offset, velocity, pointer: pointer); } /// Attempts a fling gesture starting at [startLocation], moving by [offset] /// with the given [velocity]. /// /// See [ElementTreeTester.flingFrom] for details. void flingFrom(Point startLocation, Offset offset, double velocity, { int pointer: 1 }) { elementTreeTester.flingFrom(startLocation, offset, velocity, pointer: pointer); } /// Begins a gesture at a particular point, and returns the /// [TestGesture] object which you can use to continue the gesture. TestGesture startGesture(Point downLocation, { int pointer: 1 }) { return elementTreeTester.startGesture(downLocation, pointer: pointer); } /// Returns the size of the element corresponding to the widget located by /// [finder]. /// /// This is only valid once the element's render object has been laid out at /// least once. Size getSize(Finder finder) { assert(finder != null); Element element = finder.findFirst(this); return elementTreeTester.getSize(element); } /// Returns the point at the center of the widget found by [finder]. Point getCenter(Finder finder) { Element element = finder.findFirst(this); return elementTreeTester.getCenter(element); } /// Returns the point at the top left of the given element. Point getTopLeft(Finder finder) { Element element = finder.findFirst(this); return elementTreeTester.getTopLeft(element); } /// Returns the point at the top right of the given element. This /// point is not inside the object's hit test area. Point getTopRight(Finder finder) { Element element = finder.findFirst(this); return elementTreeTester.getTopRight(element); } /// Returns the point at the bottom left of the given element. This /// point is not inside the object's hit test area. Point getBottomLeft(Finder finder) { Element element = finder.findFirst(this); return elementTreeTester.getBottomLeft(element); } /// Returns the point at the bottom right of the given element. This /// point is not inside the object's hit test area. Point getBottomRight(Finder finder) { Element element = finder.findFirst(this); return elementTreeTester.getBottomRight(element); } } /// Provides lightweight syntax for getting frequently used widget [Finder]s. class CommonFinders { const CommonFinders._(); /// Finds [Text] widgets containing string equal to [text]. Finder text(String text) => new _TextFinder(text); /// Looks for widgets that contain [Text] with [text] in it. /// /// Example: /// /// // Suppose you have a button with text 'Update' in it: /// new Button( /// child: new Text('Update') /// ) /// /// // You can find and tap on it like this: /// tester.tap(find.widgetWithText(Button, 'Update')); Finder widgetWithText(Type widgetType, String text) => new _WidgetWithTextFinder(widgetType, text); /// Finds widgets by [key]. Finder byKey(Key key) => new _KeyFinder(key); /// Finds widgets by [type]. Finder byType(Type type) => new _TypeFinder(type); /// Finds widgets equal to [config]. Finder byConfig(Widget config) => new _ConfigFinder(config); /// Finds widgets using a [predicate]. Finder byPredicate(WidgetPredicate predicate) { return new _ElementFinder((Element element) => predicate(element.widget)); } /// Finds widgets using an element [predicate]. Finder byElement(ElementPredicate predicate) => new _ElementFinder(predicate); } /// Finds [Element]s inside the element tree. abstract class Finder { Iterable find(WidgetTester tester); /// Describes what the finder is looking for. The description should be such /// that [toString] reads as a descriptive English sentence. String get description; Element findFirst(WidgetTester tester) { Iterable results = find(tester); return results.isNotEmpty ? results.first : throw new ElementNotFoundError.fromFinder(this); } @override String toString() => '[Finder for $description]'; } /// Indicates that an attempt to find a widget within the current element tree /// failed. class ElementNotFoundError extends Error { ElementNotFoundError(this.message); ElementNotFoundError.fromFinder(Finder finder) : message = 'Element not found by ${finder.description}'; final String message; @override String toString() => 'ElementNotFoundError: $message'; } class _TextFinder extends Finder { _TextFinder(this.text); final String text; @override String get description => 'text "$text"'; @override Iterable find(WidgetTester tester) { return tester.elementTreeTester.findElements((Element element) { if (element.widget is! Text) return false; Text textWidget = element.widget; return textWidget.data == text; }); } } class _WidgetWithTextFinder extends Finder { _WidgetWithTextFinder(this.widgetType, this.text); final Type widgetType; final String text; @override String get description => 'type $widgetType with text "$text"'; @override Iterable find(WidgetTester tester) { return tester.elementTreeTester.allElements .map((Element textElement) { if (textElement.widget is! Text) return null; Text textWidget = textElement.widget; if (textWidget.data == text) { Element parent; textElement.visitAncestorElements((Element element) { if (element.widget.runtimeType == widgetType) { parent = element; return false; } return true; }); return parent; } return null; }) .where((Element element) => element != null); } } class _KeyFinder extends Finder { _KeyFinder(this.key); final Key key; @override String get description => 'key $key'; @override Iterable find(WidgetTester tester) { return tester.elementTreeTester.findElements((Element element) => element.widget.key == key); } } class _TypeFinder extends Finder { _TypeFinder(this.widgetType); final Type widgetType; @override String get description => 'type "$widgetType"'; @override Iterable find(WidgetTester tester) { return tester.elementTreeTester.allElements.where((Element element) { return element.widget.runtimeType == widgetType; }); } } class _ConfigFinder extends Finder { _ConfigFinder(this.config); final Widget config; @override String get description => 'the given configuration ($config)'; @override Iterable find(WidgetTester tester) { return tester.elementTreeTester.allElements.where((Element element) { return element.widget == config; }); } } typedef bool WidgetPredicate(Widget element); typedef bool ElementPredicate(Element element); class _ElementFinder extends Finder { _ElementFinder(this.predicate); final ElementPredicate predicate; @override String get description => 'element satisfying given predicate ($predicate)'; @override Iterable find(WidgetTester tester) { return tester.elementTreeTester.allElements.where(predicate); } } class _HasWidgetMatcher extends Matcher { const _HasWidgetMatcher(this.finder); final Finder finder; @override bool matches(WidgetTester tester, Map matchState) { return tester.exists(finder); } @override Description describe(Description description) { return description.add('$finder exists in the element tree'); } @override Description describeMismatch( dynamic item, Description mismatchDescription, Map matchState, bool verbose) { return mismatchDescription.add('Does not contain $finder'); } } class _DoesNotHaveWidgetMatcher extends Matcher { const _DoesNotHaveWidgetMatcher(this.finder); final Finder finder; @override bool matches(WidgetTester tester, Map matchState) { return !tester.exists(finder); } @override Description describe(Description description) { return description.add('$finder does not exist in the element tree'); } @override Description describeMismatch( dynamic item, Description mismatchDescription, Map matchState, bool verbose) { return mismatchDescription.add('Contains $finder'); } }