1// Copyright 2016 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5import 'package:flutter/gestures.dart'; 6import 'package:flutter/material.dart'; 7import 'package:meta/meta.dart'; 8 9import 'all_elements.dart'; 10 11/// Signature for [CommonFinders.byWidgetPredicate]. 12typedef WidgetPredicate = bool Function(Widget widget); 13 14/// Signature for [CommonFinders.byElementPredicate]. 15typedef ElementPredicate = bool Function(Element element); 16 17/// Some frequently used widget [Finder]s. 18const CommonFinders find = CommonFinders._(); 19 20/// Provides lightweight syntax for getting frequently used widget [Finder]s. 21/// 22/// This class is instantiated once, as [find]. 23class CommonFinders { 24 const CommonFinders._(); 25 26 /// Finds [Text] and [EditableText] widgets containing string equal to the 27 /// `text` argument. 28 /// 29 /// ## Sample code 30 /// 31 /// ```dart 32 /// expect(find.text('Back'), findsOneWidget); 33 /// ``` 34 /// 35 /// If the `skipOffstage` argument is true (the default), then this skips 36 /// 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 /// Looks for widgets that contain a [Text] descendant with `text` 40 /// in it. 41 /// 42 /// ## Sample code 43 /// 44 /// ```dart 45 /// // Suppose you have a button with text 'Update' in it: 46 /// new Button( 47 /// child: new Text('Update') 48 /// ) 49 /// 50 /// // You can find and tap on it like this: 51 /// tester.tap(find.widgetWithText(Button, 'Update')); 52 /// ``` 53 /// 54 /// If the `skipOffstage` argument is true (the default), then this skips 55 /// 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 of: find.text(text, skipOffstage: skipOffstage), 59 matching: find.byType(widgetType, skipOffstage: skipOffstage), 60 ); 61 } 62 63 /// Finds widgets by searching for one with a particular [Key]. 64 /// 65 /// ## Sample code 66 /// 67 /// ```dart 68 /// expect(find.byKey(backKey), findsOneWidget); 69 /// ``` 70 /// 71 /// If the `skipOffstage` argument is true (the default), then this skips 72 /// 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 /// Finds widgets by searching for widgets with a particular type. 76 /// 77 /// This does not do subclass tests, so for example 78 /// `byType(StatefulWidget)` will never find anything since that's 79 /// an abstract class. 80 /// 81 /// The `type` argument must be a subclass of [Widget]. 82 /// 83 /// ## Sample code 84 /// 85 /// ```dart 86 /// expect(find.byType(IconButton), findsOneWidget); 87 /// ``` 88 /// 89 /// If the `skipOffstage` argument is true (the default), then this skips 90 /// 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 /// Finds [Icon] widgets containing icon data equal to the `icon` 94 /// argument. 95 /// 96 /// ## Sample code 97 /// 98 /// ```dart 99 /// expect(find.byIcon(Icons.inbox), findsOneWidget); 100 /// ``` 101 /// 102 /// If the `skipOffstage` argument is true (the default), then this skips 103 /// 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 /// Looks for widgets that contain an [Icon] descendant displaying [IconData] 107 /// `icon` in it. 108 /// 109 /// ## Sample code 110 /// 111 /// ```dart 112 /// // Suppose you have a button with icon 'arrow_forward' in it: 113 /// new Button( 114 /// child: new Icon(Icons.arrow_forward) 115 /// ) 116 /// 117 /// // You can find and tap on it like this: 118 /// tester.tap(find.widgetWithIcon(Button, Icons.arrow_forward)); 119 /// ``` 120 /// 121 /// If the `skipOffstage` argument is true (the default), then this skips 122 /// nodes that are [Offstage] or that are from inactive [Route]s. 123 Finder widgetWithIcon(Type widgetType, IconData icon, { bool skipOffstage = true }) { 124 return find.ancestor( 125 of: find.byIcon(icon), 126 matching: find.byType(widgetType), 127 ); 128 } 129 130 /// Finds widgets by searching for elements with a particular type. 131 /// 132 /// This does not do subclass tests, so for example 133 /// `byElementType(VirtualViewportElement)` will never find anything 134 /// since that's an abstract class. 135 /// 136 /// The `type` argument must be a subclass of [Element]. 137 /// 138 /// ## Sample code 139 /// 140 /// ```dart 141 /// expect(find.byElementType(SingleChildRenderObjectElement), findsOneWidget); 142 /// ``` 143 /// 144 /// If the `skipOffstage` argument is true (the default), then this skips 145 /// 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 /// Finds widgets whose current widget is the instance given by the 149 /// argument. 150 /// 151 /// ## Sample code 152 /// 153 /// ```dart 154 /// // Suppose you have a button created like this: 155 /// Widget myButton = new Button( 156 /// child: new Text('Update') 157 /// ); 158 /// 159 /// // You can find and tap on it like this: 160 /// tester.tap(find.byWidget(myButton)); 161 /// ``` 162 /// 163 /// If the `skipOffstage` argument is true (the default), then this skips 164 /// 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 /// ```dart 172 /// expect(find.byWidgetPredicate( 173 /// (Widget widget) => widget is Tooltip && widget.message == 'Back', 174 /// description: 'widget with tooltip "Back"', 175 /// ), findsOneWidget); 176 /// ``` 177 /// 178 /// If [description] is provided, then this uses it as the description of the 179 /// [Finder] and appears, for example, in the error message when the finder 180 /// fails to locate the desired widget. Otherwise, the description prints the 181 /// signature of the predicate function. 182 /// 183 /// If the `skipOffstage` argument is true (the default), then this skips 184 /// 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 /// Finds Tooltip widgets with the given message. 190 /// 191 /// ## Sample code 192 /// 193 /// ```dart 194 /// expect(find.byTooltip('Back'), findsOneWidget); 195 /// ``` 196 /// 197 /// If the `skipOffstage` argument is true (the default), then this skips 198 /// nodes that are [Offstage] or that are from inactive [Route]s. 199 Finder byTooltip(String message, { bool skipOffstage = true }) { 200 return byWidgetPredicate( 201 (Widget widget) => widget is Tooltip && widget.message == message, 202 skipOffstage: skipOffstage, 203 ); 204 } 205 206 /// Finds widgets using an element [predicate]. 207 /// 208 /// ## Sample code 209 /// 210 /// ```dart 211 /// expect(find.byElementPredicate( 212 /// // finds elements of type SingleChildRenderObjectElement, including 213 /// // those that are actually subclasses of that type. 214 /// // (contrast with byElementType, which only returns exact matches) 215 /// (Element element) => element is SingleChildRenderObjectElement, 216 /// description: '$SingleChildRenderObjectElement element', 217 /// ), findsOneWidget); 218 /// ``` 219 /// 220 /// If [description] is provided, then this uses it as the description of the 221 /// [Finder] and appears, for example, in the error message when the finder 222 /// fails to locate the desired widget. Otherwise, the description prints the 223 /// signature of the predicate function. 224 /// 225 /// If the `skipOffstage` argument is true (the default), then this skips 226 /// 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 /// Finds widgets that are descendants of the [of] parameter and that match 232 /// the [matching] parameter. 233 /// 234 /// ## Sample code 235 /// 236 /// ```dart 237 /// expect(find.descendant( 238 /// of: find.widgetWithText(Row, 'label_1'), matching: find.text('value_1') 239 /// ), findsOneWidget); 240 /// ``` 241 /// 242 /// If the [matchRoot] argument is true then the widget(s) specified by [of] 243 /// will be matched along with the descendants. 244 /// 245 /// If the [skipOffstage] argument is true (the default), then nodes that are 246 /// [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 /// Finds widgets that are ancestors of the [of] parameter and that match 252 /// the [matching] parameter. 253 /// 254 /// ## Sample code 255 /// 256 /// ```dart 257 /// // Test if a Text widget that contains 'faded' is the 258 /// // descendant of an Opacity widget with opacity 0.5: 259 /// expect( 260 /// tester.widget<Opacity>( 261 /// find.ancestor( 262 /// of: find.text('faded'), 263 /// matching: find.byType('Opacity'), 264 /// ) 265 /// ).opacity, 266 /// 0.5 267 /// ); 268 /// ``` 269 /// 270 /// If the [matchRoot] argument is true then the widget(s) specified by [of] 271 /// 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 /// Finds [Semantics] widgets matching the given `label`, either by 277 /// [RegExp.hasMatch] or string equality. 278 /// 279 /// The framework may combine semantics labels in certain scenarios, such as 280 /// when multiple [Text] widgets are in a [MaterialButton] widget. In such a 281 /// case, it may be preferable to match by regular expression. Consumers of 282 /// this API __must not__ introduce unsuitable content into the semantics tree 283 /// for the purposes of testing; in particular, you should prefer matching by 284 /// regular expression rather than by string if the framework has combined 285 /// your semantics, and not try to force the framework to break up the 286 /// semantics nodes. Breaking up the nodes would have an undesirable effect on 287 /// screen readers and other accessibility services. 288 /// 289 /// ## Sample code 290 /// 291 /// ```dart 292 /// expect(find.BySemanticsLabel('Back'), findsOneWidget); 293 /// ``` 294 /// 295 /// If the `skipOffstage` argument is true (the default), then this skips 296 /// nodes that are [Offstage] or that are from inactive [Route]s. 297 Finder bySemanticsLabel(Pattern label, { bool skipOffstage = true }) { 298 if (WidgetsBinding.instance.pipelineOwner.semanticsOwner == null) 299 throw StateError('Semantics are not enabled. ' 300 'Make sure to call tester.enableSemantics() before using ' 301 'this finder, and call dispose on its return value after.'); 302 return byElementPredicate( 303 (Element element) { 304 // Multiple elements can have the same renderObject - we want the "owner" 305 // of the renderObject, i.e. the RenderObjectElement. 306 if (element is! RenderObjectElement) { 307 return false; 308 } 309 final String semanticsLabel = element.renderObject?.debugSemantics?.label; 310 if (semanticsLabel == null) { 311 return false; 312 } 313 return label is RegExp 314 ? label.hasMatch(semanticsLabel) 315 : label == semanticsLabel; 316 }, 317 skipOffstage: skipOffstage, 318 ); 319 } 320} 321 322/// Searches a widget tree and returns nodes that match a particular 323/// pattern. 324abstract class Finder { 325 /// Initializes a Finder. Used by subclasses to initialize the [skipOffstage] 326 /// property. 327 Finder({ this.skipOffstage = true }); 328 329 /// Describes what the finder is looking for. The description should be 330 /// a brief English noun phrase describing the finder's pattern. 331 String get description; 332 333 /// Returns all the elements in the given list that match this 334 /// finder's pattern. 335 /// 336 /// When implementing your own Finders that inherit directly from 337 /// [Finder], this is the main method to override. If your finder 338 /// can efficiently be described just in terms of a predicate 339 /// function, consider extending [MatchFinder] instead. 340 Iterable<Element> apply(Iterable<Element> candidates); 341 342 /// Whether this finder skips nodes that are offstage. 343 /// 344 /// If this is true, then the elements are walked using 345 /// [Element.debugVisitOnstageChildren]. This skips offstage children of 346 /// [Offstage] widgets, as well as children of inactive [Route]s. 347 final bool skipOffstage; 348 349 /// Returns all the [Element]s that will be considered by this finder. 350 /// 351 /// See [collectAllElementsFrom]. 352 @protected 353 Iterable<Element> get allCandidates { 354 return collectAllElementsFrom( 355 WidgetsBinding.instance.renderViewElement, 356 skipOffstage: skipOffstage, 357 ); 358 } 359 360 Iterable<Element> _cachedResult; 361 362 /// Returns the current result. If [precache] was called and returned true, this will 363 /// cheaply return the result that was computed then. Otherwise, it creates a new 364 /// iterable to compute the answer. 365 /// 366 /// Calling this clears the cache from [precache]. 367 Iterable<Element> evaluate() { 368 final Iterable<Element> result = _cachedResult ?? apply(allCandidates); 369 _cachedResult = null; 370 return result; 371 } 372 373 /// Attempts to evaluate the finder. Returns whether any elements in the tree 374 /// matched the finder. If any did, then the result is cached and can be obtained 375 /// from [evaluate]. 376 /// 377 /// If this returns true, you must call [evaluate] before you call [precache] again. 378 bool precache() { 379 assert(_cachedResult == null); 380 final Iterable<Element> result = apply(allCandidates); 381 if (result.isNotEmpty) { 382 _cachedResult = result; 383 return true; 384 } 385 _cachedResult = null; 386 return false; 387 } 388 389 /// Returns a variant of this finder that only matches the first element 390 /// matched by this finder. 391 Finder get first => _FirstFinder(this); 392 393 /// Returns a variant of this finder that only matches the last element 394 /// matched by this finder. 395 Finder get last => _LastFinder(this); 396 397 /// Returns a variant of this finder that only matches the element at the 398 /// given index matched by this finder. 399 Finder at(int index) => _IndexFinder(this, index); 400 401 /// Returns a variant of this finder that only matches elements reachable by 402 /// a hit test. 403 /// 404 /// The [at] parameter specifies the location relative to the size of the 405 /// target element where the hit test is performed. 406 Finder hitTestable({ Alignment at = Alignment.center }) => _HitTestableFinder(this, at); 407 408 @override 409 String toString() { 410 final String additional = skipOffstage ? ' (ignoring offstage widgets)' : ''; 411 final List<Element> widgets = evaluate().toList(); 412 final int count = widgets.length; 413 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 return '$count widgets with $description$additional: $widgets'; 419 return '$count widgets with $description$additional: ${widgets[0]}, ${widgets[1]}, ${widgets[2]}, ...'; 420 } 421} 422 423/// Applies additional filtering against a [parent] [Finder]. 424abstract class ChainedFinder extends Finder { 425 /// Create a Finder chained against the candidates of another [Finder]. 426 ChainedFinder(this.parent) : assert(parent != null); 427 428 /// Another [Finder] that will run first. 429 final Finder parent; 430 431 /// Return another [Iterable] when given an [Iterable] of candidates from a 432 /// parent [Finder]. 433 /// 434 /// This is the method to implement when subclassing [ChainedFinder]. 435 Iterable<Element> filter(Iterable<Element> parentCandidates); 436 437 @override 438 Iterable<Element> apply(Iterable<Element> candidates) { 439 return filter(parent.apply(candidates)); 440 } 441 442 @override 443 Iterable<Element> get allCandidates => parent.allCandidates; 444} 445 446class _FirstFinder extends ChainedFinder { 447 _FirstFinder(Finder parent) : super(parent); 448 449 @override 450 String get description => '${parent.description} (ignoring all but first)'; 451 452 @override 453 Iterable<Element> filter(Iterable<Element> parentCandidates) sync* { 454 yield parentCandidates.first; 455 } 456} 457 458class _LastFinder extends ChainedFinder { 459 _LastFinder(Finder parent) : super(parent); 460 461 @override 462 String get description => '${parent.description} (ignoring all but last)'; 463 464 @override 465 Iterable<Element> filter(Iterable<Element> parentCandidates) sync* { 466 yield parentCandidates.last; 467 } 468} 469 470class _IndexFinder extends ChainedFinder { 471 _IndexFinder(Finder parent, this.index) : super(parent); 472 473 final int index; 474 475 @override 476 String get description => '${parent.description} (ignoring all but index $index)'; 477 478 @override 479 Iterable<Element> filter(Iterable<Element> parentCandidates) sync* { 480 yield parentCandidates.elementAt(index); 481 } 482} 483 484class _HitTestableFinder extends ChainedFinder { 485 _HitTestableFinder(Finder parent, this.alignment) : super(parent); 486 487 final Alignment alignment; 488 489 @override 490 String get description => '${parent.description} (considering only hit-testable ones)'; 491 492 @override 493 Iterable<Element> filter(Iterable<Element> parentCandidates) sync* { 494 for (final Element candidate in parentCandidates) { 495 final RenderBox box = candidate.renderObject; 496 assert(box != null); 497 final Offset absoluteOffset = box.localToGlobal(alignment.alongSize(box.size)); 498 final HitTestResult hitResult = HitTestResult(); 499 WidgetsBinding.instance.hitTest(hitResult, absoluteOffset); 500 for (final HitTestEntry entry in hitResult.path) { 501 if (entry.target == candidate.renderObject) { 502 yield candidate; 503 break; 504 } 505 } 506 } 507 } 508} 509 510/// Searches a widget tree and returns nodes that match a particular 511/// pattern. 512abstract 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 /// Returns true if the given element matches the pattern. 518 /// 519 /// When implementing your own MatchFinder, this is the main method to override. 520 bool matches(Element candidate); 521 522 @override 523 Iterable<Element> apply(Iterable<Element> candidates) { 524 return candidates.where(matches); 525 } 526} 527 528class _TextFinder extends MatchFinder { 529 _TextFinder(this.text, { bool skipOffstage = true }) : super(skipOffstage: skipOffstage); 530 531 final String text; 532 533 @override 534 String get description => 'text "$text"'; 535 536 @override 537 bool matches(Element candidate) { 538 if (candidate.widget is Text) { 539 final Text textWidget = candidate.widget; 540 if (textWidget.data != null) 541 return textWidget.data == text; 542 return textWidget.textSpan.toPlainText() == text; 543 } else if (candidate.widget is EditableText) { 544 final EditableText editable = candidate.widget; 545 return editable.controller.text == text; 546 } 547 return false; 548 } 549} 550 551class _KeyFinder extends MatchFinder { 552 _KeyFinder(this.key, { bool skipOffstage = true }) : super(skipOffstage: skipOffstage); 553 554 final Key key; 555 556 @override 557 String get description => 'key $key'; 558 559 @override 560 bool matches(Element candidate) { 561 return candidate.widget.key == key; 562 } 563} 564 565class _WidgetTypeFinder extends MatchFinder { 566 _WidgetTypeFinder(this.widgetType, { bool skipOffstage = true }) : super(skipOffstage: skipOffstage); 567 568 final Type widgetType; 569 570 @override 571 String get description => 'type "$widgetType"'; 572 573 @override 574 bool matches(Element candidate) { 575 return candidate.widget.runtimeType == widgetType; 576 } 577} 578 579class _WidgetIconFinder extends MatchFinder { 580 _WidgetIconFinder(this.icon, { bool skipOffstage = true }) : super(skipOffstage: skipOffstage); 581 582 final IconData icon; 583 584 @override 585 String get description => 'icon "$icon"'; 586 587 @override 588 bool matches(Element candidate) { 589 final Widget widget = candidate.widget; 590 return widget is Icon && widget.icon == icon; 591 } 592} 593 594class _ElementTypeFinder extends MatchFinder { 595 _ElementTypeFinder(this.elementType, { bool skipOffstage = true }) : super(skipOffstage: skipOffstage); 596 597 final Type elementType; 598 599 @override 600 String get description => 'type "$elementType"'; 601 602 @override 603 bool matches(Element candidate) { 604 return candidate.runtimeType == elementType; 605 } 606} 607 608class _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 @override 617 bool matches(Element candidate) { 618 return candidate.widget == widget; 619 } 620} 621 622class _WidgetPredicateFinder extends MatchFinder { 623 _WidgetPredicateFinder(this.predicate, { String description, bool skipOffstage = true }) 624 : _description = description, 625 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 @override 634 bool matches(Element candidate) { 635 return predicate(candidate.widget); 636 } 637} 638 639class _ElementPredicateFinder extends MatchFinder { 640 _ElementPredicateFinder(this.predicate, { String description, bool skipOffstage = true }) 641 : _description = description, 642 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 @override 651 bool matches(Element candidate) { 652 return predicate(candidate); 653 } 654} 655 656class _DescendantFinder extends Finder { 657 _DescendantFinder( 658 this.ancestor, 659 this.descendant, { 660 this.matchRoot = false, 661 bool skipOffstage = true, 662 }) : super(skipOffstage: skipOffstage); 663 664 final Finder ancestor; 665 final Finder descendant; 666 final bool matchRoot; 667 668 @override 669 String get description { 670 if (matchRoot) 671 return '${descendant.description} in the subtree(s) beginning with ${ancestor.description}'; 672 return '${descendant.description} that has ancestor(s) with ${ancestor.description}'; 673 } 674 675 @override 676 Iterable<Element> apply(Iterable<Element> candidates) { 677 return candidates.where((Element element) => descendant.evaluate().contains(element)); 678 } 679 680 @override 681 Iterable<Element> get allCandidates { 682 final Iterable<Element> ancestorElements = ancestor.evaluate(); 683 final List<Element> candidates = ancestorElements.expand<Element>( 684 (Element element) => collectAllElementsFrom(element, skipOffstage: skipOffstage) 685 ).toSet().toList(); 686 if (matchRoot) 687 candidates.insertAll(0, ancestorElements); 688 return candidates; 689 } 690} 691 692class _AncestorFinder extends Finder { 693 _AncestorFinder(this.descendant, this.ancestor, { this.matchRoot = false }) : super(skipOffstage: false); 694 695 final Finder ancestor; 696 final Finder descendant; 697 final bool matchRoot; 698 699 @override 700 String get description { 701 if (matchRoot) 702 return 'ancestor ${ancestor.description} beginning with ${descendant.description}'; 703 return '${ancestor.description} which is an ancestor of ${descendant.description}'; 704 } 705 706 @override 707 Iterable<Element> apply(Iterable<Element> candidates) { 708 return candidates.where((Element element) => ancestor.evaluate().contains(element)); 709 } 710 711 @override 712 Iterable<Element> get allCandidates { 713 final List<Element> candidates = <Element>[]; 714 for (Element root in descendant.evaluate()) { 715 final List<Element> ancestors = <Element>[]; 716 if (matchRoot) 717 ancestors.add(root); 718 root.visitAncestorElements((Element element) { 719 ancestors.add(element); 720 return true; 721 }); 722 candidates.addAll(ancestors); 723 } 724 return candidates; 725 } 726} 727