From 1fdae44c851657d7ef213c92a18aa7e6757d7bda Mon Sep 17 00:00:00 2001 From: Maikel Rehl Date: Wed, 31 Jan 2024 15:35:34 +0100 Subject: [PATCH 01/22] Add offstage flag to WidgetTreeNode based on debugVisitOnstageChildren --- lib/src/spot/element_extensions.dart | 8 ++++++++ lib/src/spot/tree_snapshot.dart | 28 ++++++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/lib/src/spot/element_extensions.dart b/lib/src/spot/element_extensions.dart index 32167f85..2c851288 100644 --- a/lib/src/spot/element_extensions.dart +++ b/lib/src/spot/element_extensions.dart @@ -28,4 +28,12 @@ extension ElementExtensions on Element { visitChildren(found.add); yield* found; } + + /// Returns only onstage children of [Element] in depth first order from the closest + /// to the leaves + Iterable get onstageChildren sync* { + final List found = []; + debugVisitOnstageChildren(found.add); + yield* found; + } } diff --git a/lib/src/spot/tree_snapshot.dart b/lib/src/spot/tree_snapshot.dart index 60a1a41b..a658d442 100644 --- a/lib/src/spot/tree_snapshot.dart +++ b/lib/src/spot/tree_snapshot.dart @@ -23,14 +23,35 @@ WidgetTreeSnapshot createWidgetTreeSnapshot() { // ignore: deprecated_member_use final rootElement = WidgetsBinding.instance.renderViewElement!; - WidgetTreeNode build(Element element, {WidgetTreeNode? parent}) { + WidgetTreeNode build( + Element element, { + WidgetTreeNode? parent, + bool isOffstage = false, + }) { + // Get all onstage children of the parent + final allOnstageChildren = parent?.element.onstageChildren.toList(); + + final bool isOffstageElement; + if (allOnstageChildren == null) { + // If there is no parent use isOffstage which is false by default + isOffstageElement = isOffstage; + } else { + // Check if the current element is offstage or use the parent's state + isOffstageElement = !allOnstageChildren.contains(element) || isOffstage; + } + final snapshot = WidgetTreeNode( element: element, parent: parent, + isOffstage: isOffstageElement, ); for (final child in element.children) { - snapshot.addChild(build(child, parent: snapshot)); + snapshot.addChild(build( + child, + parent: snapshot, + isOffstage: isOffstageElement, + )); } return snapshot; } @@ -71,12 +92,15 @@ class WidgetTreeNode { /// node of the tree. final WidgetTreeNode? parent; + final bool isOffstage; + /// Creates an [Element] in the element tree. /// /// Do not forget to call [addChild] manually after creation! WidgetTreeNode({ required this.element, required this.parent, + this.isOffstage = false, }); @override From e9ad11f371846312c2c9d80628820c9b7b2360df Mon Sep 17 00:00:00 2001 From: Maikel Rehl Date: Wed, 31 Jan 2024 16:00:17 +0100 Subject: [PATCH 02/22] Add skipOffstage flag and set it true by default --- lib/spot.dart | 36 +++++++++- lib/src/spot/onstage_element_filter.dart | 29 ++++++++ lib/src/spot/selectors.dart | 88 +++++++++++++++++++++--- lib/src/spot/text/any_text.dart | 10 ++- 4 files changed, 150 insertions(+), 13 deletions(-) create mode 100644 lib/src/spot/onstage_element_filter.dart diff --git a/lib/spot.dart b/lib/spot.dart index 9b317a2f..148d2e42 100644 --- a/lib/spot.dart +++ b/lib/spot.dart @@ -119,10 +119,12 @@ WidgetSelector get allWidgets => WidgetSelector.all; @useResult @Deprecated('Use spot().atMost(1)') WidgetSelector spotSingle({ + bool skipOffstage = true, List parents = const [], List children = const [], }) { return _global.spotSingle( + skipOffstage: skipOffstage, parents: parents, children: children, ); @@ -138,10 +140,12 @@ WidgetSelector spotSingle({ /// does not accidentally match a [Center] Widget, that extends [Align]. @useResult WidgetSelector spot({ + bool skipOffstage = true, List parents = const [], List children = const [], }) { return _global.spot( + skipOffstage: skipOffstage, parents: parents, children: children, ); @@ -153,11 +157,13 @@ WidgetSelector spot({ /// The comparison happens by identity (===) WidgetSelector spotWidget( W widget, { + bool skipOffstage = true, List parents = const [], List children = const [], }) { return _global.spotWidget( widget, + skipOffstage: skipOffstage, parents: parents, children: children, ); @@ -169,11 +175,13 @@ WidgetSelector spotWidget( @Deprecated('Use spotWidget().atMost(1)') WidgetSelector spotSingleWidget( W widget, { + bool skipOffstage = true, List parents = const [], List children = const [], }) { return _global.spotSingleWidget( widget, + skipOffstage: skipOffstage, parents: parents, children: children, ); @@ -184,11 +192,13 @@ WidgetSelector spotSingleWidget( @Deprecated('Use spotWidget()') WidgetSelector spotWidgets( W widget, { + bool skipOffstage = true, List parents = const [], List children = const [], }) { return _global.spotWidgets( widget, + skipOffstage: skipOffstage, parents: parents, children: children, ); @@ -199,10 +209,16 @@ WidgetSelector spotWidgets( @useResult WidgetSelector spotElement( Element element, { + bool skipOffstage = true, List parents = const [], List children = const [], }) { - return _global.spotElement(element); + return _global.spotElement( + element, + skipOffstage: skipOffstage, + parents: parents, + children: children, + ); } /// Finds text on the screen @@ -223,12 +239,14 @@ WidgetSelector spotElement( @useResult WidgetSelector spotText( Pattern text, { + bool skipOffstage = true, List parents = const [], List children = const [], bool exact = false, }) { return _global.spotText( text, + skipOffstage: skipOffstage, parents: parents, children: children, exact: exact, @@ -252,11 +270,13 @@ WidgetSelector spotText( @useResult WidgetSelector spotTextWhere( void Function(Subject) match, { + bool skipOffstage = true, List parents = const [], List children = const [], }) { return _global.spotTextWhere( match, + skipOffstage: skipOffstage, parents: parents, children: children, ); @@ -275,12 +295,14 @@ WidgetSelector spotTextWhere( @useResult WidgetSelector spotSingleText( String text, { + bool skipOffstage = true, List parents = const [], List children = const [], bool findRichText = false, }) { return _global.spotSingleText( text, + skipOffstage: skipOffstage, parents: parents, children: children, findRichText: findRichText, @@ -302,12 +324,14 @@ WidgetSelector spotSingleText( @useResult WidgetSelector spotTexts( String text, { + bool skipOffstage = true, List parents = const [], List children = const [], bool findRichText = false, }) { return _global.spotTexts( text, + skipOffstage: skipOffstage, parents: parents, children: children, findRichText: findRichText, @@ -326,6 +350,7 @@ WidgetSelector spotSingleIcon( }) { return _global.spotSingleIcon( icon, + skipOffstage: skipOffstage, parents: parents, children: children, ); @@ -336,11 +361,13 @@ WidgetSelector spotSingleIcon( @useResult WidgetSelector spotIcon( IconData icon, { + bool skipOffstage = true, List parents = const [], List children = const [], }) { return _global.spotIcon( icon, + skipOffstage: skipOffstage, parents: parents, children: children, ); @@ -358,6 +385,7 @@ WidgetSelector spotIcons( }) { return _global.spotIcons( icon, + skipOffstage: skipOffstage, parents: parents, children: children, ); @@ -368,11 +396,13 @@ WidgetSelector spotIcons( @Deprecated('Use spotKey().atMost(1)') WidgetSelector spotSingleKey( Key key, { + bool skipOffstage = true, List parents = const [], List children = const [], }) { return _global.spotSingleKey( key, + skipOffstage: skipOffstage, parents: parents, children: children, ); @@ -382,11 +412,13 @@ WidgetSelector spotSingleKey( @useResult WidgetSelector spotKey( Key key, { + bool skipOffstage = true, List parents = const [], List children = const [], }) { return _global.spotKey( key, + skipOffstage: skipOffstage, parents: parents, children: children, ); @@ -398,11 +430,13 @@ WidgetSelector spotKey( @Deprecated('Use spotKey()') WidgetSelector spotKeys( Key key, { + bool skipOffstage = true, List parents = const [], List children = const [], }) { return _global.spotKeys( key, + skipOffstage: skipOffstage, parents: parents, children: children, ); diff --git a/lib/src/spot/onstage_element_filter.dart b/lib/src/spot/onstage_element_filter.dart new file mode 100644 index 00000000..d693695c --- /dev/null +++ b/lib/src/spot/onstage_element_filter.dart @@ -0,0 +1,29 @@ +import 'package:dartx/dartx.dart'; +import 'package:flutter/widgets.dart'; +import 'package:spot/src/spot/snapshot.dart'; +import 'package:spot/src/spot/widget_selector.dart'; + +/// Removes all [WidgetTreeNode] that are offstage +class OnstageFilter implements ElementFilter { + @override + Iterable filter(Iterable candidates) { + final List matchingChildNodes = []; + + for (final WidgetTreeNode candidate in candidates) { + if (!candidate.isOffstage) { + matchingChildNodes.add(candidate); + } + } + return matchingChildNodes; + } + + @override + String get description { + return 'removes all offstage elements'; + } + + @override + String toString() { + return 'OnstageFilter which $description'; + } +} diff --git a/lib/src/spot/selectors.dart b/lib/src/spot/selectors.dart index 3ebdded4..1ba0c520 100644 --- a/lib/src/spot/selectors.dart +++ b/lib/src/spot/selectors.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; import 'package:spot/src/checks/checks_nullability.dart'; +import 'package:spot/src/spot/onstage_element_filter.dart'; import 'package:spot/src/spot/snapshot.dart' as snapshot_file show snapshot; import 'package:spot/src/spot/snapshot.dart'; import 'package:spot/src/spot/text/any_text.dart'; @@ -48,6 +49,7 @@ mixin ChainableSelectors { /// ``` @useResult WidgetSelector spot({ + bool skipOffstage = true, List parents = const [], List children = const [], }) { @@ -55,6 +57,9 @@ mixin ChainableSelectors { props: [ WidgetTypePredicate(), ], + elementFilters: [ + if (skipOffstage) OnstageFilter(), + ], parents: [if (self != null) self!, ...parents], children: children, ); @@ -78,10 +83,15 @@ mixin ChainableSelectors { @useResult @Deprecated('Use spot().atMost(1)') WidgetSelector spotSingle({ + bool skipOffstage = true, List parents = const [], List children = const [], }) { - return spot(parents: parents, children: children).atMost(1); + return spot( + skipOffstage: skipOffstage, + parents: parents, + children: children, + ).atMost(1); } /// Creates a [WidgetSelector] that finds [widget] by identity and returns all @@ -91,6 +101,7 @@ mixin ChainableSelectors { @useResult WidgetSelector spotWidget( W widget, { + bool skipOffstage = true, List parents = const [], List children = const [], }) { @@ -102,6 +113,9 @@ mixin ChainableSelectors { description: 'Widget === $widget', ), ], + elementFilters: [ + if (skipOffstage) OnstageFilter(), + ], parents: [if (self != null) self!, ...parents], children: children, ); @@ -115,11 +129,16 @@ mixin ChainableSelectors { @Deprecated('Use spotWidget().atMost(1)') WidgetSelector spotSingleWidget( W widget, { + bool skipOffstage = true, List parents = const [], List children = const [], }) { - return spotWidgets(widget, parents: parents, children: children) - .atMost(1); + return spotWidgets( + widget, + skipOffstage: skipOffstage, + parents: parents, + children: children, + ).atMost(1); } /// Creates a [WidgetSelector] that finds all [widget] by identity @@ -129,10 +148,16 @@ mixin ChainableSelectors { @Deprecated('Use spotWidget().atMost(1)') WidgetSelector spotWidgets( W widget, { + bool skipOffstage = true, List parents = const [], List children = const [], }) { - return spotWidget(widget, parents: parents, children: children); + return spotWidget( + widget, + skipOffstage: skipOffstage, + parents: parents, + children: children, + ); } /// Creates a [WidgetSelector] that finds the widget that is associated with @@ -142,6 +167,7 @@ mixin ChainableSelectors { @useResult WidgetSelector spotElement( Element element, { + bool skipOffstage = true, List parents = const [], List children = const [], }) { @@ -153,6 +179,9 @@ mixin ChainableSelectors { description: 'Element === $element', ), ], + elementFilters: [ + if (skipOffstage) OnstageFilter(), + ], parents: [if (self != null) self!, ...parents], children: children, ); @@ -177,6 +206,7 @@ mixin ChainableSelectors { @useResult WidgetSelector spotText( Pattern text, { + bool skipOffstage = true, List parents = const [], List children = const [], bool exact = false, @@ -189,6 +219,7 @@ mixin ChainableSelectors { } return spotTextWhere( (it) => it.equals(text), + skipOffstage: skipOffstage, description: 'with text "$text"', parents: parents, children: children, @@ -198,6 +229,7 @@ mixin ChainableSelectors { // default with contains return spotTextWhere( (it) => it.contains(text), + skipOffstage: skipOffstage, description: 'contains text "$text"', parents: parents, children: children, @@ -219,6 +251,7 @@ mixin ChainableSelectors { @useResult WidgetSelector spotTextWhere( void Function(Subject) match, { + bool skipOffstage = true, List parents = const [], List children = const [], String? description, @@ -236,6 +269,7 @@ mixin ChainableSelectors { description: 'Widget with text $name', ), ], + skipOffstage: skipOffstage, parents: [if (self != null) self!, ...parents], children: children, ); @@ -250,12 +284,14 @@ mixin ChainableSelectors { @useResult WidgetSelector spotSingleText( String text, { + bool skipOffstage = true, List parents = const [], List children = const [], bool findRichText = false, }) { return spotTexts( text, + skipOffstage: skipOffstage, parents: parents, children: children, findRichText: findRichText, @@ -271,6 +307,7 @@ mixin ChainableSelectors { @useResult WidgetSelector spotTexts( String text, { + bool skipOffstage = true, List parents = const [], List children = const [], bool findRichText = false, @@ -301,6 +338,9 @@ mixin ChainableSelectors { description: 'Widget with exact text: "$text"', ), ], + elementFilters: [ + if (skipOffstage) OnstageFilter(), + ], parents: [if (self != null) self!, ...parents], children: children, ); @@ -327,6 +367,9 @@ mixin ChainableSelectors { description: 'Widget with icon: "$icon"', ), ], + elementFilters: [ + if (skipOffstage) OnstageFilter(), + ], parents: [if (self != null) self!, ...parents], children: children, ); @@ -375,6 +418,7 @@ mixin ChainableSelectors { @useResult WidgetSelector spotKey( Key key, { + bool skipOffstage = true, List parents = const [], List children = const [], }) { @@ -386,6 +430,9 @@ mixin ChainableSelectors { description: 'with key: "$key"', ), ], + elementFilters: [ + if (skipOffstage) OnstageFilter(), + ], parents: [if (self != null) self!, ...parents], children: children, ); @@ -396,11 +443,13 @@ mixin ChainableSelectors { @Deprecated('Use spotKey().atMost(1)') WidgetSelector spotSingleKey( Key key, { + bool skipOffstage = true, List parents = const [], List children = const [], }) { return spotKey( key, + skipOffstage: skipOffstage, parents: parents, children: children, ).atMost(1); @@ -411,11 +460,13 @@ mixin ChainableSelectors { @Deprecated('Use spotKey()') WidgetSelector spotKeys( Key key, { + bool skipOffstage = true, List parents = const [], List children = const [], }) { return spotKey( key, + skipOffstage: skipOffstage, parents: parents, children: children, ); @@ -432,9 +483,12 @@ mixin ChainableSelectors { /// spot
().first().spotText('Pepe').existsOnce(); /// ``` @useResult - WidgetSelector first() { + WidgetSelector first({bool skipOffstage = true}) { // TODO add names to the elementFilters, for a better WidgetSelector.toString() - return self!.copyWith(elementFilters: [_FirstElement()]); + return self!.copyWith(elementFilters: [ + if (skipOffstage) OnstageFilter(), + _FirstElement(), + ]); } /// Selects the last of n widgets @@ -448,8 +502,11 @@ mixin ChainableSelectors { /// spot
().last().spotText('Pepe').existsOnce(); /// ``` @useResult - WidgetSelector last() { - return self!.copyWith(elementFilters: [_LastElement()]); + WidgetSelector last({bool skipOffstage = true}) { + return self!.copyWith(elementFilters: [ + if (skipOffstage) OnstageFilter(), + _LastElement(), + ]); } /// Selects the widget at a specified [index] in the list of found widgets. @@ -459,8 +516,11 @@ mixin ChainableSelectors { /// spot().atIndex(2) // Selects the third widget ///``` @useResult - WidgetSelector atIndex(int index) { - return self!.copyWith(elementFilters: [_ElementAtIndex(index)]); + WidgetSelector atIndex(int index, {bool skipOffstage = true}) { + return self!.copyWith(elementFilters: [ + if (skipOffstage) OnstageFilter(), + _ElementAtIndex(index), + ]); } } @@ -540,6 +600,7 @@ extension SelectorQueries on WidgetSelector { WidgetSelector whereElement( bool Function(Element element) predicate, { required String description, + bool skipOffstage = true, }) { return self.copyWith( props: [ @@ -549,6 +610,9 @@ extension SelectorQueries on WidgetSelector { description: description, ), ], + elementFilters: [ + if (skipOffstage) OnstageFilter(), + ], ); } @@ -569,6 +633,7 @@ extension SelectorQueries on WidgetSelector { WidgetSelector whereWidget( bool Function(W widget) predicate, { required String description, + bool skipOffstage = true, }) { return self.copyWith( props: [ @@ -581,6 +646,9 @@ extension SelectorQueries on WidgetSelector { description: description, ), ], + elementFilters: [ + if (skipOffstage) OnstageFilter(), + ], ); } } diff --git a/lib/src/spot/text/any_text.dart b/lib/src/spot/text/any_text.dart index 4d6b0beb..32d65fa4 100644 --- a/lib/src/spot/text/any_text.dart +++ b/lib/src/spot/text/any_text.dart @@ -3,6 +3,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:spot/src/checks/checks_nullability.dart'; +import 'package:spot/src/spot/onstage_element_filter.dart'; import 'package:spot/src/spot/widget_selector.dart'; /// A union type for any text widget that can be found in the widget tree. @@ -238,9 +239,12 @@ class AnyTextWidgetSelector extends WidgetSelector { /// - `parents`: Parent selectors to include in the match. AnyTextWidgetSelector({ required super.props, + this.skipOffstage = true, super.children, super.parents, - }) : super(mapElementToWidget: _mapElementToAnyText); + }) : super(mapElementToWidget: _mapElementToAnyText, elementFilters: [ + if (skipOffstage) OnstageFilter(), + ]); static AnyText _mapElementToAnyText(Element element) { if (element.widget is RichText) { @@ -257,10 +261,12 @@ class AnyTextWidgetSelector extends WidgetSelector { ); } + final bool skipOffstage; + @override List createElementFilters() { + // Matches multiple widget types, can't filter by synthetic type AnyText return super.createElementFilters() - // Matches multiple widget types, can't filter by synthetic type AnyText ..removeWhere((it) => it is WidgetTypeFilter); } } From b80be9e305a37f5e6852da848e740e7e2ba1ff33 Mon Sep 17 00:00:00 2001 From: Maikel Rehl Date: Mon, 5 Feb 2024 16:08:18 +0100 Subject: [PATCH 03/22] Revert "Add skipOffstage flag and set it true by default" This reverts commit e9ad11f371846312c2c9d80628820c9b7b2360df. --- lib/spot.dart | 36 +--------- lib/src/spot/onstage_element_filter.dart | 29 -------- lib/src/spot/selectors.dart | 88 +++--------------------- lib/src/spot/text/any_text.dart | 10 +-- 4 files changed, 13 insertions(+), 150 deletions(-) delete mode 100644 lib/src/spot/onstage_element_filter.dart diff --git a/lib/spot.dart b/lib/spot.dart index 148d2e42..9b317a2f 100644 --- a/lib/spot.dart +++ b/lib/spot.dart @@ -119,12 +119,10 @@ WidgetSelector get allWidgets => WidgetSelector.all; @useResult @Deprecated('Use spot().atMost(1)') WidgetSelector spotSingle({ - bool skipOffstage = true, List parents = const [], List children = const [], }) { return _global.spotSingle( - skipOffstage: skipOffstage, parents: parents, children: children, ); @@ -140,12 +138,10 @@ WidgetSelector spotSingle({ /// does not accidentally match a [Center] Widget, that extends [Align]. @useResult WidgetSelector spot({ - bool skipOffstage = true, List parents = const [], List children = const [], }) { return _global.spot( - skipOffstage: skipOffstage, parents: parents, children: children, ); @@ -157,13 +153,11 @@ WidgetSelector spot({ /// The comparison happens by identity (===) WidgetSelector spotWidget( W widget, { - bool skipOffstage = true, List parents = const [], List children = const [], }) { return _global.spotWidget( widget, - skipOffstage: skipOffstage, parents: parents, children: children, ); @@ -175,13 +169,11 @@ WidgetSelector spotWidget( @Deprecated('Use spotWidget().atMost(1)') WidgetSelector spotSingleWidget( W widget, { - bool skipOffstage = true, List parents = const [], List children = const [], }) { return _global.spotSingleWidget( widget, - skipOffstage: skipOffstage, parents: parents, children: children, ); @@ -192,13 +184,11 @@ WidgetSelector spotSingleWidget( @Deprecated('Use spotWidget()') WidgetSelector spotWidgets( W widget, { - bool skipOffstage = true, List parents = const [], List children = const [], }) { return _global.spotWidgets( widget, - skipOffstage: skipOffstage, parents: parents, children: children, ); @@ -209,16 +199,10 @@ WidgetSelector spotWidgets( @useResult WidgetSelector spotElement( Element element, { - bool skipOffstage = true, List parents = const [], List children = const [], }) { - return _global.spotElement( - element, - skipOffstage: skipOffstage, - parents: parents, - children: children, - ); + return _global.spotElement(element); } /// Finds text on the screen @@ -239,14 +223,12 @@ WidgetSelector spotElement( @useResult WidgetSelector spotText( Pattern text, { - bool skipOffstage = true, List parents = const [], List children = const [], bool exact = false, }) { return _global.spotText( text, - skipOffstage: skipOffstage, parents: parents, children: children, exact: exact, @@ -270,13 +252,11 @@ WidgetSelector spotText( @useResult WidgetSelector spotTextWhere( void Function(Subject) match, { - bool skipOffstage = true, List parents = const [], List children = const [], }) { return _global.spotTextWhere( match, - skipOffstage: skipOffstage, parents: parents, children: children, ); @@ -295,14 +275,12 @@ WidgetSelector spotTextWhere( @useResult WidgetSelector spotSingleText( String text, { - bool skipOffstage = true, List parents = const [], List children = const [], bool findRichText = false, }) { return _global.spotSingleText( text, - skipOffstage: skipOffstage, parents: parents, children: children, findRichText: findRichText, @@ -324,14 +302,12 @@ WidgetSelector spotSingleText( @useResult WidgetSelector spotTexts( String text, { - bool skipOffstage = true, List parents = const [], List children = const [], bool findRichText = false, }) { return _global.spotTexts( text, - skipOffstage: skipOffstage, parents: parents, children: children, findRichText: findRichText, @@ -350,7 +326,6 @@ WidgetSelector spotSingleIcon( }) { return _global.spotSingleIcon( icon, - skipOffstage: skipOffstage, parents: parents, children: children, ); @@ -361,13 +336,11 @@ WidgetSelector spotSingleIcon( @useResult WidgetSelector spotIcon( IconData icon, { - bool skipOffstage = true, List parents = const [], List children = const [], }) { return _global.spotIcon( icon, - skipOffstage: skipOffstage, parents: parents, children: children, ); @@ -385,7 +358,6 @@ WidgetSelector spotIcons( }) { return _global.spotIcons( icon, - skipOffstage: skipOffstage, parents: parents, children: children, ); @@ -396,13 +368,11 @@ WidgetSelector spotIcons( @Deprecated('Use spotKey().atMost(1)') WidgetSelector spotSingleKey( Key key, { - bool skipOffstage = true, List parents = const [], List children = const [], }) { return _global.spotSingleKey( key, - skipOffstage: skipOffstage, parents: parents, children: children, ); @@ -412,13 +382,11 @@ WidgetSelector spotSingleKey( @useResult WidgetSelector spotKey( Key key, { - bool skipOffstage = true, List parents = const [], List children = const [], }) { return _global.spotKey( key, - skipOffstage: skipOffstage, parents: parents, children: children, ); @@ -430,13 +398,11 @@ WidgetSelector spotKey( @Deprecated('Use spotKey()') WidgetSelector spotKeys( Key key, { - bool skipOffstage = true, List parents = const [], List children = const [], }) { return _global.spotKeys( key, - skipOffstage: skipOffstage, parents: parents, children: children, ); diff --git a/lib/src/spot/onstage_element_filter.dart b/lib/src/spot/onstage_element_filter.dart deleted file mode 100644 index d693695c..00000000 --- a/lib/src/spot/onstage_element_filter.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:dartx/dartx.dart'; -import 'package:flutter/widgets.dart'; -import 'package:spot/src/spot/snapshot.dart'; -import 'package:spot/src/spot/widget_selector.dart'; - -/// Removes all [WidgetTreeNode] that are offstage -class OnstageFilter implements ElementFilter { - @override - Iterable filter(Iterable candidates) { - final List matchingChildNodes = []; - - for (final WidgetTreeNode candidate in candidates) { - if (!candidate.isOffstage) { - matchingChildNodes.add(candidate); - } - } - return matchingChildNodes; - } - - @override - String get description { - return 'removes all offstage elements'; - } - - @override - String toString() { - return 'OnstageFilter which $description'; - } -} diff --git a/lib/src/spot/selectors.dart b/lib/src/spot/selectors.dart index 1ba0c520..3ebdded4 100644 --- a/lib/src/spot/selectors.dart +++ b/lib/src/spot/selectors.dart @@ -4,7 +4,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; import 'package:spot/src/checks/checks_nullability.dart'; -import 'package:spot/src/spot/onstage_element_filter.dart'; import 'package:spot/src/spot/snapshot.dart' as snapshot_file show snapshot; import 'package:spot/src/spot/snapshot.dart'; import 'package:spot/src/spot/text/any_text.dart'; @@ -49,7 +48,6 @@ mixin ChainableSelectors { /// ``` @useResult WidgetSelector spot({ - bool skipOffstage = true, List parents = const [], List children = const [], }) { @@ -57,9 +55,6 @@ mixin ChainableSelectors { props: [ WidgetTypePredicate(), ], - elementFilters: [ - if (skipOffstage) OnstageFilter(), - ], parents: [if (self != null) self!, ...parents], children: children, ); @@ -83,15 +78,10 @@ mixin ChainableSelectors { @useResult @Deprecated('Use spot().atMost(1)') WidgetSelector spotSingle({ - bool skipOffstage = true, List parents = const [], List children = const [], }) { - return spot( - skipOffstage: skipOffstage, - parents: parents, - children: children, - ).atMost(1); + return spot(parents: parents, children: children).atMost(1); } /// Creates a [WidgetSelector] that finds [widget] by identity and returns all @@ -101,7 +91,6 @@ mixin ChainableSelectors { @useResult WidgetSelector spotWidget( W widget, { - bool skipOffstage = true, List parents = const [], List children = const [], }) { @@ -113,9 +102,6 @@ mixin ChainableSelectors { description: 'Widget === $widget', ), ], - elementFilters: [ - if (skipOffstage) OnstageFilter(), - ], parents: [if (self != null) self!, ...parents], children: children, ); @@ -129,16 +115,11 @@ mixin ChainableSelectors { @Deprecated('Use spotWidget().atMost(1)') WidgetSelector spotSingleWidget( W widget, { - bool skipOffstage = true, List parents = const [], List children = const [], }) { - return spotWidgets( - widget, - skipOffstage: skipOffstage, - parents: parents, - children: children, - ).atMost(1); + return spotWidgets(widget, parents: parents, children: children) + .atMost(1); } /// Creates a [WidgetSelector] that finds all [widget] by identity @@ -148,16 +129,10 @@ mixin ChainableSelectors { @Deprecated('Use spotWidget().atMost(1)') WidgetSelector spotWidgets( W widget, { - bool skipOffstage = true, List parents = const [], List children = const [], }) { - return spotWidget( - widget, - skipOffstage: skipOffstage, - parents: parents, - children: children, - ); + return spotWidget(widget, parents: parents, children: children); } /// Creates a [WidgetSelector] that finds the widget that is associated with @@ -167,7 +142,6 @@ mixin ChainableSelectors { @useResult WidgetSelector spotElement( Element element, { - bool skipOffstage = true, List parents = const [], List children = const [], }) { @@ -179,9 +153,6 @@ mixin ChainableSelectors { description: 'Element === $element', ), ], - elementFilters: [ - if (skipOffstage) OnstageFilter(), - ], parents: [if (self != null) self!, ...parents], children: children, ); @@ -206,7 +177,6 @@ mixin ChainableSelectors { @useResult WidgetSelector spotText( Pattern text, { - bool skipOffstage = true, List parents = const [], List children = const [], bool exact = false, @@ -219,7 +189,6 @@ mixin ChainableSelectors { } return spotTextWhere( (it) => it.equals(text), - skipOffstage: skipOffstage, description: 'with text "$text"', parents: parents, children: children, @@ -229,7 +198,6 @@ mixin ChainableSelectors { // default with contains return spotTextWhere( (it) => it.contains(text), - skipOffstage: skipOffstage, description: 'contains text "$text"', parents: parents, children: children, @@ -251,7 +219,6 @@ mixin ChainableSelectors { @useResult WidgetSelector spotTextWhere( void Function(Subject) match, { - bool skipOffstage = true, List parents = const [], List children = const [], String? description, @@ -269,7 +236,6 @@ mixin ChainableSelectors { description: 'Widget with text $name', ), ], - skipOffstage: skipOffstage, parents: [if (self != null) self!, ...parents], children: children, ); @@ -284,14 +250,12 @@ mixin ChainableSelectors { @useResult WidgetSelector spotSingleText( String text, { - bool skipOffstage = true, List parents = const [], List children = const [], bool findRichText = false, }) { return spotTexts( text, - skipOffstage: skipOffstage, parents: parents, children: children, findRichText: findRichText, @@ -307,7 +271,6 @@ mixin ChainableSelectors { @useResult WidgetSelector spotTexts( String text, { - bool skipOffstage = true, List parents = const [], List children = const [], bool findRichText = false, @@ -338,9 +301,6 @@ mixin ChainableSelectors { description: 'Widget with exact text: "$text"', ), ], - elementFilters: [ - if (skipOffstage) OnstageFilter(), - ], parents: [if (self != null) self!, ...parents], children: children, ); @@ -367,9 +327,6 @@ mixin ChainableSelectors { description: 'Widget with icon: "$icon"', ), ], - elementFilters: [ - if (skipOffstage) OnstageFilter(), - ], parents: [if (self != null) self!, ...parents], children: children, ); @@ -418,7 +375,6 @@ mixin ChainableSelectors { @useResult WidgetSelector spotKey( Key key, { - bool skipOffstage = true, List parents = const [], List children = const [], }) { @@ -430,9 +386,6 @@ mixin ChainableSelectors { description: 'with key: "$key"', ), ], - elementFilters: [ - if (skipOffstage) OnstageFilter(), - ], parents: [if (self != null) self!, ...parents], children: children, ); @@ -443,13 +396,11 @@ mixin ChainableSelectors { @Deprecated('Use spotKey().atMost(1)') WidgetSelector spotSingleKey( Key key, { - bool skipOffstage = true, List parents = const [], List children = const [], }) { return spotKey( key, - skipOffstage: skipOffstage, parents: parents, children: children, ).atMost(1); @@ -460,13 +411,11 @@ mixin ChainableSelectors { @Deprecated('Use spotKey()') WidgetSelector spotKeys( Key key, { - bool skipOffstage = true, List parents = const [], List children = const [], }) { return spotKey( key, - skipOffstage: skipOffstage, parents: parents, children: children, ); @@ -483,12 +432,9 @@ mixin ChainableSelectors { /// spot
().first().spotText('Pepe').existsOnce(); /// ``` @useResult - WidgetSelector first({bool skipOffstage = true}) { + WidgetSelector first() { // TODO add names to the elementFilters, for a better WidgetSelector.toString() - return self!.copyWith(elementFilters: [ - if (skipOffstage) OnstageFilter(), - _FirstElement(), - ]); + return self!.copyWith(elementFilters: [_FirstElement()]); } /// Selects the last of n widgets @@ -502,11 +448,8 @@ mixin ChainableSelectors { /// spot
().last().spotText('Pepe').existsOnce(); /// ``` @useResult - WidgetSelector last({bool skipOffstage = true}) { - return self!.copyWith(elementFilters: [ - if (skipOffstage) OnstageFilter(), - _LastElement(), - ]); + WidgetSelector last() { + return self!.copyWith(elementFilters: [_LastElement()]); } /// Selects the widget at a specified [index] in the list of found widgets. @@ -516,11 +459,8 @@ mixin ChainableSelectors { /// spot().atIndex(2) // Selects the third widget ///``` @useResult - WidgetSelector atIndex(int index, {bool skipOffstage = true}) { - return self!.copyWith(elementFilters: [ - if (skipOffstage) OnstageFilter(), - _ElementAtIndex(index), - ]); + WidgetSelector atIndex(int index) { + return self!.copyWith(elementFilters: [_ElementAtIndex(index)]); } } @@ -600,7 +540,6 @@ extension SelectorQueries on WidgetSelector { WidgetSelector whereElement( bool Function(Element element) predicate, { required String description, - bool skipOffstage = true, }) { return self.copyWith( props: [ @@ -610,9 +549,6 @@ extension SelectorQueries on WidgetSelector { description: description, ), ], - elementFilters: [ - if (skipOffstage) OnstageFilter(), - ], ); } @@ -633,7 +569,6 @@ extension SelectorQueries on WidgetSelector { WidgetSelector whereWidget( bool Function(W widget) predicate, { required String description, - bool skipOffstage = true, }) { return self.copyWith( props: [ @@ -646,9 +581,6 @@ extension SelectorQueries on WidgetSelector { description: description, ), ], - elementFilters: [ - if (skipOffstage) OnstageFilter(), - ], ); } } diff --git a/lib/src/spot/text/any_text.dart b/lib/src/spot/text/any_text.dart index 32d65fa4..4d6b0beb 100644 --- a/lib/src/spot/text/any_text.dart +++ b/lib/src/spot/text/any_text.dart @@ -3,7 +3,6 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:spot/src/checks/checks_nullability.dart'; -import 'package:spot/src/spot/onstage_element_filter.dart'; import 'package:spot/src/spot/widget_selector.dart'; /// A union type for any text widget that can be found in the widget tree. @@ -239,12 +238,9 @@ class AnyTextWidgetSelector extends WidgetSelector { /// - `parents`: Parent selectors to include in the match. AnyTextWidgetSelector({ required super.props, - this.skipOffstage = true, super.children, super.parents, - }) : super(mapElementToWidget: _mapElementToAnyText, elementFilters: [ - if (skipOffstage) OnstageFilter(), - ]); + }) : super(mapElementToWidget: _mapElementToAnyText); static AnyText _mapElementToAnyText(Element element) { if (element.widget is RichText) { @@ -261,12 +257,10 @@ class AnyTextWidgetSelector extends WidgetSelector { ); } - final bool skipOffstage; - @override List createElementFilters() { - // Matches multiple widget types, can't filter by synthetic type AnyText return super.createElementFilters() + // Matches multiple widget types, can't filter by synthetic type AnyText ..removeWhere((it) => it is WidgetTypeFilter); } } From c41af2645922f38df1f058a182c5160629e3e34b Mon Sep 17 00:00:00 2001 From: Maikel Rehl Date: Tue, 6 Feb 2024 11:33:00 +0100 Subject: [PATCH 04/22] Add new offstage onstage functionality --- lib/spot.dart | 28 ++++++++++++- lib/src/spot/filters/onstage_filter.dart | 26 ++++++++++++ lib/src/spot/selectors.dart | 52 ++++++++++++++++++++---- lib/src/spot/snapshot.dart | 9 ++++ lib/src/spot/text/any_text.dart | 1 + lib/src/spot/tree_snapshot.dart | 1 + lib/src/spot/widget_selector.dart | 7 ++++ test/selectors/filter_test.dart | 44 ++++++++++++++++++++ 8 files changed, 159 insertions(+), 9 deletions(-) create mode 100644 lib/src/spot/filters/onstage_filter.dart diff --git a/lib/spot.dart b/lib/spot.dart index 3aed8588..1274ef94 100644 --- a/lib/spot.dart +++ b/lib/spot.dart @@ -206,6 +206,32 @@ WidgetSelector spot({ ); } +/// Creates a [WidgetSelector] that includes offstage widgets in the selection. +/// +/// Offstage widgets are those that are not currently visible on the screen, +/// but are still part of the widget tree. This can be useful when you want to +/// select and perform operations on widgets that are not currently visible to the user. +/// +/// Returns a new [WidgetSelector] that includes offstage widgets. +/// +/// ### Example usage: +/// ```dart +/// final text = spotOffstage() +/// .spotText('text'); +/// ``` +@useResult +WidgetSelector spotOffstage({ + List parents = const [], + List children = const [], +}) { + return _global + .spot( + parents: parents, + children: children, + ) + .offstage(); +} + /// Creates a [WidgetSelector] that finds [widget] by identity and returns all /// occurrences of it in the widget tree /// @@ -379,7 +405,6 @@ WidgetSelector spotTexts( @Deprecated('Use spotIcon().atMost(1)') WidgetSelector spotSingleIcon( IconData icon, { - bool skipOffstage = true, List parents = const [], List children = const [], }) { @@ -411,7 +436,6 @@ WidgetSelector spotIcon( @Deprecated('Use spotIcon()') WidgetSelector spotIcons( IconData icon, { - bool skipOffstage = true, List parents = const [], List children = const [], }) { diff --git a/lib/src/spot/filters/onstage_filter.dart b/lib/src/spot/filters/onstage_filter.dart new file mode 100644 index 00000000..7066ee11 --- /dev/null +++ b/lib/src/spot/filters/onstage_filter.dart @@ -0,0 +1,26 @@ +import 'package:spot/src/spot/widget_selector.dart'; + +/// Removes all [WidgetTreeNode] that are offstage +class OnstageFilter implements ElementFilter { + @override + Iterable filter(Iterable candidates) { + final List matchingChildNodes = []; + + for (final WidgetTreeNode candidate in candidates) { + if (!candidate.isOffstage) { + matchingChildNodes.add(candidate); + } + } + return matchingChildNodes; + } + + @override + String get description { + return 'removes all offstage elements'; + } + + @override + String toString() { + return 'OnstageFilter which $description'; + } +} diff --git a/lib/src/spot/selectors.dart b/lib/src/spot/selectors.dart index 27691bcb..b8f1a766 100644 --- a/lib/src/spot/selectors.dart +++ b/lib/src/spot/selectors.dart @@ -57,12 +57,49 @@ mixin ChainableSelectors { if (children.isNotEmpty) ChildFilter(children), if (p.isNotEmpty) ParentFilter(p), ], + includeOffstage: self?.includeOffstage, ); return selector; } - /// Creates a [WidgetSelector] that matches a single Widgets of - /// type [W] that is in the scope of the parent [WidgetSelector]. + /// Creates a [WidgetSelector] that includes offstage widgets in the selection. + /// + /// Offstage widgets are those that are not currently visible on the screen, + /// but are still part of the widget tree. This can be useful when you want to + /// select and perform operations on widgets that are not currently visible to the user. + /// + /// Returns a new [WidgetSelector] that includes offstage widgets. + /// + /// ### Example usage: + /// ```dart + /// final text = spotText('text') + /// .offstage(); + /// ``` + @useResult + WidgetSelector offstage() { + return self!.copyWith(includeOffstage: true); + } + + /// Creates a [WidgetSelector] that excludes offstage widgets from the selection. + /// + /// This is the default behavior of a [WidgetSelector], but this method can be useful + /// if you have previously called `offstage` on the selector and want to revert back + /// to only selecting widgets that are currently visible on the screen. + /// + /// Returns a new [WidgetSelector] that excludes offstage widgets. + /// + /// ### Example usage: + /// ```dart + /// final text = spotText('text') + /// .offstage() + /// .onstage(); + /// ``` + @useResult + WidgetSelector onstage() { + return self!.copyWith(includeOffstage: false); + } + + /// Creates a [WidgetSelector] that excludes offstage widgets from the selection. /// /// This selector compares the Widgets by runtimeType rather than by super /// type (see [WidgetTypeFilter]). This makes sure that e.g. `spot()` @@ -104,6 +141,7 @@ mixin ChainableSelectors { if (children.isNotEmpty) ChildFilter(children), if (p.isNotEmpty) ParentFilter(p), ], + includeOffstage: self?.includeOffstage, ); return selector; } @@ -157,6 +195,7 @@ mixin ChainableSelectors { if (children.isNotEmpty) ChildFilter(children), if (p.isNotEmpty) ParentFilter(p), ], + includeOffstage: self?.includeOffstage, ); return selector; } @@ -242,6 +281,7 @@ mixin ChainableSelectors { if (children.isNotEmpty) ChildFilter(children), if (p.isNotEmpty) ParentFilter(p), ], + includeOffstage: self?.includeOffstage, ); return selector; } @@ -309,6 +349,7 @@ mixin ChainableSelectors { if (children.isNotEmpty) ChildFilter(children), if (p.isNotEmpty) ParentFilter(p), ], + includeOffstage: self?.includeOffstage, ); return selector; } @@ -317,7 +358,6 @@ mixin ChainableSelectors { @useResult WidgetSelector spotIcon( IconData icon, { - bool skipOffstage = true, List parents = const [], List children = const [], }) { @@ -337,6 +377,7 @@ mixin ChainableSelectors { if (children.isNotEmpty) ChildFilter(children), if (p.isNotEmpty) ParentFilter(p), ], + includeOffstage: self?.includeOffstage, ); return selector; } @@ -346,13 +387,11 @@ mixin ChainableSelectors { @Deprecated('Use spotIcon().atMost(1)') WidgetSelector spotSingleIcon( IconData icon, { - bool skipOffstage = true, List parents = const [], List children = const [], }) { return spotIcon( icon, - skipOffstage: skipOffstage, parents: parents, children: children, ).atMost(1); @@ -363,13 +402,11 @@ mixin ChainableSelectors { @Deprecated('Use spotIcon()') WidgetSelector spotIcons( IconData icon, { - bool skipOffstage = true, List parents = const [], List children = const [], }) { return spotIcon( icon, - skipOffstage: skipOffstage, parents: parents, children: children, ); @@ -398,6 +435,7 @@ mixin ChainableSelectors { if (children.isNotEmpty) ChildFilter(children), if (p.isNotEmpty) ParentFilter(p), ], + includeOffstage: self?.includeOffstage, ); return selector; } diff --git a/lib/src/spot/snapshot.dart b/lib/src/spot/snapshot.dart index 92858927..bdc69bae 100644 --- a/lib/src/spot/snapshot.dart +++ b/lib/src/spot/snapshot.dart @@ -2,6 +2,7 @@ import 'package:dartx/dartx.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; +import 'package:spot/src/spot/filters/onstage_filter.dart'; import 'package:spot/src/spot/widget_selector.dart'; /// A type alias for a snapshot that can contain multiple widgets. @@ -62,6 +63,7 @@ class WidgetSnapshot { /// /// Provides convenience methods to transform a widget snapshot into matchers /// for single or multiple widgets. +// TODO make WidgetSnapshot implement WidgetMatcher and MultiWidgetMatcher extension ToWidgetMatcher on WidgetSnapshot { /// Converts the snapshot to a [MultiWidgetMatcher], /// which can match multiple widgets. @@ -150,6 +152,13 @@ WidgetSnapshot snapshot( ), ]; + if (!selector.includeOffstage) { + final stage = OnstageFilter(); + final before = stageResults.last.candidates.toUnmodifiable(); + final after = stage.filter(before).toList().toUnmodifiable(); + stageResults.add((filter: stage, candidates: after)); + } + for (final stage in selector.stages) { // using unmodifiable copies to prevent accidental modification during filtering final before = stageResults.last.candidates.toUnmodifiable(); diff --git a/lib/src/spot/text/any_text.dart b/lib/src/spot/text/any_text.dart index ff782741..005eeb2c 100644 --- a/lib/src/spot/text/any_text.dart +++ b/lib/src/spot/text/any_text.dart @@ -238,6 +238,7 @@ class AnyTextWidgetSelector extends WidgetSelector { /// - `parents`: Parent selectors to include in the match. AnyTextWidgetSelector({ required super.stages, + super.includeOffstage, }) : super(mapElementToWidget: _mapElementToAnyText); static AnyText _mapElementToAnyText(Element element) { diff --git a/lib/src/spot/tree_snapshot.dart b/lib/src/spot/tree_snapshot.dart index a658d442..2c54c5f1 100644 --- a/lib/src/spot/tree_snapshot.dart +++ b/lib/src/spot/tree_snapshot.dart @@ -92,6 +92,7 @@ class WidgetTreeNode { /// node of the tree. final WidgetTreeNode? parent; + /// Whether the widget is offstage or onstage final bool isOffstage; /// Creates an [Element] in the element tree. diff --git a/lib/src/spot/widget_selector.dart b/lib/src/spot/widget_selector.dart index fc8c1223..1c86fc5b 100644 --- a/lib/src/spot/widget_selector.dart +++ b/lib/src/spot/widget_selector.dart @@ -49,8 +49,10 @@ class WidgetSelector with ChainableSelectors { @Deprecated('Use quantityConstraint instead') ExpectedQuantity expectedQuantity = ExpectedQuantity.multi, QuantityConstraint? quantityConstraint, + bool? includeOffstage, W Function(Element element)? mapElementToWidget, }) : stages = List.unmodifiable(stages), + includeOffstage = includeOffstage ?? false, quantityConstraint = quantityConstraint ?? // ignore: deprecated_member_use_from_same_package (expectedQuantity == ExpectedQuantity.single @@ -92,6 +94,9 @@ class WidgetSelector with ChainableSelectors { /// The runtime type of the widget this selector is intended for. Type get type => W; + /// Whether to include offstage widgets in the selection + final bool includeOffstage; + @override String toString() { final sb = StringBuffer(); @@ -252,11 +257,13 @@ class WidgetSelector with ChainableSelectors { // ignore: deprecated_member_use_from_same_package ExpectedQuantity? expectedQuantity, QuantityConstraint? quantityConstraint, + bool? includeOffstage, W Function(Element element)? mapElementToWidget, }) { return WidgetSelector( stages: stages ?? this.stages, quantityConstraint: quantityConstraint ?? this.quantityConstraint, + includeOffstage: includeOffstage ?? this.includeOffstage, mapElementToWidget: mapElementToWidget ?? this.mapElementToWidget, ); } diff --git a/test/selectors/filter_test.dart b/test/selectors/filter_test.dart index bcf6c71f..89dcfb8e 100644 --- a/test/selectors/filter_test.dart +++ b/test/selectors/filter_test.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; +import 'package:spot/src/spot/selectors.dart'; void main() { group('first', () { @@ -127,4 +128,47 @@ void main() { // just report nothing found spot().atIndex(4).doesNotExist(); }); + + testWidgets('do not select offstage widgets by default', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Row( + children: [ + Text('a'), + Text('b'), + Offstage(child: Text('c')), + ], + ), + ), + ); + + spot().atMost(2); + spotText('c').doesNotExist(); + }); + + testWidgets('select offstage widgets when use offstage()', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Row( + children: [ + Text('a'), + Text('b'), + Offstage(child: Text('c')), + ], + ), + ), + ); + + spotOffstage().spot().atMost(3); + spotOffstage().spotText('c').existsOnce(); + spotOffstage().onstage().spotText('c').doesNotExist(); + + spotText('c').doesNotExist(); + spotText('c').offstage().existsOnce(); + spotText('c').offstage().onstage().doesNotExist(); + spotText('c').offstage().onstage().offstage().existsOnce(); + + spot().withText('c').doesNotExist(); + spot().withText('c').offstage().existsOnce(); + }); } From 25732e6ab9f51af1bb924ce0d07f1dadf3ca0f35 Mon Sep 17 00:00:00 2001 From: Maikel Rehl Date: Tue, 6 Feb 2024 11:38:56 +0100 Subject: [PATCH 05/22] Remove TODO --- lib/src/spot/snapshot.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/src/spot/snapshot.dart b/lib/src/spot/snapshot.dart index bdc69bae..22fae38f 100644 --- a/lib/src/spot/snapshot.dart +++ b/lib/src/spot/snapshot.dart @@ -63,7 +63,6 @@ class WidgetSnapshot { /// /// Provides convenience methods to transform a widget snapshot into matchers /// for single or multiple widgets. -// TODO make WidgetSnapshot implement WidgetMatcher and MultiWidgetMatcher extension ToWidgetMatcher on WidgetSnapshot { /// Converts the snapshot to a [MultiWidgetMatcher], /// which can match multiple widgets. From 107dd23c5d2bb40dceb10d87101dc23c54f57464 Mon Sep 17 00:00:00 2001 From: Maikel Rehl Date: Tue, 6 Feb 2024 11:51:43 +0100 Subject: [PATCH 06/22] Fix lint issues --- lib/src/spot/tree_snapshot.dart | 12 +++++++----- test/selectors/filter_test.dart | 1 - 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/src/spot/tree_snapshot.dart b/lib/src/spot/tree_snapshot.dart index 2c54c5f1..7b05b718 100644 --- a/lib/src/spot/tree_snapshot.dart +++ b/lib/src/spot/tree_snapshot.dart @@ -47,11 +47,13 @@ WidgetTreeSnapshot createWidgetTreeSnapshot() { ); for (final child in element.children) { - snapshot.addChild(build( - child, - parent: snapshot, - isOffstage: isOffstageElement, - )); + snapshot.addChild( + build( + child, + parent: snapshot, + isOffstage: isOffstageElement, + ), + ); } return snapshot; } diff --git a/test/selectors/filter_test.dart b/test/selectors/filter_test.dart index 89dcfb8e..d422e2e9 100644 --- a/test/selectors/filter_test.dart +++ b/test/selectors/filter_test.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; -import 'package:spot/src/spot/selectors.dart'; void main() { group('first', () { From a2ee7977c43479732a4c2e7853d62c8b082b5bfd Mon Sep 17 00:00:00 2001 From: Pascal Welsch Date: Tue, 6 Feb 2024 13:29:02 +0100 Subject: [PATCH 07/22] Improve docs --- lib/src/spot/element_extensions.dart | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/src/spot/element_extensions.dart b/lib/src/spot/element_extensions.dart index 2c851288..eb662e4c 100644 --- a/lib/src/spot/element_extensions.dart +++ b/lib/src/spot/element_extensions.dart @@ -21,16 +21,17 @@ extension ElementExtensions on Element { } } - /// Returns all children of [Element] in depth first order from the closest - /// to the leaves + /// Returns all children of [Element], only direct children Iterable get children sync* { final List found = []; visitChildren(found.add); yield* found; } - /// Returns only onstage children of [Element] in depth first order from the closest - /// to the leaves + /// Returns only onstage children of [Element], only direct children + /// + /// Children of [Offstage] or [Overlay] are eventually not returned, + /// thus marking them as offstage Iterable get onstageChildren sync* { final List found = []; debugVisitOnstageChildren(found.add); From be0be85968db3be7546828e917ae2aad71347b4c Mon Sep 17 00:00:00 2001 From: Pascal Welsch Date: Tue, 6 Feb 2024 13:50:10 +0100 Subject: [PATCH 08/22] Optimize createWidgetTreeSnapshot() --- lib/src/spot/tree_snapshot.dart | 54 +++++++++++++++++---------------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/lib/src/spot/tree_snapshot.dart b/lib/src/spot/tree_snapshot.dart index 7b05b718..d6237123 100644 --- a/lib/src/spot/tree_snapshot.dart +++ b/lib/src/spot/tree_snapshot.dart @@ -23,42 +23,44 @@ WidgetTreeSnapshot createWidgetTreeSnapshot() { // ignore: deprecated_member_use final rootElement = WidgetsBinding.instance.renderViewElement!; - WidgetTreeNode build( + WidgetTreeNode buildTreeNode( Element element, { WidgetTreeNode? parent, - bool isOffstage = false, + bool isParentOffstage = false, }) { - // Get all onstage children of the parent - final allOnstageChildren = parent?.element.onstageChildren.toList(); - - final bool isOffstageElement; - if (allOnstageChildren == null) { - // If there is no parent use isOffstage which is false by default - isOffstageElement = isOffstage; - } else { - // Check if the current element is offstage or use the parent's state - isOffstageElement = !allOnstageChildren.contains(element) || isOffstage; - } - - final snapshot = WidgetTreeNode( + final node = WidgetTreeNode( element: element, parent: parent, - isOffstage: isOffstageElement, + isOffstage: isParentOffstage, ); - for (final child in element.children) { - snapshot.addChild( - build( - child, - parent: snapshot, - isOffstage: isOffstageElement, - ), + // children contains onstage and offstage children + final children = element.children.toList(); + + for (final child in children) { + // Once a part of the widget tree is offstage, all children are offstage, too. + // There is now way a child might become onstage again + final isOffstage = isParentOffstage || + () { + final onstageOnly = element.onstageChildren.toList(); + final offstageOnly = element.children + .where((it) => !onstageOnly.contains(it)) + .toList(); + return offstageOnly.contains(child); + }(); + + final childNode = buildTreeNode( + child, + parent: node, + isParentOffstage: isOffstage, ); + node.addChild(childNode); } - return snapshot; + return node; } - final origin = build(rootElement); + // TODO replace recursion with iteration to prevent stack overflow for giant widget trees + final origin = buildTreeNode(rootElement); return WidgetTreeSnapshot( origin: origin, @@ -103,7 +105,7 @@ class WidgetTreeNode { WidgetTreeNode({ required this.element, required this.parent, - this.isOffstage = false, + required this.isOffstage, }); @override From 224db9e642811b595f97db8c712389e0a33d7e57 Mon Sep 17 00:00:00 2001 From: Pascal Welsch Date: Tue, 6 Feb 2024 20:01:17 +0100 Subject: [PATCH 09/22] Add parents to WidgetSelector again --- lib/src/spot/finder_interop.dart | 6 +-- lib/src/spot/selectors.dart | 50 +++++++----------- lib/src/spot/snapshot.dart | 30 ++++++++--- lib/src/spot/text/any_text.dart | 3 +- lib/src/spot/widget_selector.dart | 65 +++++++++++++++++++++--- test/spot/parents_and_children_test.dart | 29 ++--------- 6 files changed, 107 insertions(+), 76 deletions(-) diff --git a/lib/src/spot/finder_interop.dart b/lib/src/spot/finder_interop.dart index 553b2b7c..0af62ad9 100644 --- a/lib/src/spot/finder_interop.dart +++ b/lib/src/spot/finder_interop.dart @@ -31,10 +31,8 @@ extension SpotToFinder on WidgetSelector { @useResult WidgetSelector spotFinder(Finder finder) { return WidgetSelector( - stages: [ - FinderFilter(finder), - ParentFilter([this]), - ], + stages: [FinderFilter(finder)], + parents: [this], ); } } diff --git a/lib/src/spot/selectors.dart b/lib/src/spot/selectors.dart index b8f1a766..7f05797b 100644 --- a/lib/src/spot/selectors.dart +++ b/lib/src/spot/selectors.dart @@ -50,14 +50,12 @@ mixin ChainableSelectors { List parents = const [], List children = const [], }) { - final p = [if (self != null) self!, ...parents]; final selector = WidgetSelector( stages: [ WidgetTypeFilter(), - if (children.isNotEmpty) ChildFilter(children), - if (p.isNotEmpty) ParentFilter(p), ], - includeOffstage: self?.includeOffstage, + parents: [if (self != null) self!, ...parents], + children: children, ); return selector; } @@ -130,7 +128,6 @@ mixin ChainableSelectors { List parents = const [], List children = const [], }) { - final p = [if (self != null) self!, ...parents]; final selector = WidgetSelector( stages: [ WidgetTypeFilter(), @@ -138,10 +135,9 @@ mixin ChainableSelectors { predicate: (Element e) => identical(e.widget, widget), description: 'Widget === $widget', ), - if (children.isNotEmpty) ChildFilter(children), - if (p.isNotEmpty) ParentFilter(p), ], - includeOffstage: self?.includeOffstage, + parents: [if (self != null) self!, ...parents], + children: children, ); return selector; } @@ -184,7 +180,6 @@ mixin ChainableSelectors { List parents = const [], List children = const [], }) { - final p = [if (self != null) self!, ...parents]; final selector = WidgetSelector( stages: [ WidgetTypeFilter(), @@ -192,10 +187,9 @@ mixin ChainableSelectors { predicate: (Element e) => identical(e, element), description: 'Element === $element', ), - if (children.isNotEmpty) ChildFilter(children), - if (p.isNotEmpty) ParentFilter(p), ], - includeOffstage: self?.includeOffstage, + parents: [if (self != null) self!, ...parents], + children: children, ); return selector; } @@ -271,17 +265,15 @@ mixin ChainableSelectors { match(subject); return describe(subject).map((it) => it.trim()).toList().join(' '); }(); - final p = [if (self != null) self!, ...parents]; final selector = AnyTextWidgetSelector( stages: [ MatchTextFilter( match: (it) => match(it), description: 'Widget with text $name', ), - if (children.isNotEmpty) ChildFilter(children), - if (p.isNotEmpty) ParentFilter(p), ], - includeOffstage: self?.includeOffstage, + parents: [if (self != null) self!, ...parents], + children: children, ); return selector; } @@ -320,7 +312,6 @@ mixin ChainableSelectors { List children = const [], bool findRichText = false, }) { - final p = [if (self != null) self!, ...parents]; final selector = WidgetSelector( stages: [ WidgetTypeFilter(), @@ -346,10 +337,9 @@ mixin ChainableSelectors { }, description: 'Widget with exact text: "$text"', ), - if (children.isNotEmpty) ChildFilter(children), - if (p.isNotEmpty) ParentFilter(p), ], - includeOffstage: self?.includeOffstage, + parents: [if (self != null) self!, ...parents], + children: children, ); return selector; } @@ -361,7 +351,6 @@ mixin ChainableSelectors { List parents = const [], List children = const [], }) { - final p = [if (self != null) self!, ...parents]; final selector = WidgetSelector( stages: [ WidgetTypeFilter(), @@ -374,10 +363,9 @@ mixin ChainableSelectors { }, description: 'Widget with icon: "$icon"', ), - if (children.isNotEmpty) ChildFilter(children), - if (p.isNotEmpty) ParentFilter(p), ], - includeOffstage: self?.includeOffstage, + parents: [if (self != null) self!, ...parents], + children: children, ); return selector; } @@ -424,7 +412,6 @@ mixin ChainableSelectors { List parents = const [], List children = const [], }) { - final p = [if (self != null) self!, ...parents]; final selector = WidgetSelector( stages: [ WidgetTypeFilter(), @@ -432,10 +419,9 @@ mixin ChainableSelectors { predicate: (Element e) => e.widget.key == key, description: 'with key: "$key"', ), - if (children.isNotEmpty) ChildFilter(children), - if (p.isNotEmpty) ParentFilter(p), ], - includeOffstage: self?.includeOffstage, + parents: [if (self != null) self!, ...parents], + children: children, ); return selector; } @@ -877,7 +863,7 @@ extension RelativeSelectors on WidgetSelector { /// - [withChildren] requires [children] to be children of the widget to match. @useResult WidgetSelector withParent(WidgetSelector parent) { - return addStage(ParentFilter([parent])); + return copyWith(parents: [...parents, parent]); } /// Returns a [WidgetSelector] that requires [parents] to be parents of the @@ -894,7 +880,7 @@ extension RelativeSelectors on WidgetSelector { /// - [withChildren] requires [children] to be children of the widget to match. @useResult WidgetSelector withParents(List parents) { - return addStage(ParentFilter(parents)); + return copyWith(parents: [...this.parents, ...parents]); } /// Returns a [WidgetSelector] that requires [child] to be a child of the @@ -911,7 +897,7 @@ extension RelativeSelectors on WidgetSelector { /// - [withChildren] requires [children] to be children of the widget to match. @useResult WidgetSelector withChild(WidgetSelector child) { - return addStage(ChildFilter([child])); + return copyWith(children: [...children, child]); } /// Returns a [WidgetSelector] that requires [children] to be children of the @@ -928,6 +914,6 @@ extension RelativeSelectors on WidgetSelector { /// - [withChild] requires [child] to be a child of the widget to match. @useResult WidgetSelector withChildren(List children) { - return addStage(ChildFilter(children)); + return copyWith(children: [...this.children, ...children]); } } diff --git a/lib/src/spot/snapshot.dart b/lib/src/spot/snapshot.dart index 22fae38f..1eed4d86 100644 --- a/lib/src/spot/snapshot.dart +++ b/lib/src/spot/snapshot.dart @@ -141,6 +141,7 @@ WidgetSnapshot snapshot( TestAsyncUtils.guardSync(); final treeSnapshot = currentWidgetTreeSnapshot(); + final List candidates = treeSnapshot.allNodes; // an easy to debug list of all filters and their individual results @@ -151,14 +152,27 @@ WidgetSnapshot snapshot( ), ]; - if (!selector.includeOffstage) { - final stage = OnstageFilter(); - final before = stageResults.last.candidates.toUnmodifiable(); - final after = stage.filter(before).toList().toUnmodifiable(); - stageResults.add((filter: stage, candidates: after)); + final offstage = selector.isAnyOffstage(); + + if (selector.stages.any((e) => e is ParentFilter)) { + throw ArgumentError( + 'ParentFilter is not allowed in stages. Use the parents parameter instead.', + ); + } + if (selector.stages.any((e) => e is ChildFilter)) { + throw ArgumentError( + 'ChildFilter is not allowed in stages. Use the children parameter instead.', + ); } + final stages = [ + if (!offstage) OnstageFilter(), + ...selector.stages, + if (selector.children.isNotEmpty) ChildFilter(selector.children), + // sort ParentFilter last for better performance + if (selector.parents.isNotEmpty) ParentFilter(selector.parents), + ]; - for (final stage in selector.stages) { + for (final stage in stages) { // using unmodifiable copies to prevent accidental modification during filtering final before = stageResults.last.candidates.toUnmodifiable(); final after = stage.filter(before).toList().toUnmodifiable(); @@ -506,6 +520,10 @@ extension LessSpecificSelectors on WidgetSelector { final List Function(WidgetSelector)> criteria = [ for (final stage in stages) (s) => s.copyWith(stages: [...s.stages, stage]), + for (final child in children) + (s) => s.copyWith(children: [...s.children, child]), + for (final parent in parents) + (s) => s.copyWith(parents: [...s.parents, parent]), ]; if (criteria.length <= 1) { return; diff --git a/lib/src/spot/text/any_text.dart b/lib/src/spot/text/any_text.dart index 005eeb2c..d5ae98c7 100644 --- a/lib/src/spot/text/any_text.dart +++ b/lib/src/spot/text/any_text.dart @@ -238,7 +238,8 @@ class AnyTextWidgetSelector extends WidgetSelector { /// - `parents`: Parent selectors to include in the match. AnyTextWidgetSelector({ required super.stages, - super.includeOffstage, + required super.children, + required super.parents, }) : super(mapElementToWidget: _mapElementToAnyText); static AnyText _mapElementToAnyText(Element element) { diff --git a/lib/src/spot/widget_selector.dart b/lib/src/spot/widget_selector.dart index 1c86fc5b..cbe2c13e 100644 --- a/lib/src/spot/widget_selector.dart +++ b/lib/src/spot/widget_selector.dart @@ -46,12 +46,16 @@ class WidgetSelector with ChainableSelectors { /// Allows specifying various parameters for customizing the selection criteria. WidgetSelector({ required List stages, + List? parents, + List? children, @Deprecated('Use quantityConstraint instead') ExpectedQuantity expectedQuantity = ExpectedQuantity.multi, QuantityConstraint? quantityConstraint, bool? includeOffstage, W Function(Element element)? mapElementToWidget, - }) : stages = List.unmodifiable(stages), + }) : parents = parents ?? [], + children = children ?? [], + stages = List.unmodifiable(stages), includeOffstage = includeOffstage ?? false, quantityConstraint = quantityConstraint ?? // ignore: deprecated_member_use_from_same_package @@ -71,6 +75,19 @@ class WidgetSelector with ChainableSelectors { /// - [WidgetTypeFilter] final List stages; + /// A list of parent selectors used to define a hierarchical + /// context for the selection. + /// + /// These selectors specify the parent widgets in the tree that the current + /// selector's widget must be a descendant of. + final List parents; + + /// A list of child selectors used to filter widgets based on their children. + /// + /// These selectors are applied to the children of the widget being matched, + /// allowing for selection based on child widget properties. + final List children; + /// Whether this selector expects to find a single or multiple widgets @Deprecated('Use quantityConstraint instead') ExpectedQuantity get expectedQuantity => quantityConstraint.max == 1 @@ -97,11 +114,29 @@ class WidgetSelector with ChainableSelectors { /// Whether to include offstage widgets in the selection final bool includeOffstage; + bool isAnyOffstage() { + if (includeOffstage) { + return true; + } + for (final parent in parents) { + if (parent.isAnyOffstage()) { + return true; + } + } + return false; + } + @override String toString() { final sb = StringBuffer(); - for (int i = 0; i < stages.length; i++) { - final stage = stages[i]; + final s = [ + ...stages, + if (children.isNotEmpty) ChildFilter(children), + if (parents.isNotEmpty) ParentFilter(parents), + ]; + + for (int i = 0; i < s.length; i++) { + final stage = s[i]; if (stage is ParentFilter) { var desc = stage.parents.first.toString(); if (desc.contains(' with parent ') || desc.contains(' with child ')) { @@ -131,7 +166,7 @@ class WidgetSelector with ChainableSelectors { } } - final isLast = i == stages.length - 1; + final isLast = i == s.length - 1; if (stage is WidgetTypeFilter) { sb.write(' '); } else if (!isLast) { @@ -149,8 +184,14 @@ class WidgetSelector with ChainableSelectors { /// hierarchy of the selection. String toStringBreadcrumb() { var sb = StringBuffer(); - for (int i = 0; i < stages.length; i++) { - final stage = stages[i]; + final s = [ + ...stages, + if (children.isNotEmpty) ChildFilter(children), + if (parents.isNotEmpty) ParentFilter(parents), + ]; + + for (int i = 0; i < s.length; i++) { + final stage = s[i]; if (stage is ParentFilter) { var child = sb.toString(); if (child.endsWith(stageSeparator)) { @@ -201,7 +242,7 @@ class WidgetSelector with ChainableSelectors { } } - final isLast = i == stages.length - 1; + final isLast = i == s.length - 1; if (stage is WidgetTypeFilter) { if (!sb.toString().endsWith(' ')) { sb.write(' '); @@ -259,12 +300,20 @@ class WidgetSelector with ChainableSelectors { QuantityConstraint? quantityConstraint, bool? includeOffstage, W Function(Element element)? mapElementToWidget, + List? parents, + List? children, }) { return WidgetSelector( - stages: stages ?? this.stages, + stages: stages ?? + this + .stages + .where((it) => it is! ChildFilter && it is! ParentFilter) + .toList(), quantityConstraint: quantityConstraint ?? this.quantityConstraint, includeOffstage: includeOffstage ?? this.includeOffstage, mapElementToWidget: mapElementToWidget ?? this.mapElementToWidget, + parents: parents ?? this.parents, + children: children ?? this.children, ); } } diff --git a/test/spot/parents_and_children_test.dart b/test/spot/parents_and_children_test.dart index 809fe093..fbc5d14c 100644 --- a/test/spot/parents_and_children_test.dart +++ b/test/spot/parents_and_children_test.dart @@ -1,6 +1,5 @@ // ignore_for_file: avoid_unnecessary_containers, prefer_const_constructors -import 'package:dartx/dartx.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -240,18 +239,8 @@ void main() { .withChildren([spot
(), spot()]) ..existsOnce(); - expect( - withChildTwice.stages - .flatMap((it) => it is ChildFilter ? it.childSelectors : []) - .length, - 2, - ); - expect( - withChildren.stages - .flatMap((it) => it is ChildFilter ? it.childSelectors : []) - .length, - 2, - ); + expect(withChildTwice.children.length, 2); + expect(withChildren.children.length, 2); }); testWidgets('withParent', (tester) async { @@ -300,17 +289,7 @@ void main() { .withParents([spot
(), spot()]) ..existsOnce(); - expect( - withParentTwice.stages - .flatMap((it) => it is ParentFilter ? it.parents : []) - .length, - 2, - ); - expect( - withParents.stages - .flatMap((it) => it is ParentFilter ? it.parents : []) - .length, - 2, - ); + expect(withParentTwice.parents.length, 2); + expect(withParents.parents.length, 2); }); } From 25e92424ddca0c9b20515b99e240cc3ea5bac7a3 Mon Sep 17 00:00:00 2001 From: Pascal Welsch Date: Sun, 3 Mar 2024 19:47:22 +0100 Subject: [PATCH 10/22] Allow filtering offstage only in a certain subtree --- lib/spot.dart | 2 +- lib/src/spot/filters/parent_filter.dart | 3 +- lib/src/spot/finder_interop.dart | 6 +- lib/src/spot/selectors.dart | 77 +++++++++++++----- lib/src/spot/snapshot.dart | 98 ++++++++++++++++------- lib/src/spot/text/any_text.dart | 7 +- lib/src/spot/widget_selector.dart | 102 ++++++++++++------------ test/selectors/filter_test.dart | 65 ++++++++++++++- 8 files changed, 248 insertions(+), 112 deletions(-) diff --git a/lib/spot.dart b/lib/spot.dart index 1274ef94..da190e73 100644 --- a/lib/spot.dart +++ b/lib/spot.dart @@ -229,7 +229,7 @@ WidgetSelector spotOffstage({ parents: parents, children: children, ) - .offstage(); + .overrideIncludeOffstage(true); } /// Creates a [WidgetSelector] that finds [widget] by identity and returns all diff --git a/lib/src/spot/filters/parent_filter.dart b/lib/src/spot/filters/parent_filter.dart index 93046306..0ea86126 100644 --- a/lib/src/spot/filters/parent_filter.dart +++ b/lib/src/spot/filters/parent_filter.dart @@ -26,6 +26,7 @@ class ParentFilter implements ElementFilter { final List> parentSnapshots = parents.map((selector) { final WidgetSnapshot widgetSnapshot = snapshot(selector); + // TODO unnecessary? snapshot does this by default already widgetSnapshot.validateQuantity(); return widgetSnapshot; }).toList(); @@ -83,6 +84,6 @@ class ParentFilter implements ElementFilter { @override String toString() { - return 'ParentFilter $description'; + return 'ParentFilter which keeps $description Widget'; } } diff --git a/lib/src/spot/finder_interop.dart b/lib/src/spot/finder_interop.dart index 0af62ad9..553b2b7c 100644 --- a/lib/src/spot/finder_interop.dart +++ b/lib/src/spot/finder_interop.dart @@ -31,8 +31,10 @@ extension SpotToFinder on WidgetSelector { @useResult WidgetSelector spotFinder(Finder finder) { return WidgetSelector( - stages: [FinderFilter(finder)], - parents: [this], + stages: [ + FinderFilter(finder), + ParentFilter([this]), + ], ); } } diff --git a/lib/src/spot/selectors.dart b/lib/src/spot/selectors.dart index 7f05797b..6b24ad2b 100644 --- a/lib/src/spot/selectors.dart +++ b/lib/src/spot/selectors.dart @@ -34,6 +34,21 @@ mixin ChainableSelectors { /// This is `null` for the root of the chain. WidgetSelector? get self; + List _childAndParentFilters( + List children, + List parents, + ) { + final List filters = []; + if (children.isNotEmpty) { + filters.add(ChildFilter(children)); + } + final list = [if (self != null) self!, ...parents]; + if (list.isNotEmpty) { + filters.add(ParentFilter(list)); + } + return filters; + } + /// Creates a [WidgetSelector] that matches a all Widgets of /// type [W] that are in the scope of the parent [WidgetSelector]. /// @@ -53,9 +68,8 @@ mixin ChainableSelectors { final selector = WidgetSelector( stages: [ WidgetTypeFilter(), + ..._childAndParentFilters(children, parents), ], - parents: [if (self != null) self!, ...parents], - children: children, ); return selector; } @@ -71,11 +85,38 @@ mixin ChainableSelectors { /// ### Example usage: /// ```dart /// final text = spotText('text') - /// .offstage(); + /// .overrideIncludeOffstage(); + /// ``` + @useResult + WidgetSelector overrideIncludeOffstage(bool offstage) { + if (offstage == self!.includeOffstage) { + return self!; + } + return self!.copyWith(includeOffstage: offstage); + } + + /// Creates a [WidgetSelector] that includes offstage widgets in the selection. + /// + /// Offstage widgets are those that are not currently visible on the screen, + /// but are still part of the widget tree. This can be useful when you want to + /// select and perform operations on widgets that are not currently visible to the user. + /// + /// Returns a new [WidgetSelector] that includes offstage widgets. + /// + /// ### Example usage: + /// ```dart + /// final text = spotOffstage() + /// .spotText('text'); /// ``` @useResult - WidgetSelector offstage() { - return self!.copyWith(includeOffstage: true); + WidgetSelector spotOffstage({ + List parents = const [], + List children = const [], + }) { + return WidgetSelector( + includeOffstage: true, + stages: _childAndParentFilters(children, parents), + ); } /// Creates a [WidgetSelector] that excludes offstage widgets from the selection. @@ -135,9 +176,8 @@ mixin ChainableSelectors { predicate: (Element e) => identical(e.widget, widget), description: 'Widget === $widget', ), + ..._childAndParentFilters(children, parents), ], - parents: [if (self != null) self!, ...parents], - children: children, ); return selector; } @@ -187,9 +227,8 @@ mixin ChainableSelectors { predicate: (Element e) => identical(e, element), description: 'Element === $element', ), + ..._childAndParentFilters(children, parents), ], - parents: [if (self != null) self!, ...parents], - children: children, ); return selector; } @@ -271,9 +310,8 @@ mixin ChainableSelectors { match: (it) => match(it), description: 'Widget with text $name', ), + ..._childAndParentFilters(children, parents), ], - parents: [if (self != null) self!, ...parents], - children: children, ); return selector; } @@ -337,9 +375,8 @@ mixin ChainableSelectors { }, description: 'Widget with exact text: "$text"', ), + ..._childAndParentFilters(children, parents), ], - parents: [if (self != null) self!, ...parents], - children: children, ); return selector; } @@ -363,9 +400,8 @@ mixin ChainableSelectors { }, description: 'Widget with icon: "$icon"', ), + ..._childAndParentFilters(children, parents), ], - parents: [if (self != null) self!, ...parents], - children: children, ); return selector; } @@ -419,9 +455,8 @@ mixin ChainableSelectors { predicate: (Element e) => e.widget.key == key, description: 'with key: "$key"', ), + ..._childAndParentFilters(children, parents), ], - parents: [if (self != null) self!, ...parents], - children: children, ); return selector; } @@ -863,7 +898,7 @@ extension RelativeSelectors on WidgetSelector { /// - [withChildren] requires [children] to be children of the widget to match. @useResult WidgetSelector withParent(WidgetSelector parent) { - return copyWith(parents: [...parents, parent]); + return addStage(ParentFilter([parent])); } /// Returns a [WidgetSelector] that requires [parents] to be parents of the @@ -880,7 +915,7 @@ extension RelativeSelectors on WidgetSelector { /// - [withChildren] requires [children] to be children of the widget to match. @useResult WidgetSelector withParents(List parents) { - return copyWith(parents: [...this.parents, ...parents]); + return addStage(ParentFilter(parents)); } /// Returns a [WidgetSelector] that requires [child] to be a child of the @@ -897,7 +932,7 @@ extension RelativeSelectors on WidgetSelector { /// - [withChildren] requires [children] to be children of the widget to match. @useResult WidgetSelector withChild(WidgetSelector child) { - return copyWith(children: [...children, child]); + return addStage(ChildFilter([child])); } /// Returns a [WidgetSelector] that requires [children] to be children of the @@ -914,6 +949,6 @@ extension RelativeSelectors on WidgetSelector { /// - [withChild] requires [child] to be a child of the widget to match. @useResult WidgetSelector withChildren(List children) { - return copyWith(children: [...this.children, ...children]); + return addStage(ChildFilter(children)); } } diff --git a/lib/src/spot/snapshot.dart b/lib/src/spot/snapshot.dart index 1eed4d86..3836f0fe 100644 --- a/lib/src/spot/snapshot.dart +++ b/lib/src/spot/snapshot.dart @@ -1,4 +1,5 @@ import 'package:dartx/dartx.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; @@ -125,6 +126,25 @@ extension WidgetSnapshotShorthands on WidgetSnapshot { discovered.map((e) => e.element).toList(); } +/// prints debug information during [snapshot] that can be enabled in the +/// assert block +void _snapshotDebugPrint(String text) { + // always false in production, because asserts are not executed + bool enableDebugPrint = false; + assert(() { + // switch here to enable debug print + enableDebugPrint = false; // <-- Switch! + return true; + }()); + + if (kDebugMode && enableDebugPrint) { + // ignore: avoid_print + print('${''.padLeft(_depth * 4)}$text'); + } +} + +int _depth = -1; + /// Creates a snapshot of widgets that match the specified [selector]. /// /// This function captures the current state of widgets that match the criteria @@ -141,50 +161,55 @@ WidgetSnapshot snapshot( TestAsyncUtils.guardSync(); final treeSnapshot = currentWidgetTreeSnapshot(); - final List candidates = treeSnapshot.allNodes; + final isAnyOffstage = selector.isAnyOffstage(); + // an easy to debug list of all filters and their individual results - final stageResults = [ - ( - filter: WidgetSelector.all.stages.first, - candidates: candidates.toUnmodifiable(), - ), - ]; + final initialStage = _StageResult( + index: -1, + filter: WidgetSelector.all.stages.first, + candidates: candidates.toUnmodifiable(), + ); - final offstage = selector.isAnyOffstage(); + final List<_StageResult> stageResults = [initialStage]; - if (selector.stages.any((e) => e is ParentFilter)) { - throw ArgumentError( - 'ParentFilter is not allowed in stages. Use the parents parameter instead.', - ); - } - if (selector.stages.any((e) => e is ChildFilter)) { - throw ArgumentError( - 'ChildFilter is not allowed in stages. Use the children parameter instead.', - ); - } + _depth++; + _snapshotDebugPrint('$selector, offstage ${selector.includeOffstage}'); final stages = [ - if (!offstage) OnstageFilter(), + if (!isAnyOffstage) OnstageFilter(), ...selector.stages, - if (selector.children.isNotEmpty) ChildFilter(selector.children), - // sort ParentFilter last for better performance - if (selector.parents.isNotEmpty) ParentFilter(selector.parents), ]; - for (final stage in stages) { + for (int i = 0; i < stages.length; i++) { + final stage = stages[i]; // using unmodifiable copies to prevent accidental modification during filtering - final before = stageResults.last.candidates.toUnmodifiable(); - final after = stage.filter(before).toList().toUnmodifiable(); - stageResults.add((filter: stage, candidates: after)); + final remainingCandidatesFromPreviousStage = + stageResults.last.candidates.toUnmodifiable(); + + _snapshotDebugPrint( + "+ Stage $i: $stage, " + "input-candidates: ${remainingCandidatesFromPreviousStage.length}", + ); + final after = stage + .filter(remainingCandidatesFromPreviousStage) + .toList() + .toUnmodifiable(); + _snapshotDebugPrint("- Stage $i: $stage, " + "output-candidates: ${after.length}"); + stageResults.add(_StageResult(index: i, filter: stage, candidates: after)); } + _snapshotDebugPrint( + '$selector, ${stageResults.last.candidates.length} matches', + ); final snapshot = WidgetSnapshot( selector: selector, discovered: stageResults.last.candidates, scope: treeSnapshot, debugCandidates: candidates.map((element) => element.element).toList(), ); + _depth--; if (validateQuantity) { snapshot.validateQuantity(); @@ -193,6 +218,23 @@ WidgetSnapshot snapshot( return snapshot; } +class _StageResult { + final int index; + final ElementFilter filter; + final List candidates; + + const _StageResult({ + required this.index, + required this.filter, + required this.candidates, + }); + + @override + String toString() { + return 'StageResult(#$index, candidates: ${candidates.length}, filter: $filter)'; + } +} + /// Extension on [WidgetSnapshot] providing methods to validate the quantity of discovered widgets. extension ValidateQuantity on WidgetSnapshot { /// Validates the quantity of [discovered] to match [WidgetSelector.quantityConstraint] @@ -520,10 +562,6 @@ extension LessSpecificSelectors on WidgetSelector { final List Function(WidgetSelector)> criteria = [ for (final stage in stages) (s) => s.copyWith(stages: [...s.stages, stage]), - for (final child in children) - (s) => s.copyWith(children: [...s.children, child]), - for (final parent in parents) - (s) => s.copyWith(parents: [...s.parents, parent]), ]; if (criteria.length <= 1) { return; diff --git a/lib/src/spot/text/any_text.dart b/lib/src/spot/text/any_text.dart index d5ae98c7..334045c2 100644 --- a/lib/src/spot/text/any_text.dart +++ b/lib/src/spot/text/any_text.dart @@ -238,8 +238,6 @@ class AnyTextWidgetSelector extends WidgetSelector { /// - `parents`: Parent selectors to include in the match. AnyTextWidgetSelector({ required super.stages, - required super.children, - required super.parents, }) : super(mapElementToWidget: _mapElementToAnyText); static AnyText _mapElementToAnyText(Element element) { @@ -308,6 +306,11 @@ class MatchTextFilter implements ElementFilter { } throw _UnsupportedWidgetTypeException(e.widget); } + + @override + String toString() { + return 'MatchTextFilter which keeps $description'; + } } class _UnsupportedWidgetTypeException implements Exception { diff --git a/lib/src/spot/widget_selector.dart b/lib/src/spot/widget_selector.dart index cbe2c13e..6e20e1a2 100644 --- a/lib/src/spot/widget_selector.dart +++ b/lib/src/spot/widget_selector.dart @@ -1,3 +1,4 @@ +import 'package:dartx/dartx.dart'; import 'package:flutter/widgets.dart'; import 'package:meta/meta.dart'; import 'package:spot/src/spot/filters/child_filter.dart'; @@ -46,16 +47,12 @@ class WidgetSelector with ChainableSelectors { /// Allows specifying various parameters for customizing the selection criteria. WidgetSelector({ required List stages, - List? parents, - List? children, @Deprecated('Use quantityConstraint instead') ExpectedQuantity expectedQuantity = ExpectedQuantity.multi, QuantityConstraint? quantityConstraint, bool? includeOffstage, W Function(Element element)? mapElementToWidget, - }) : parents = parents ?? [], - children = children ?? [], - stages = List.unmodifiable(stages), + }) : stages = List.unmodifiable(stages), includeOffstage = includeOffstage ?? false, quantityConstraint = quantityConstraint ?? // ignore: deprecated_member_use_from_same_package @@ -75,19 +72,6 @@ class WidgetSelector with ChainableSelectors { /// - [WidgetTypeFilter] final List stages; - /// A list of parent selectors used to define a hierarchical - /// context for the selection. - /// - /// These selectors specify the parent widgets in the tree that the current - /// selector's widget must be a descendant of. - final List parents; - - /// A list of child selectors used to filter widgets based on their children. - /// - /// These selectors are applied to the children of the widget being matched, - /// allowing for selection based on child widget properties. - final List children; - /// Whether this selector expects to find a single or multiple widgets @Deprecated('Use quantityConstraint instead') ExpectedQuantity get expectedQuantity => quantityConstraint.max == 1 @@ -114,6 +98,20 @@ class WidgetSelector with ChainableSelectors { /// Whether to include offstage widgets in the selection final bool includeOffstage; + /// All parent selectors of all stages this widget selector depends on + List get parents { + return stages.whereType().flatMap((e) => e.parents).toList(); + } + + /// All child selectors of all stages this widget selector depends on + List get children { + return stages + .whereType() + .flatMap((e) => e.childSelectors) + .toList(); + } + + /// Recursively checks if this or any parent selector includes offstage widgets bool isAnyOffstage() { if (includeOffstage) { return true; @@ -129,16 +127,16 @@ class WidgetSelector with ChainableSelectors { @override String toString() { final sb = StringBuffer(); - final s = [ - ...stages, - if (children.isNotEmpty) ChildFilter(children), - if (parents.isNotEmpty) ParentFilter(parents), - ]; - - for (int i = 0; i < s.length; i++) { - final stage = s[i]; + + for (int i = 0; i < stages.length; i++) { + final stage = stages[i]; if (stage is ParentFilter) { - var desc = stage.parents.first.toString(); + String desc; + if (stages.length == 1 && includeOffstage) { + desc = 'find offstage Widgets'; + } else { + desc = stage.parents.first.toString(); + } if (desc.contains(' with parent ') || desc.contains(' with child ')) { desc = '($desc)'; } @@ -166,16 +164,20 @@ class WidgetSelector with ChainableSelectors { } } - final isLast = i == s.length - 1; + final isLast = i == stages.length - 1; if (stage is WidgetTypeFilter) { sb.write(' '); } else if (!isLast) { sb.write(stageSeparator); } } - final parts = - [sb.toString().trim(), _quantityToString()].where((e) => e != null); - return parts.join(' '); + final parts = [ + sb.toString().trim(), + _quantityToString(), + if (includeOffstage) '(include offstage)', + ].where((e) => e != null); + final out = parts.join(' '); + return out; } /// Generates a breadcrumb-like string representation of this selector. @@ -184,16 +186,16 @@ class WidgetSelector with ChainableSelectors { /// hierarchy of the selection. String toStringBreadcrumb() { var sb = StringBuffer(); - final s = [ - ...stages, - if (children.isNotEmpty) ChildFilter(children), - if (parents.isNotEmpty) ParentFilter(parents), - ]; - - for (int i = 0; i < s.length; i++) { - final stage = s[i]; + + for (int i = 0; i < stages.length; i++) { + final stage = stages[i]; if (stage is ParentFilter) { - var child = sb.toString(); + String child; + if (stages.length == 1 && includeOffstage) { + child = 'find offstage Widgets'; + } else { + child = sb.toString(); + } if (child.endsWith(stageSeparator)) { // Remove stage separator from the end child = child.substring(0, child.length - stageSeparator.length); @@ -242,7 +244,7 @@ class WidgetSelector with ChainableSelectors { } } - final isLast = i == s.length - 1; + final isLast = i == stages.length - 1; if (stage is WidgetTypeFilter) { if (!sb.toString().endsWith(' ')) { sb.write(' '); @@ -255,9 +257,13 @@ class WidgetSelector with ChainableSelectors { } } - final parts = - [sb.toString().trim(), _quantityToString()].where((e) => e != null); - return parts.join(' '); + final parts = [ + sb.toString().trim(), + _quantityToString(), + if (includeOffstage) '(include offstage)', + ].where((e) => e != null); + final out = parts.join(' '); + return out; } /// Generates a string representation of the quantity constraints. @@ -304,16 +310,10 @@ class WidgetSelector with ChainableSelectors { List? children, }) { return WidgetSelector( - stages: stages ?? - this - .stages - .where((it) => it is! ChildFilter && it is! ParentFilter) - .toList(), + stages: stages ?? this.stages, quantityConstraint: quantityConstraint ?? this.quantityConstraint, includeOffstage: includeOffstage ?? this.includeOffstage, mapElementToWidget: mapElementToWidget ?? this.mapElementToWidget, - parents: parents ?? this.parents, - children: children ?? this.children, ); } } diff --git a/test/selectors/filter_test.dart b/test/selectors/filter_test.dart index d422e2e9..dca8a8a6 100644 --- a/test/selectors/filter_test.dart +++ b/test/selectors/filter_test.dart @@ -163,11 +163,68 @@ void main() { spotOffstage().onstage().spotText('c').doesNotExist(); spotText('c').doesNotExist(); - spotText('c').offstage().existsOnce(); - spotText('c').offstage().onstage().doesNotExist(); - spotText('c').offstage().onstage().offstage().existsOnce(); + spotText('c').overrideIncludeOffstage(true).existsOnce(); + spotText('c').overrideIncludeOffstage(true).onstage().doesNotExist(); + spotText('c') + .overrideIncludeOffstage(true) + .onstage() + .overrideIncludeOffstage(true) + .existsOnce(); spot().withText('c').doesNotExist(); - spot().withText('c').offstage().existsOnce(); + spot().withText('c').overrideIncludeOffstage(true).existsOnce(); + }); + + testWidgets('only include offstage widget in the parts that mattes', + (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Row( + children: [ + Expanded( + child: Offstage( + child: Scaffold( + body: Column( + children: [ + Text('a'), + Text('b'), + ], + ), + ), + ), + ), + Expanded( + child: Scaffold( + body: Column( + children: [ + Text('a'), + Offstage(child: Text('b')), + ], + ), + ), + ), + ], + ), + ), + ); + + spotText('a').existsExactlyNTimes(1); + spotText('b').existsExactlyNTimes(0); + + spotOffstage().spotText('a').existsExactlyNTimes(2); + spotOffstage().spotText('b').existsExactlyNTimes(2); + + // ignores offstage Scaffold + spot().spotText('a').existsExactlyNTimes(1); + + // includes the offstage Scaffold + spotOffstage().spot().spotText('a').existsExactlyNTimes(2); + + //only search for widgets within the onstage Scaffold + spot().spotText('a').existsExactlyNTimes(1); + spot().spotOffstage().spotText('a').existsExactlyNTimes(1); + + // find offstage widgets within the onstage Scaffold only! + spot().spotOffstage().spotText('b').existsExactlyNTimes(1); }); } From f01e2741727c02a1e80434f9293014baa9dd28d9 Mon Sep 17 00:00:00 2001 From: Pascal Welsch Date: Sun, 3 Mar 2024 22:53:58 +0100 Subject: [PATCH 11/22] Handle offstage for child selectors --- lib/spot.dart | 2 +- lib/src/spot/filters/onstage_filter.dart | 31 +++++++++++-- lib/src/spot/filters/parent_filter.dart | 43 +++++++++++++----- lib/src/spot/snapshot.dart | 9 ++-- test/selectors/filter_test.dart | 57 +++++++++++++++++++++--- 5 files changed, 116 insertions(+), 26 deletions(-) diff --git a/lib/spot.dart b/lib/spot.dart index da190e73..9986fb56 100644 --- a/lib/spot.dart +++ b/lib/spot.dart @@ -206,7 +206,7 @@ WidgetSelector spot({ ); } -/// Creates a [WidgetSelector] that includes offstage widgets in the selection. +/// Creates a [WidgetSelector] that includes only offstage widgets in the selection. /// /// Offstage widgets are those that are not currently visible on the screen, /// but are still part of the widget tree. This can be useful when you want to diff --git a/lib/src/spot/filters/onstage_filter.dart b/lib/src/spot/filters/onstage_filter.dart index 7066ee11..0c4a6237 100644 --- a/lib/src/spot/filters/onstage_filter.dart +++ b/lib/src/spot/filters/onstage_filter.dart @@ -4,14 +4,14 @@ import 'package:spot/src/spot/widget_selector.dart'; class OnstageFilter implements ElementFilter { @override Iterable filter(Iterable candidates) { - final List matchingChildNodes = []; + final List onstage = []; for (final WidgetTreeNode candidate in candidates) { if (!candidate.isOffstage) { - matchingChildNodes.add(candidate); + onstage.add(candidate); } } - return matchingChildNodes; + return onstage; } @override @@ -24,3 +24,28 @@ class OnstageFilter implements ElementFilter { return 'OnstageFilter which $description'; } } + +/// Removes all [WidgetTreeNode] that are onstage +class OffstageFilter implements ElementFilter { + @override + Iterable filter(Iterable candidates) { + final List offstage = []; + + for (final WidgetTreeNode candidate in candidates) { + if (candidate.isOffstage) { + offstage.add(candidate); + } + } + return offstage; + } + + @override + String get description { + return 'keeps only offstage elements'; + } + + @override + String toString() { + return 'OffstageFilter which $description'; + } +} diff --git a/lib/src/spot/filters/parent_filter.dart b/lib/src/spot/filters/parent_filter.dart index 0ea86126..b19ed055 100644 --- a/lib/src/spot/filters/parent_filter.dart +++ b/lib/src/spot/filters/parent_filter.dart @@ -31,28 +31,43 @@ class ParentFilter implements ElementFilter { return widgetSnapshot; }).toList(); - // Take a snapshot from each parent and get the snapshots of all nodes that match final List>> discoveryByParent = - parentSnapshots.map((WidgetSnapshot parentSnapshot) { + []; + + for (final parentSnapshot in parentSnapshots) { final Map> groups = {}; if (parentSnapshot.discovered.isEmpty) { - return groups; + discoveryByParent.add(groups); + continue; } for (final WidgetTreeNode node in parentSnapshot.discovered) { + groups[node] ??= []; + // final s2 = + // snapshot(spotAllWidgets().withParent(parentSnapshot.selector)); + // groups[node]!.add(s2); + + // TODO what's the difference between s1 and s2? + // s2 works but is slow, s1 is fast but doesn't work + + final root = node.isOffstage ? spotOffstage() : spotAllWidgets(); + final subtree = tree.scope(node); - final snapshot = WidgetSnapshot( - selector: WidgetSelector.all.withParent(parentSnapshot.selector), - discovered: subtree.allNodes, + final s1 = WidgetSnapshot( + selector: root.withParent(parentSnapshot.selector), + discovered: [ + // TODO is returning the root correct? + node, + ...subtree.allNodes, + ], scope: subtree, debugCandidates: candidates.map((it) => it.element).toList(), ); - groups[node] ??= []; - groups[node]!.add(snapshot); + groups[node]!.add(s1); } - return groups; - }).toList(); + discoveryByParent.add(groups); + } final List discoveredSnapshots = discoveryByParent.map((it) => it.values).flatten().flatten().toList(); @@ -77,9 +92,13 @@ class ParentFilter implements ElementFilter { }); }).toList(); - return elementsInAllParents.mapNotNull((e) { - return candidates.firstOrNullWhere((node) => node.element == e); + final remaining = elementsInAllParents.mapNotNull((e) { + return candidates.firstOrNullWhere((node) { + return node.element == e; + }); }).toList(); + + return remaining; } @override diff --git a/lib/src/spot/snapshot.dart b/lib/src/spot/snapshot.dart index 3836f0fe..35d704c9 100644 --- a/lib/src/spot/snapshot.dart +++ b/lib/src/spot/snapshot.dart @@ -175,9 +175,12 @@ WidgetSnapshot snapshot( final List<_StageResult> stageResults = [initialStage]; _depth++; - _snapshotDebugPrint('$selector, offstage ${selector.includeOffstage}'); + _snapshotDebugPrint( + 'snapshot() ${selector.toStringBreadcrumb()}, ' + 'offstage ${selector.includeOffstage}', + ); final stages = [ - if (!isAnyOffstage) OnstageFilter(), + if (isAnyOffstage) OffstageFilter() else OnstageFilter(), ...selector.stages, ]; @@ -201,7 +204,7 @@ WidgetSnapshot snapshot( } _snapshotDebugPrint( - '$selector, ${stageResults.last.candidates.length} matches', + '${selector.toStringBreadcrumb()}, ${stageResults.last.candidates.length} matches', ); final snapshot = WidgetSnapshot( selector: selector, diff --git a/test/selectors/filter_test.dart b/test/selectors/filter_test.dart index dca8a8a6..123131a4 100644 --- a/test/selectors/filter_test.dart +++ b/test/selectors/filter_test.dart @@ -175,8 +175,7 @@ void main() { spot().withText('c').overrideIncludeOffstage(true).existsOnce(); }); - testWidgets('only include offstage widget in the parts that mattes', - (tester) async { + testWidgets('filter offstage in subtree of parent', (tester) async { await tester.pumpWidget( MaterialApp( home: Row( @@ -211,20 +210,64 @@ void main() { spotText('a').existsExactlyNTimes(1); spotText('b').existsExactlyNTimes(0); - spotOffstage().spotText('a').existsExactlyNTimes(2); + spotOffstage().spotText('a').existsExactlyNTimes(1); spotOffstage().spotText('b').existsExactlyNTimes(2); // ignores offstage Scaffold spot().spotText('a').existsExactlyNTimes(1); - // includes the offstage Scaffold - spotOffstage().spot().spotText('a').existsExactlyNTimes(2); + // only search the offstage scaffold + spotOffstage().spotText('b').existsExactlyNTimes(2); + + spotOffstage().spot().existsExactlyNTimes(1); + spotOffstage().spotText('b').existsExactlyNTimes(2); + spotOffstage().spot().spotText('a').existsExactlyNTimes(1); - //only search for widgets within the onstage Scaffold + // only search for widgets within the onstage Scaffold spot().spotText('a').existsExactlyNTimes(1); - spot().spotOffstage().spotText('a').existsExactlyNTimes(1); // find offstage widgets within the onstage Scaffold only! + spot().spotOffstage().spotText('a').existsExactlyNTimes(0); spot().spotOffstage().spotText('b').existsExactlyNTimes(1); }); + + testWidgets('filter offstage in subtree of child', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Row( + children: [ + Expanded( + child: Scaffold( + body: Column( + children: [ + Text('a'), + Text('b'), + ], + ), + ), + ), + Expanded( + child: Scaffold( + body: Column( + children: [ + Text('a'), + Offstage(child: Text('b')), + ], + ), + ), + ), + ], + ), + ), + ); + + spot().existsExactlyNTimes(2); + + spot().withChild(spotText('a')).existsExactlyNTimes(2); + spot().withChild(spotText('b')).existsOnce(); + + // find onstage Expanded, with offstage Text b + spot().withChild(spotOffstage().spotText('b')).existsOnce(); + spot().withChild(spotOffstage().spotText('a')).doesNotExist(); + }); } From b6769cac4b2cd2038f470ce721651e80f7a6adba Mon Sep 17 00:00:00 2001 From: Pascal Welsch Date: Sun, 3 Mar 2024 23:05:06 +0100 Subject: [PATCH 12/22] Cleanup --- lib/src/spot/filters/parent_filter.dart | 7 ------- 1 file changed, 7 deletions(-) diff --git a/lib/src/spot/filters/parent_filter.dart b/lib/src/spot/filters/parent_filter.dart index b19ed055..a0b89082 100644 --- a/lib/src/spot/filters/parent_filter.dart +++ b/lib/src/spot/filters/parent_filter.dart @@ -43,15 +43,8 @@ class ParentFilter implements ElementFilter { for (final WidgetTreeNode node in parentSnapshot.discovered) { groups[node] ??= []; - // final s2 = - // snapshot(spotAllWidgets().withParent(parentSnapshot.selector)); - // groups[node]!.add(s2); - - // TODO what's the difference between s1 and s2? - // s2 works but is slow, s1 is fast but doesn't work final root = node.isOffstage ? spotOffstage() : spotAllWidgets(); - final subtree = tree.scope(node); final s1 = WidgetSnapshot( selector: root.withParent(parentSnapshot.selector), From ed18386bcf3b386cf69371dfdd36d06cd499119d Mon Sep 17 00:00:00 2001 From: Pascal Welsch Date: Sun, 3 Mar 2024 23:05:26 +0100 Subject: [PATCH 13/22] Replace onstage() with overrideIncludeOffstage(false) --- lib/src/spot/selectors.dart | 19 ------------------- lib/src/spot/snapshot.dart | 1 + test/selectors/filter_test.dart | 13 +++++++++---- 3 files changed, 10 insertions(+), 23 deletions(-) diff --git a/lib/src/spot/selectors.dart b/lib/src/spot/selectors.dart index 20d80ded..792f8faf 100644 --- a/lib/src/spot/selectors.dart +++ b/lib/src/spot/selectors.dart @@ -118,25 +118,6 @@ mixin ChainableSelectors { ); } - /// Creates a [WidgetSelector] that excludes offstage widgets from the selection. - /// - /// This is the default behavior of a [WidgetSelector], but this method can be useful - /// if you have previously called `offstage` on the selector and want to revert back - /// to only selecting widgets that are currently visible on the screen. - /// - /// Returns a new [WidgetSelector] that excludes offstage widgets. - /// - /// ### Example usage: - /// ```dart - /// final text = spotText('text') - /// .offstage() - /// .onstage(); - /// ``` - @useResult - WidgetSelector onstage() { - return self!.copyWith(includeOffstage: false); - } - /// Creates a [WidgetSelector] that excludes offstage widgets from the selection. /// /// This selector compares the Widgets by runtimeType rather than by super diff --git a/lib/src/spot/snapshot.dart b/lib/src/spot/snapshot.dart index 35d704c9..a33c71d4 100644 --- a/lib/src/spot/snapshot.dart +++ b/lib/src/spot/snapshot.dart @@ -194,6 +194,7 @@ WidgetSnapshot snapshot( "+ Stage $i: $stage, " "input-candidates: ${remainingCandidatesFromPreviousStage.length}", ); + final after = stage .filter(remainingCandidatesFromPreviousStage) .toList() diff --git a/test/selectors/filter_test.dart b/test/selectors/filter_test.dart index 123131a4..3d287a5e 100644 --- a/test/selectors/filter_test.dart +++ b/test/selectors/filter_test.dart @@ -145,7 +145,8 @@ void main() { spotText('c').doesNotExist(); }); - testWidgets('select offstage widgets when use offstage()', (tester) async { + testWidgets('select offstage widgets when use .overrideIncludeOffstage(true)', + (tester) async { await tester.pumpWidget( MaterialApp( home: Row( @@ -160,14 +161,18 @@ void main() { spotOffstage().spot().atMost(3); spotOffstage().spotText('c').existsOnce(); - spotOffstage().onstage().spotText('c').doesNotExist(); + spotOffstage().overrideIncludeOffstage(false).spotText('c').doesNotExist(); spotText('c').doesNotExist(); spotText('c').overrideIncludeOffstage(true).existsOnce(); - spotText('c').overrideIncludeOffstage(true).onstage().doesNotExist(); + spotOffstage().spotText('c').existsOnce(); + spotText('c') + .overrideIncludeOffstage(true) + .overrideIncludeOffstage(false) + .doesNotExist(); spotText('c') .overrideIncludeOffstage(true) - .onstage() + .overrideIncludeOffstage(false) .overrideIncludeOffstage(true) .existsOnce(); From aa812fbed7d0eb5613837948aa24fb8781677669 Mon Sep 17 00:00:00 2001 From: Pascal Welsch Date: Sun, 3 Mar 2024 23:09:42 +0100 Subject: [PATCH 14/22] Cleanup --- lib/src/spot/widget_selector.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/src/spot/widget_selector.dart b/lib/src/spot/widget_selector.dart index 6e20e1a2..873f27f1 100644 --- a/lib/src/spot/widget_selector.dart +++ b/lib/src/spot/widget_selector.dart @@ -306,8 +306,6 @@ class WidgetSelector with ChainableSelectors { QuantityConstraint? quantityConstraint, bool? includeOffstage, W Function(Element element)? mapElementToWidget, - List? parents, - List? children, }) { return WidgetSelector( stages: stages ?? this.stages, From 60867fe6c39f3bd87b26846f232592cb3491d92e Mon Sep 17 00:00:00 2001 From: Pascal Welsch Date: Sun, 3 Mar 2024 23:36:49 +0100 Subject: [PATCH 15/22] Improve PartenFilter performance by detecting subtrees --- lib/spot.dart | 15 +++++++++------ lib/src/spot/filters/parent_filter.dart | 16 +++++++++++----- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/lib/spot.dart b/lib/spot.dart index 51bd32e0..0218d90d 100644 --- a/lib/spot.dart +++ b/lib/spot.dart @@ -216,12 +216,15 @@ WidgetSelector spotOffstage({ List parents = const [], List children = const [], }) { - return _global - .spot( - parents: parents, - children: children, - ) - .overrideIncludeOffstage(true); + return WidgetSelector( + stages: [ + PredicateFilter( + predicate: (e) => true, + description: 'any Offstage Widget', + ), + ], + includeOffstage: true, + ); } /// Creates a [WidgetSelector] that finds [widget] by identity and returns all diff --git a/lib/src/spot/filters/parent_filter.dart b/lib/src/spot/filters/parent_filter.dart index a0b89082..3d385ea4 100644 --- a/lib/src/spot/filters/parent_filter.dart +++ b/lib/src/spot/filters/parent_filter.dart @@ -41,22 +41,28 @@ class ParentFilter implements ElementFilter { continue; } - for (final WidgetTreeNode node in parentSnapshot.discovered) { + // remove elements when the parent is already in the list. This prevents searching all element of a subtree, resulting in always the same items + final rootNodes = parentSnapshot.discovered + .whereNot( + (element) => parentSnapshot.discovered.contains(element.parent), + ) + .toList(); + + for (final WidgetTreeNode node in rootNodes) { groups[node] ??= []; final root = node.isOffstage ? spotOffstage() : spotAllWidgets(); final subtree = tree.scope(node); - final s1 = WidgetSnapshot( - selector: root.withParent(parentSnapshot.selector), + final snapshot = WidgetSnapshot( + selector: root.withParent(spotElement(node.element)), discovered: [ - // TODO is returning the root correct? node, ...subtree.allNodes, ], scope: subtree, debugCandidates: candidates.map((it) => it.element).toList(), ); - groups[node]!.add(s1); + groups[node]!.add(snapshot); } discoveryByParent.add(groups); From dd6ec8a968040e5bc4117b0ef59115cd659df8b4 Mon Sep 17 00:00:00 2001 From: Pascal Welsch Date: Mon, 18 Mar 2024 13:13:41 +0100 Subject: [PATCH 16/22] Group offstage tests --- test/selectors/filter_test.dart | 232 ++++++++++++++++---------------- 1 file changed, 119 insertions(+), 113 deletions(-) diff --git a/test/selectors/filter_test.dart b/test/selectors/filter_test.dart index 3d287a5e..03bb1c4b 100644 --- a/test/selectors/filter_test.dart +++ b/test/selectors/filter_test.dart @@ -128,151 +128,157 @@ void main() { spot().atIndex(4).doesNotExist(); }); - testWidgets('do not select offstage widgets by default', (tester) async { - await tester.pumpWidget( - MaterialApp( - home: Row( - children: [ - Text('a'), - Text('b'), - Offstage(child: Text('c')), - ], + group('offstage', () { + testWidgets('do not select offstage widgets by default', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Row( + children: [ + Text('a'), + Text('b'), + Offstage(child: Text('c')), + ], + ), ), - ), - ); + ); - spot().atMost(2); - spotText('c').doesNotExist(); - }); + spot().atMost(2); + spotText('c').doesNotExist(); + }); - testWidgets('select offstage widgets when use .overrideIncludeOffstage(true)', - (tester) async { - await tester.pumpWidget( - MaterialApp( - home: Row( - children: [ - Text('a'), - Text('b'), - Offstage(child: Text('c')), - ], + testWidgets( + 'select offstage widgets when use .overrideIncludeOffstage(true)', + (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Row( + children: [ + Text('a'), + Text('b'), + Offstage(child: Text('c')), + ], + ), ), - ), - ); + ); - spotOffstage().spot().atMost(3); - spotOffstage().spotText('c').existsOnce(); - spotOffstage().overrideIncludeOffstage(false).spotText('c').doesNotExist(); + spotOffstage().spot().atMost(3); + spotOffstage().spotText('c').existsOnce(); + spotOffstage() + .overrideIncludeOffstage(false) + .spotText('c') + .doesNotExist(); - spotText('c').doesNotExist(); - spotText('c').overrideIncludeOffstage(true).existsOnce(); - spotOffstage().spotText('c').existsOnce(); - spotText('c') - .overrideIncludeOffstage(true) - .overrideIncludeOffstage(false) - .doesNotExist(); - spotText('c') - .overrideIncludeOffstage(true) - .overrideIncludeOffstage(false) - .overrideIncludeOffstage(true) - .existsOnce(); + spotText('c').doesNotExist(); + spotText('c').overrideIncludeOffstage(true).existsOnce(); + spotOffstage().spotText('c').existsOnce(); + spotText('c') + .overrideIncludeOffstage(true) + .overrideIncludeOffstage(false) + .doesNotExist(); + spotText('c') + .overrideIncludeOffstage(true) + .overrideIncludeOffstage(false) + .overrideIncludeOffstage(true) + .existsOnce(); - spot().withText('c').doesNotExist(); - spot().withText('c').overrideIncludeOffstage(true).existsOnce(); - }); + spot().withText('c').doesNotExist(); + spot().withText('c').overrideIncludeOffstage(true).existsOnce(); + }); - testWidgets('filter offstage in subtree of parent', (tester) async { - await tester.pumpWidget( - MaterialApp( - home: Row( - children: [ - Expanded( - child: Offstage( + testWidgets('filter offstage in subtree of parent', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Row( + children: [ + Expanded( + child: Offstage( + child: Scaffold( + body: Column( + children: [ + Text('a'), + Text('b'), + ], + ), + ), + ), + ), + Expanded( child: Scaffold( body: Column( children: [ Text('a'), - Text('b'), + Offstage(child: Text('b')), ], ), ), ), - ), - Expanded( - child: Scaffold( - body: Column( - children: [ - Text('a'), - Offstage(child: Text('b')), - ], - ), - ), - ), - ], + ], + ), ), - ), - ); + ); - spotText('a').existsExactlyNTimes(1); - spotText('b').existsExactlyNTimes(0); + spotText('a').existsExactlyNTimes(1); + spotText('b').existsExactlyNTimes(0); - spotOffstage().spotText('a').existsExactlyNTimes(1); - spotOffstage().spotText('b').existsExactlyNTimes(2); + spotOffstage().spotText('a').existsExactlyNTimes(1); + spotOffstage().spotText('b').existsExactlyNTimes(2); - // ignores offstage Scaffold - spot().spotText('a').existsExactlyNTimes(1); + // ignores offstage Scaffold + spot().spotText('a').existsExactlyNTimes(1); - // only search the offstage scaffold - spotOffstage().spotText('b').existsExactlyNTimes(2); + // only search the offstage scaffold + spotOffstage().spotText('b').existsExactlyNTimes(2); - spotOffstage().spot().existsExactlyNTimes(1); - spotOffstage().spotText('b').existsExactlyNTimes(2); - spotOffstage().spot().spotText('a').existsExactlyNTimes(1); + spotOffstage().spot().existsExactlyNTimes(1); + spotOffstage().spotText('b').existsExactlyNTimes(2); + spotOffstage().spot().spotText('a').existsExactlyNTimes(1); - // only search for widgets within the onstage Scaffold - spot().spotText('a').existsExactlyNTimes(1); + // only search for widgets within the onstage Scaffold + spot().spotText('a').existsExactlyNTimes(1); - // find offstage widgets within the onstage Scaffold only! - spot().spotOffstage().spotText('a').existsExactlyNTimes(0); - spot().spotOffstage().spotText('b').existsExactlyNTimes(1); - }); + // find offstage widgets within the onstage Scaffold only! + spot().spotOffstage().spotText('a').existsExactlyNTimes(0); + spot().spotOffstage().spotText('b').existsExactlyNTimes(1); + }); - testWidgets('filter offstage in subtree of child', (tester) async { - await tester.pumpWidget( - MaterialApp( - home: Row( - children: [ - Expanded( - child: Scaffold( - body: Column( - children: [ - Text('a'), - Text('b'), - ], + testWidgets('filter offstage in subtree of child', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Row( + children: [ + Expanded( + child: Scaffold( + body: Column( + children: [ + Text('a'), + Text('b'), + ], + ), ), ), - ), - Expanded( - child: Scaffold( - body: Column( - children: [ - Text('a'), - Offstage(child: Text('b')), - ], + Expanded( + child: Scaffold( + body: Column( + children: [ + Text('a'), + Offstage(child: Text('b')), + ], + ), ), ), - ), - ], + ], + ), ), - ), - ); + ); - spot().existsExactlyNTimes(2); + spot().existsExactlyNTimes(2); - spot().withChild(spotText('a')).existsExactlyNTimes(2); - spot().withChild(spotText('b')).existsOnce(); + spot().withChild(spotText('a')).existsExactlyNTimes(2); + spot().withChild(spotText('b')).existsOnce(); - // find onstage Expanded, with offstage Text b - spot().withChild(spotOffstage().spotText('b')).existsOnce(); - spot().withChild(spotOffstage().spotText('a')).doesNotExist(); + // find onstage Expanded, with offstage Text b + spot().withChild(spotOffstage().spotText('b')).existsOnce(); + spot().withChild(spotOffstage().spotText('a')).doesNotExist(); + }); }); } From a0de725a070271545a3076b244e2af5ef4e8074d Mon Sep 17 00:00:00 2001 From: Pascal Welsch Date: Mon, 18 Mar 2024 13:44:17 +0100 Subject: [PATCH 17/22] Add dialog offstage example --- test/selectors/filter_test.dart | 39 +++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/test/selectors/filter_test.dart b/test/selectors/filter_test.dart index 03bb1c4b..541c5f06 100644 --- a/test/selectors/filter_test.dart +++ b/test/selectors/filter_test.dart @@ -280,5 +280,44 @@ void main() { spot().withChild(spotOffstage().spotText('b')).existsOnce(); spot().withChild(spotOffstage().spotText('a')).doesNotExist(); }); + + testWidgets('fullscreenDialog', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Center( + child: ElevatedButton( + onPressed: () { + final navigator = + Navigator.of(tester.element(find.byType(ElevatedButton))); + navigator.push( + MaterialPageRoute( + // fullscreenDialog: true, + builder: (context) { + return AlertDialog( + content: Text('dialog content'), + ); + }, + ), + ); + }, + child: Text('open dialog'), + ), + ), + ), + ); + + spotText('open dialog').existsOnce(); + spotText('dialog content').doesNotExist(); + spotOffstage().spotText('dialog content').doesNotExist(); + + await tester.tap(find.text('open dialog')); + await tester.pumpAndSettle(); + + spot().existsOnce(); + spotText('dialog content').existsOnce(); + + spotText('open dialog').doesNotExist(); + spotOffstage().spotText('open dialog').existsOnce(); + }); }); } From 6f7e04893ed53efd669147c607a2e98b560bae34 Mon Sep 17 00:00:00 2001 From: Maikel Rehl Date: Tue, 19 Mar 2024 15:19:25 +0100 Subject: [PATCH 18/22] Replace isOffstage bool with enum --- lib/spot.dart | 3 +- lib/src/spot/filters/parent_filter.dart | 9 +- lib/src/spot/selectors.dart | 19 +++- lib/src/spot/snapshot.dart | 8 +- lib/src/spot/widget_selector.dart | 53 +++++++-- test/selectors/filter_test.dart | 139 ++++++++++++++++++++++-- test/spot/widget_selector_test.dart | 2 +- 7 files changed, 201 insertions(+), 32 deletions(-) diff --git a/lib/spot.dart b/lib/spot.dart index 0218d90d..6aa53d15 100644 --- a/lib/spot.dart +++ b/lib/spot.dart @@ -4,6 +4,7 @@ library spot; import 'package:flutter/material.dart'; import 'package:spot/spot.dart'; import 'package:spot/src/spot/selectors.dart' show Spot; +import 'package:spot/src/spot/widget_selector.dart'; export 'package:checks/checks.dart' hide @@ -223,7 +224,7 @@ WidgetSelector spotOffstage({ description: 'any Offstage Widget', ), ], - includeOffstage: true, + visibilityMode: VisibilityMode.offstage, ); } diff --git a/lib/src/spot/filters/parent_filter.dart b/lib/src/spot/filters/parent_filter.dart index 62647505..dfcafa30 100644 --- a/lib/src/spot/filters/parent_filter.dart +++ b/lib/src/spot/filters/parent_filter.dart @@ -3,6 +3,7 @@ import 'package:flutter/widgets.dart'; import 'package:spot/spot.dart'; import 'package:spot/src/spot/snapshot.dart'; import 'package:spot/src/spot/tree_snapshot.dart'; +import 'package:spot/src/spot/widget_selector.dart'; /// A filter that checks if the candidates are children of all [parents] class ParentFilter implements ElementFilter { @@ -57,10 +58,16 @@ class ParentFilter implements ElementFilter { ) .toList(); + final visibilityMode = parentSnapshot.selector.visibilityMode; + for (final WidgetTreeNode node in rootNodes) { groups[node] ??= []; - final root = node.isOffstage ? spotOffstage() : spotAllWidgets(); + final WidgetSelector root = switch (visibilityMode) { + VisibilityMode.onstage => spot(), + VisibilityMode.offstage => spotOffstage(), + VisibilityMode.combined => spotAllWidgets(), + }; final subtree = tree.scope(node); final snapshot = WidgetSnapshot( selector: root.withParent(spotElement(node.element)), diff --git a/lib/src/spot/selectors.dart b/lib/src/spot/selectors.dart index 792f8faf..98eb9a98 100644 --- a/lib/src/spot/selectors.dart +++ b/lib/src/spot/selectors.dart @@ -5,6 +5,7 @@ import 'package:spot/src/checks/checks_nullability.dart'; import 'package:spot/src/spot/snapshot.dart' as snapshot_file show snapshot; import 'package:spot/src/spot/snapshot.dart'; import 'package:spot/src/spot/text/any_text.dart'; +import 'package:spot/src/spot/widget_selector.dart'; export 'package:checks/context.dart'; @@ -66,7 +67,13 @@ mixin ChainableSelectors { }) { final selector = WidgetSelector( stages: [ - WidgetTypeFilter(), + if (W == Widget) + PredicateFilter( + predicate: (e) => true, + description: 'any Widget', + ) + else + WidgetTypeFilter(), ..._childAndParentFilters(children, parents), ], ); @@ -84,14 +91,14 @@ mixin ChainableSelectors { /// ### Example usage: /// ```dart /// final text = spotText('text') - /// .overrideIncludeOffstage(); + /// .overrideVisibilityMode(VisibilityMode.combined); /// ``` @useResult - WidgetSelector overrideIncludeOffstage(bool offstage) { - if (offstage == self!.includeOffstage) { + WidgetSelector overrideVisibilityMode(VisibilityMode mode) { + if (mode == self!.visibilityMode) { return self!; } - return self!.copyWith(includeOffstage: offstage); + return self!.copyWith(visibilityMode: mode); } /// Creates a [WidgetSelector] that includes offstage widgets in the selection. @@ -113,7 +120,7 @@ mixin ChainableSelectors { List children = const [], }) { return WidgetSelector( - includeOffstage: true, + visibilityMode: VisibilityMode.offstage, stages: _childAndParentFilters(children, parents), ); } diff --git a/lib/src/spot/snapshot.dart b/lib/src/spot/snapshot.dart index a33c71d4..120db1f1 100644 --- a/lib/src/spot/snapshot.dart +++ b/lib/src/spot/snapshot.dart @@ -164,6 +164,7 @@ WidgetSnapshot snapshot( final List candidates = treeSnapshot.allNodes; final isAnyOffstage = selector.isAnyOffstage(); + final isAnyCombined = selector.isAnyCombined(); // an easy to debug list of all filters and their individual results final initialStage = _StageResult( @@ -177,10 +178,13 @@ WidgetSnapshot snapshot( _depth++; _snapshotDebugPrint( 'snapshot() ${selector.toStringBreadcrumb()}, ' - 'offstage ${selector.includeOffstage}', + 'visibility-mode ${selector.visibilityMode}', ); final stages = [ - if (isAnyOffstage) OffstageFilter() else OnstageFilter(), + if (!isAnyCombined && isAnyOffstage) + OffstageFilter() + else if (!isAnyCombined && !isAnyOffstage) + OnstageFilter(), ...selector.stages, ]; diff --git a/lib/src/spot/widget_selector.dart b/lib/src/spot/widget_selector.dart index 873f27f1..107bf9d3 100644 --- a/lib/src/spot/widget_selector.dart +++ b/lib/src/spot/widget_selector.dart @@ -16,6 +16,7 @@ export 'package:spot/src/spot/tree_snapshot.dart'; class WidgetSelector with ChainableSelectors { /// Matches any widget currently mounted static final WidgetSelector all = WidgetSelector( + visibilityMode: VisibilityMode.combined, stages: [ PredicateFilter( predicate: (e) => true, @@ -50,10 +51,10 @@ class WidgetSelector with ChainableSelectors { @Deprecated('Use quantityConstraint instead') ExpectedQuantity expectedQuantity = ExpectedQuantity.multi, QuantityConstraint? quantityConstraint, - bool? includeOffstage, + VisibilityMode? visibilityMode, W Function(Element element)? mapElementToWidget, }) : stages = List.unmodifiable(stages), - includeOffstage = includeOffstage ?? false, + visibilityMode = visibilityMode ?? VisibilityMode.onstage, quantityConstraint = quantityConstraint ?? // ignore: deprecated_member_use_from_same_package (expectedQuantity == ExpectedQuantity.single @@ -96,7 +97,7 @@ class WidgetSelector with ChainableSelectors { Type get type => W; /// Whether to include offstage widgets in the selection - final bool includeOffstage; + final VisibilityMode visibilityMode; /// All parent selectors of all stages this widget selector depends on List get parents { @@ -113,7 +114,7 @@ class WidgetSelector with ChainableSelectors { /// Recursively checks if this or any parent selector includes offstage widgets bool isAnyOffstage() { - if (includeOffstage) { + if (visibilityMode == VisibilityMode.offstage) { return true; } for (final parent in parents) { @@ -124,6 +125,19 @@ class WidgetSelector with ChainableSelectors { return false; } + /// Recursively checks if this or any parent selector includes onstage and offstage widgets + bool isAnyCombined() { + if (visibilityMode == VisibilityMode.combined) { + return true; + } + for (final parent in parents) { + if (parent.isAnyCombined()) { + return true; + } + } + return false; + } + @override String toString() { final sb = StringBuffer(); @@ -132,8 +146,11 @@ class WidgetSelector with ChainableSelectors { final stage = stages[i]; if (stage is ParentFilter) { String desc; - if (stages.length == 1 && includeOffstage) { - desc = 'find offstage Widgets'; + if (stages.length == 1 && (visibilityMode == VisibilityMode.offstage)) { + desc = 'find only offstage Widgets'; + } else if (stages.length == 1 && + (visibilityMode == VisibilityMode.combined)) { + desc = 'find onstage and offstage Widgets'; } else { desc = stage.parents.first.toString(); } @@ -174,7 +191,8 @@ class WidgetSelector with ChainableSelectors { final parts = [ sb.toString().trim(), _quantityToString(), - if (includeOffstage) '(include offstage)', + if (visibilityMode == VisibilityMode.offstage) '(only offstage)', + if (visibilityMode == VisibilityMode.combined) '(onstage and offstage)', ].where((e) => e != null); final out = parts.join(' '); return out; @@ -191,8 +209,11 @@ class WidgetSelector with ChainableSelectors { final stage = stages[i]; if (stage is ParentFilter) { String child; - if (stages.length == 1 && includeOffstage) { - child = 'find offstage Widgets'; + if (stages.length == 1 && (visibilityMode == VisibilityMode.offstage)) { + child = 'find only offstage Widgets'; + } else if (stages.length == 1 && + (visibilityMode == VisibilityMode.combined)) { + child = 'find onstage and offstage Widgets'; } else { child = sb.toString(); } @@ -260,7 +281,8 @@ class WidgetSelector with ChainableSelectors { final parts = [ sb.toString().trim(), _quantityToString(), - if (includeOffstage) '(include offstage)', + if (visibilityMode == VisibilityMode.offstage) '(only offstage)', + if (visibilityMode == VisibilityMode.combined) '(onstage and offstage)', ].where((e) => e != null); final out = parts.join(' '); return out; @@ -304,13 +326,13 @@ class WidgetSelector with ChainableSelectors { // ignore: deprecated_member_use_from_same_package ExpectedQuantity? expectedQuantity, QuantityConstraint? quantityConstraint, - bool? includeOffstage, + VisibilityMode? visibilityMode, W Function(Element element)? mapElementToWidget, }) { return WidgetSelector( stages: stages ?? this.stages, quantityConstraint: quantityConstraint ?? this.quantityConstraint, - includeOffstage: includeOffstage ?? this.includeOffstage, + visibilityMode: visibilityMode ?? this.visibilityMode, mapElementToWidget: mapElementToWidget ?? this.mapElementToWidget, ); } @@ -428,3 +450,10 @@ enum ExpectedQuantity { /// A selector that matches multiple widgets multi, } + +/// A filter that matches widgets based on a predicate function. +enum VisibilityMode { + onstage, + offstage, + combined, +} diff --git a/test/selectors/filter_test.dart b/test/selectors/filter_test.dart index 541c5f06..4fb6945f 100644 --- a/test/selectors/filter_test.dart +++ b/test/selectors/filter_test.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; +import 'package:spot/src/spot/widget_selector.dart'; void main() { group('first', () { @@ -146,8 +147,31 @@ void main() { spotText('c').doesNotExist(); }); + testWidgets('do not select onstage widgets when spot offstage', + (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Row( + children: [ + Text('a'), + Text('b'), + Offstage(child: Text('c')), + ], + ), + ), + ); + + spot().atMost(2); + spotText('a').existsExactlyNTimes(1); + spotText('c').doesNotExist(); + + spotOffstage().spot().atMost(1); + spotOffstage().spotText('a').doesNotExist(); + spotOffstage().spotText('c').existsOnce(); + }); + testWidgets( - 'select offstage widgets when use .overrideIncludeOffstage(true)', + 'select offstage widgets when use .overrideVisibilityMode(VisibilityMode.offstage)', (tester) async { await tester.pumpWidget( MaterialApp( @@ -162,27 +186,124 @@ void main() { ); spotOffstage().spot().atMost(3); + spotOffstage().spotText('a').doesNotExist(); spotOffstage().spotText('c').existsOnce(); spotOffstage() - .overrideIncludeOffstage(false) + .overrideVisibilityMode(VisibilityMode.onstage) + .spotText('a') + .existsOnce(); + spotOffstage() + .overrideVisibilityMode(VisibilityMode.onstage) .spotText('c') .doesNotExist(); + spotText('a').existsOnce(); spotText('c').doesNotExist(); - spotText('c').overrideIncludeOffstage(true).existsOnce(); + spotText('a') + .overrideVisibilityMode(VisibilityMode.offstage) + .doesNotExist(); + spotText('c') + .overrideVisibilityMode(VisibilityMode.offstage) + .existsOnce(); + + spotOffstage().spotText('a').doesNotExist(); spotOffstage().spotText('c').existsOnce(); + spotText('a') + .overrideVisibilityMode(VisibilityMode.offstage) + .overrideVisibilityMode(VisibilityMode.onstage) + .existsOnce(); spotText('c') - .overrideIncludeOffstage(true) - .overrideIncludeOffstage(false) + .overrideVisibilityMode(VisibilityMode.offstage) + .overrideVisibilityMode(VisibilityMode.onstage) + .doesNotExist(); + spotText('a') + .overrideVisibilityMode(VisibilityMode.offstage) + .overrideVisibilityMode(VisibilityMode.onstage) + .overrideVisibilityMode(VisibilityMode.offstage) .doesNotExist(); spotText('c') - .overrideIncludeOffstage(true) - .overrideIncludeOffstage(false) - .overrideIncludeOffstage(true) + .overrideVisibilityMode(VisibilityMode.offstage) + .overrideVisibilityMode(VisibilityMode.onstage) + .overrideVisibilityMode(VisibilityMode.offstage) .existsOnce(); + spot().withText('a').existsOnce(); spot().withText('c').doesNotExist(); - spot().withText('c').overrideIncludeOffstage(true).existsOnce(); + spot() + .withText('a') + .overrideVisibilityMode(VisibilityMode.offstage) + .doesNotExist(); + spot() + .withText('c') + .overrideVisibilityMode(VisibilityMode.offstage) + .existsOnce(); + }); + + testWidgets( + 'select offstage widgets when use .overrideVisibilityMode(VisibilityMode.combined)', + (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Row( + children: [ + Text('a'), + Text('b'), + Offstage(child: Text('c')), + ], + ), + ), + ); + + spotAllWidgets().spot().atMost(3); + spotAllWidgets().spotText('a').existsOnce(); + spotAllWidgets().spotText('c').existsOnce(); + spotAllWidgets() + .overrideVisibilityMode(VisibilityMode.onstage) + .spotText('a') + .existsOnce(); + spotAllWidgets() + .overrideVisibilityMode(VisibilityMode.onstage) + .spotText('c') + .doesNotExist(); + + spotText('a').existsOnce(); + spotText('c').doesNotExist(); + spotText('a') + .overrideVisibilityMode(VisibilityMode.combined) + .existsOnce(); + spotText('c') + .overrideVisibilityMode(VisibilityMode.combined) + .existsOnce(); + + spotAllWidgets().spotText('a').existsOnce(); + spotAllWidgets().spotText('c').existsOnce(); + spotText('a') + .overrideVisibilityMode(VisibilityMode.offstage) + .overrideVisibilityMode(VisibilityMode.combined) + .existsOnce(); + spotText('c') + .overrideVisibilityMode(VisibilityMode.offstage) + .overrideVisibilityMode(VisibilityMode.combined) + .existsOnce(); + spotText('a') + .overrideVisibilityMode(VisibilityMode.onstage) + .overrideVisibilityMode(VisibilityMode.combined) + .existsOnce(); + spotText('c') + .overrideVisibilityMode(VisibilityMode.onstage) + .overrideVisibilityMode(VisibilityMode.combined) + .existsOnce(); + + spot().withText('a').existsOnce(); + spot().withText('c').doesNotExist(); + spot() + .withText('a') + .overrideVisibilityMode(VisibilityMode.combined) + .existsOnce(); + spot() + .withText('c') + .overrideVisibilityMode(VisibilityMode.combined) + .existsOnce(); }); testWidgets('filter offstage in subtree of parent', (tester) async { diff --git a/test/spot/widget_selector_test.dart b/test/spot/widget_selector_test.dart index 0465f6ca..79e3bc38 100644 --- a/test/spot/widget_selector_test.dart +++ b/test/spot/widget_selector_test.dart @@ -61,7 +61,7 @@ void main() { ); expect( lessSpecificSelectors[1].toStringBreadcrumb(), - WidgetSelector.all.withParent(spot
()).toStringBreadcrumb(), + spot().withParent(spot
()).toStringBreadcrumb(), ); }); From bf4c0696b0609c419fbcdd2545534d20c9c149b9 Mon Sep 17 00:00:00 2001 From: Maikel Rehl Date: Tue, 19 Mar 2024 15:45:24 +0100 Subject: [PATCH 19/22] Rename VisibilityMode to WidgetPresence --- lib/spot.dart | 2 +- lib/src/spot/filters/parent_filter.dart | 8 ++-- lib/src/spot/selectors.dart | 10 ++-- lib/src/spot/snapshot.dart | 2 +- lib/src/spot/widget_selector.dart | 39 ++++++++------- test/selectors/filter_test.dart | 64 ++++++++++++------------- 6 files changed, 65 insertions(+), 60 deletions(-) diff --git a/lib/spot.dart b/lib/spot.dart index 6aa53d15..46c08eb6 100644 --- a/lib/spot.dart +++ b/lib/spot.dart @@ -224,7 +224,7 @@ WidgetSelector spotOffstage({ description: 'any Offstage Widget', ), ], - visibilityMode: VisibilityMode.offstage, + widgetPresence: WidgetPresence.offstage, ); } diff --git a/lib/src/spot/filters/parent_filter.dart b/lib/src/spot/filters/parent_filter.dart index dfcafa30..da8a4ac2 100644 --- a/lib/src/spot/filters/parent_filter.dart +++ b/lib/src/spot/filters/parent_filter.dart @@ -58,15 +58,15 @@ class ParentFilter implements ElementFilter { ) .toList(); - final visibilityMode = parentSnapshot.selector.visibilityMode; + final visibilityMode = parentSnapshot.selector.widgetPresence; for (final WidgetTreeNode node in rootNodes) { groups[node] ??= []; final WidgetSelector root = switch (visibilityMode) { - VisibilityMode.onstage => spot(), - VisibilityMode.offstage => spotOffstage(), - VisibilityMode.combined => spotAllWidgets(), + WidgetPresence.onstage => spot(), + WidgetPresence.offstage => spotOffstage(), + WidgetPresence.combined => spotAllWidgets(), }; final subtree = tree.scope(node); final snapshot = WidgetSnapshot( diff --git a/lib/src/spot/selectors.dart b/lib/src/spot/selectors.dart index 98eb9a98..6faf5e84 100644 --- a/lib/src/spot/selectors.dart +++ b/lib/src/spot/selectors.dart @@ -91,14 +91,14 @@ mixin ChainableSelectors { /// ### Example usage: /// ```dart /// final text = spotText('text') - /// .overrideVisibilityMode(VisibilityMode.combined); + /// .overrideWidgetPresence(WidgetPresence.combined); /// ``` @useResult - WidgetSelector overrideVisibilityMode(VisibilityMode mode) { - if (mode == self!.visibilityMode) { + WidgetSelector overrideWidgetPresence(WidgetPresence presence) { + if (presence == self!.widgetPresence) { return self!; } - return self!.copyWith(visibilityMode: mode); + return self!.copyWith(widgetPresence: presence); } /// Creates a [WidgetSelector] that includes offstage widgets in the selection. @@ -120,7 +120,7 @@ mixin ChainableSelectors { List children = const [], }) { return WidgetSelector( - visibilityMode: VisibilityMode.offstage, + widgetPresence: WidgetPresence.offstage, stages: _childAndParentFilters(children, parents), ); } diff --git a/lib/src/spot/snapshot.dart b/lib/src/spot/snapshot.dart index 120db1f1..be75e012 100644 --- a/lib/src/spot/snapshot.dart +++ b/lib/src/spot/snapshot.dart @@ -178,7 +178,7 @@ WidgetSnapshot snapshot( _depth++; _snapshotDebugPrint( 'snapshot() ${selector.toStringBreadcrumb()}, ' - 'visibility-mode ${selector.visibilityMode}', + 'visibility-mode ${selector.widgetPresence}', ); final stages = [ if (!isAnyCombined && isAnyOffstage) diff --git a/lib/src/spot/widget_selector.dart b/lib/src/spot/widget_selector.dart index 107bf9d3..62c31008 100644 --- a/lib/src/spot/widget_selector.dart +++ b/lib/src/spot/widget_selector.dart @@ -16,7 +16,7 @@ export 'package:spot/src/spot/tree_snapshot.dart'; class WidgetSelector with ChainableSelectors { /// Matches any widget currently mounted static final WidgetSelector all = WidgetSelector( - visibilityMode: VisibilityMode.combined, + widgetPresence: WidgetPresence.combined, stages: [ PredicateFilter( predicate: (e) => true, @@ -51,10 +51,10 @@ class WidgetSelector with ChainableSelectors { @Deprecated('Use quantityConstraint instead') ExpectedQuantity expectedQuantity = ExpectedQuantity.multi, QuantityConstraint? quantityConstraint, - VisibilityMode? visibilityMode, + WidgetPresence? widgetPresence, W Function(Element element)? mapElementToWidget, }) : stages = List.unmodifiable(stages), - visibilityMode = visibilityMode ?? VisibilityMode.onstage, + widgetPresence = widgetPresence ?? WidgetPresence.onstage, quantityConstraint = quantityConstraint ?? // ignore: deprecated_member_use_from_same_package (expectedQuantity == ExpectedQuantity.single @@ -97,7 +97,7 @@ class WidgetSelector with ChainableSelectors { Type get type => W; /// Whether to include offstage widgets in the selection - final VisibilityMode visibilityMode; + final WidgetPresence widgetPresence; /// All parent selectors of all stages this widget selector depends on List get parents { @@ -114,7 +114,7 @@ class WidgetSelector with ChainableSelectors { /// Recursively checks if this or any parent selector includes offstage widgets bool isAnyOffstage() { - if (visibilityMode == VisibilityMode.offstage) { + if (widgetPresence == WidgetPresence.offstage) { return true; } for (final parent in parents) { @@ -127,7 +127,7 @@ class WidgetSelector with ChainableSelectors { /// Recursively checks if this or any parent selector includes onstage and offstage widgets bool isAnyCombined() { - if (visibilityMode == VisibilityMode.combined) { + if (widgetPresence == WidgetPresence.combined) { return true; } for (final parent in parents) { @@ -146,10 +146,10 @@ class WidgetSelector with ChainableSelectors { final stage = stages[i]; if (stage is ParentFilter) { String desc; - if (stages.length == 1 && (visibilityMode == VisibilityMode.offstage)) { + if (stages.length == 1 && (widgetPresence == WidgetPresence.offstage)) { desc = 'find only offstage Widgets'; } else if (stages.length == 1 && - (visibilityMode == VisibilityMode.combined)) { + (widgetPresence == WidgetPresence.combined)) { desc = 'find onstage and offstage Widgets'; } else { desc = stage.parents.first.toString(); @@ -191,8 +191,8 @@ class WidgetSelector with ChainableSelectors { final parts = [ sb.toString().trim(), _quantityToString(), - if (visibilityMode == VisibilityMode.offstage) '(only offstage)', - if (visibilityMode == VisibilityMode.combined) '(onstage and offstage)', + if (widgetPresence == WidgetPresence.offstage) '(only offstage)', + if (widgetPresence == WidgetPresence.combined) '(onstage and offstage)', ].where((e) => e != null); final out = parts.join(' '); return out; @@ -209,10 +209,10 @@ class WidgetSelector with ChainableSelectors { final stage = stages[i]; if (stage is ParentFilter) { String child; - if (stages.length == 1 && (visibilityMode == VisibilityMode.offstage)) { + if (stages.length == 1 && (widgetPresence == WidgetPresence.offstage)) { child = 'find only offstage Widgets'; } else if (stages.length == 1 && - (visibilityMode == VisibilityMode.combined)) { + (widgetPresence == WidgetPresence.combined)) { child = 'find onstage and offstage Widgets'; } else { child = sb.toString(); @@ -281,8 +281,8 @@ class WidgetSelector with ChainableSelectors { final parts = [ sb.toString().trim(), _quantityToString(), - if (visibilityMode == VisibilityMode.offstage) '(only offstage)', - if (visibilityMode == VisibilityMode.combined) '(onstage and offstage)', + if (widgetPresence == WidgetPresence.offstage) '(only offstage)', + if (widgetPresence == WidgetPresence.combined) '(onstage and offstage)', ].where((e) => e != null); final out = parts.join(' '); return out; @@ -326,13 +326,13 @@ class WidgetSelector with ChainableSelectors { // ignore: deprecated_member_use_from_same_package ExpectedQuantity? expectedQuantity, QuantityConstraint? quantityConstraint, - VisibilityMode? visibilityMode, + WidgetPresence? widgetPresence, W Function(Element element)? mapElementToWidget, }) { return WidgetSelector( stages: stages ?? this.stages, quantityConstraint: quantityConstraint ?? this.quantityConstraint, - visibilityMode: visibilityMode ?? this.visibilityMode, + widgetPresence: widgetPresence ?? this.widgetPresence, mapElementToWidget: mapElementToWidget ?? this.mapElementToWidget, ); } @@ -452,8 +452,13 @@ enum ExpectedQuantity { } /// A filter that matches widgets based on a predicate function. -enum VisibilityMode { +enum WidgetPresence { + /// Only widgets that are currently onstage onstage, + + /// Only widgets that are currently offstage offstage, + + /// All widgets, both onstage and offstage combined, } diff --git a/test/selectors/filter_test.dart b/test/selectors/filter_test.dart index 4fb6945f..46996b53 100644 --- a/test/selectors/filter_test.dart +++ b/test/selectors/filter_test.dart @@ -171,7 +171,7 @@ void main() { }); testWidgets( - 'select offstage widgets when use .overrideVisibilityMode(VisibilityMode.offstage)', + 'select offstage widgets when use .overrideWidgetPresence(VisibilityMode.offstage)', (tester) async { await tester.pumpWidget( MaterialApp( @@ -189,58 +189,58 @@ void main() { spotOffstage().spotText('a').doesNotExist(); spotOffstage().spotText('c').existsOnce(); spotOffstage() - .overrideVisibilityMode(VisibilityMode.onstage) + .overrideWidgetPresence(WidgetPresence.onstage) .spotText('a') .existsOnce(); spotOffstage() - .overrideVisibilityMode(VisibilityMode.onstage) + .overrideWidgetPresence(WidgetPresence.onstage) .spotText('c') .doesNotExist(); spotText('a').existsOnce(); spotText('c').doesNotExist(); spotText('a') - .overrideVisibilityMode(VisibilityMode.offstage) + .overrideWidgetPresence(WidgetPresence.offstage) .doesNotExist(); spotText('c') - .overrideVisibilityMode(VisibilityMode.offstage) + .overrideWidgetPresence(WidgetPresence.offstage) .existsOnce(); spotOffstage().spotText('a').doesNotExist(); spotOffstage().spotText('c').existsOnce(); spotText('a') - .overrideVisibilityMode(VisibilityMode.offstage) - .overrideVisibilityMode(VisibilityMode.onstage) + .overrideWidgetPresence(WidgetPresence.offstage) + .overrideWidgetPresence(WidgetPresence.onstage) .existsOnce(); spotText('c') - .overrideVisibilityMode(VisibilityMode.offstage) - .overrideVisibilityMode(VisibilityMode.onstage) + .overrideWidgetPresence(WidgetPresence.offstage) + .overrideWidgetPresence(WidgetPresence.onstage) .doesNotExist(); spotText('a') - .overrideVisibilityMode(VisibilityMode.offstage) - .overrideVisibilityMode(VisibilityMode.onstage) - .overrideVisibilityMode(VisibilityMode.offstage) + .overrideWidgetPresence(WidgetPresence.offstage) + .overrideWidgetPresence(WidgetPresence.onstage) + .overrideWidgetPresence(WidgetPresence.offstage) .doesNotExist(); spotText('c') - .overrideVisibilityMode(VisibilityMode.offstage) - .overrideVisibilityMode(VisibilityMode.onstage) - .overrideVisibilityMode(VisibilityMode.offstage) + .overrideWidgetPresence(WidgetPresence.offstage) + .overrideWidgetPresence(WidgetPresence.onstage) + .overrideWidgetPresence(WidgetPresence.offstage) .existsOnce(); spot().withText('a').existsOnce(); spot().withText('c').doesNotExist(); spot() .withText('a') - .overrideVisibilityMode(VisibilityMode.offstage) + .overrideWidgetPresence(WidgetPresence.offstage) .doesNotExist(); spot() .withText('c') - .overrideVisibilityMode(VisibilityMode.offstage) + .overrideWidgetPresence(WidgetPresence.offstage) .existsOnce(); }); testWidgets( - 'select offstage widgets when use .overrideVisibilityMode(VisibilityMode.combined)', + 'select offstage widgets when use .overrideWidgetPresence(VisibilityMode.combined)', (tester) async { await tester.pumpWidget( MaterialApp( @@ -258,51 +258,51 @@ void main() { spotAllWidgets().spotText('a').existsOnce(); spotAllWidgets().spotText('c').existsOnce(); spotAllWidgets() - .overrideVisibilityMode(VisibilityMode.onstage) + .overrideWidgetPresence(WidgetPresence.onstage) .spotText('a') .existsOnce(); spotAllWidgets() - .overrideVisibilityMode(VisibilityMode.onstage) + .overrideWidgetPresence(WidgetPresence.onstage) .spotText('c') .doesNotExist(); spotText('a').existsOnce(); spotText('c').doesNotExist(); spotText('a') - .overrideVisibilityMode(VisibilityMode.combined) + .overrideWidgetPresence(WidgetPresence.combined) .existsOnce(); spotText('c') - .overrideVisibilityMode(VisibilityMode.combined) + .overrideWidgetPresence(WidgetPresence.combined) .existsOnce(); spotAllWidgets().spotText('a').existsOnce(); spotAllWidgets().spotText('c').existsOnce(); spotText('a') - .overrideVisibilityMode(VisibilityMode.offstage) - .overrideVisibilityMode(VisibilityMode.combined) + .overrideWidgetPresence(WidgetPresence.offstage) + .overrideWidgetPresence(WidgetPresence.combined) .existsOnce(); spotText('c') - .overrideVisibilityMode(VisibilityMode.offstage) - .overrideVisibilityMode(VisibilityMode.combined) + .overrideWidgetPresence(WidgetPresence.offstage) + .overrideWidgetPresence(WidgetPresence.combined) .existsOnce(); spotText('a') - .overrideVisibilityMode(VisibilityMode.onstage) - .overrideVisibilityMode(VisibilityMode.combined) + .overrideWidgetPresence(WidgetPresence.onstage) + .overrideWidgetPresence(WidgetPresence.combined) .existsOnce(); spotText('c') - .overrideVisibilityMode(VisibilityMode.onstage) - .overrideVisibilityMode(VisibilityMode.combined) + .overrideWidgetPresence(WidgetPresence.onstage) + .overrideWidgetPresence(WidgetPresence.combined) .existsOnce(); spot().withText('a').existsOnce(); spot().withText('c').doesNotExist(); spot() .withText('a') - .overrideVisibilityMode(VisibilityMode.combined) + .overrideWidgetPresence(WidgetPresence.combined) .existsOnce(); spot() .withText('c') - .overrideVisibilityMode(VisibilityMode.combined) + .overrideWidgetPresence(WidgetPresence.combined) .existsOnce(); }); From 4daaed45ce5c4a6a0355a0a9bd6b972df088b092 Mon Sep 17 00:00:00 2001 From: Maikel Rehl Date: Tue, 19 Mar 2024 15:45:38 +0100 Subject: [PATCH 20/22] Add WidgetPresence to README --- README.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/README.md b/README.md index dfcec7d0..f9b0c635 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,46 @@ final WidgetSelector usernameTextField = A `WidgetSelector` may return 0, 1 or N widgets. Depending on how many widgets you expect to find, you should use the corresponding matchers. +### WidgetPresence + +By default, spot only finds widgets that are onstage. +You can also change the widget presence to `offstage` or `combined` to find widgets that are only offstage or both. + +```dart +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:spot/spot.dart'; + +void main() { + testWidgets('Spot offstage and combined widgets', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Row( + children: [ + Text('a'), + Text('b'), + Offstage(child: Text('c')), + ], + ), + ), + ); + + spot().withText('a').existsOnce(); + spot().withText('c').doesNotExist(); + spot().withText('c').overrideWidgetPresence(WidgetPresence.offstage).existsOnce(); + + spotOffstage().spot().atMost(3); + spotOffstage().spotText('c').existsOnce(); + spotOffstage().overrideWidgetPresence(WidgetPresence.onstage).spotText('c').doesNotExist(); + + spotAllWidgets().spotText('a').existsOnce(); + spotAllWidgets().spotText('c').existsOnce(); + spotOffstage().overrideWidgetPresence(WidgetPresence.combined).spotText('a').existsOnce(); + spotOffstage().overrideWidgetPresence(WidgetPresence.combined).spotText('c').existsOnce(); + }); +} +``` + ### Matchers After creating a selector, you want to assert the widgets it found. From 816fec6b85de8e14aa6cabc7218fac2f287e56a0 Mon Sep 17 00:00:00 2001 From: Maikel Rehl Date: Tue, 19 Mar 2024 15:52:26 +0100 Subject: [PATCH 21/22] Remove import --- lib/src/spot/filters/parent_filter.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/src/spot/filters/parent_filter.dart b/lib/src/spot/filters/parent_filter.dart index da8a4ac2..d1fb7606 100644 --- a/lib/src/spot/filters/parent_filter.dart +++ b/lib/src/spot/filters/parent_filter.dart @@ -2,7 +2,6 @@ import 'package:dartx/dartx.dart'; import 'package:flutter/widgets.dart'; import 'package:spot/spot.dart'; import 'package:spot/src/spot/snapshot.dart'; -import 'package:spot/src/spot/tree_snapshot.dart'; import 'package:spot/src/spot/widget_selector.dart'; /// A filter that checks if the candidates are children of all [parents] From 744d229b605b549ab5b952ff292e24f436a8bed3 Mon Sep 17 00:00:00 2001 From: Pascal Welsch Date: Wed, 20 Mar 2024 01:24:47 +0100 Subject: [PATCH 22/22] Document WidgetPresence --- README.md | 84 ++++++++++++++++--------------- lib/spot.dart | 4 ++ lib/src/spot/widget_selector.dart | 5 +- 3 files changed, 51 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index f9b0c635..db56ebcd 100644 --- a/README.md +++ b/README.md @@ -123,46 +123,6 @@ final WidgetSelector usernameTextField = A `WidgetSelector` may return 0, 1 or N widgets. Depending on how many widgets you expect to find, you should use the corresponding matchers. -### WidgetPresence - -By default, spot only finds widgets that are onstage. -You can also change the widget presence to `offstage` or `combined` to find widgets that are only offstage or both. - -```dart -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:spot/spot.dart'; - -void main() { - testWidgets('Spot offstage and combined widgets', (tester) async { - await tester.pumpWidget( - MaterialApp( - home: Row( - children: [ - Text('a'), - Text('b'), - Offstage(child: Text('c')), - ], - ), - ), - ); - - spot().withText('a').existsOnce(); - spot().withText('c').doesNotExist(); - spot().withText('c').overrideWidgetPresence(WidgetPresence.offstage).existsOnce(); - - spotOffstage().spot().atMost(3); - spotOffstage().spotText('c').existsOnce(); - spotOffstage().overrideWidgetPresence(WidgetPresence.onstage).spotText('c').doesNotExist(); - - spotAllWidgets().spotText('a').existsOnce(); - spotAllWidgets().spotText('c').existsOnce(); - spotOffstage().overrideWidgetPresence(WidgetPresence.combined).spotText('a').existsOnce(); - spotOffstage().overrideWidgetPresence(WidgetPresence.combined).spotText('c').existsOnce(); - }); -} -``` - ### Matchers After creating a selector, you want to assert the widgets it found. @@ -235,6 +195,50 @@ spot() .hasTriggerMode(TooltipTriggerMode.longPress); // matcher ``` +### Find offstage widgets + +By default, `spot()` only finds widgets that are "onstage", not hidden with the [`Offstage`](https://api.flutter.dev/flutter/widgets/Offstage-class.html) widget. + +To find offstage widgets, start your widget selector with `spotOffstage()`. +Search for both - the on- and offstage widgets - with `spotAllWidgets()`. + +For existing selectors, use `overrideWidgetPresence(WidgetPresence presence)` to modify the presence to `offstage`, `onstage` or `combined`. + +```dart +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:spot/spot.dart'; + +void main() { + testWidgets('Spot offstage and combined widgets', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Row( + children: [ + Text('a'), + Text('b'), + Offstage(child: Text('c')), + ], + ), + ), + ); + + spot().withText('a').existsOnce(); + spot().withText('c').doesNotExist(); + spot().withText('c').overrideWidgetPresence(WidgetPresence.offstage).existsOnce(); + + spotOffstage().spot().atMost(3); + spotOffstage().spotText('c').existsOnce(); + spotOffstage().overrideWidgetPresence(WidgetPresence.onstage).spotText('c').doesNotExist(); + + spotAllWidgets().spotText('a').existsOnce(); + spotAllWidgets().spotText('c').existsOnce(); + spotOffstage().overrideWidgetPresence(WidgetPresence.combined).spotText('a').existsOnce(); + spotOffstage().overrideWidgetPresence(WidgetPresence.combined).spotText('c').existsOnce(); + }); +} +``` + ### Better errors In case the settings icon doesn't exist you usually would get the following error using `findsOneWidget` diff --git a/lib/spot.dart b/lib/spot.dart index 46c08eb6..ab46ea60 100644 --- a/lib/spot.dart +++ b/lib/spot.dart @@ -188,6 +188,10 @@ WidgetSelector spotSingle({ /// This selector compares the Widgets by runtimeType rather than by super /// type (see [WidgetTypeFilter]). This makes sure that e.g. `spot()` /// does not accidentally match a [Center] Widget, that extends [Align]. +/// +/// [spot] ignores Offstage widgets. +/// To find offstage widgets, use `spotOffstage().spot()`. +/// See [spotOffstage] and [spotAllWidgets] @useResult WidgetSelector spot({ List parents = const [], diff --git a/lib/src/spot/widget_selector.dart b/lib/src/spot/widget_selector.dart index 62c31008..9d7d6ab4 100644 --- a/lib/src/spot/widget_selector.dart +++ b/lib/src/spot/widget_selector.dart @@ -451,12 +451,13 @@ enum ExpectedQuantity { multi, } -/// A filter that matches widgets based on a predicate function. +/// Specifies whether a [WidgetSelector] should search for onstage, offstage, +/// or both types of widgets enum WidgetPresence { /// Only widgets that are currently onstage onstage, - /// Only widgets that are currently offstage + /// Only widgets that are currently offstage, meaning wrapped with [Offstage] offstage, /// All widgets, both onstage and offstage