diff --git a/lib/spot.dart b/lib/spot.dart index cce7711a..0b12710c 100644 --- a/lib/spot.dart +++ b/lib/spot.dart @@ -47,7 +47,9 @@ export 'package:spot/src/spot/props.dart' MatchPropNullable, NamedElementProp, NamedRenderObjectProp, + NamedStateProp, NamedWidgetProp, + PropGetter, PropSelectorQueries, WidgetSelectorProp, elementProp, diff --git a/lib/src/spot/props.dart b/lib/src/spot/props.dart index 689bdf9c..2a25a140 100644 --- a/lib/src/spot/props.dart +++ b/lib/src/spot/props.dart @@ -73,6 +73,53 @@ extension WidgetSelectorProp on WidgetSelector { } } +/// Read properties directly from a selector +extension PropGetter on WidgetSelector { + /// 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(NamedWidgetProp 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(NamedStateProp 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(NamedElementProp 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( + NamedRenderObjectProp 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 on ChainableSelectors { /// Creates a filter for the widgets of the discovered elements which is @@ -225,6 +272,32 @@ class NamedWidgetProp { }); } +/// A property of a [State] with a [name] that can be extracted with [get]. +/// +/// Use with [WidgetMatcherExtensions.hasElementProp] or [SelectorQueries.whereElementProp]. +NamedStateProp stateProp( + 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 { + /// 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]. diff --git a/lib/src/spot/widget_matcher.dart b/lib/src/spot/widget_matcher.dart index d5d47ca8..3fcc47dc 100644 --- a/lib/src/spot/widget_matcher.dart +++ b/lib/src/spot/widget_matcher.dart @@ -221,6 +221,54 @@ extension WidgetMatcherExtensions on WidgetMatcher { 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(NamedStateProp 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().existsOnce().hasStateProp( + /// prop: stateProp('value', (state) => state.value), + /// match: (it) => it.isTrue(), + /// ); + /// ``` + WidgetMatcher hasStateProp({ + required NamedStateProp prop, + required MatchProp match, + }) { + void condition(Subject subject) { + final Subject value = subject.context.nest( + () => ['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 diff --git a/test/spot/read_prop_test.dart b/test/spot/read_prop_test.dart new file mode 100644 index 00000000..65be8011 --- /dev/null +++ b/test/spot/read_prop_test.dart @@ -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('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', (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( + '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', (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); + } +}