finders.dart 30.6 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 28 29 30 31 32 33 34 35 36 37 38 39
  /// Finds [Text], [EditableText], and optionally [RichText] widgets
  /// containing string equal to the `text` argument.
  ///
  /// If `findRichText` is false, all standalone [RichText] widgets are
  /// ignored and `text` is matched with [Text.data] or [Text.textSpan].
  /// If `findRichText` is true, [RichText] widgets (and therefore also
  /// [Text] and [Text.rich] widgets) are matched by comparing the
  /// [InlineSpan.toPlainText] with the given `text`.
  ///
  /// For [EditableText] widgets, the `text` is always compared to the current
  /// value of the [EditableText.controller].
  ///
  /// If the `skipOffstage` argument is true (the default), then this skips
  /// nodes that are [Offstage] or that are from inactive [Route]s.
40
  ///
41
  /// ## Sample code
42
  ///
43 44 45
  /// ```dart
  /// expect(find.text('Back'), findsOneWidget);
  /// ```
46
  ///
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
  /// This will match [Text], [Text.rich], and [EditableText] widgets that
  /// contain the "Back" string.
  ///
  /// ```dart
  /// expect(find.text('Close', findRichText: true), findsOneWidget);
  /// ```
  ///
  /// This will match [Text], [Text.rich], [EditableText], as well as standalone
  /// [RichText] widgets that contain the "Close" string.
  Finder text(
    String text, {
    bool findRichText = false,
    bool skipOffstage = true,
  }) {
    return _TextFinder(
      text,
      findRichText: findRichText,
      skipOffstage: skipOffstage,
    );
  }
67

68 69 70 71 72 73 74 75 76 77 78 79 80 81
  /// Finds [Text] and [EditableText], and optionally [RichText] widgets
  /// which contain the given `pattern` argument.
  ///
  /// If `findRichText` is false, all standalone [RichText] widgets are
  /// ignored and `pattern` is matched with [Text.data] or [Text.textSpan].
  /// If `findRichText` is true, [RichText] widgets (and therefore also
  /// [Text] and [Text.rich] widgets) are matched by comparing the
  /// [InlineSpan.toPlainText] with the given `pattern`.
  ///
  /// For [EditableText] widgets, the `pattern` is always compared to the currentt
  /// value of the [EditableText.controller].
  ///
  /// If the `skipOffstage` argument is true (the default), then this skips
  /// nodes that are [Offstage] or that are from inactive [Route]s.
82 83 84 85
  ///
  /// ## Sample code
  ///
  /// ```dart
86 87
  /// expect(find.textContaining('Back'), findsOneWidget);
  /// expect(find.textContaining(RegExp(r'(\w+)')), findsOneWidget);
88 89
  /// ```
  ///
90 91 92 93
  /// This will match [Text], [Text.rich], and [EditableText] widgets that
  /// contain the given pattern : 'Back' or RegExp(r'(\w+)').
  ///
  /// ```dart
94 95
  /// expect(find.textContaining('Close', findRichText: true), findsOneWidget);
  /// expect(find.textContaining(RegExp(r'(\w+)'), findRichText: true), findsOneWidget);
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110
  /// ```
  ///
  /// This will match [Text], [Text.rich], [EditableText], as well as standalone
  /// [RichText] widgets that contain the given pattern : 'Close' or RegExp(r'(\w+)').
  Finder textContaining(
    Pattern pattern, {
    bool findRichText = false,
    bool skipOffstage = true,
  }) {
    return _TextContainingFinder(
      pattern,
      findRichText: findRichText,
      skipOffstage: skipOffstage
    );
  }
111

112 113 114
  /// Looks for widgets that contain a [Text] descendant with `text`
  /// in it.
  ///
115
  /// ## Sample code
116
  ///
117 118
  /// ```dart
  /// // Suppose you have a button with text 'Update' in it:
Anas35's avatar
Anas35 committed
119 120
  /// Button(
  ///   child: Text('Update')
121
  /// )
122
  ///
123 124 125
  /// // You can find and tap on it like this:
  /// tester.tap(find.widgetWithText(Button, 'Update'));
  /// ```
126
  ///
127 128
  /// If the `skipOffstage` argument is true (the default), then this skips
  /// nodes that are [Offstage] or that are from inactive [Route]s.
129
  Finder widgetWithText(Type widgetType, String text, { bool skipOffstage = true }) {
130
    return find.ancestor(
131 132
      of: find.text(text, skipOffstage: skipOffstage),
      matching: find.byType(widgetType, skipOffstage: skipOffstage),
133
    );
134 135
  }

136 137 138 139 140 141 142 143 144 145 146 147 148
  /// Finds [Image] and [FadeInImage] widgets containing `image` equal to the
  /// `image` argument.
  ///
  /// ## Sample code
  ///
  /// ```dart
  /// expect(find.image(FileImage(File(filePath))), findsOneWidget);
  /// ```
  ///
  /// If the `skipOffstage` argument is true (the default), then this skips
  /// nodes that are [Offstage] or that are from inactive [Route]s.
  Finder image(ImageProvider image, { bool skipOffstage = true }) => _WidgetImageFinder(image, skipOffstage: skipOffstage);

149 150
  /// Finds widgets by searching for one with a particular [Key].
  ///
151
  /// ## Sample code
152
  ///
153 154 155
  /// ```dart
  /// expect(find.byKey(backKey), findsOneWidget);
  /// ```
156
  ///
157 158
  /// If the `skipOffstage` argument is true (the default), then this skips
  /// nodes that are [Offstage] or that are from inactive [Route]s.
159
  Finder byKey(Key key, { bool skipOffstage = true }) => _KeyFinder(key, skipOffstage: skipOffstage);
160

161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178
  /// Finds widgets by searching for widgets implementing a particular type.
  ///
  /// This matcher accepts subtypes. For example a
  /// `bySubtype<StatefulWidget>()` will find any stateful widget.
  ///
  /// ## Sample code
  ///
  /// ```dart
  /// expect(find.bySubtype<IconButton>(), findsOneWidget);
  /// ```
  ///
  /// If the `skipOffstage` argument is true (the default), then this skips
  /// nodes that are [Offstage] or that are from inactive [Route]s.
  ///
  /// See also:
  /// * [byType], which does not do subtype tests.
  Finder bySubtype<T extends Widget>({ bool skipOffstage = true }) => _WidgetSubtypeFinder<T>(skipOffstage: skipOffstage);

179 180 181 182 183 184 185 186
  /// 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].
  ///
187
  /// ## Sample code
188
  ///
189 190 191
  /// ```dart
  /// expect(find.byType(IconButton), findsOneWidget);
  /// ```
192
  ///
193 194
  /// If the `skipOffstage` argument is true (the default), then this skips
  /// nodes that are [Offstage] or that are from inactive [Route]s.
195 196 197
  ///
  /// See also:
  /// * [bySubtype], which allows subtype tests.
198
  Finder byType(Type type, { bool skipOffstage = true }) => _WidgetTypeFinder(type, skipOffstage: skipOffstage);
199

200 201
  /// Finds [Icon] widgets containing icon data equal to the `icon`
  /// argument.
202
  ///
203
  /// ## Sample code
204
  ///
205 206 207
  /// ```dart
  /// expect(find.byIcon(Icons.inbox), findsOneWidget);
  /// ```
208
  ///
209 210
  /// If the `skipOffstage` argument is true (the default), then this skips
  /// nodes that are [Offstage] or that are from inactive [Route]s.
211
  Finder byIcon(IconData icon, { bool skipOffstage = true }) => _WidgetIconFinder(icon, skipOffstage: skipOffstage);
212

213 214 215 216 217 218 219
  /// 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
220 221
  /// Button(
  ///   child: Icon(Icons.arrow_forward)
222 223 224 225 226 227
  /// )
  ///
  /// // You can find and tap on it like this:
  /// tester.tap(find.widgetWithIcon(Button, Icons.arrow_forward));
  /// ```
  ///
228 229
  /// If the `skipOffstage` argument is true (the default), then this skips
  /// nodes that are [Offstage] or that are from inactive [Route]s.
230
  Finder widgetWithIcon(Type widgetType, IconData icon, { bool skipOffstage = true }) {
231 232 233 234 235 236
    return find.ancestor(
      of: find.byIcon(icon),
      matching: find.byType(widgetType),
    );
  }

237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260
  /// Looks for widgets that contain an [Image] descendant displaying [ImageProvider]
  /// `image` in it.
  ///
  /// ## Sample code
  ///
  /// ```dart
  /// // Suppose you have a button with image in it:
  /// Button(
  ///   child: Image.file(filePath)
  /// )
  ///
  /// // You can find and tap on it like this:
  /// tester.tap(find.widgetWithImage(Button, FileImage(filePath)));
  /// ```
  ///
  /// If the `skipOffstage` argument is true (the default), then this skips
  /// nodes that are [Offstage] or that are from inactive [Route]s.
  Finder widgetWithImage(Type widgetType, ImageProvider image, { bool skipOffstage = true }) {
    return find.ancestor(
      of: find.image(image),
      matching: find.byType(widgetType),
    );
  }

261 262 263 264 265 266 267 268
  /// 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].
  ///
269
  /// ## Sample code
270
  ///
271 272 273
  /// ```dart
  /// expect(find.byElementType(SingleChildRenderObjectElement), findsOneWidget);
  /// ```
274
  ///
275 276
  /// If the `skipOffstage` argument is true (the default), then this skips
  /// nodes that are [Offstage] or that are from inactive [Route]s.
277
  Finder byElementType(Type type, { bool skipOffstage = true }) => _ElementTypeFinder(type, skipOffstage: skipOffstage);
278 279 280 281

  /// Finds widgets whose current widget is the instance given by the
  /// argument.
  ///
282
  /// ## Sample code
283
  ///
284 285
  /// ```dart
  /// // Suppose you have a button created like this:
Anas35's avatar
Anas35 committed
286 287
  /// Widget myButton = Button(
  ///   child: Text('Update')
288
  /// );
289
  ///
290 291 292
  /// // You can find and tap on it like this:
  /// tester.tap(find.byWidget(myButton));
  /// ```
293
  ///
294 295
  /// If the `skipOffstage` argument is true (the default), then this skips
  /// nodes that are [Offstage] or that are from inactive [Route]s.
296
  Finder byWidget(Widget widget, { bool skipOffstage = true }) => _WidgetFinder(widget, skipOffstage: skipOffstage);
297

298
  /// Finds widgets using a widget [predicate].
299
  ///
300
  /// ## Sample code
301
  ///
302 303 304 305 306 307
  /// ```dart
  /// expect(find.byWidgetPredicate(
  ///   (Widget widget) => widget is Tooltip && widget.message == 'Back',
  ///   description: 'widget with tooltip "Back"',
  /// ), findsOneWidget);
  /// ```
308
  ///
309 310 311 312 313
  /// 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.
  ///
314 315
  /// If the `skipOffstage` argument is true (the default), then this skips
  /// nodes that are [Offstage] or that are from inactive [Route]s.
316
  Finder byWidgetPredicate(WidgetPredicate predicate, { String? description, bool skipOffstage = true }) {
317
    return _WidgetPredicateFinder(predicate, description: description, skipOffstage: skipOffstage);
318 319
  }

320 321
  /// Finds Tooltip widgets with the given message.
  ///
322
  /// ## Sample code
323
  ///
324 325 326
  /// ```dart
  /// expect(find.byTooltip('Back'), findsOneWidget);
  /// ```
327
  ///
328 329
  /// If the `skipOffstage` argument is true (the default), then this skips
  /// nodes that are [Offstage] or that are from inactive [Route]s.
330
  Finder byTooltip(String message, { bool skipOffstage = true }) {
331 332 333 334 335 336
    return byWidgetPredicate(
      (Widget widget) => widget is Tooltip && widget.message == message,
      skipOffstage: skipOffstage,
    );
  }

337
  /// Finds widgets using an element [predicate].
338
  ///
339
  /// ## Sample code
340
  ///
341 342 343 344 345 346 347 348 349
  /// ```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);
  /// ```
350
  ///
351 352 353 354 355
  /// 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.
  ///
356 357
  /// If the `skipOffstage` argument is true (the default), then this skips
  /// nodes that are [Offstage] or that are from inactive [Route]s.
358
  Finder byElementPredicate(ElementPredicate predicate, { String? description, bool skipOffstage = true }) {
359
    return _ElementPredicateFinder(predicate, description: description, skipOffstage: skipOffstage);
360
  }
361

362 363
  /// Finds widgets that are descendants of the [of] parameter and that match
  /// the [matching] parameter.
364
  ///
365
  /// ## Sample code
366
  ///
367 368 369 370 371
  /// ```dart
  /// expect(find.descendant(
  ///   of: find.widgetWithText(Row, 'label_1'), matching: find.text('value_1')
  /// ), findsOneWidget);
  /// ```
372
  ///
373 374 375
  /// If the [matchRoot] argument is true then the widget(s) specified by [of]
  /// will be matched along with the descendants.
  ///
376 377
  /// 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
378
  Finder descendant({
379 380
    required Finder of,
    required Finder matching,
Ian Hickson's avatar
Ian Hickson committed
381 382 383
    bool matchRoot = false,
    bool skipOffstage = true,
  }) {
384
    return _DescendantFinder(of, matching, matchRoot: matchRoot, skipOffstage: skipOffstage);
385
  }
386 387 388 389

  /// Finds widgets that are ancestors of the [of] parameter and that match
  /// the [matching] parameter.
  ///
390
  /// ## Sample code
391
  ///
392 393 394 395 396 397 398 399 400 401 402 403 404
  /// ```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
  /// );
  /// ```
405 406 407
  ///
  /// 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
408
  Finder ancestor({
409 410
    required Finder of,
    required Finder matching,
Ian Hickson's avatar
Ian Hickson committed
411 412
    bool matchRoot = false,
  }) {
413
    return _AncestorFinder(of, matching, matchRoot: matchRoot);
414
  }
415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431

  /// 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
432
  /// expect(find.bySemanticsLabel('Back'), findsOneWidget);
433 434 435 436 437
  /// ```
  ///
  /// 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 }) {
438
    if (WidgetsBinding.instance.pipelineOwner.semanticsOwner == null) {
439
      throw StateError('Semantics are not enabled. '
440
                       'Make sure to call tester.ensureSemantics() before using '
441
                       'this finder, and call dispose on its return value after.');
442
    }
443 444 445 446 447 448 449
    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;
        }
450
        final String? semanticsLabel = element.renderObject.debugSemantics?.label;
451 452 453 454 455 456 457 458 459 460
        if (semanticsLabel == null) {
          return false;
        }
        return label is RegExp
            ? label.hasMatch(semanticsLabel)
            : label == semanticsLabel;
      },
      skipOffstage: skipOffstage,
    );
  }
461 462 463 464 465
}

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

470 471 472 473 474 475 476 477 478 479 480 481 482
  /// 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);

483 484
  /// Whether this finder skips nodes that are offstage.
  ///
485 486 487
  /// 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.
488 489
  final bool skipOffstage;

490 491 492
  /// Returns all the [Element]s that will be considered by this finder.
  ///
  /// See [collectAllElementsFrom].
493 494
  @protected
  Iterable<Element> get allCandidates {
495
    return collectAllElementsFrom(
496
      WidgetsBinding.instance.renderViewElement!,
497
      skipOffstage: skipOffstage,
498 499
    );
  }
500

501
  Iterable<Element>? _cachedResult;
502 503 504 505 506 507 508

  /// 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() {
509
    final Iterable<Element> result = _cachedResult ?? apply(allCandidates);
510 511 512 513 514 515 516 517 518 519 520
    _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);
521
    final Iterable<Element> result = apply(allCandidates);
522 523 524 525 526 527 528 529
    if (result.isNotEmpty) {
      _cachedResult = result;
      return true;
    }
    _cachedResult = null;
    return false;
  }

530 531
  /// Returns a variant of this finder that only matches the first element
  /// matched by this finder.
532
  Finder get first => _FirstFinder(this);
533 534 535

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

538 539
  /// Returns a variant of this finder that only matches the element at the
  /// given index matched by this finder.
540
  Finder at(int index) => _IndexFinder(this, index);
541

542 543 544 545 546
  /// 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.
547
  Finder hitTestable({ Alignment at = Alignment.center }) => _HitTestableFinder(this, at);
548

549 550
  @override
  String toString() {
551
    final String additional = skipOffstage ? ' (ignoring offstage widgets)' : '';
552 553
    final List<Element> widgets = evaluate().toList();
    final int count = widgets.length;
554
    if (count == 0) {
555
      return 'zero widgets with $description$additional';
556 557
    }
    if (count == 1) {
558
      return 'exactly one widget with $description$additional: ${widgets.single}';
559 560
    }
    if (count < 4) {
561
      return '$count widgets with $description$additional: $widgets';
562
    }
563
    return '$count widgets with $description$additional: ${widgets[0]}, ${widgets[1]}, ${widgets[2]}, ...';
564 565 566
  }
}

567 568 569 570
/// 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);
571

572
  /// Another [Finder] that will run first.
573 574
  final Finder parent;

575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590
  /// 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 {
591
  _FirstFinder(super.parent);
592

593 594 595 596
  @override
  String get description => '${parent.description} (ignoring all but first)';

  @override
597 598
  Iterable<Element> filter(Iterable<Element> parentCandidates) sync* {
    yield parentCandidates.first;
599 600 601
  }
}

602
class _LastFinder extends ChainedFinder {
603
  _LastFinder(super.parent);
604 605 606 607 608

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

  @override
609 610
  Iterable<Element> filter(Iterable<Element> parentCandidates) sync* {
    yield parentCandidates.last;
611 612 613
  }
}

614
class _IndexFinder extends ChainedFinder {
615
  _IndexFinder(super.parent, this.index);
616 617 618 619 620 621 622

  final int index;

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

  @override
623 624
  Iterable<Element> filter(Iterable<Element> parentCandidates) sync* {
    yield parentCandidates.elementAt(index);
625 626 627
  }
}

628
class _HitTestableFinder extends ChainedFinder {
629
  _HitTestableFinder(super.parent, this.alignment);
630

631
  final Alignment alignment;
632 633 634 635 636

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

  @override
637 638
  Iterable<Element> filter(Iterable<Element> parentCandidates) sync* {
    for (final Element candidate in parentCandidates) {
639
      final RenderBox box = candidate.renderObject! as RenderBox;
640
      final Offset absoluteOffset = box.localToGlobal(alignment.alongSize(box.size));
641
      final HitTestResult hitResult = HitTestResult();
642
      WidgetsBinding.instance.hitTest(hitResult, absoluteOffset);
643 644 645 646 647 648 649 650 651 652
      for (final HitTestEntry entry in hitResult.path) {
        if (entry.target == candidate.renderObject) {
          yield candidate;
          break;
        }
      }
    }
  }
}

653 654 655
/// Searches a widget tree and returns nodes that match a particular
/// pattern.
abstract class MatchFinder extends Finder {
656
  /// Initializes a predicate-based Finder. Used by subclasses to initialize the
657
  /// [skipOffstage] property.
658
  MatchFinder({ super.skipOffstage });
659

660 661 662 663 664 665 666 667 668 669 670
  /// 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);
  }
}

671 672
abstract class _MatchTextFinder extends MatchFinder {
  _MatchTextFinder({
673
    this.findRichText = false,
674 675
    super.skipOffstage,
  });
676

677 678 679 680 681 682 683 684 685 686 687 688 689
  /// Whether standalone [RichText] widgets should be found or not.
  ///
  /// Defaults to `false`.
  ///
  /// If disabled, only [Text] widgets will be matched. [RichText] widgets
  /// *without* a [Text] ancestor will be ignored.
  /// If enabled, only [RichText] widgets will be matched. This *implicitly*
  /// matches [Text] widgets as well since they always insert a [RichText]
  /// child.
  ///
  /// In either case, [EditableText] widgets will also be matched.
  final bool findRichText;

690
  bool matchesText(String textToMatch);
691 692 693

  @override
  bool matches(Element candidate) {
694
    final Widget widget = candidate.widget;
695
    if (widget is EditableText) {
696
      return _matchesEditableText(widget);
697
    }
698

699
    if (!findRichText) {
700
      return _matchesNonRichText(widget);
701
    }
702 703 704 705 706 707 708 709 710
    // It would be sufficient to always use _matchesRichText if we wanted to
    // match both standalone RichText widgets as well as Text widgets. However,
    // the find.text() finder used to always ignore standalone RichText widgets,
    // which is why we need the _matchesNonRichText method in order to not be
    // backwards-compatible and not break existing tests.
    return _matchesRichText(widget);
  }

  bool _matchesRichText(Widget widget) {
711 712 713
    if (widget is RichText) {
      return matchesText(widget.text.toPlainText());
    }
714 715 716 717
    return false;
  }

  bool _matchesNonRichText(Widget widget) {
718
    if (widget is Text) {
719 720 721
      if (widget.data != null) {
        return matchesText(widget.data!);
      }
722
      assert(widget.textSpan != null);
723
      return matchesText(widget.textSpan!.toPlainText());
724 725
    }
    return false;
726
  }
727 728

  bool _matchesEditableText(EditableText widget) {
729
    return matchesText(widget.controller.text);
730
  }
731 732
}

733 734 735
class _TextFinder extends _MatchTextFinder {
  _TextFinder(
    this.text, {
736 737 738
    super.findRichText,
    super.skipOffstage,
  });
739 740 741 742 743 744 745 746 747 748 749 750 751 752 753

  final String text;

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

  @override
  bool matchesText(String textToMatch) {
    return textToMatch == text;
  }
}

class _TextContainingFinder extends _MatchTextFinder {
  _TextContainingFinder(
    this.pattern, {
754 755 756
    super.findRichText,
    super.skipOffstage,
  });
757 758 759 760 761 762 763

  final Pattern pattern;

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

  @override
764 765
  bool matchesText(String textToMatch) {
    return textToMatch.contains(pattern);
766 767 768
  }
}

769
class _KeyFinder extends MatchFinder {
770
  _KeyFinder(this.key, { super.skipOffstage });
771 772 773 774 775 776 777 778 779 780 781 782

  final Key key;

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

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

783
class _WidgetSubtypeFinder<T extends Widget> extends MatchFinder {
784
  _WidgetSubtypeFinder({ super.skipOffstage });
785 786 787 788 789 790 791 792 793 794

  @override
  String get description => 'is "$T"';

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

795
class _WidgetTypeFinder extends MatchFinder {
796
  _WidgetTypeFinder(this.widgetType, { super.skipOffstage });
797 798 799 800 801 802 803 804 805 806 807 808

  final Type widgetType;

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

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

809
class _WidgetImageFinder extends MatchFinder {
810
  _WidgetImageFinder(this.image, { super.skipOffstage });
811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828

  final ImageProvider image;

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

  @override
  bool matches(Element candidate) {
    final Widget widget = candidate.widget;
    if (widget is Image) {
      return widget.image == image;
    } else if (widget is FadeInImage) {
      return widget.image == image;
    }
    return false;
  }
}

829
class _WidgetIconFinder extends MatchFinder {
830
  _WidgetIconFinder(this.icon, { super.skipOffstage });
831 832 833 834 835 836 837 838 839 840 841 842 843

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

844
class _ElementTypeFinder extends MatchFinder {
845
  _ElementTypeFinder(this.elementType, { super.skipOffstage });
846 847 848 849 850 851 852 853 854 855 856 857

  final Type elementType;

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

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

858
class _WidgetFinder extends MatchFinder {
859
  _WidgetFinder(this.widget, { super.skipOffstage });
860

861
  final Widget widget;
862 863

  @override
864
  String get description => 'the given widget ($widget)';
865 866 867

  @override
  bool matches(Element candidate) {
868
    return candidate.widget == widget;
869 870 871 872
  }
}

class _WidgetPredicateFinder extends MatchFinder {
873 874
  _WidgetPredicateFinder(this.predicate, { String? description, super.skipOffstage })
    : _description = description;
875 876

  final WidgetPredicate predicate;
877
  final String? _description;
878 879

  @override
880
  String get description => _description ?? 'widget matching predicate ($predicate)';
881 882 883 884 885 886 887 888

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

class _ElementPredicateFinder extends MatchFinder {
889 890
  _ElementPredicateFinder(this.predicate, { String? description, super.skipOffstage })
    : _description = description;
891 892

  final ElementPredicate predicate;
893
  final String? _description;
894 895

  @override
896
  String get description => _description ?? 'element matching predicate ($predicate)';
897 898 899 900 901 902

  @override
  bool matches(Element candidate) {
    return predicate(candidate);
  }
}
903 904

class _DescendantFinder extends Finder {
905 906 907
  _DescendantFinder(
    this.ancestor,
    this.descendant, {
908
    this.matchRoot = false,
909 910
    super.skipOffstage,
  });
911 912 913

  final Finder ancestor;
  final Finder descendant;
914
  final bool matchRoot;
915 916

  @override
917
  String get description {
918
    if (matchRoot) {
919
      return '${descendant.description} in the subtree(s) beginning with ${ancestor.description}';
920
    }
921 922
    return '${descendant.description} that has ancestor(s) with ${ancestor.description}';
  }
923 924 925 926 927 928 929 930

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

  @override
  Iterable<Element> get allCandidates {
931
    final Iterable<Element> ancestorElements = ancestor.evaluate();
932
    final List<Element> candidates = ancestorElements.expand<Element>(
933 934
      (Element element) => collectAllElementsFrom(element, skipOffstage: skipOffstage)
    ).toSet().toList();
935
    if (matchRoot) {
936
      candidates.insertAll(0, ancestorElements);
937
    }
938
    return candidates;
939 940
  }
}
941 942

class _AncestorFinder extends Finder {
943
  _AncestorFinder(this.descendant, this.ancestor, { this.matchRoot = false }) : super(skipOffstage: false);
944 945 946 947 948 949 950

  final Finder ancestor;
  final Finder descendant;
  final bool matchRoot;

  @override
  String get description {
951
    if (matchRoot) {
952
      return 'ancestor ${ancestor.description} beginning with ${descendant.description}';
953
    }
954 955 956 957 958 959 960 961 962 963 964
    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>[];
965
    for (final Element root in descendant.evaluate()) {
966
      final List<Element> ancestors = <Element>[];
967
      if (matchRoot) {
968
        ancestors.add(root);
969
      }
970 971 972 973 974 975 976 977 978
      root.visitAncestorElements((Element element) {
        ancestors.add(element);
        return true;
      });
      candidates.addAll(ancestors);
    }
    return candidates;
  }
}