Added find.ancestor to CommonFinders (#13691)


diff --git a/packages/flutter_test/lib/src/finders.dart b/packages/flutter_test/lib/src/finders.dart
index 4e81551..73f8f90 100644
--- a/packages/flutter_test/lib/src/finders.dart
+++ b/packages/flutter_test/lib/src/finders.dart
@@ -198,6 +198,29 @@
   Finder descendant({ Finder of, Finder matching, bool matchRoot: false, bool skipOffstage: true }) {
     return new _DescendantFinder(of, matching, matchRoot: matchRoot, skipOffstage: skipOffstage);
   }
+
+  /// Finds widgets that are ancestors of the [of] parameter and that match
+  /// the [matching] parameter.
+  ///
+  /// Example:
+  ///
+  ///     // Test if a Text widget that contains 'faded' is the
+  ///     // descendant of an Opacity widget with opacity 0.5:
+  ///     expect(
+  ///       tester.widget<Opacity>(
+  ///         find.ancestor(
+  ///           of: find.text('faded'),
+  ///           matching: find.byType('Opacity'),
+  ///         )
+  ///       ).opacity,
+  ///       0.5
+  ///     );
+  ///
+  /// If the [matchRoot] argument is true then the widget(s) specified by [of]
+  /// will be matched along with the ancestors.
+  Finder ancestor({ Finder of, Finder matching, bool matchRoot: false}) {
+    return new _AncestorFinder(of, matching, matchRoot: matchRoot);
+  }
 }
 
 /// Searches a widget tree and returns nodes that match a particular
@@ -583,3 +606,39 @@
     return candidates;
   }
 }
+
+class _AncestorFinder extends Finder {
+  _AncestorFinder(this.descendant, this.ancestor, { this.matchRoot: false }) : super(skipOffstage: false);
+
+  final Finder ancestor;
+  final Finder descendant;
+  final bool matchRoot;
+
+  @override
+  String get description {
+    if (matchRoot)
+      return 'ancestor ${ancestor.description} beginning with ${descendant.description}';
+    return '${ancestor.description} which is an ancestor of ${descendant.description}';
+  }
+
+  @override
+  Iterable<Element> apply(Iterable<Element> candidates) {
+    return candidates.where((Element element) => ancestor.evaluate().contains(element));
+  }
+
+  @override
+  Iterable<Element> get allCandidates {
+    final List<Element> candidates = <Element>[];
+    for (Element root in descendant.evaluate()) {
+      final List<Element> ancestors = <Element>[];
+      if (matchRoot)
+        ancestors.add(root);
+      root.visitAncestorElements((Element element) {
+        ancestors.add(element);
+        return true;
+      });
+      candidates.addAll(ancestors);
+    }
+    return candidates;
+  }
+}
diff --git a/packages/flutter_test/test/widget_tester_test.dart b/packages/flutter_test/test/widget_tester_test.dart
index af4903b..85e38005 100644
--- a/packages/flutter_test/test/widget_tester_test.dart
+++ b/packages/flutter_test/test/widget_tester_test.dart
@@ -198,6 +198,66 @@
         contains('Actual: ?:<zero widgets with text "bar" that has ancestor(s) with type Column with text "foo"')
       );
     });
+  });
+
+  group('find.ancestor', () {
+    testWidgets('finds one ancestor', (WidgetTester tester) async {
+      await tester.pumpWidget(new Row(
+        textDirection: TextDirection.ltr,
+        children: <Widget>[
+          new Column(children: fooBarTexts),
+        ],
+      ));
+
+      expect(find.ancestor(
+        of: find.text('bar'),
+        matching: find.widgetWithText(Row, 'foo'),
+      ), findsOneWidget);
+    });
+
+    testWidgets('finds two matching ancestors, one descendant', (WidgetTester tester) async {
+      await tester.pumpWidget(
+        new Directionality(
+          textDirection: TextDirection.ltr,
+          child: new Row(
+            children: <Widget>[
+              new Row(children: fooBarTexts),
+            ],
+          ),
+        ),
+      );
+
+      expect(find.ancestor(
+        of: find.text('bar'),
+        matching: find.byType(Row),
+      ), findsNWidgets(2));
+    });
+
+    testWidgets('fails with a descriptive message', (WidgetTester tester) async {
+      await tester.pumpWidget(new Row(
+        textDirection: TextDirection.ltr,
+        children: <Widget>[
+          new Column(children: <Text>[const Text('foo', textDirection: TextDirection.ltr)]),
+          const Text('bar', textDirection: TextDirection.ltr),
+        ],
+      ));
+
+      TestFailure failure;
+      try {
+        expect(find.ancestor(
+          of: find.text('bar'),
+          matching: find.widgetWithText(Column, 'foo'),
+        ), findsOneWidget);
+      } catch (e) {
+        failure = e;
+      }
+
+      expect(failure, isNotNull);
+      expect(
+        failure.message,
+        contains('Actual: ?:<zero widgets with type Column with text "foo" which is an ancestor of text "bar"'),
+      );
+    });
 
     testWidgets('Root not matched by default', (WidgetTester tester) async {
       await tester.pumpWidget(new Row(
@@ -207,9 +267,9 @@
         ],
       ));
 
-      expect(find.descendant(
-        of: find.widgetWithText(Row, 'foo'),
-        matching: find.byType(Row),
+      expect(find.ancestor(
+        of: find.byType(Column),
+        matching: find.widgetWithText(Column, 'foo'),
       ), findsNothing);
     });
 
@@ -222,12 +282,11 @@
       ));
 
       expect(find.descendant(
-        of: find.widgetWithText(Row, 'foo'),
-        matching: find.byType(Row),
+        of: find.byType(Column),
+        matching: find.widgetWithText(Column, 'foo'),
         matchRoot: true,
       ), findsOneWidget);
     });
-
   });
 
   testWidgets('hasRunningAnimations control test', (WidgetTester tester) async {