Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add onstage offstage support #45

Merged
merged 27 commits into from
Mar 20, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1fdae44
Add offstage flag to WidgetTreeNode based on debugVisitOnstageChildren
rehlma Jan 31, 2024
e9ad11f
Add skipOffstage flag and set it true by default
rehlma Jan 31, 2024
b80be9e
Revert "Add skipOffstage flag and set it true by default"
rehlma Feb 5, 2024
885f49b
Merge branch 'main' into add-onstage-offstage-support
rehlma Feb 5, 2024
a058310
Merge branch 'main' into add-onstage-offstage-support
rehlma Feb 6, 2024
c41af26
Add new offstage onstage functionality
rehlma Feb 6, 2024
25732e6
Remove TODO
rehlma Feb 6, 2024
107dd23
Fix lint issues
rehlma Feb 6, 2024
a2ee797
Improve docs
passsy Feb 6, 2024
be0be85
Optimize createWidgetTreeSnapshot()
passsy Feb 6, 2024
224db9e
Add parents to WidgetSelector again
passsy Feb 6, 2024
254add0
Merge branch 'main' into add-onstage-offstage-support
rehlma Feb 20, 2024
25e9242
Allow filtering offstage only in a certain subtree
passsy Mar 3, 2024
f01e274
Handle offstage for child selectors
passsy Mar 3, 2024
0bc1a29
Merge branch 'offstage-2' into add-onstage-offstage-support
passsy Mar 3, 2024
b6769ca
Cleanup
passsy Mar 3, 2024
ed18386
Replace onstage() with overrideIncludeOffstage(false)
passsy Mar 3, 2024
aa812fb
Cleanup
passsy Mar 3, 2024
60867fe
Improve PartenFilter performance by detecting subtrees
passsy Mar 3, 2024
dd6ec8a
Group offstage tests
passsy Mar 18, 2024
f4b5991
Merge branch 'main' into add-onstage-offstage-support
passsy Mar 18, 2024
a0de725
Add dialog offstage example
passsy Mar 18, 2024
6f7e048
Replace isOffstage bool with enum
rehlma Mar 19, 2024
bf4c069
Rename VisibilityMode to WidgetPresence
rehlma Mar 19, 2024
4daaed4
Add WidgetPresence to README
rehlma Mar 19, 2024
816fec6
Remove import
rehlma Mar 19, 2024
744d229
Document WidgetPresence
passsy Mar 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions lib/spot.dart
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,32 @@ WidgetSelector<W> spot<W extends Widget>({
);
}

/// 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<Widget> spotOffstage({
List<WidgetSelector> parents = const [],
List<WidgetSelector> children = const [],
}) {
return _global
.spot<Widget>(
parents: parents,
children: children,
)
.offstage();
}

/// Creates a [WidgetSelector] that finds [widget] by identity and returns all
/// occurrences of it in the widget tree
///
Expand Down Expand Up @@ -379,7 +405,6 @@ WidgetSelector<W> spotTexts<W extends Widget>(
@Deprecated('Use spotIcon<W>().atMost(1)')
WidgetSelector<Icon> spotSingleIcon(
IconData icon, {
bool skipOffstage = true,
List<WidgetSelector> parents = const [],
List<WidgetSelector> children = const [],
}) {
Expand Down Expand Up @@ -411,7 +436,6 @@ WidgetSelector<Icon> spotIcon(
@Deprecated('Use spotIcon()')
WidgetSelector<Icon> spotIcons(
IconData icon, {
bool skipOffstage = true,
List<WidgetSelector> parents = const [],
List<WidgetSelector> children = const [],
}) {
Expand Down
13 changes: 11 additions & 2 deletions lib/src/spot/element_extensions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,20 @@ 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<Element> get children sync* {
final List<Element> found = [];
visitChildren(found.add);
yield* found;
}

/// Returns only onstage children of [Element], only direct children
///
/// Children of [Offstage] or [Overlay] are eventually not returned,
/// thus marking them as offstage
Iterable<Element> get onstageChildren sync* {
final List<Element> found = [];
debugVisitOnstageChildren(found.add);
yield* found;
}
}
26 changes: 26 additions & 0 deletions lib/src/spot/filters/onstage_filter.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import 'package:spot/src/spot/widget_selector.dart';

/// Removes all [WidgetTreeNode] that are offstage
class OnstageFilter implements ElementFilter {
@override
Iterable<WidgetTreeNode> filter(Iterable<WidgetTreeNode> candidates) {
final List<WidgetTreeNode> 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';
}
}
52 changes: 45 additions & 7 deletions lib/src/spot/selectors.dart
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,49 @@ mixin ChainableSelectors<T extends Widget> {
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<T> 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<T> 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<Align>()`
Expand Down Expand Up @@ -104,6 +141,7 @@ mixin ChainableSelectors<T extends Widget> {
if (children.isNotEmpty) ChildFilter(children),
if (p.isNotEmpty) ParentFilter(p),
],
includeOffstage: self?.includeOffstage,
);
return selector;
}
Expand Down Expand Up @@ -157,6 +195,7 @@ mixin ChainableSelectors<T extends Widget> {
if (children.isNotEmpty) ChildFilter(children),
if (p.isNotEmpty) ParentFilter(p),
],
includeOffstage: self?.includeOffstage,
);
return selector;
}
Expand Down Expand Up @@ -242,6 +281,7 @@ mixin ChainableSelectors<T extends Widget> {
if (children.isNotEmpty) ChildFilter(children),
if (p.isNotEmpty) ParentFilter(p),
],
includeOffstage: self?.includeOffstage,
);
return selector;
}
Expand Down Expand Up @@ -309,6 +349,7 @@ mixin ChainableSelectors<T extends Widget> {
if (children.isNotEmpty) ChildFilter(children),
if (p.isNotEmpty) ParentFilter(p),
],
includeOffstage: self?.includeOffstage,
);
return selector;
}
Expand All @@ -317,7 +358,6 @@ mixin ChainableSelectors<T extends Widget> {
@useResult
WidgetSelector<Icon> spotIcon(
IconData icon, {
bool skipOffstage = true,
List<WidgetSelector> parents = const [],
List<WidgetSelector> children = const [],
}) {
Expand All @@ -337,6 +377,7 @@ mixin ChainableSelectors<T extends Widget> {
if (children.isNotEmpty) ChildFilter(children),
if (p.isNotEmpty) ParentFilter(p),
],
includeOffstage: self?.includeOffstage,
);
return selector;
}
Expand All @@ -346,13 +387,11 @@ mixin ChainableSelectors<T extends Widget> {
@Deprecated('Use spotIcon().atMost(1)')
WidgetSelector<Icon> spotSingleIcon(
IconData icon, {
bool skipOffstage = true,
List<WidgetSelector> parents = const [],
List<WidgetSelector> children = const [],
}) {
return spotIcon(
icon,
skipOffstage: skipOffstage,
parents: parents,
children: children,
).atMost(1);
Expand All @@ -363,13 +402,11 @@ mixin ChainableSelectors<T extends Widget> {
@Deprecated('Use spotIcon()')
WidgetSelector<Icon> spotIcons(
IconData icon, {
bool skipOffstage = true,
List<WidgetSelector> parents = const [],
List<WidgetSelector> children = const [],
}) {
return spotIcon(
icon,
skipOffstage: skipOffstage,
parents: parents,
children: children,
);
Expand Down Expand Up @@ -398,6 +435,7 @@ mixin ChainableSelectors<T extends Widget> {
if (children.isNotEmpty) ChildFilter(children),
if (p.isNotEmpty) ParentFilter(p),
],
includeOffstage: self?.includeOffstage,
);
return selector;
}
Expand Down
8 changes: 8 additions & 0 deletions lib/src/spot/snapshot.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -150,6 +151,13 @@ WidgetSnapshot<W> snapshot<W extends Widget>(
),
];

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();
Expand Down
1 change: 1 addition & 0 deletions lib/src/spot/text/any_text.dart
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ class AnyTextWidgetSelector extends WidgetSelector<AnyText> {
/// - `parents`: Parent selectors to include in the match.
AnyTextWidgetSelector({
required super.stages,
super.includeOffstage,
}) : super(mapElementToWidget: _mapElementToAnyText);

static AnyText _mapElementToAnyText(Element element) {
Expand Down
31 changes: 29 additions & 2 deletions lib/src/spot/tree_snapshot.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,37 @@ 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;
}
Expand Down Expand Up @@ -71,12 +94,16 @@ 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.
///
/// Do not forget to call [addChild] manually after creation!
WidgetTreeNode({
required this.element,
required this.parent,
this.isOffstage = false,
});

@override
Expand Down
7 changes: 7 additions & 0 deletions lib/src/spot/widget_selector.dart
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,10 @@ class WidgetSelector<W extends Widget> with ChainableSelectors<W> {
@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
Expand Down Expand Up @@ -92,6 +94,9 @@ class WidgetSelector<W extends Widget> with ChainableSelectors<W> {
/// 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();
Expand Down Expand Up @@ -252,11 +257,13 @@ class WidgetSelector<W extends Widget> with ChainableSelectors<W> {
// ignore: deprecated_member_use_from_same_package
ExpectedQuantity? expectedQuantity,
QuantityConstraint? quantityConstraint,
bool? includeOffstage,
W Function(Element element)? mapElementToWidget,
}) {
return WidgetSelector<W>(
stages: stages ?? this.stages,
quantityConstraint: quantityConstraint ?? this.quantityConstraint,
includeOffstage: includeOffstage ?? this.includeOffstage,
mapElementToWidget: mapElementToWidget ?? this.mapElementToWidget,
);
}
Expand Down
Loading
Loading