blob: 610b638d04971e8ae7b02ebfebfae23a44890b57 [file]
// Copyright 2016 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/widgets.dart';
import 'all_elements.dart';
/// Signature for [CommonFinders.byPredicate].
typedef bool WidgetPredicate(Widget widget);
/// Signature for [CommonFinders.byElement].
typedef bool ElementPredicate(Element element);
/// Some frequently used widget [Finder]s.
final CommonFinders find = const CommonFinders._();
/// Provides lightweight syntax for getting frequently used widget [Finder]s.
///
/// This class is instantiated once, as [find].
class CommonFinders {
const CommonFinders._();
/// Finds [Text] widgets containing string equal to the `text`
/// argument.
///
/// Example:
///
/// expect(tester, hasWidget(find.text('Back')));
Finder text(String text) => new _TextFinder(text);
/// Looks for widgets that contain a [Text] descendant 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) {
return new _WidgetWithTextFinder(widgetType, text);
}
/// Finds widgets by searching for one with a particular [Key].
///
/// Example:
///
/// expect(tester, hasWidget(find.byKey(backKey)));
Finder byKey(Key key) => new _KeyFinder(key);
/// Finds widgets by searching for widgets with a particular type.
///
/// This does not do subclass tests, so for example
/// `byType(StatefulWidget)` will never find anything since that's
/// an abstract class.
///
/// The `type` argument must be a subclass of [Widget].
///
/// Example:
///
/// expect(tester, hasWidget(find.byType(IconButton)));
Finder byType(Type type) => new _WidgetTypeFinder(type);
/// Finds widgets by searching for elements with a particular type.
///
/// This does not do subclass tests, so for example
/// `byElementType(VirtualViewportElement)` will never find anything
/// since that's an abstract class.
///
/// The `type` argument must be a subclass of [Element].
///
/// Example:
///
/// expect(tester, hasWidget(find.byElementType(SingleChildRenderObjectElement)));
Finder byElementType(Type type) => new _ElementTypeFinder(type);
/// 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);
/// Finds widgets using a widget predicate.
///
/// Example:
///
/// expect(tester, hasWidget(find.byWidgetPredicate(
/// (Widget widget) => widget is Tooltip && widget.message == 'Back'
/// )));
Finder byWidgetPredicate(WidgetPredicate predicate) {
return new _WidgetPredicateFinder(predicate);
}
/// Finds widgets using an element predicate.
///
/// Example:
///
/// expect(tester, hasWidget(find.byWidgetPredicate(
/// // finds elements of type SingleChildRenderObjectElement, including
/// // those that are actually subclasses of that type.
/// // (contrast with byElementType, which only returns exact matches)
/// (Element element) => element is SingleChildRenderObjectElement
/// )));
Finder byElementPredicate(ElementPredicate predicate) {
return new _ElementPredicateFinder(predicate);
}
}
/// Searches a widget tree and returns nodes that match a particular
/// pattern.
abstract class Finder {
/// Describes what the finder is looking for. The description should be
/// a brief English noun phrase describing the finder's pattern.
String get description;
/// Returns all the elements in the given list that match this
/// finder's pattern.
///
/// When implementing your own Finders that inherit directly from
/// [Finder], this is the main method to override. If your finder
/// can efficiently be described just in terms of a predicate
/// function, consider extending [MatchFinder] instead.
Iterable<Element> apply(Iterable<Element> candidates);
// Right now this is hard-coded to just grab the elements from the binding.
//
// One could imagine a world where CommonFinders and Finder can be configured
// to work from a specific subtree, but we'll implement that when it's needed.
static Iterable<Element> get _allElements => collectAllElementsFrom(WidgetsBinding.instance.renderViewElement);
Iterable<Element> _cachedResult;
/// Returns the current result. If [precache] was called and returned true, this will
/// cheaply return the result that was computed then. Otherwise, it creates a new
/// iterable to compute the answer.
///
/// Calling this clears the cache from [precache].
Iterable<Element> evaluate() {
final Iterable<Element> result = _cachedResult ?? apply(_allElements);
_cachedResult = null;
return result;
}
/// Attempts to evaluate the finder. Returns whether any elements in the tree
/// matched the finder. If any did, then the result is cached and can be obtained
/// from [evaluate].
///
/// If this returns true, you must call [evaluate] before you call [precache] again.
bool precache() {
assert(_cachedResult == null);
final Iterable<Element> result = apply(_allElements);
if (result.isNotEmpty) {
_cachedResult = result;
return true;
}
_cachedResult = null;
return false;
}
@override
String toString() {
final List<Element> widgets = evaluate().toList();
final int count = widgets.length;
if (count == 0)
return 'zero widgets with $description';
if (count == 1)
return 'exactly one widget with $description: ${widgets.single}';
if (count < 4)
return '$count widgets with $description: $widgets';
return '$count widgets with $description: ${widgets[0]}, ${widgets[1]}, ${widgets[2]}, ...';
}
}
/// Searches a widget tree and returns nodes that match a particular
/// pattern.
abstract class MatchFinder extends Finder {
/// Returns true if the given element matches the pattern.
///
/// When implementing your own MatchFinder, this is the main method to override.
bool matches(Element candidate);
@override
Iterable<Element> apply(Iterable<Element> candidates) {
return candidates.where(matches);
}
}
class _TextFinder extends MatchFinder {
_TextFinder(this.text);
final String text;
@override
String get description => 'text "$text"';
@override
bool matches(Element candidate) {
if (candidate.widget is! Text)
return false;
Text textWidget = candidate.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<Element> apply(Iterable<Element> candidates) {
return candidates
.map((Element textElement) {
if (textElement.widget is! Text)
return null;
Text textWidget = textElement.widget;
if (textWidget.data == text) {
try {
textElement.visitAncestorElements((Element element) {
if (element.widget.runtimeType == widgetType)
throw element;
return true;
});
} on Element catch (result) {
return result;
}
}
return null;
})
.where((Element element) => element != null);
}
}
class _KeyFinder extends MatchFinder {
_KeyFinder(this.key);
final Key key;
@override
String get description => 'key $key';
@override
bool matches(Element candidate) {
return candidate.widget.key == key;
}
}
class _WidgetTypeFinder extends MatchFinder {
_WidgetTypeFinder(this.widgetType);
final Type widgetType;
@override
String get description => 'type "$widgetType"';
@override
bool matches(Element candidate) {
return candidate.widget.runtimeType == widgetType;
}
}
class _ElementTypeFinder extends MatchFinder {
_ElementTypeFinder(this.elementType);
final Type elementType;
@override
String get description => 'type "$elementType"';
@override
bool matches(Element candidate) {
return candidate.runtimeType == elementType;
}
}
class _ConfigFinder extends MatchFinder {
_ConfigFinder(this.config);
final Widget config;
@override
String get description => 'the given configuration ($config)';
@override
bool matches(Element candidate) {
return candidate.widget == config;
}
}
class _WidgetPredicateFinder extends MatchFinder {
_WidgetPredicateFinder(this.predicate);
final WidgetPredicate predicate;
@override
String get description => 'widget matching predicate ($predicate)';
@override
bool matches(Element candidate) {
return predicate(candidate.widget);
}
}
class _ElementPredicateFinder extends MatchFinder {
_ElementPredicateFinder(this.predicate);
final ElementPredicate predicate;
@override
String get description => 'element matching predicate ($predicate)';
@override
bool matches(Element candidate) {
return predicate(candidate);
}
}