├── .gitignore ├── .metadata ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── lib ├── flutter_test_ui.dart └── src │ └── test_ui.dart ├── pubspec.yaml └── test └── flutter_test_ui_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | pubspec.lock 2 | 3 | # Miscellaneous 4 | *.class 5 | *.log 6 | *.pyc 7 | *.swp 8 | .DS_Store 9 | .atom/ 10 | .buildlog/ 11 | .history 12 | .svn/ 13 | 14 | # IntelliJ related 15 | *.iml 16 | *.ipr 17 | *.iws 18 | .idea/ 19 | 20 | # The .vscode folder contains launch configuration and tasks you configure in 21 | # VS Code which you may wish to be included in version control, so this line 22 | # is commented out by default. 23 | #.vscode/ 24 | 25 | # Flutter/Dart/Pub related 26 | **/doc/api/ 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | build/ 34 | 35 | # Android related 36 | **/android/**/gradle-wrapper.jar 37 | **/android/.gradle 38 | **/android/captures/ 39 | **/android/gradlew 40 | **/android/gradlew.bat 41 | **/android/local.properties 42 | **/android/**/GeneratedPluginRegistrant.java 43 | 44 | # iOS/XCode related 45 | **/ios/**/*.mode1v3 46 | **/ios/**/*.mode2v3 47 | **/ios/**/*.moved-aside 48 | **/ios/**/*.pbxuser 49 | **/ios/**/*.perspectivev3 50 | **/ios/**/*sync/ 51 | **/ios/**/.sconsign.dblite 52 | **/ios/**/.tags* 53 | **/ios/**/.vagrant/ 54 | **/ios/**/DerivedData/ 55 | **/ios/**/Icon? 56 | **/ios/**/Pods/ 57 | **/ios/**/.symlinks/ 58 | **/ios/**/profile 59 | **/ios/**/xcuserdata 60 | **/ios/.generated/ 61 | **/ios/Flutter/App.framework 62 | **/ios/Flutter/Flutter.framework 63 | **/ios/Flutter/Flutter.podspec 64 | **/ios/Flutter/Generated.xcconfig 65 | **/ios/Flutter/app.flx 66 | **/ios/Flutter/app.zip 67 | **/ios/Flutter/flutter_assets/ 68 | **/ios/Flutter/flutter_export_environment.sh 69 | **/ios/ServiceDefinitions.json 70 | **/ios/Runner/GeneratedPluginRegistrant.* 71 | 72 | # Exceptions to above rules. 73 | !**/ios/**/default.mode1v3 74 | !**/ios/**/default.mode2v3 75 | !**/ios/**/default.pbxuser 76 | !**/ios/**/default.perspectivev3 77 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 78 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: e6b34c2b5c96bb95325269a29a84e83ed8909b5f 8 | channel: unknown 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.0.0 2 | 3 | * Null-safety support 4 | 5 | ## 1.0.0 6 | 7 | * Initial release 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Sander Kersten 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flutter_test_ui 2 | 3 | Wrapper of `testWidgets`, `setUp`, and `tearDown` that provide the `WidgetTesterCallback` argument 4 | to the set-up and tear-down functions: `testUI`, `setUpUI`, and `tearDownUI`. 5 | 6 | This allows a single set-up to be shared by several tests and the set-up to be continued in subgroups. 7 | In particular, it allows test to be written in rspec style for better readability. 8 | 9 | ```dart 10 | group("testUI and setUpUI example test", () { 11 | setUpUI((tester) async { 12 | await tester.pumpWidget(MaterialApp( 13 | home: Builder( 14 | builder: (context) => GestureDetector( 15 | onTap: () => Navigator.of(context).push( 16 | MaterialPageRoute( 17 | builder: (context) => Container( 18 | color: Colors.green, 19 | child: const Text("page 2"), 20 | ), 21 | ), 22 | ), 23 | child: Container( 24 | color: Colors.red, 25 | child: const Text("page 1"), 26 | ), 27 | ), 28 | ), 29 | )); 30 | }); 31 | 32 | testUI("first page is shown", (tester) async { 33 | expect(find.text("page 1"), findsOneWidget); 34 | }); 35 | 36 | group("tapping the text", () { 37 | setUpUI((tester) async { 38 | await tester.tap(find.text("page 1")); 39 | await tester.pumpAndSettle(); 40 | }); 41 | 42 | testUI("second page is shown", (tester) async { 43 | expect(find.text("page 2"), findsOneWidget); 44 | }); 45 | 46 | group("pop the second page", () { 47 | setUpUI((tester) async { 48 | final nav = tester.state(find.byType(Navigator)); 49 | nav.pop(); 50 | await tester.pumpAndSettle(); 51 | }); 52 | 53 | testUI("second page isn't visible anymore", (tester) async { 54 | expect(find.text("page 2"), findsNothing); 55 | }); 56 | 57 | testUI("first page is visible again", (tester) async { 58 | expect(find.text("page 1"), findsOneWidget); 59 | }); 60 | }); 61 | }); 62 | }); 63 | ``` -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | analyzer: 2 | strong-mode: 3 | implicit-casts: false 4 | implicit-dynamic: false 5 | exclude: 6 | - lib/**/*.g.dart 7 | - test/**.g.dart 8 | - test/**/*.g.dart 9 | 10 | linter: 11 | rules: 12 | - always_declare_return_types 13 | # - always_put_control_body_on_new_line -- DISABLED: DARTFMT INCOMPATIBLE 14 | - always_put_required_named_parameters_first 15 | - always_require_non_null_named_parameters 16 | # - always_specify_types -- DISABLED: LEADS TO FLUFFY CODE 17 | - annotate_overrides 18 | # - avoid_annotating_with_dynamic -- DISABLED: Gives false positives for function arguments where it is needed 19 | # - avoid_as 20 | - avoid_bool_literals_in_conditional_expressions 21 | - avoid_catches_without_on_clauses 22 | - avoid_catching_errors 23 | - avoid_classes_with_only_static_members 24 | - avoid_double_and_int_checks 25 | - avoid_empty_else 26 | - avoid_equals_and_hash_code_on_mutable_classes 27 | - avoid_field_initializers_in_const_classes 28 | # - avoid_function_literals_in_foreach_calls -- DISABLED: forEach is often clearer than for-loop 29 | - avoid_implementing_value_types 30 | - avoid_init_to_null 31 | - avoid_js_rounded_ints 32 | - avoid_null_checks_in_equality_operators 33 | - avoid_positional_boolean_parameters 34 | # - avoid_print -- Will be assessed separately 35 | - avoid_private_typedef_functions 36 | # - avoid_redundant_argument_values -- DISABLED: This rule is to be supported in a future Dart release 37 | - avoid_relative_lib_imports 38 | - avoid_renaming_method_parameters 39 | - avoid_return_types_on_setters 40 | - avoid_returning_null 41 | - avoid_returning_null_for_future 42 | - avoid_returning_null_for_void 43 | - avoid_returning_this 44 | # - avoid_setters_without_getters -- DISABLED: Sometimes it makes sense to have write-only member 45 | - avoid_shadowing_type_parameters 46 | - avoid_single_cascade_in_expression_statements 47 | - avoid_slow_async_io 48 | - avoid_types_as_parameter_names 49 | # - avoid_types_on_closure_parameters -- DISABLED: INCOMPATIBLE WITH IMPLICIT TYPE CASTS DISABLED 50 | - avoid_unnecessary_containers 51 | - avoid_unused_constructor_parameters 52 | - avoid_void_async 53 | - avoid_web_libraries_in_flutter 54 | - await_only_futures 55 | - camel_case_extensions 56 | - camel_case_types 57 | - cancel_subscriptions 58 | # - cascade_invocations -- DISABLED: Sometimes it's preferable to declare a variable with an intermediate result for readability 59 | - close_sinks 60 | - comment_references 61 | - constant_identifier_names 62 | - control_flow_in_finally 63 | # - curly_braces_in_flow_control_structures -- DISABLED: WE DO NOT WANT THIS 64 | # - diagnostic_describe_all_properties -- DISABLED: experimental feature 65 | - directives_ordering 66 | - empty_catches 67 | - empty_constructor_bodies 68 | - empty_statements 69 | - file_names 70 | # - flutter_style_todos -- DISABLED: TOO MUCH? 71 | - hash_and_equals 72 | - implementation_imports 73 | - invariant_booleans 74 | - iterable_contains_unrelated_type 75 | - join_return_with_assignment 76 | - library_names 77 | - library_prefixes 78 | # - lines_longer_than_80_chars -- DISABLED: WE DO NOT WANT THIS 79 | - list_remove_unrelated_type 80 | - literal_only_boolean_expressions 81 | # - missing_whitespace_between_adjacent_strings -- DISABLED: This rule is to be supported in a future Dart release 82 | - no_adjacent_strings_in_list 83 | - no_duplicate_case_values 84 | # - no_logic_in_create_state -- DISABLED: This rule is to be supported in a future Dart release 85 | # - no_runtimeType_toString -- DISABLED: This rule is to be supported in a future Dart release 86 | - non_constant_identifier_names 87 | - null_closures 88 | - omit_local_variable_types 89 | - one_member_abstracts 90 | - only_throw_errors 91 | - overridden_fields 92 | - package_api_docs 93 | - package_names 94 | - package_prefixed_library_names 95 | - parameter_assignments 96 | - prefer_adjacent_string_concatenation 97 | - prefer_asserts_in_initializer_lists 98 | # - prefer_asserts_with_message 99 | - prefer_collection_literals 100 | - prefer_conditional_assignment 101 | - prefer_const_constructors 102 | - prefer_const_constructors_in_immutables 103 | - prefer_const_declarations 104 | - prefer_const_literals_to_create_immutables 105 | - prefer_constructors_over_static_methods 106 | - prefer_contains 107 | # - prefer_double_quotes -- Will be assessed separately 108 | - prefer_equal_for_default_values 109 | - prefer_expression_function_bodies 110 | - prefer_final_fields 111 | - prefer_final_in_for_each 112 | - prefer_final_locals 113 | - prefer_for_elements_to_map_fromIterable 114 | - prefer_foreach 115 | - prefer_function_declarations_over_variables 116 | - prefer_generic_function_type_aliases 117 | - prefer_if_elements_to_conditional_expressions 118 | - prefer_if_null_operators 119 | - prefer_initializing_formals 120 | - prefer_inlined_adds 121 | # - prefer_int_literals -- DISABLED: ADDED VALUE IS UNCLEAR 122 | - prefer_interpolation_to_compose_strings 123 | - prefer_is_empty 124 | - prefer_is_not_empty 125 | - prefer_is_not_operator 126 | - prefer_iterable_whereType 127 | - prefer_mixin 128 | - prefer_null_aware_operators 129 | # - prefer_relative_imports -- Will be assessed separately 130 | # - prefer_single_quotes -- DISABLED: ADDED VALUE IS UNCLEAR 131 | - prefer_spread_collections 132 | - prefer_typing_uninitialized_variables 133 | - prefer_void_to_null 134 | # - provide_deprecation_messages -- DISABLED: This rule is to be supported in a future Dart release 135 | - public_member_api_docs 136 | - recursive_getters 137 | - slash_for_doc_comments 138 | - sort_child_properties_last 139 | - sort_constructors_first 140 | - sort_pub_dependencies 141 | - sort_unnamed_constructors_first 142 | - test_types_in_equals 143 | - throw_in_finally 144 | # - type_annotate_public_apis -- DISABLED: ADDED VALUE IS UNCLEAR 145 | - type_init_formals 146 | - unawaited_futures 147 | - unnecessary_await_in_return 148 | - unnecessary_brace_in_string_interps 149 | - unnecessary_const 150 | # - unnecessary_final 151 | - unnecessary_getters_setters 152 | - unnecessary_lambdas 153 | - unnecessary_new 154 | - unnecessary_null_aware_assignments 155 | - unnecessary_null_in_if_null_operators 156 | - unnecessary_overrides 157 | - unnecessary_parenthesis 158 | - unnecessary_statements 159 | # - unnecessary_string_interpolations -- DISABLED: This rule is to be supported in a future Dart release 160 | - unnecessary_this 161 | - unrelated_type_equality_checks 162 | - unsafe_html 163 | - use_full_hex_values_for_flutter_colors 164 | - use_function_type_syntax_for_parameters 165 | # - use_key_in_widget_constructors -- DISABLED: This rule is to be supported in a future Dart release 166 | - use_rethrow_when_possible 167 | - use_setters_to_change_properties 168 | - use_string_buffers 169 | - use_to_and_as_if_applicable 170 | - valid_regexps 171 | - void_checks -------------------------------------------------------------------------------- /lib/flutter_test_ui.dart: -------------------------------------------------------------------------------- 1 | library flutter_test_ui; 2 | 3 | export 'src/test_ui.dart'; 4 | -------------------------------------------------------------------------------- /lib/src/test_ui.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:meta/meta.dart'; 3 | 4 | /// Registers a function to be run before tests. 5 | /// 6 | /// Functions registered by [setUpUI] will be run after all other functions registered by [setUp] in parent or 7 | /// child groups. The functions will only be run when [testUI] is used to create the test case. 8 | void setUpUI(WidgetTesterCallback cb) { 9 | final setUpFunction = _UniqueWrapper(cb, StackTrace.current); 10 | setUp(() { 11 | setUpFunction.ran = false; 12 | _setUpUIs.add(setUpFunction); 13 | }); 14 | tearDown(() { 15 | assert(_setUpUIs.contains(setUpFunction)); 16 | assert( 17 | setUpFunction.ran, 18 | "A setUpUI function wasn't run. Likely, this means that testWidgets was used to create the test instead of testUI.\n" 19 | "${setUpFunction.stackTrace}", 20 | ); 21 | _setUpUIs.remove(setUpFunction); 22 | }); 23 | tearDownAll(() { 24 | assert(_setUpUIs.isEmpty); 25 | }); 26 | } 27 | 28 | /// Registers a function to be run after tests. 29 | /// 30 | /// Functions registered by [tearDownUI] will be run before all other functions registered by [tearDown] in parent or 31 | /// child groups. The functions will only be run when [testUI] is used to create the test case. 32 | void tearDownUI(WidgetTesterCallback cb) { 33 | final tearDownFunction = _UniqueWrapper(cb, StackTrace.current); 34 | setUp(() { 35 | tearDownFunction.ran = false; 36 | _tearDownUIs.add(tearDownFunction); 37 | }); 38 | tearDown(() { 39 | assert(_tearDownUIs.contains(tearDownFunction)); 40 | assert( 41 | tearDownFunction.ran, 42 | "A tearDownUI function wasn't run. Likely, this means that testWidgets was used to create the test instead of testUI.\n" 43 | "${tearDownFunction.stackTrace}", 44 | ); 45 | _tearDownUIs.remove(tearDownFunction); 46 | }); 47 | tearDownAll(() { 48 | assert(_tearDownUIs.isEmpty); 49 | }); 50 | } 51 | 52 | /// Runs the [callback] inside the Flutter test environment. 53 | /// 54 | /// Use this function to create test cases instead of [testWidgets] to be able to use [setUpUI] and [tearDownUI]. 55 | /// 56 | /// See [testWidgets] for details 57 | @isTest 58 | void testUI( 59 | String description, 60 | WidgetTesterCallback callback, { 61 | bool skip = false, 62 | Timeout? timeout, 63 | Duration? initialTimeout, 64 | bool semanticsEnabled = true, 65 | TestVariant variant = const DefaultTestVariant(), 66 | }) { 67 | testWidgets( 68 | description, 69 | (tester) async { 70 | for (final s in _setUpUIs) await s(tester); 71 | await callback(tester); 72 | for (final t in _tearDownUIs.reversed) await t(tester); 73 | }, 74 | skip: skip, 75 | timeout: timeout, 76 | initialTimeout: initialTimeout, 77 | semanticsEnabled: semanticsEnabled, 78 | variant: variant, 79 | ); 80 | } 81 | 82 | List _setUpUIs = <_UniqueWrapper>[]; 83 | List _tearDownUIs = <_UniqueWrapper>[]; 84 | 85 | // so every call to setUpUI/tearDownUI can be saved uniquely, even if a setup function were to be registered twice 86 | class _UniqueWrapper { 87 | _UniqueWrapper(this.callback, this.stackTrace); 88 | 89 | final Future Function(WidgetTester widgetTester) callback; 90 | final StackTrace stackTrace; 91 | bool ran = false; 92 | 93 | Future call(WidgetTester tester) { 94 | ran = true; 95 | return callback(tester); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_test_ui 2 | description: Wrapper for flutter_test that adds a tester argument to setUp and tearDown functions 3 | version: 2.0.0 4 | homepage: https://github.com/spkersten/flutter_test_ui 5 | 6 | environment: 7 | sdk: '>=2.12.0 <3.0.0' 8 | flutter: ">=1.17.0" 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | flutter_test: 14 | sdk: flutter 15 | meta: ">=1.1.5 <2.0.0" 16 | 17 | flutter: 18 | -------------------------------------------------------------------------------- /test/flutter_test_ui_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:flutter_test_ui/flutter_test_ui.dart'; 4 | 5 | void main() { 6 | group("testUI and setUpUI example test", () { 7 | setUpUI((tester) async { 8 | await tester.pumpWidget(MaterialApp( 9 | home: Builder( 10 | builder: (context) => GestureDetector( 11 | onTap: () => Navigator.of(context).push( 12 | MaterialPageRoute( 13 | builder: (context) => Container( 14 | color: Colors.green, 15 | child: const Text("page 2"), 16 | ), 17 | ), 18 | ), 19 | child: Container( 20 | color: Colors.red, 21 | child: const Text("page 1"), 22 | ), 23 | ), 24 | ), 25 | )); 26 | }); 27 | 28 | testUI("first page is shown", (tester) async { 29 | expect(find.text("page 1"), findsOneWidget); 30 | }); 31 | 32 | group("tapping the text", () { 33 | setUpUI((tester) async { 34 | await tester.tap(find.text("page 1")); 35 | await tester.pumpAndSettle(); 36 | }); 37 | 38 | testUI("second page is shown", (tester) async { 39 | expect(find.text("page 2"), findsOneWidget); 40 | }); 41 | 42 | group("pop the second page", () { 43 | setUpUI((tester) async { 44 | final nav = tester.state(find.byType(Navigator)); 45 | nav.pop(); 46 | await tester.pumpAndSettle(); 47 | }); 48 | 49 | testUI("second page isn't visible anymore", (tester) async { 50 | expect(find.text("page 2"), findsNothing); 51 | }); 52 | 53 | testUI("first page is visible again", (tester) async { 54 | expect(find.text("page 1"), findsOneWidget); 55 | }); 56 | }); 57 | }); 58 | }); 59 | } 60 | --------------------------------------------------------------------------------