finders.dart 23.8 KB
Newer Older
1 2 3 4
// 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.

5
import 'package:flutter/gestures.dart';
6
import 'package:flutter/material.dart';
7
import 'package:meta/meta.dart';
8 9 10

import 'all_elements.dart';

11
/// Signature for [CommonFinders.byWidgetPredicate].
12
typedef WidgetPredicate = bool Function(Widget widget);
13

14
/// Signature for [CommonFinders.byElementPredicate].
15
typedef ElementPredicate = bool Function(Element element);
16 17

/// Some frequently used widget [Finder]s.
18
const CommonFinders find = CommonFinders._();
19 20 21 22 23 24 25

/// Provides lightweight syntax for getting frequently used widget [Finder]s.
///
/// This class is instantiated once, as [find].
class CommonFinders {
  const CommonFinders._();

26 27
  /// Finds [Text] and [EditableText] widgets containing string equal to the
  /// `text` argument.
28
  ///
29
  /// ## Sample code
30
  ///
31 32 33
  /// ```dart
  /// expect(find.text('Back'), findsOneWidget);
  /// ```
34
  ///
35 36
  /// If the `skipOffstage` argument is true (the default), then this skips
  /// nodes that are [Offstage] or that are from inactive [Route]s.
37
  Finder text(String text, { bool skipOffstage = true }) => _TextFinder(text, skipOffstage: skipOffstage);
38 39 40 41

  /// Looks for widgets that contain a [Text] descendant with `text`
  /// in it.
  ///
42
  /// ## Sample code
43
  ///
44 45 46 47 48
  /// ```dart
  /// // Suppose you have a button with text 'Update' in it:
  /// new Button(
  ///   child: new Text('Update')
  /// )
49
  ///
50 51 52
  /// // You can find and tap on it like this:
  /// tester.tap(find.widgetWithText(Button, 'Update'));
  /// ```
53
  ///
54 55
  /// If the `skipOffstage` argument is true (the default), then this skips
  /// nodes that are [Offstage] or that are from inactive [Route]s.
56
  Finder widgetWithText(Type widgetType, String text, { bool skipOffstage = true }) {
57
    return find.ancestor(
58 59
      of: find.text(text, skipOffstage: skipOffstage),
      matching: find.byType(widgetType, skipOffstage: skipOffstage),
60
    );
61 62 63 64
  }

  /// Finds widgets by searching for one with a particular [Key].
  ///
65
  /// ## Sample code
66
  ///
67 68 69
  /// ```dart
  /// expect(find.byKey(backKey), findsOneWidget);
  /// ```
70
  ///
71 72
  /// If the `skipOffstage` argument is true (the default), then this skips
  /// nodes that are [Offstage] or that are from inactive [Route]s.
73
  Finder byKey(Key key, { bool skipOffstage = true }) => _KeyFinder(key, skipOffstage: skipOffstage);
74 75 76 77 78 79 80 81 82

  /// 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].
  ///
83
  /// ## Sample code
84
  ///
85 86 87
  /// ```dart
  /// expect(find.byType(IconButton), findsOneWidget);
  /// ```
88
  ///
89 90
  /// If the `skipOffstage` argument is true (the default), then this skips
  /// nodes that are [Offstage] or that are from inactive [Route]s.
91
  Finder byType(Type type, { bool skipOffstage = true }) => _WidgetTypeFinder(type, skipOffstage: skipOffstage);
92

93 94
  /// Finds [Icon] widgets containing icon data equal to the `icon`
  /// argument.
95
  ///
96
  /// ## Sample code
97
  ///
98 99 100
  /// ```dart
  /// expect(find.byIcon(Icons.inbox), findsOneWidget);
  /// ```
101
  ///
102 103
  /// If the `skipOffstage` argument is true (the default), then this skips
  /// nodes that are [Offstage] or that are from inactive [Route]s.
104
  Finder byIcon(IconData icon, { bool skipOffstage = true }) => _WidgetIconFinder(icon, skipOffstage: skipOffstage);
105

106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
  /// Looks for widgets that contain an [Icon] descendant displaying [IconData]
  /// `icon` in it.
  ///
  /// ## Sample code
  ///
  /// ```dart
  /// // Suppose you have a button with icon 'arrow_forward' in it:
  /// new Button(
  ///   child: new Icon(Icons.arrow_forward)
  /// )
  ///
  /// // You can find and tap on it like this:
  /// tester.tap(find.widgetWithIcon(Button, Icons.arrow_forward));
  /// ```
  ///
121 122
  /// If the `skipOffstage` argument is true (the default), then this skips
  /// nodes that are [Offstage] or that are from inactive [Route]s.
123
  Finder widgetWithIcon(Type widgetType, IconData icon, { bool skipOffstage = true }) {
124 125 126 127 128 129
    return find.ancestor(
      of: find.byIcon(icon),
      matching: find.byType(widgetType),
    );
  }

130 131 132 133 134 135 136 137
  /// 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].
  ///
138
  /// ## Sample code
139
  ///
140 141 142
  /// ```dart
  /// expect(find.byElementType(SingleChildRenderObjectElement), findsOneWidget);
  /// ```
143
  ///
144 145
  /// If the `skipOffstage` argument is true (the default), then this skips
  /// nodes that are [Offstage] or that are from inactive [Route]s.
146
  Finder byElementType(Type type, { bool skipOffstage = true }) => _ElementTypeFinder(type, skipOffstage: skipOffstage);
147 148 149 150

  /// Finds widgets whose current widget is the instance given by the
  /// argument.
  ///
151
  /// ## Sample code
152
  ///
153 154 155 156 157
  /// ```dart
  /// // Suppose you have a button created like this:
  /// Widget myButton = new Button(
  ///   child: new Text('Update')
  /// );
158
  ///
159 160 161
  /// // You can find and tap on it like this:
  /// tester.tap(find.byWidget(myButton));
  /// ```
162
  ///
163 164
  /// If the `skipOffstage` argument is true (the default), then this skips
  /// nodes that are [Offstage] or that are from inactive [Route]s.
165
  Finder byWidget(Widget widget, { bool skipOffstage = true }) => _WidgetFinder(widget, skipOffstage: skipOffstage);
166

167
  /// Finds widgets using a widget [predicate].
168
  ///
169
  /// ## Sample code
170
  ///
171 172 173 174 175 176
  /// ```dart
  /// expect(find.byWidgetPredicate(
  ///   (Widget widget) => widget is Tooltip && widget.message == 'Back',
  ///   description: 'widget with tooltip "Back"',
  /// ), findsOneWidget);
  /// ```
177
  ///
178 179 180 181 182
  /// If [description] is provided, then this uses it as the description of the
  /// [Finder] and appears, for example, in the error message when the finder
  /// fails to locate the desired widget. Otherwise, the description prints the
  /// signature of the predicate function.
  ///
183 184
  /// If the `skipOffstage` argument is true (the default), then this skips
  /// nodes that are [Offstage] or that are from inactive [Route]s.
185
  Finder byWidgetPredicate(WidgetPredicate predicate, { String description, bool skipOffstage = true }) {
186
    return _WidgetPredicateFinder(predicate, description: description, skipOffstage: skipOffstage);
187 188
  }

189 190
  /// Finds Tooltip widgets with the given message.
  ///
191
  /// ## Sample code
192
  ///
193 194 195
  /// ```dart
  /// expect(find.byTooltip('Back'), findsOneWidget);
  /// ```
196
  ///
197 198
  /// If the `skipOffstage` argument is true (the default), then this skips
  /// nodes that are [Offstage] or that are from inactive [Route]s.
199
  Finder byTooltip(String message, { bool skipOffstage = true }) {
200 201 202 203 204 205
    return byWidgetPredicate(
      (Widget widget) => widget is Tooltip && widget.message == message,
      skipOffstage: skipOffstage,
    );
  }

206
  /// Finds widgets using an element [predicate].
207
  ///
208
  /// ## Sample code
209
  ///
210 211 212 213 214 215 216 217 218
  /// ```dart
  /// expect(find.byElementPredicate(
  ///   // 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,
  ///   description: '$SingleChildRenderObjectElement element',
  /// ), findsOneWidget);
  /// ```
219
  ///
220 221 222 223 224
  /// If [description] is provided, then this uses it as the description of the
  /// [Finder] and appears, for example, in the error message when the finder
  /// fails to locate the desired widget. Otherwise, the description prints the
  /// signature of the predicate function.
  ///
225 226
  /// If the `skipOffstage` argument is true (the default), then this skips
  /// nodes that are [Offstage] or that are from inactive [Route]s.
227
  Finder byElementPredicate(ElementPredicate predicate, { String description, bool skipOffstage = true }) {
228
    return _ElementPredicateFinder(predicate, description: description, skipOffstage: skipOffstage);
229
  }
230

231 232
  /// Finds widgets that are descendants of the [of] parameter and that match
  /// the [matching] parameter.
233
  ///
234
  /// ## Sample code
235
  ///
236 237 238 239 240
  /// ```dart
  /// expect(find.descendant(
  ///   of: find.widgetWithText(Row, 'label_1'), matching: find.text('value_1')
  /// ), findsOneWidget);
  /// ```
241
  ///
242 243 244
  /// If the [matchRoot] argument is true then the widget(s) specified by [of]
  /// will be matched along with the descendants.
  ///
245 246 247
  /// If the [skipOffstage] argument is true (the default), then nodes that are
  /// [Offstage] or that are from inactive [Route]s are skipped.
  Finder descendant({ Finder of, Finder matching, bool matchRoot = false, bool skipOffstage = true }) {
248
    return _DescendantFinder(of, matching, matchRoot: matchRoot, skipOffstage: skipOffstage);
249
  }
250 251 252 253

  /// Finds widgets that are ancestors of the [of] parameter and that match
  /// the [matching] parameter.
  ///
254
  /// ## Sample code
255
  ///
256 257 258 259 260 261 262 263 264 265 266 267 268
  /// ```dart
  /// // 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
  /// );
  /// ```
269 270 271
  ///
  /// If the [matchRoot] argument is true then the widget(s) specified by [of]
  /// will be matched along with the ancestors.
272
  Finder ancestor({ Finder of, Finder matching, bool matchRoot = false }) {
273
    return _AncestorFinder(of, matching, matchRoot: matchRoot);
274
  }
275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319

  /// Finds [Semantics] widgets matching the given `label`, either by
  /// [RegExp.hasMatch] or string equality.
  ///
  /// The framework may combine semantics labels in certain scenarios, such as
  /// when multiple [Text] widgets are in a [MaterialButton] widget. In such a
  /// case, it may be preferable to match by regular expression. Consumers of
  /// this API __must not__ introduce unsuitable content into the semantics tree
  /// for the purposes of testing; in particular, you should prefer matching by
  /// regular expression rather than by string if the framework has combined
  /// your semantics, and not try to force the framework to break up the
  /// semantics nodes. Breaking up the nodes would have an undesirable effect on
  /// screen readers and other accessibility services.
  ///
  /// ## Sample code
  ///
  /// ```dart
  /// expect(find.BySemanticsLabel('Back'), findsOneWidget);
  /// ```
  ///
  /// If the `skipOffstage` argument is true (the default), then this skips
  /// nodes that are [Offstage] or that are from inactive [Route]s.
  Finder bySemanticsLabel(Pattern label, { bool skipOffstage = true }) {
    if (WidgetsBinding.instance.pipelineOwner.semanticsOwner == null)
      throw StateError('Semantics are not enabled. '
                       'Make sure to call tester.enableSemantics() before using '
                       'this finder, and call dispose on its return value after.');
    return byElementPredicate(
      (Element element) {
        // Multiple elements can have the same renderObject - we want the "owner"
        // of the renderObject, i.e. the RenderObjectElement.
        if (element is! RenderObjectElement) {
          return false;
        }
        final String semanticsLabel = element.renderObject?.debugSemantics?.label;
        if (semanticsLabel == null) {
          return false;
        }
        return label is RegExp
            ? label.hasMatch(semanticsLabel)
            : label == semanticsLabel;
      },
      skipOffstage: skipOffstage,
    );
  }
320 321 322 323 324
}

/// Searches a widget tree and returns nodes that match a particular
/// pattern.
abstract class Finder {
325
  /// Initializes a Finder. Used by subclasses to initialize the [skipOffstage]
326
  /// property.
327
  Finder({ this.skipOffstage = true });
328

329 330 331 332 333 334 335 336 337 338 339 340 341
  /// 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);

342 343
  /// Whether this finder skips nodes that are offstage.
  ///
344 345 346
  /// If this is true, then the elements are walked using
  /// [Element.debugVisitOnstageChildren]. This skips offstage children of
  /// [Offstage] widgets, as well as children of inactive [Route]s.
347 348
  final bool skipOffstage;

349 350 351
  /// Returns all the [Element]s that will be considered by this finder.
  ///
  /// See [collectAllElementsFrom].
352 353
  @protected
  Iterable<Element> get allCandidates {
354 355
    return collectAllElementsFrom(
      WidgetsBinding.instance.renderViewElement,
356
      skipOffstage: skipOffstage,
357 358
    );
  }
359 360 361 362 363 364 365 366 367

  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() {
368
    final Iterable<Element> result = _cachedResult ?? apply(allCandidates);
369 370 371 372 373 374 375 376 377 378 379
    _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);
380
    final Iterable<Element> result = apply(allCandidates);
381 382 383 384 385 386 387 388
    if (result.isNotEmpty) {
      _cachedResult = result;
      return true;
    }
    _cachedResult = null;
    return false;
  }

389 390
  /// Returns a variant of this finder that only matches the first element
  /// matched by this finder.
391
  Finder get first => _FirstFinder(this);
392 393 394

  /// Returns a variant of this finder that only matches the last element
  /// matched by this finder.
395
  Finder get last => _LastFinder(this);
396

397 398
  /// Returns a variant of this finder that only matches the element at the
  /// given index matched by this finder.
399
  Finder at(int index) => _IndexFinder(this, index);
400

401 402 403 404 405
  /// Returns a variant of this finder that only matches elements reachable by
  /// a hit test.
  ///
  /// The [at] parameter specifies the location relative to the size of the
  /// target element where the hit test is performed.
406
  Finder hitTestable({ Alignment at = Alignment.center }) => _HitTestableFinder(this, at);
407

408 409
  @override
  String toString() {
410
    final String additional = skipOffstage ? ' (ignoring offstage widgets)' : '';
411 412 413
    final List<Element> widgets = evaluate().toList();
    final int count = widgets.length;
    if (count == 0)
414
      return 'zero widgets with $description$additional';
415
    if (count == 1)
416
      return 'exactly one widget with $description$additional: ${widgets.single}';
417
    if (count < 4)
418 419
      return '$count widgets with $description$additional: $widgets';
    return '$count widgets with $description$additional: ${widgets[0]}, ${widgets[1]}, ${widgets[2]}, ...';
420 421 422
  }
}

423 424 425 426
/// Applies additional filtering against a [parent] [Finder].
abstract class ChainedFinder extends Finder {
  /// Create a Finder chained against the candidates of another [Finder].
  ChainedFinder(this.parent) : assert(parent != null);
427

428
  /// Another [Finder] that will run first.
429 430
  final Finder parent;

431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448
  /// Return another [Iterable] when given an [Iterable] of candidates from a
  /// parent [Finder].
  ///
  /// This is the method to implement when subclassing [ChainedFinder].
  Iterable<Element> filter(Iterable<Element> parentCandidates);

  @override
  Iterable<Element> apply(Iterable<Element> candidates) {
    return filter(parent.apply(candidates));
  }

  @override
  Iterable<Element> get allCandidates => parent.allCandidates;
}

class _FirstFinder extends ChainedFinder {
  _FirstFinder(Finder parent) : super(parent);

449 450 451 452
  @override
  String get description => '${parent.description} (ignoring all but first)';

  @override
453 454
  Iterable<Element> filter(Iterable<Element> parentCandidates) sync* {
    yield parentCandidates.first;
455 456 457
  }
}

458 459
class _LastFinder extends ChainedFinder {
  _LastFinder(Finder parent) : super(parent);
460 461 462 463 464

  @override
  String get description => '${parent.description} (ignoring all but last)';

  @override
465 466
  Iterable<Element> filter(Iterable<Element> parentCandidates) sync* {
    yield parentCandidates.last;
467 468 469
  }
}

470 471
class _IndexFinder extends ChainedFinder {
  _IndexFinder(Finder parent, this.index) : super(parent);
472 473 474 475 476 477 478

  final int index;

  @override
  String get description => '${parent.description} (ignoring all but index $index)';

  @override
479 480
  Iterable<Element> filter(Iterable<Element> parentCandidates) sync* {
    yield parentCandidates.elementAt(index);
481 482 483
  }
}

484 485
class _HitTestableFinder extends ChainedFinder {
  _HitTestableFinder(Finder parent, this.alignment) : super(parent);
486

487
  final Alignment alignment;
488 489 490 491 492

  @override
  String get description => '${parent.description} (considering only hit-testable ones)';

  @override
493 494
  Iterable<Element> filter(Iterable<Element> parentCandidates) sync* {
    for (final Element candidate in parentCandidates) {
495 496
      final RenderBox box = candidate.renderObject;
      assert(box != null);
497
      final Offset absoluteOffset = box.localToGlobal(alignment.alongSize(box.size));
498
      final HitTestResult hitResult = HitTestResult();
499 500 501 502 503 504 505 506 507 508 509
      WidgetsBinding.instance.hitTest(hitResult, absoluteOffset);
      for (final HitTestEntry entry in hitResult.path) {
        if (entry.target == candidate.renderObject) {
          yield candidate;
          break;
        }
      }
    }
  }
}

510 511 512
/// Searches a widget tree and returns nodes that match a particular
/// pattern.
abstract class MatchFinder extends Finder {
513
  /// Initializes a predicate-based Finder. Used by subclasses to initialize the
514
  /// [skipOffstage] property.
515
  MatchFinder({ bool skipOffstage = true }) : super(skipOffstage: skipOffstage);
516

517 518 519 520 521 522 523 524 525 526 527 528
  /// 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 {
529
  _TextFinder(this.text, { bool skipOffstage = true }) : super(skipOffstage: skipOffstage);
530 531 532 533 534 535 536 537

  final String text;

  @override
  String get description => 'text "$text"';

  @override
  bool matches(Element candidate) {
538 539
    if (candidate.widget is Text) {
      final Text textWidget = candidate.widget;
540 541 542
      if (textWidget.data != null)
        return textWidget.data == text;
      return textWidget.textSpan.toPlainText() == text;
543 544 545 546 547
    } else if (candidate.widget is EditableText) {
      final EditableText editable = candidate.widget;
      return editable.controller.text == text;
    }
    return false;
548 549 550 551
  }
}

class _KeyFinder extends MatchFinder {
552
  _KeyFinder(this.key, { bool skipOffstage = true }) : super(skipOffstage: skipOffstage);
553 554 555 556 557 558 559 560 561 562 563 564 565

  final Key key;

  @override
  String get description => 'key $key';

  @override
  bool matches(Element candidate) {
    return candidate.widget.key == key;
  }
}

class _WidgetTypeFinder extends MatchFinder {
566
  _WidgetTypeFinder(this.widgetType, { bool skipOffstage = true }) : super(skipOffstage: skipOffstage);
567 568 569 570 571 572 573 574 575 576 577 578

  final Type widgetType;

  @override
  String get description => 'type "$widgetType"';

  @override
  bool matches(Element candidate) {
    return candidate.widget.runtimeType == widgetType;
  }
}

579
class _WidgetIconFinder extends MatchFinder {
580
  _WidgetIconFinder(this.icon, { bool skipOffstage = true }) : super(skipOffstage: skipOffstage);
581 582 583 584 585 586 587 588 589 590 591 592 593

  final IconData icon;

  @override
  String get description => 'icon "$icon"';

  @override
  bool matches(Element candidate) {
    final Widget widget = candidate.widget;
    return widget is Icon && widget.icon == icon;
  }
}

594
class _ElementTypeFinder extends MatchFinder {
595
  _ElementTypeFinder(this.elementType, { bool skipOffstage = true }) : super(skipOffstage: skipOffstage);
596 597 598 599 600 601 602 603 604 605 606 607

  final Type elementType;

  @override
  String get description => 'type "$elementType"';

  @override
  bool matches(Element candidate) {
    return candidate.runtimeType == elementType;
  }
}

608
class _WidgetFinder extends MatchFinder {
609
  _WidgetFinder(this.widget, { bool skipOffstage = true }) : super(skipOffstage: skipOffstage);
610

611
  final Widget widget;
612 613

  @override
614
  String get description => 'the given widget ($widget)';
615 616 617

  @override
  bool matches(Element candidate) {
618
    return candidate.widget == widget;
619 620 621 622
  }
}

class _WidgetPredicateFinder extends MatchFinder {
623
  _WidgetPredicateFinder(this.predicate, { String description, bool skipOffstage = true })
624 625
    : _description = description,
      super(skipOffstage: skipOffstage);
626 627

  final WidgetPredicate predicate;
628
  final String _description;
629 630

  @override
631
  String get description => _description ?? 'widget matching predicate ($predicate)';
632 633 634 635 636 637 638 639

  @override
  bool matches(Element candidate) {
    return predicate(candidate.widget);
  }
}

class _ElementPredicateFinder extends MatchFinder {
640
  _ElementPredicateFinder(this.predicate, { String description, bool skipOffstage = true })
641 642
    : _description = description,
      super(skipOffstage: skipOffstage);
643 644

  final ElementPredicate predicate;
645
  final String _description;
646 647

  @override
648
  String get description => _description ?? 'element matching predicate ($predicate)';
649 650 651 652 653 654

  @override
  bool matches(Element candidate) {
    return predicate(candidate);
  }
}
655 656

class _DescendantFinder extends Finder {
657 658 659
  _DescendantFinder(
    this.ancestor,
    this.descendant, {
660 661
    this.matchRoot = false,
    bool skipOffstage = true,
662
  }) : super(skipOffstage: skipOffstage);
663 664 665

  final Finder ancestor;
  final Finder descendant;
666
  final bool matchRoot;
667 668

  @override
669 670 671 672 673
  String get description {
    if (matchRoot)
      return '${descendant.description} in the subtree(s) beginning with ${ancestor.description}';
    return '${descendant.description} that has ancestor(s) with ${ancestor.description}';
  }
674 675 676 677 678 679 680 681

  @override
  Iterable<Element> apply(Iterable<Element> candidates) {
    return candidates.where((Element element) => descendant.evaluate().contains(element));
  }

  @override
  Iterable<Element> get allCandidates {
682
    final Iterable<Element> ancestorElements = ancestor.evaluate();
683
    final List<Element> candidates = ancestorElements.expand<Element>(
684 685
      (Element element) => collectAllElementsFrom(element, skipOffstage: skipOffstage)
    ).toSet().toList();
686 687 688
    if (matchRoot)
      candidates.insertAll(0, ancestorElements);
    return candidates;
689 690
  }
}
691 692

class _AncestorFinder extends Finder {
693
  _AncestorFinder(this.descendant, this.ancestor, { this.matchRoot = false }) : super(skipOffstage: false);
694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726

  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;
  }
}