finders.dart 21.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 121 122
  /// 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));
  /// ```
  ///
  /// 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
  /// If the [skipOffstage] argument is true (the default), then nodes that are
  /// [Offstage] or that are from inactive [Route]s are skipped.
247
  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
}

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

284 285 286 287 288 289 290 291 292 293 294 295 296
  /// 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);

297 298 299
  /// Whether this finder skips nodes that are offstage.
  ///
  /// If this is true, then the elements are walked using
300
  /// [Element.debugVisitOnstageChildren]. This skips offstage children of
301 302 303
  /// [Offstage] widgets, as well as children of inactive [Route]s.
  final bool skipOffstage;

304 305 306
  /// Returns all the [Element]s that will be considered by this finder.
  ///
  /// See [collectAllElementsFrom].
307 308
  @protected
  Iterable<Element> get allCandidates {
309 310 311 312 313
    return collectAllElementsFrom(
      WidgetsBinding.instance.renderViewElement,
      skipOffstage: skipOffstage
    );
  }
314 315 316 317 318 319 320 321 322

  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() {
323
    final Iterable<Element> result = _cachedResult ?? apply(allCandidates);
324 325 326 327 328 329 330 331 332 333 334
    _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);
335
    final Iterable<Element> result = apply(allCandidates);
336 337 338 339 340 341 342 343
    if (result.isNotEmpty) {
      _cachedResult = result;
      return true;
    }
    _cachedResult = null;
    return false;
  }

344 345
  /// Returns a variant of this finder that only matches the first element
  /// matched by this finder.
346
  Finder get first => _FirstFinder(this);
347 348 349

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

352 353
  /// Returns a variant of this finder that only matches the element at the
  /// given index matched by this finder.
354
  Finder at(int index) => _IndexFinder(this, index);
355

356 357 358 359 360
  /// 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.
361
  Finder hitTestable({ Alignment at = Alignment.center }) => _HitTestableFinder(this, at);
362

363 364
  @override
  String toString() {
365
    final String additional = skipOffstage ? ' (ignoring offstage widgets)' : '';
366 367 368
    final List<Element> widgets = evaluate().toList();
    final int count = widgets.length;
    if (count == 0)
369
      return 'zero widgets with $description$additional';
370
    if (count == 1)
371
      return 'exactly one widget with $description$additional: ${widgets.single}';
372
    if (count < 4)
373 374
      return '$count widgets with $description$additional: $widgets';
    return '$count widgets with $description$additional: ${widgets[0]}, ${widgets[1]}, ${widgets[2]}, ...';
375 376 377
  }
}

378 379 380 381
/// 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);
382

383
  /// Another [Finder] that will run first.
384 385
  final Finder parent;

386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403
  /// 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);

404 405 406 407
  @override
  String get description => '${parent.description} (ignoring all but first)';

  @override
408 409
  Iterable<Element> filter(Iterable<Element> parentCandidates) sync* {
    yield parentCandidates.first;
410 411 412
  }
}

413 414
class _LastFinder extends ChainedFinder {
  _LastFinder(Finder parent) : super(parent);
415 416 417 418 419

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

  @override
420 421
  Iterable<Element> filter(Iterable<Element> parentCandidates) sync* {
    yield parentCandidates.last;
422 423 424
  }
}

425 426
class _IndexFinder extends ChainedFinder {
  _IndexFinder(Finder parent, this.index) : super(parent);
427 428 429 430 431 432 433

  final int index;

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

  @override
434 435
  Iterable<Element> filter(Iterable<Element> parentCandidates) sync* {
    yield parentCandidates.elementAt(index);
436 437 438
  }
}

439 440
class _HitTestableFinder extends ChainedFinder {
  _HitTestableFinder(Finder parent, this.alignment) : super(parent);
441

442
  final Alignment alignment;
443 444 445 446 447

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

  @override
448 449
  Iterable<Element> filter(Iterable<Element> parentCandidates) sync* {
    for (final Element candidate in parentCandidates) {
450 451
      final RenderBox box = candidate.renderObject;
      assert(box != null);
452
      final Offset absoluteOffset = box.localToGlobal(alignment.alongSize(box.size));
453
      final HitTestResult hitResult = HitTestResult();
454 455 456 457 458 459 460 461 462 463 464
      WidgetsBinding.instance.hitTest(hitResult, absoluteOffset);
      for (final HitTestEntry entry in hitResult.path) {
        if (entry.target == candidate.renderObject) {
          yield candidate;
          break;
        }
      }
    }
  }
}

465 466 467
/// Searches a widget tree and returns nodes that match a particular
/// pattern.
abstract class MatchFinder extends Finder {
468
  /// Initializes a predicate-based Finder. Used by subclasses to initialize the
469
  /// [skipOffstage] property.
470
  MatchFinder({ bool skipOffstage = true }) : super(skipOffstage: skipOffstage);
471

472 473 474 475 476 477 478 479 480 481 482 483
  /// 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 {
484
  _TextFinder(this.text, { bool skipOffstage = true }) : super(skipOffstage: skipOffstage);
485 486 487 488 489 490 491 492

  final String text;

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

  @override
  bool matches(Element candidate) {
493 494
    if (candidate.widget is Text) {
      final Text textWidget = candidate.widget;
495 496 497
      if (textWidget.data != null)
        return textWidget.data == text;
      return textWidget.textSpan.toPlainText() == text;
498 499 500 501 502
    } else if (candidate.widget is EditableText) {
      final EditableText editable = candidate.widget;
      return editable.controller.text == text;
    }
    return false;
503 504 505 506
  }
}

class _KeyFinder extends MatchFinder {
507
  _KeyFinder(this.key, { bool skipOffstage = true }) : super(skipOffstage: skipOffstage);
508 509 510 511 512 513 514 515 516 517 518 519 520

  final Key key;

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

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

class _WidgetTypeFinder extends MatchFinder {
521
  _WidgetTypeFinder(this.widgetType, { bool skipOffstage = true }) : super(skipOffstage: skipOffstage);
522 523 524 525 526 527 528 529 530 531 532 533

  final Type widgetType;

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

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

534
class _WidgetIconFinder extends MatchFinder {
535
  _WidgetIconFinder(this.icon, { bool skipOffstage = true }) : super(skipOffstage: skipOffstage);
536 537 538 539 540 541 542 543 544 545 546 547 548

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

549
class _ElementTypeFinder extends MatchFinder {
550
  _ElementTypeFinder(this.elementType, { bool skipOffstage = true }) : super(skipOffstage: skipOffstage);
551 552 553 554 555 556 557 558 559 560 561 562

  final Type elementType;

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

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

563
class _WidgetFinder extends MatchFinder {
564
  _WidgetFinder(this.widget, { bool skipOffstage = true }) : super(skipOffstage: skipOffstage);
565

566
  final Widget widget;
567 568

  @override
569
  String get description => 'the given widget ($widget)';
570 571 572

  @override
  bool matches(Element candidate) {
573
    return candidate.widget == widget;
574 575 576 577
  }
}

class _WidgetPredicateFinder extends MatchFinder {
578
  _WidgetPredicateFinder(this.predicate, { String description, bool skipOffstage = true })
579 580
    : _description = description,
      super(skipOffstage: skipOffstage);
581 582

  final WidgetPredicate predicate;
583
  final String _description;
584 585

  @override
586
  String get description => _description ?? 'widget matching predicate ($predicate)';
587 588 589 590 591 592 593 594

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

class _ElementPredicateFinder extends MatchFinder {
595
  _ElementPredicateFinder(this.predicate, { String description, bool skipOffstage = true })
596 597
    : _description = description,
      super(skipOffstage: skipOffstage);
598 599

  final ElementPredicate predicate;
600
  final String _description;
601 602

  @override
603
  String get description => _description ?? 'element matching predicate ($predicate)';
604 605 606 607 608 609

  @override
  bool matches(Element candidate) {
    return predicate(candidate);
  }
}
610 611

class _DescendantFinder extends Finder {
612
  _DescendantFinder(this.ancestor, this.descendant, {
613 614
    this.matchRoot = false,
    bool skipOffstage = true,
615
  }) : super(skipOffstage: skipOffstage);
616 617 618

  final Finder ancestor;
  final Finder descendant;
619
  final bool matchRoot;
620 621

  @override
622 623 624 625 626
  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}';
  }
627 628 629 630 631 632 633 634

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

  @override
  Iterable<Element> get allCandidates {
635
    final Iterable<Element> ancestorElements = ancestor.evaluate();
636
    final List<Element> candidates = ancestorElements.expand<Element>(
637 638
      (Element element) => collectAllElementsFrom(element, skipOffstage: skipOffstage)
    ).toSet().toList();
639 640 641
    if (matchRoot)
      candidates.insertAll(0, ancestorElements);
    return candidates;
642 643
  }
}
644 645

class _AncestorFinder extends Finder {
646
  _AncestorFinder(this.descendant, this.ancestor, { this.matchRoot = false }) : super(skipOffstage: false);
647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679

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