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 get*Prop methods to WidgetSelector #71

Merged
merged 2 commits into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions lib/spot.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ export 'package:spot/src/spot/props.dart'
MatchPropNullable,
NamedElementProp,
NamedRenderObjectProp,
NamedStateProp,
NamedWidgetProp,
PropGetter,
PropSelectorQueries,
WidgetSelectorProp,
elementProp,
Expand Down
73 changes: 73 additions & 0 deletions lib/src/spot/props.dart
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,53 @@ extension WidgetSelectorProp<W extends Widget> on WidgetSelector<W> {
}
}

/// Read properties directly from a selector
extension PropGetter<W extends Widget> on WidgetSelector<W> {
/// Retrieves a specific property from the matched widget.
///
/// Use this method to get a property value directly from the widget for
/// further processing or assertions.
T getWidgetProp<T>(NamedWidgetProp<W, T> prop) {
final matcher = existsOnce();
return prop.get(matcher.widget);
}

/// Retrieves a specific property from the matched widget's element.
///
/// Use this method to get a property value directly from the element of
/// the widget for further processing or assertions.
T getStateProp<T, S extends State>(NamedStateProp<T, S> prop) {
final matcher = existsOnce();
final element = matcher.element;
if (element is! StatefulElement) {
throw StateError('Element is not a StatefulElement');
}
final state = element.state as S;
return prop.get(state);
}

/// Retrieves a specific property from the matched widget's element.
///
/// Use this method to get a property value directly from the element of
/// the widget for further processing or assertions.
T getElementProp<T>(NamedElementProp<T> prop) {
final matcher = existsOnce();
return prop.get(matcher.element);
}

/// Retrieves a specific property from the matched widget's render object.
///
/// Use this method to get a property value directly from the render object
/// of the widget for further processing or assertions.
T getRenderObjectProp<T, R extends RenderObject>(
NamedRenderObjectProp<R, T> prop,
) {
final matcher = existsOnce();
final renderObject = matcher.element.renderObject! as R;
return prop.get(renderObject);
}
}

/// The newer version of [WidgetSelector.withProp] that uses [NamedWidgetProp].
extension PropSelectorQueries<W extends Widget> on ChainableSelectors<W> {
/// Creates a filter for the widgets of the discovered elements which is
Expand Down Expand Up @@ -225,6 +272,32 @@ class NamedWidgetProp<W extends Widget, T> {
});
}

/// A property of a [State] with a [name] that can be extracted with [get].
///
/// Use with [WidgetMatcherExtensions.hasElementProp] or [SelectorQueries.whereElementProp].
NamedStateProp<T, S> stateProp<T, S extends State>(
String name,
T Function(S element) get,
) {
return NamedStateProp._(name: name, get: get);
}

/// A property of a [State] that can be extracted from the element.
///
/// Use [stateProp] to create a [NamedStateProp].
class NamedStateProp<T, S extends State> {
/// The name of the element property.
final String name;

/// The function that extracts the property from a [State].
final T Function(S state) get;

NamedStateProp._({
required this.name,
required this.get,
});
}

/// A property of an Element with a [name] that can be extracted with [get].
///
/// Use with [WidgetMatcherExtensions.hasElementProp] or [SelectorQueries.whereElementProp].
Expand Down
48 changes: 48 additions & 0 deletions lib/src/spot/widget_matcher.dart
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,54 @@ extension WidgetMatcherExtensions<W extends Widget> on WidgetMatcher<W> {
return this;
}

/// Retrieves a specific property from the matched widget's state.
///
/// Use this method to get a property value directly from the state of
/// the widget for further processing or assertions.
T getStateProp<T, S extends State>(NamedStateProp<T, S> prop) {
if (element is! StatefulElement) {
throw Exception('$element is not a StatefulElement');
}
final e = element as StatefulElement;
final state = e.state as S;
return prop.get(state);
}

/// Asserts that a state property meets a specific condition.
///
/// This method is useful for making assertions on properties within the
/// [State] of the widget.
///
/// #### Example usage:
/// ```dart
/// spot<Checkbox>().existsOnce().hasStateProp(
/// prop: stateProp('value', (state) => state.value),
/// match: (it) => it.isTrue(),
/// );
/// ```
WidgetMatcher<W> hasStateProp<T, S extends State>({
required NamedStateProp<T, S> prop,
required MatchProp<T> match,
}) {
void condition(Subject<Element> subject) {
final Subject<T> value = subject.context.nest<T?>(
() => ['State of $W', "with prop '${prop.name}'"],
(element) {
final value = getStateProp(prop);
return Extracted.value(value);
},
).hideNullability();

match(value);
}

final failure = softCheckHideNull(element, condition);
_addAssertionToTimeline(failure, condition, element);
failure.throwPropertyCheckFailure(condition, element);

return this;
}

/// Retrieves a specific property from the matched widget's render object.
///
/// Use this method to get a property value directly from the render object
Expand Down
127 changes: 127 additions & 0 deletions test/spot/read_prop_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:spot/spot.dart';
import 'package:spot/src/spot/props.dart';

void main() {
group('WidgetSelector', () {
testWidgets('getWidgetProp', (tester) async {
await tester.pumpWidget(const _MyContainer(color: Colors.white54));
final color = spot<_MyContainer>()
.getWidgetProp(widgetProp('color', (widget) => widget.color));
expect(color, Colors.white54);
});

testWidgets('getWidgetProp AnyText', (tester) async {
await tester
.pumpWidget(const MaterialApp(home: Scaffold(body: Text('hello'))));
final text = spotText('hello')
.getWidgetProp(widgetProp('data', (widget) => widget.text));
expect(text, 'hello');
});

testWidgets('getElementProp', (tester) async {
await tester.pumpWidget(const _MyContainer(color: Colors.white54));
final innerValue = spot<_MyContainer>().getElementProp(
elementProp('innerValue', (el) {
return ((el as StatefulElement).state as _MyContainerState)
.innerValue;
}),
);
expect(innerValue, 'stateValue');
});

testWidgets('getStateProp', (tester) async {
await tester.pumpWidget(const _MyContainer(color: Colors.white54));
final innerValue = spot<_MyContainer>().getStateProp(
stateProp<String, _MyContainerState>('innerValue', (s) => s.innerValue),
);
expect(innerValue, 'stateValue');

// alternate syntax
spot<_MyContainer>().getStateProp(
stateProp('innerValue', (_MyContainerState s) => s.innerValue),
);
});

testWidgets('getRenderObjectProp', (tester) async {
await tester.pumpWidget(const _MyContainer(color: Colors.white54));
final size = spot<_MyContainer>().getRenderObjectProp(
renderObjectProp<Size, RenderBox>('size', (r) => r.size),
);
expect(size, const Size(800.0, 600.0));
});
});

group('WidgetMatcher', () {
testWidgets('getWidgetProp', (tester) async {
await tester.pumpWidget(const _MyContainer(color: Colors.white54));
final color = spot<_MyContainer>()
.existsOnce()
.getWidgetProp(widgetProp('color', (widget) => widget.color));
expect(color, Colors.white54);
});

testWidgets('getWidgetProp AnyText', (tester) async {
await tester
.pumpWidget(const MaterialApp(home: Scaffold(body: Text('hello'))));
final text = spotText('hello')
.existsOnce()
.getWidgetProp(widgetProp('data', (widget) => widget.text));
expect(text, 'hello');
});

testWidgets('getElementProp', (tester) async {
await tester.pumpWidget(const _MyContainer(color: Colors.white54));
final innerValue = spot<_MyContainer>().existsOnce().getElementProp(
elementProp('innerValue', (el) {
return ((el as StatefulElement).state as _MyContainerState)
.innerValue;
}),
);
expect(innerValue, 'stateValue');
});

testWidgets('getStateProp', (tester) async {
await tester.pumpWidget(const _MyContainer(color: Colors.white54));
final innerValue = spot<_MyContainer>().existsOnce().getStateProp(
stateProp<String, _MyContainerState>(
'innerValue',
(s) => s.innerValue,
),
);

expect(innerValue, 'stateValue');

// alternate syntax
spot<_MyContainer>().existsOnce().getStateProp(
stateProp('innerValue', (_MyContainerState s) => s.innerValue),
);
});

testWidgets('getRenderObjectProp', (tester) async {
await tester.pumpWidget(const _MyContainer(color: Colors.white54));
final size = spot<_MyContainer>().existsOnce().getRenderObjectProp(
renderObjectProp<Size, RenderBox>('size', (r) => r.size),
);
expect(size, const Size(800.0, 600.0));
});
});
}

class _MyContainer extends StatefulWidget {
const _MyContainer({required this.color});

final Color color;

@override
State<_MyContainer> createState() => _MyContainerState();
}

class _MyContainerState extends State<_MyContainer> {
final innerValue = 'stateValue';
@override
Widget build(BuildContext context) {
return Placeholder(color: widget.color);
}
}
Loading