Skip to content

Commit

Permalink
Implement negate
Browse files Browse the repository at this point in the history
  • Loading branch information
passsy committed Jan 8, 2024
1 parent 08377eb commit d3892b2
Show file tree
Hide file tree
Showing 3 changed files with 178 additions and 35 deletions.
111 changes: 96 additions & 15 deletions lib/src/spot/selectors.dart
Original file line number Diff line number Diff line change
Expand Up @@ -705,11 +705,16 @@ extension WidgetMatcherExtensions<W extends Widget> on WidgetMatcher<W> {
.getProperties()
.firstOrNullWhere((e) => e.name == propName);

final unconstrainedSelector =
selector.overrideQuantityConstraint(QuantityConstraint.unconstrained);
final actual = prop?.value as T? ?? prop?.getDefaultValue<T>();

final ConditionSubject<T?> conditionSubject = it<T?>();
final Subject<T> subject = conditionSubject.context.nest<T>(
() => [selector.toStringBreadcrumb(), 'with property $propName'],
() => [
unconstrainedSelector.toStringBreadcrumb(),
'with property $propName',
],
(value) {
if (prop == null) {
return Extracted.rejection(which: ['Has no prop "$propName"']);
Expand Down Expand Up @@ -920,8 +925,20 @@ class PredicateWithDescription {
}
}

class WidgetTypePredicate<W extends Widget> extends PredicateWithDescription {
WidgetTypePredicate() : super((e) => e.widget is W, description: '$W');
class WidgetTypePredicate<W extends Widget>
implements PredicateWithDescription {
WidgetTypePredicate();

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

@override
bool Function(Element e) get predicate => (e) => e.widget is W;

@override
String toString() {
return 'WidgetTypePredicate<$W>()';
}
}

/// A [WidgetSelector] that intends to resolve to a single widget
Expand Down Expand Up @@ -1005,6 +1022,17 @@ class ChildFilter implements ElementFilter {
@override
Iterable<WidgetTreeNode> filter(Iterable<WidgetTreeNode> candidates) {
final tree = currentWidgetTreeSnapshot();

// First check all negate selectors (where maxQuantity == 0)
final negates = childSelectors.where((e) => e.quantityConstraint.max == 0);
for (final negate in negates) {
final s = snapshot(negate, validateQuantity: false);
if (s.discovered.isNotEmpty) {
// this negate selector matches, which it shouldn't
return [];
}
}

final List<WidgetTreeNode> matchingChildNodes = [];

// Then check for every queryMatch if the children and props match
Expand All @@ -1013,19 +1041,21 @@ class ChildFilter implements ElementFilter {

final ScopedWidgetTreeSnapshot subtree = tree.scope(candidate);
final List<WidgetTreeNode> subtreeNodes = subtree.allNodes;
for (final WidgetSelector<Widget> childSelector in childSelectors) {
for (final WidgetSelector<Widget> childSelector
in childSelectors - negates) {
matchesPerChild[childSelector] = [];
// TODO instead of searching the children, starting from the root widget, find a way to reverse the search and
// start form the subtree.
// Keep in mind, each child selector might be defined with parents which are outside of the subtree
final WidgetSnapshot ss =
snapshot(childSelector, validateQuantity: false);
final discoveredInSubtree =
ss.discovered.where((node) => subtreeNodes.contains(node)).toList();

final minConstraint = childSelector.quantityConstraint.min;
final maxConstraint = childSelector.quantityConstraint.max;

final discoveredInSubtree =
ss.discovered.where((node) => subtreeNodes.contains(node)).toList();

if (minConstraint == null &&
maxConstraint == null &&
discoveredInSubtree.isEmpty) {
Expand Down Expand Up @@ -1172,9 +1202,31 @@ class WidgetSelector<W extends Widget> with Selectors<W> {
final filters = elementFilters.isNotEmpty
? elementFilters.map((e) => e.description).join(' ')
: null;
final quantity = () {
if (quantityConstraint.min == null && quantityConstraint.max == 0) {
return '(amount: 0)';
}
if (quantityConstraint.min == 0 && quantityConstraint.max == 0) {
return '(amount: 0)';
}
if (quantityConstraint.min != null &&
quantityConstraint.min == quantityConstraint.max) {
return '(amount: ${quantityConstraint.min})';
}
if (quantityConstraint.min != null && quantityConstraint.max != null) {
return '(amount: ${quantityConstraint.min}...${quantityConstraint.max})';
}
if (quantityConstraint.min != null) {
return '(amount: >=${quantityConstraint.min})';
}
if (quantityConstraint.max != null) {
return '(amount: <=${quantityConstraint.max})';
}
return null;
}();

final constraints =
[props, children, parents, filters].where((e) => e != null);
[props, quantity, children, parents, filters].where((e) => e != null);
if (constraints.isEmpty) {
return '';
}
Expand All @@ -1191,8 +1243,31 @@ class WidgetSelector<W extends Widget> with Selectors<W> {
final filters = elementFilters.isNotEmpty
? elementFilters.map((e) => e.description).join(' ')
: null;
final quantity = () {
if (quantityConstraint.min == null && quantityConstraint.max == 0) {
return '(amount: 0)';
}
if (quantityConstraint.min == 0 && quantityConstraint.max == 0) {
return '(amount: 0)';
}
if (quantityConstraint.min != null &&
quantityConstraint.min == quantityConstraint.max) {
return '(amount: ${quantityConstraint.min})';
}
if (quantityConstraint.min != null && quantityConstraint.max != null) {
return '(amount: ${quantityConstraint.min}...${quantityConstraint.max})';
}
if (quantityConstraint.min != null) {
return '(amount: >=${quantityConstraint.min})';
}
if (quantityConstraint.max != null) {
return '(amount: <=${quantityConstraint.max})';
}
return null;
}();

final constraints = [props, children, filters].where((e) => e != null);
final constraints =
[props, quantity, children, filters].where((e) => e != null);
return constraints.join(' ');
}

Expand Down Expand Up @@ -1326,11 +1401,16 @@ class WidgetSelector<W extends Widget> with Selectors<W> {
.getProperties()
.firstOrNullWhere((e) => e.name == propName);

final unconstrainedSelector =
overrideQuantityConstraint(QuantityConstraint.unconstrained);
final actual = prop?.value as T? ?? prop?.getDefaultValue<T>();

final ConditionSubject<T?> conditionSubject = it<T?>();
final Subject<T> subject = conditionSubject.context.nest<T>(
() => [toStringBreadcrumb(), 'with prop "$propName"'],
() => [
unconstrainedSelector.toStringBreadcrumb(),
'with prop "$propName"',
],
(value) {
if (prop == null) {
return Extracted.rejection(which: ['Has no prop "$propName"']);
Expand Down Expand Up @@ -1369,10 +1449,11 @@ extension CreateWidgetMatcher<W extends Widget> on WidgetSnapshot<W> {
/// The number of widgets that are expected to be found
class QuantityConstraint {
static const QuantityConstraint unconstrained = QuantityConstraint();
static const QuantityConstraint none = QuantityConstraint.exactly(0);
static const QuantityConstraint zero = QuantityConstraint.atMost(0);
static const QuantityConstraint single = QuantityConstraint.atMost(1);

const QuantityConstraint({this.min, this.max});
const QuantityConstraint({this.min, this.max})
: assert(min == null || max == null || min <= max);

const QuantityConstraint.exactly(int n)
: min = n,
Expand All @@ -1394,8 +1475,8 @@ class QuantityConstraint {
if (min == null && max == null) {
return 'QuantityConstraint.unconstrained';
}
if (min == 0 && max == 0) {
return 'QuantityConstraint.none';
if (max == 0) {
return 'QuantityConstraint.zero';
}
return 'QuantityConstraint{min: $min, max: $max}';
}
Expand All @@ -1420,7 +1501,7 @@ extension SelectorToSnapshot<W extends Widget> on WidgetSelector<W> {

@useResult
WidgetSelector<W> get multi {
return overrideQuantityConstraint(QuantityConstraint.none);
return overrideQuantityConstraint(QuantityConstraint.unconstrained);
}

@Deprecated('Use .atMost(1) or .amount(1)')
Expand Down Expand Up @@ -1563,7 +1644,7 @@ extension QuantityMatchers<W extends Widget> on WidgetSelector<W> {

void doesNotExist() {
TestAsyncUtils.guardSync();
final none = copyWith(quantityConstraint: QuantityConstraint.none);
final none = copyWith(quantityConstraint: QuantityConstraint.zero);
snapshot(none);
}

Expand Down
46 changes: 26 additions & 20 deletions lib/src/spot/snapshot.dart
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,9 @@ extension _ValidateQuantity<W extends Widget> on WidgetSnapshot<W> {
final minimumConstraint = selector.quantityConstraint.min;
final maximumConstraint = selector.quantityConstraint.max;

final unconstrainedSelector =
selector.overrideQuantityConstraint(QuantityConstraint.unconstrained);

String significantWidgetTree() {
final set = discoveredElements.toSet();
return findCommonAncestor(set).toStringDeep();
Expand All @@ -156,21 +159,21 @@ extension _ValidateQuantity<W extends Widget> on WidgetSnapshot<W> {
if (count == 0) {
_tryMatchingLessSpecificCriteria(this);
throw TestFailure(
'Could not find ${selector.toStringBreadcrumb()} in widget tree, '
'Could not find ${unconstrainedSelector.toStringBreadcrumb()} in widget tree, '
'expected at least $minimumConstraint\n'
'${significantWidgetTree()}'
'Could not find ${selector.toStringBreadcrumb()} in widget tree, '
'Could not find ${unconstrainedSelector.toStringBreadcrumb()} in widget tree, '
'expected at least $minimumConstraint',
);
}

if (minimumConstraint > count) {
_tryMatchingLessSpecificCriteria(this);
throw TestFailure(
'Found $count elements matching ${selector.toStringBreadcrumb()} in widget tree, '
'Found $count elements matching ${unconstrainedSelector.toStringBreadcrumb()} in widget tree, '
'expected at least $minimumConstraint\n'
'${significantWidgetTree()}'
'Found $count elements matching ${selector.toStringBreadcrumb()} in widget tree, '
'Found $count elements matching ${unconstrainedSelector.toStringBreadcrumb()} in widget tree, '
'expected at least $minimumConstraint',
);
}
Expand All @@ -179,10 +182,10 @@ extension _ValidateQuantity<W extends Widget> on WidgetSnapshot<W> {
if (maximumConstraint != null && minimumConstraint == null) {
if (maximumConstraint < count) {
throw TestFailure(
'Found $count elements matching ${selector.toStringBreadcrumb()} in widget tree, '
'Found $count elements matching ${unconstrainedSelector.toStringBreadcrumb()} in widget tree, '
'expected at most $maximumConstraint\n'
'${significantWidgetTree()}'
'Found $count elements matching ${selector.toStringBreadcrumb()} in widget tree, '
'Found $count elements matching ${unconstrainedSelector.toStringBreadcrumb()} in widget tree, '
'expected at most $maximumConstraint',
);
}
Expand All @@ -198,30 +201,30 @@ extension _ValidateQuantity<W extends Widget> on WidgetSnapshot<W> {
if (count == 0) {
_tryMatchingLessSpecificCriteria(this);
throw TestFailure(
'Could not find ${selector.toStringBreadcrumb()} in widget tree, '
'Could not find ${unconstrainedSelector.toStringBreadcrumb()} in widget tree, '
'expected exactly $exactCount\n'
'${significantWidgetTree()}'
'Could not find ${selector.toStringBreadcrumb()} in widget tree, '
'Could not find ${unconstrainedSelector.toStringBreadcrumb()} in widget tree, '
'expected exactly $exactCount',
);
} else {
_tryMatchingLessSpecificCriteria(this);
throw TestFailure(
'Found $count elements matching ${selector.toStringBreadcrumb()} in widget tree, '
'Found $count elements matching ${unconstrainedSelector.toStringBreadcrumb()} in widget tree, '
'expected exactly $exactCount\n'
'${significantWidgetTree()}'
'Found $count elements matching ${selector.toStringBreadcrumb()} in widget tree, '
'Found $count elements matching ${unconstrainedSelector.toStringBreadcrumb()} in widget tree, '
'expected exactly $exactCount',
);
}
}
} else {
// out of range
throw TestFailure(
'Found $count elements matching ${selector.toStringBreadcrumb()} in widget tree, '
'Found $count elements matching ${unconstrainedSelector.toStringBreadcrumb()} in widget tree, '
'expected between $minimumConstraint and $maximumConstraint\n'
'${significantWidgetTree()}'
'Found $count elements matching ${selector.toStringBreadcrumb()} in widget tree, '
'Found $count elements matching ${unconstrainedSelector.toStringBreadcrumb()} in widget tree, '
'expected between $minimumConstraint and $maximumConstraint',
);
}
Expand Down Expand Up @@ -317,26 +320,29 @@ void _tryMatchingLessSpecificCriteria(WidgetSnapshot snapshot) {
final lessSpecificCount = lessSpecificSnapshot.discovered.length;
final minimumConstraint = selector.quantityConstraint.min;
final maximumConstraint = selector.quantityConstraint.max;
final unconstrainedSelector =
selector.overrideQuantityConstraint(QuantityConstraint.unconstrained);

// error that selector could not be found, but instead spot detected lessSpecificSnapshot, which might be useful
if (lessSpecificCount > count) {
if (minimumConstraint != null && maximumConstraint == null) {
if (count == 0) {
errorBuilder.writeln(
'Could not find ${selector.toStringBreadcrumb()} in widget tree, '
'Could not find ${unconstrainedSelector.toStringBreadcrumb()} in widget tree, '
'expected at least $minimumConstraint');
}

if (minimumConstraint > count) {
errorBuilder.writeln(
'Found $count elements matching ${selector.toStringBreadcrumb()} in widget tree, '
'Found $count elements matching ${unconstrainedSelector.toStringBreadcrumb()} in widget tree, '
'expected at least $minimumConstraint');
}
}

if (maximumConstraint != null && minimumConstraint == null) {
if (maximumConstraint < count) {
errorBuilder.writeln(
'Found $count elements matching ${selector.toStringBreadcrumb()} in widget tree, '
'Found $count elements matching ${unconstrainedSelector.toStringBreadcrumb()} in widget tree, '
'expected at most $maximumConstraint');
}
}
Expand All @@ -346,17 +352,17 @@ void _tryMatchingLessSpecificCriteria(WidgetSnapshot snapshot) {
final exactCount = minimumConstraint;
if (count == 0) {
errorBuilder.writeln(
'Could not find ${selector.toStringBreadcrumb()} in widget tree, '
'Could not find ${unconstrainedSelector.toStringBreadcrumb()} in widget tree, '
'expected exactly $exactCount');
} else {
errorBuilder.writeln(
'Found $count elements matching ${selector.toStringBreadcrumb()} in widget tree, '
'Found $count elements matching ${unconstrainedSelector.toStringBreadcrumb()} in widget tree, '
'expected exactly $exactCount');
}
} else {
// out of range
errorBuilder.writeln(
'Found $count elements matching ${selector.toStringBreadcrumb()} in widget tree, '
'Found $count elements matching ${unconstrainedSelector.toStringBreadcrumb()} in widget tree, '
'expected between $minimumConstraint and $maximumConstraint');
}
}
Expand All @@ -365,7 +371,7 @@ void _tryMatchingLessSpecificCriteria(WidgetSnapshot snapshot) {
"A less specific search ($lessSpecificSelector) discovered $lessSpecificCount matches!",
);
errorBuilder.writeln(
'Maybe you have to adjust your WidgetSelector ($selector) to cover those missing elements.\n',
'Maybe you have to adjust your WidgetSelector ($unconstrainedSelector) to cover those missing elements.\n',
);
int index = 0;
for (final Element match in lessSpecificSnapshot.discoveredElements) {
Expand Down Expand Up @@ -454,7 +460,7 @@ extension _LessSpecificSelectors<W extends Widget> on WidgetSelector<W> {
props: [],
parents: [],
children: [],
quantityConstraint: QuantityConstraint.none,
quantityConstraint: QuantityConstraint.unconstrained,
);
for (final criteria in criteria) {
s = criteria(s);
Expand Down
Loading

0 comments on commit d3892b2

Please sign in to comment.