finders.dart 25.1 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// 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 7
import 'package:flutter/material.dart' show Tooltip;
import 'package:flutter/widgets.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 42 43 44 45 46 47 48 49 50 51 52
  /// Finds [Text] and [EditableText] widgets which contain the given
  /// `pattern` argument.
  ///
  /// ## Sample code
  ///
  /// ```dart
  /// expect(find.textContain('Back'), findsOneWidget);
  /// expect(find.textContain(RegExp(r'(\w+)')), findsOneWidget);
  /// ```
  ///
  /// If the `skipOffstage` argument is true (the default), then this skips
  /// nodes that are [Offstage] or that are from inactive [Route]s.
  Finder textContaining(Pattern pattern, { bool skipOffstage = true }) => _TextContainingFinder(pattern, skipOffstage: skipOffstage);

53 54 55
  /// Looks for widgets that contain a [Text] descendant with `text`
  /// in it.
  ///
56
  /// ## Sample code
57
  ///
58 59
  /// ```dart
  /// // Suppose you have a button with text 'Update' in it:
Anas35's avatar
Anas35 committed
60 61
  /// Button(
  ///   child: Text('Update')
62
  /// )
63
  ///
64 65 66
  /// // You can find and tap on it like this:
  /// tester.tap(find.widgetWithText(Button, 'Update'));
  /// ```
67
  ///
68 69
  /// If the `skipOffstage` argument is true (the default), then this skips
  /// nodes that are [Offstage] or that are from inactive [Route]s.
70
  Finder widgetWithText(Type widgetType, String text, { bool skipOffstage = true }) {
71
    return find.ancestor(
72 73
      of: find.text(text, skipOffstage: skipOffstage),
      matching: find.byType(widgetType, skipOffstage: skipOffstage),
74
    );
75 76 77 78
  }

  /// Finds widgets by searching for one with a particular [Key].
  ///
79
  /// ## Sample code
80
  ///
81 82 83
  /// ```dart
  /// expect(find.byKey(backKey), findsOneWidget);
  /// ```
84
  ///
85 86
  /// If the `skipOffstage` argument is true (the default), then this skips
  /// nodes that are [Offstage] or that are from inactive [Route]s.
87
  Finder byKey(Key key, { bool skipOffstage = true }) => _KeyFinder(key, skipOffstage: skipOffstage);
88 89 90 91 92 93 94 95 96

  /// 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].
  ///
97
  /// ## Sample code
98
  ///
99 100 101
  /// ```dart
  /// expect(find.byType(IconButton), findsOneWidget);
  /// ```
102
  ///
103 104
  /// If the `skipOffstage` argument is true (the default), then this skips
  /// nodes that are [Offstage] or that are from inactive [Route]s.
105
  Finder byType(Type type, { bool skipOffstage = true }) => _WidgetTypeFinder(type, skipOffstage: skipOffstage);
106

107 108
  /// Finds [Icon] widgets containing icon data equal to the `icon`
  /// argument.
109
  ///
110
  /// ## Sample code
111
  ///
112 113 114
  /// ```dart
  /// expect(find.byIcon(Icons.inbox), findsOneWidget);
  /// ```
115
  ///
116 117
  /// If the `skipOffstage` argument is true (the default), then this skips
  /// nodes that are [Offstage] or that are from inactive [Route]s.
118
  Finder byIcon(IconData icon, { bool skipOffstage = true }) => _WidgetIconFinder(icon, skipOffstage: skipOffstage);
119

120 121 122 123 124 125 126
  /// 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:
Anas35's avatar
Anas35 committed
127 128
  /// Button(
  ///   child: Icon(Icons.arrow_forward)
129 130 131 132 133 134
  /// )
  ///
  /// // You can find and tap on it like this:
  /// tester.tap(find.widgetWithIcon(Button, Icons.arrow_forward));
  /// ```
  ///
135 136
  /// If the `skipOffstage` argument is true (the default), then this skips
  /// nodes that are [Offstage] or that are from inactive [Route]s.
137
  Finder widgetWithIcon(Type widgetType, IconData icon, { bool skipOffstage = true }) {
138 139 140 141 142 143
    return find.ancestor(
      of: find.byIcon(icon),
      matching: find.byType(widgetType),
    );
  }

144 145 146 147 148 149 150 151
  /// 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].
  ///
152
  /// ## Sample code
153
  ///
154 155 156
  /// ```dart
  /// expect(find.byElementType(SingleChildRenderObjectElement), findsOneWidget);
  /// ```
157
  ///
158 159
  /// If the `skipOffstage` argument is true (the default), then this skips
  /// nodes that are [Offstage] or that are from inactive [Route]s.
160
  Finder byElementType(Type type, { bool skipOffstage = true }) => _ElementTypeFinder(type, skipOffstage: skipOffstage);
161 162 163 164

  /// Finds widgets whose current widget is the instance given by the
  /// argument.
  ///
165
  /// ## Sample code
166
  ///
167 168
  /// ```dart
  /// // Suppose you have a button created like this:
Anas35's avatar
Anas35 committed
169 170
  /// Widget myButton = Button(
  ///   child: Text('Update')
171
  /// );
172
  ///
173 174 175
  /// // You can find and tap on it like this:
  /// tester.tap(find.byWidget(myButton));
  /// ```
176
  ///
177 178
  /// If the `skipOffstage` argument is true (the default), then this skips
  /// nodes that are [Offstage] or that are from inactive [Route]s.
179
  Finder byWidget(Widget widget, { bool skipOffstage = true }) => _WidgetFinder(widget, skipOffstage: skipOffstage);
180

181
  /// Finds widgets using a widget [predicate].
182
  ///
183
  /// ## Sample code
184
  ///
185 186 187 188 189 190
  /// ```dart
  /// expect(find.byWidgetPredicate(
  ///   (Widget widget) => widget is Tooltip && widget.message == 'Back',
  ///   description: 'widget with tooltip "Back"',
  /// ), findsOneWidget);
  /// ```
191
  ///
192 193 194 195 196
  /// 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.
  ///
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 byWidgetPredicate(WidgetPredicate predicate, { String? description, bool skipOffstage = true }) {
200
    return _WidgetPredicateFinder(predicate, description: description, skipOffstage: skipOffstage);
201 202
  }

203 204
  /// Finds Tooltip widgets with the given message.
  ///
205
  /// ## Sample code
206
  ///
207 208 209
  /// ```dart
  /// expect(find.byTooltip('Back'), findsOneWidget);
  /// ```
210
  ///
211 212
  /// If the `skipOffstage` argument is true (the default), then this skips
  /// nodes that are [Offstage] or that are from inactive [Route]s.
213
  Finder byTooltip(String message, { bool skipOffstage = true }) {
214 215 216 217 218 219
    return byWidgetPredicate(
      (Widget widget) => widget is Tooltip && widget.message == message,
      skipOffstage: skipOffstage,
    );
  }

220
  /// Finds widgets using an element [predicate].
221
  ///
222
  /// ## Sample code
223
  ///
224 225 226 227 228 229 230 231 232
  /// ```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);
  /// ```
233
  ///
234 235 236 237 238
  /// 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.
  ///
239 240
  /// If the `skipOffstage` argument is true (the default), then this skips
  /// nodes that are [Offstage] or that are from inactive [Route]s.
241
  Finder byElementPredicate(ElementPredicate predicate, { String? description, bool skipOffstage = true }) {
242
    return _ElementPredicateFinder(predicate, description: description, skipOffstage: skipOffstage);
243
  }
244

245 246
  /// Finds widgets that are descendants of the [of] parameter and that match
  /// the [matching] parameter.
247
  ///
248
  /// ## Sample code
249
  ///
250 251 252 253 254
  /// ```dart
  /// expect(find.descendant(
  ///   of: find.widgetWithText(Row, 'label_1'), matching: find.text('value_1')
  /// ), findsOneWidget);
  /// ```
255
  ///
256 257 258
  /// If the [matchRoot] argument is true then the widget(s) specified by [of]
  /// will be matched along with the descendants.
  ///
259 260
  /// If the [skipOffstage] argument is true (the default), then nodes that are
  /// [Offstage] or that are from inactive [Route]s are skipped.
Ian Hickson's avatar
Ian Hickson committed
261
  Finder descendant({
262 263
    required Finder of,
    required Finder matching,
Ian Hickson's avatar
Ian Hickson committed
264 265 266
    bool matchRoot = false,
    bool skipOffstage = true,
  }) {
267
    return _DescendantFinder(of, matching, matchRoot: matchRoot, skipOffstage: skipOffstage);
268
  }
269 270 271 272

  /// Finds widgets that are ancestors of the [of] parameter and that match
  /// the [matching] parameter.
  ///
273
  /// ## Sample code
274
  ///
275 276 277 278 279 280 281 282 283 284 285 286 287
  /// ```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
  /// );
  /// ```
288 289 290
  ///
  /// If the [matchRoot] argument is true then the widget(s) specified by [of]
  /// will be matched along with the ancestors.
Ian Hickson's avatar
Ian Hickson committed
291
  Finder ancestor({
292 293
    required Finder of,
    required Finder matching,
Ian Hickson's avatar
Ian Hickson committed
294 295
    bool matchRoot = false,
  }) {
296
    return _AncestorFinder(of, matching, matchRoot: matchRoot);
297
  }
298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314

  /// 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
315
  /// expect(find.bySemanticsLabel('Back'), findsOneWidget);
316 317 318 319 320
  /// ```
  ///
  /// 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 }) {
321
    if (WidgetsBinding.instance!.pipelineOwner.semanticsOwner == null)
322
      throw StateError('Semantics are not enabled. '
323
                       'Make sure to call tester.ensureSemantics() before using '
324 325 326 327 328 329 330 331
                       '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;
        }
332
        final String? semanticsLabel = element.renderObject.debugSemantics?.label;
333 334 335 336 337 338 339 340 341 342
        if (semanticsLabel == null) {
          return false;
        }
        return label is RegExp
            ? label.hasMatch(semanticsLabel)
            : label == semanticsLabel;
      },
      skipOffstage: skipOffstage,
    );
  }
343 344 345 346 347
}

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

352 353 354 355 356 357 358 359 360 361 362 363 364
  /// 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);

365 366
  /// Whether this finder skips nodes that are offstage.
  ///
367 368 369
  /// 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.
370 371
  final bool skipOffstage;

372 373 374
  /// Returns all the [Element]s that will be considered by this finder.
  ///
  /// See [collectAllElementsFrom].
375 376
  @protected
  Iterable<Element> get allCandidates {
377
    return collectAllElementsFrom(
378
      WidgetsBinding.instance!.renderViewElement!,
379
      skipOffstage: skipOffstage,
380 381
    );
  }
382

383
  Iterable<Element>? _cachedResult;
384 385 386 387 388 389 390

  /// 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() {
391
    final Iterable<Element> result = _cachedResult ?? apply(allCandidates);
392 393 394 395 396 397 398 399 400 401 402
    _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);
403
    final Iterable<Element> result = apply(allCandidates);
404 405 406 407 408 409 410 411
    if (result.isNotEmpty) {
      _cachedResult = result;
      return true;
    }
    _cachedResult = null;
    return false;
  }

412 413
  /// Returns a variant of this finder that only matches the first element
  /// matched by this finder.
414
  Finder get first => _FirstFinder(this);
415 416 417

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

420 421
  /// Returns a variant of this finder that only matches the element at the
  /// given index matched by this finder.
422
  Finder at(int index) => _IndexFinder(this, index);
423

424 425 426 427 428
  /// 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.
429
  Finder hitTestable({ Alignment at = Alignment.center }) => _HitTestableFinder(this, at);
430

431 432
  @override
  String toString() {
433
    final String additional = skipOffstage ? ' (ignoring offstage widgets)' : '';
434 435 436
    final List<Element> widgets = evaluate().toList();
    final int count = widgets.length;
    if (count == 0)
437
      return 'zero widgets with $description$additional';
438
    if (count == 1)
439
      return 'exactly one widget with $description$additional: ${widgets.single}';
440
    if (count < 4)
441 442
      return '$count widgets with $description$additional: $widgets';
    return '$count widgets with $description$additional: ${widgets[0]}, ${widgets[1]}, ${widgets[2]}, ...';
443 444 445
  }
}

446 447 448 449
/// 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);
450

451
  /// Another [Finder] that will run first.
452 453
  final Finder parent;

454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471
  /// 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);

472 473 474 475
  @override
  String get description => '${parent.description} (ignoring all but first)';

  @override
476 477
  Iterable<Element> filter(Iterable<Element> parentCandidates) sync* {
    yield parentCandidates.first;
478 479 480
  }
}

481 482
class _LastFinder extends ChainedFinder {
  _LastFinder(Finder parent) : super(parent);
483 484 485 486 487

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

  @override
488 489
  Iterable<Element> filter(Iterable<Element> parentCandidates) sync* {
    yield parentCandidates.last;
490 491 492
  }
}

493 494
class _IndexFinder extends ChainedFinder {
  _IndexFinder(Finder parent, this.index) : super(parent);
495 496 497 498 499 500 501

  final int index;

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

  @override
502 503
  Iterable<Element> filter(Iterable<Element> parentCandidates) sync* {
    yield parentCandidates.elementAt(index);
504 505 506
  }
}

507 508
class _HitTestableFinder extends ChainedFinder {
  _HitTestableFinder(Finder parent, this.alignment) : super(parent);
509

510
  final Alignment alignment;
511 512 513 514 515

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

  @override
516 517
  Iterable<Element> filter(Iterable<Element> parentCandidates) sync* {
    for (final Element candidate in parentCandidates) {
518
      final RenderBox box = candidate.renderObject! as RenderBox;
519
      final Offset absoluteOffset = box.localToGlobal(alignment.alongSize(box.size));
520
      final HitTestResult hitResult = HitTestResult();
521
      WidgetsBinding.instance!.hitTest(hitResult, absoluteOffset);
522 523 524 525 526 527 528 529 530 531
      for (final HitTestEntry entry in hitResult.path) {
        if (entry.target == candidate.renderObject) {
          yield candidate;
          break;
        }
      }
    }
  }
}

532 533 534
/// Searches a widget tree and returns nodes that match a particular
/// pattern.
abstract class MatchFinder extends Finder {
535
  /// Initializes a predicate-based Finder. Used by subclasses to initialize the
536
  /// [skipOffstage] property.
537
  MatchFinder({ bool skipOffstage = true }) : super(skipOffstage: skipOffstage);
538

539 540 541 542 543 544 545 546 547 548 549 550
  /// 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 {
551
  _TextFinder(this.text, { bool skipOffstage = true }) : super(skipOffstage: skipOffstage);
552 553 554 555 556 557 558 559

  final String text;

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

  @override
  bool matches(Element candidate) {
560 561 562 563
    final Widget widget = candidate.widget;
    if (widget is Text) {
      if (widget.data != null)
        return widget.data == text;
564 565
      assert(widget.textSpan != null);
      return widget.textSpan!.toPlainText() == text;
566 567
    } else if (widget is EditableText) {
      return widget.controller.text == text;
568 569
    }
    return false;
570 571 572
  }
}

573 574 575 576 577 578 579 580 581 582 583 584 585 586
class _TextContainingFinder extends MatchFinder {
  _TextContainingFinder(this.pattern, {bool skipOffstage = true})
      : super(skipOffstage: skipOffstage);

  final Pattern pattern;

  @override
  String get description => 'text containing $pattern';

  @override
  bool matches(Element candidate) {
    final Widget widget = candidate.widget;
    if (widget is Text) {
      if (widget.data != null)
587 588 589
        return widget.data!.contains(pattern);
      assert(widget.textSpan != null);
      return widget.textSpan!.toPlainText().contains(pattern);
590 591 592 593 594 595 596
    } else if (widget is EditableText) {
      return widget.controller.text.contains(pattern);
    }
    return false;
  }
}

597
class _KeyFinder extends MatchFinder {
598
  _KeyFinder(this.key, { bool skipOffstage = true }) : super(skipOffstage: skipOffstage);
599 600 601 602 603 604 605 606 607 608 609 610 611

  final Key key;

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

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

class _WidgetTypeFinder extends MatchFinder {
612
  _WidgetTypeFinder(this.widgetType, { bool skipOffstage = true }) : super(skipOffstage: skipOffstage);
613 614 615 616 617 618 619 620 621 622 623 624

  final Type widgetType;

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

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

625
class _WidgetIconFinder extends MatchFinder {
626
  _WidgetIconFinder(this.icon, { bool skipOffstage = true }) : super(skipOffstage: skipOffstage);
627 628 629 630 631 632 633 634 635 636 637 638 639

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

640
class _ElementTypeFinder extends MatchFinder {
641
  _ElementTypeFinder(this.elementType, { bool skipOffstage = true }) : super(skipOffstage: skipOffstage);
642 643 644 645 646 647 648 649 650 651 652 653

  final Type elementType;

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

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

654
class _WidgetFinder extends MatchFinder {
655
  _WidgetFinder(this.widget, { bool skipOffstage = true }) : super(skipOffstage: skipOffstage);
656

657
  final Widget widget;
658 659

  @override
660
  String get description => 'the given widget ($widget)';
661 662 663

  @override
  bool matches(Element candidate) {
664
    return candidate.widget == widget;
665 666 667 668
  }
}

class _WidgetPredicateFinder extends MatchFinder {
669
  _WidgetPredicateFinder(this.predicate, { String? description, bool skipOffstage = true })
670 671
    : _description = description,
      super(skipOffstage: skipOffstage);
672 673

  final WidgetPredicate predicate;
674
  final String? _description;
675 676

  @override
677
  String get description => _description ?? 'widget matching predicate ($predicate)';
678 679 680 681 682 683 684 685

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

class _ElementPredicateFinder extends MatchFinder {
686
  _ElementPredicateFinder(this.predicate, { String? description, bool skipOffstage = true })
687 688
    : _description = description,
      super(skipOffstage: skipOffstage);
689 690

  final ElementPredicate predicate;
691
  final String? _description;
692 693

  @override
694
  String get description => _description ?? 'element matching predicate ($predicate)';
695 696 697 698 699 700

  @override
  bool matches(Element candidate) {
    return predicate(candidate);
  }
}
701 702

class _DescendantFinder extends Finder {
703 704 705
  _DescendantFinder(
    this.ancestor,
    this.descendant, {
706 707
    this.matchRoot = false,
    bool skipOffstage = true,
708
  }) : super(skipOffstage: skipOffstage);
709 710 711

  final Finder ancestor;
  final Finder descendant;
712
  final bool matchRoot;
713 714

  @override
715 716 717 718 719
  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}';
  }
720 721 722 723 724 725 726 727

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

  @override
  Iterable<Element> get allCandidates {
728
    final Iterable<Element> ancestorElements = ancestor.evaluate();
729
    final List<Element> candidates = ancestorElements.expand<Element>(
730 731
      (Element element) => collectAllElementsFrom(element, skipOffstage: skipOffstage)
    ).toSet().toList();
732 733 734
    if (matchRoot)
      candidates.insertAll(0, ancestorElements);
    return candidates;
735 736
  }
}
737 738

class _AncestorFinder extends Finder {
739
  _AncestorFinder(this.descendant, this.ancestor, { this.matchRoot = false }) : super(skipOffstage: false);
740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759

  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>[];
760
    for (final Element root in descendant.evaluate()) {
761 762 763 764 765 766 767 768 769 770 771 772
      final List<Element> ancestors = <Element>[];
      if (matchRoot)
        ancestors.add(root);
      root.visitAncestorElements((Element element) {
        ancestors.add(element);
        return true;
      });
      candidates.addAll(ancestors);
    }
    return candidates;
  }
}