From a0bdac4056270eccab325ed9b2885bffb88f560c Mon Sep 17 00:00:00 2001 From: Dimitar Stoyanov Date: Mon, 20 May 2024 12:11:26 +0300 Subject: [PATCH 1/2] *Add Patrol Test for TextDialog widget --- .../example/android/app/build.gradle | 6 +-- .../example/android/build.gradle | 4 +- .../configuration/build_app.dart | 23 +++++++++++ .../configuration/config_params.dart | 23 +++++++++++ .../configuration/patrol_base_config.dart | 40 +++++++++++++++++++ .../integration_test/pages/base_page.dart | 15 +++++++ .../pages/edit_fields_page.dart | 14 +++++++ .../tests/edit_fields_test.dart | 22 ++++++++++ .../lib/keys_testing/base_page_keys.dart | 3 ++ .../keys_testing/edit_field_page_keys.dart | 5 +++ packages/widget_toolkit/example/lib/main.dart | 7 ++++ packages/widget_toolkit/example/pubspec.yaml | 7 ++++ .../ui_components/input_text_field.dart | 3 ++ .../views/text_field_dialog.dart | 6 +++ .../views/text_field_dialog_page.dart | 6 +++ .../buttons/gradient_fill_button.dart | 4 ++ 16 files changed, 183 insertions(+), 5 deletions(-) create mode 100644 packages/widget_toolkit/example/integration_test/configuration/build_app.dart create mode 100644 packages/widget_toolkit/example/integration_test/configuration/config_params.dart create mode 100644 packages/widget_toolkit/example/integration_test/configuration/patrol_base_config.dart create mode 100644 packages/widget_toolkit/example/integration_test/pages/base_page.dart create mode 100644 packages/widget_toolkit/example/integration_test/pages/edit_fields_page.dart create mode 100644 packages/widget_toolkit/example/integration_test/tests/edit_fields_test.dart create mode 100644 packages/widget_toolkit/example/lib/keys_testing/base_page_keys.dart create mode 100644 packages/widget_toolkit/example/lib/keys_testing/edit_field_page_keys.dart diff --git a/packages/widget_toolkit/example/android/app/build.gradle b/packages/widget_toolkit/example/android/app/build.gradle index 7071baea..8598ca66 100644 --- a/packages/widget_toolkit/example/android/app/build.gradle +++ b/packages/widget_toolkit/example/android/app/build.gradle @@ -26,7 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion flutter.compileSdkVersion + compileSdk 34 ndkVersion flutter.ndkVersion compileOptions { @@ -47,7 +47,7 @@ android { applicationId "com.primeholding.example" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. - minSdkVersion flutter.minSdkVersion + minSdkVersion 21 targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName @@ -67,5 +67,5 @@ flutter { } dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" } diff --git a/packages/widget_toolkit/example/android/build.gradle b/packages/widget_toolkit/example/android/build.gradle index 58a8c74b..2406875c 100644 --- a/packages/widget_toolkit/example/android/build.gradle +++ b/packages/widget_toolkit/example/android/build.gradle @@ -1,12 +1,12 @@ buildscript { - ext.kotlin_version = '1.7.10' + ext.kotlin_version = '1.8.21' repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.2.0' + classpath 'com.android.tools.build:gradle:7.3.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/packages/widget_toolkit/example/integration_test/configuration/build_app.dart b/packages/widget_toolkit/example/integration_test/configuration/build_app.dart new file mode 100644 index 00000000..6951b664 --- /dev/null +++ b/packages/widget_toolkit/example/integration_test/configuration/build_app.dart @@ -0,0 +1,23 @@ +import 'package:example/main.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:patrol/patrol.dart'; + +export 'package:flutter/foundation.dart'; +export 'package:flutter_test/flutter_test.dart'; +export 'package:patrol/patrol.dart'; + +class BuildApp { + late PatrolIntegrationTester $; + + BuildApp(this.$); + + Future buildApp() async { + final FlutterExceptionHandler? originalOnError = FlutterError.onError; + + main(); + + await $.pumpAndSettle(); + FlutterError.onError = originalOnError; + } +} diff --git a/packages/widget_toolkit/example/integration_test/configuration/config_params.dart b/packages/widget_toolkit/example/integration_test/configuration/config_params.dart new file mode 100644 index 00000000..509bd59a --- /dev/null +++ b/packages/widget_toolkit/example/integration_test/configuration/config_params.dart @@ -0,0 +1,23 @@ +import 'build_app.dart'; + +class ConfigParams { + const ConfigParams(); + + ///General PatrolTesterConfig parameters + static const generalExistsTimeout = Duration(seconds: 10); + static const generalVisibleTimeout = Duration(seconds: 10); + static const generalSettleTimeout = Duration(seconds: 10); + static const generalSettlePolicy = SettlePolicy.settle; + + ///General NativeAutomatorConfig parameters + static const generalConnectionTimeout = Duration(seconds: 60); + static const generalFindTimeout = Duration(seconds: 10); + + ///Specific widget-related PatrolTesterConfig parameters + static const pageVisibleTimeout = Duration(seconds: 15); + static const mobileOnboardingScanQRTimeout = Duration(minutes: 1); + static const dashboardAccountCardScrollStep = 370.0; + static const dashboardAccountCardMaxScrolls = 1; + static const utilityBillsTabsScrollStep = 500.0; + static const utilityBillsItemsListScrollStep = 300.0; +} diff --git a/packages/widget_toolkit/example/integration_test/configuration/patrol_base_config.dart b/packages/widget_toolkit/example/integration_test/configuration/patrol_base_config.dart new file mode 100644 index 00000000..39c0b013 --- /dev/null +++ b/packages/widget_toolkit/example/integration_test/configuration/patrol_base_config.dart @@ -0,0 +1,40 @@ +import 'build_app.dart'; +import 'config_params.dart'; + +class PatrolBaseConfig { + // The default values for PatrolTesterConfig fields are set at ConfigParams class. + // All these global values can be overridden at its Specific widget-related + // parameters section. More info -> at the class documentation. + PatrolTesterConfig customPatrolTesterConfig() { + PatrolTesterConfig patrolTesterConfig = const PatrolTesterConfig( + existsTimeout: ConfigParams.generalExistsTimeout, + visibleTimeout: ConfigParams.generalVisibleTimeout, + settleTimeout: ConfigParams.generalSettleTimeout, + settlePolicy: ConfigParams.generalSettlePolicy); + return patrolTesterConfig; + } + + // Patrol NativeAutomatorConfig has far more options. Here only the most + // common ones are listed. More info -> at the class documentation. + // The default values are set at ConfigParams class. + NativeAutomatorConfig customNativeAutomatorConfig() { + NativeAutomatorConfig nativeAutomatorConfig = const NativeAutomatorConfig( + connectionTimeout: ConfigParams.generalConnectionTimeout, + findTimeout: ConfigParams.generalFindTimeout); + return nativeAutomatorConfig; + } + + void patrol( + String description, + Future Function(PatrolIntegrationTester) callback, { + bool? skip, + }) { + patrolTest( + description, + callback, + config: customPatrolTesterConfig(), + nativeAutomatorConfig: customNativeAutomatorConfig(), + skip: skip, + ); + } +} diff --git a/packages/widget_toolkit/example/integration_test/pages/base_page.dart b/packages/widget_toolkit/example/integration_test/pages/base_page.dart new file mode 100644 index 00000000..58d3caa2 --- /dev/null +++ b/packages/widget_toolkit/example/integration_test/pages/base_page.dart @@ -0,0 +1,15 @@ +import 'package:example/keys_testing/base_page_keys.dart' as keys; +import 'package:patrol/patrol.dart'; + +abstract class BasePage { + late PatrolIntegrationTester $; + + BasePage(this.$); + + Future tapWidget(var widget) async => $(widget).tap(); + + Future setInputField(var widget, String inputText) async => + $(widget).enterText(inputText); + + Future tapNextButton() async => $(keys.nextButton).tap(); +} diff --git a/packages/widget_toolkit/example/integration_test/pages/edit_fields_page.dart b/packages/widget_toolkit/example/integration_test/pages/edit_fields_page.dart new file mode 100644 index 00000000..5c2c28b8 --- /dev/null +++ b/packages/widget_toolkit/example/integration_test/pages/edit_fields_page.dart @@ -0,0 +1,14 @@ +import 'package:example/keys_testing/edit_field_page_keys.dart' as keys; + +import 'base_page.dart'; + +class EditFieldsPage extends BasePage { + EditFieldsPage(super.$); + + Future tapSaveButton() async => tapWidget(keys.saveButton); + + Future tapFirstNameDialog() async => tapWidget(keys.firstNameDialog); + + Future setFirstName(String firstName) async => + setInputField(keys.firstNameTextField, firstName); +} diff --git a/packages/widget_toolkit/example/integration_test/tests/edit_fields_test.dart b/packages/widget_toolkit/example/integration_test/tests/edit_fields_test.dart new file mode 100644 index 00000000..7f3ea217 --- /dev/null +++ b/packages/widget_toolkit/example/integration_test/tests/edit_fields_test.dart @@ -0,0 +1,22 @@ +import '../configuration/build_app.dart'; +import '../configuration/patrol_base_config.dart'; +import '../pages/edit_fields_page.dart'; + +void main() { + final patrolBaseConfig = PatrolBaseConfig(); + + patrolBaseConfig.patrol('TextFieldDialog debug test', ($) async { + const testInput = 'Test Input'; + + final editFieldsPage = EditFieldsPage($); + + await BuildApp($).buildApp(); + + await editFieldsPage.tapNextButton(); + await editFieldsPage.tapNextButton(); + await editFieldsPage.tapFirstNameDialog(); + await editFieldsPage.setFirstName(testInput); + await editFieldsPage.tapSaveButton(); + await Future.delayed(const Duration(seconds: 30)); + }); +} diff --git a/packages/widget_toolkit/example/lib/keys_testing/base_page_keys.dart b/packages/widget_toolkit/example/lib/keys_testing/base_page_keys.dart new file mode 100644 index 00000000..dd89a5c1 --- /dev/null +++ b/packages/widget_toolkit/example/lib/keys_testing/base_page_keys.dart @@ -0,0 +1,3 @@ +import 'package:flutter/cupertino.dart'; + +const nextButton = Key('basePageNextButton'); diff --git a/packages/widget_toolkit/example/lib/keys_testing/edit_field_page_keys.dart b/packages/widget_toolkit/example/lib/keys_testing/edit_field_page_keys.dart new file mode 100644 index 00000000..0e4c7978 --- /dev/null +++ b/packages/widget_toolkit/example/lib/keys_testing/edit_field_page_keys.dart @@ -0,0 +1,5 @@ +import 'package:flutter/cupertino.dart'; + +const firstNameDialog = Key('editFieldsPageFirstNameDialog'); +const saveButton = Key('editFieldsPageSaveButton'); +const firstNameTextField = Key('editFieldsPageFirstNameTextField'); diff --git a/packages/widget_toolkit/example/lib/main.dart b/packages/widget_toolkit/example/lib/main.dart index 5cfac6c5..4fc256f5 100644 --- a/packages/widget_toolkit/example/lib/main.dart +++ b/packages/widget_toolkit/example/lib/main.dart @@ -4,6 +4,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_rx_bloc/rx_form.dart'; import 'package:widget_toolkit/widget_toolkit.dart'; +import 'keys_testing/base_page_keys.dart' as base_page_keys; +import 'keys_testing/edit_field_page_keys.dart' as edit_field_page_keys; + void main() { runApp(const MyApp()); } @@ -82,6 +85,7 @@ class _MyHomePageState extends State { Widget build(BuildContext context) => Scaffold( appBar: AppBar(title: Text(title), actions: [ IconButton( + key: base_page_keys.nextButton, onPressed: () { pageController.animateToPage(nextPageIndex, duration: const Duration(milliseconds: 800), @@ -343,6 +347,9 @@ class EditFieldsPage extends StatelessWidget { WidgetSection( description: 'TextFieldDialog', child: TextFieldDialog( + textFormFieldKey: edit_field_page_keys.firstNameTextField, + saveButtonKey: edit_field_page_keys.saveButton, + key: edit_field_page_keys.firstNameDialog, translateError: (error) => TranslateErrorUtil.translateError(error, context), label: 'First Name', diff --git a/packages/widget_toolkit/example/pubspec.yaml b/packages/widget_toolkit/example/pubspec.yaml index a3bb30eb..6d845963 100644 --- a/packages/widget_toolkit/example/pubspec.yaml +++ b/packages/widget_toolkit/example/pubspec.yaml @@ -18,6 +18,13 @@ dev_dependencies: flutter_lints: ^2.0.1 flutter_test: sdk: flutter + patrol: ^3.6.1 + test: any + +patrol: + app_name: example + android: + package_name: com.primeholding.example dependency_overrides: widget_toolkit: diff --git a/packages/widget_toolkit/lib/src/lib_text_field_dialog/ui_components/input_text_field.dart b/packages/widget_toolkit/lib/src/lib_text_field_dialog/ui_components/input_text_field.dart index 8d2d1075..aa922583 100644 --- a/packages/widget_toolkit/lib/src/lib_text_field_dialog/ui_components/input_text_field.dart +++ b/packages/widget_toolkit/lib/src/lib_text_field_dialog/ui_components/input_text_field.dart @@ -30,6 +30,7 @@ class InputTextField extends StatefulWidget { this.suffixIcon, this.obBlurCallback, this.keyBoardType = TextInputType.text, + this.textFormFieldKey, super.key, }); @@ -54,6 +55,7 @@ class InputTextField extends StatefulWidget { final Color? defaultValueColor; final Widget? suffixIcon; final VoidCallback? obBlurCallback; + final Key? textFormFieldKey; @override InputTextFieldState createState() => InputTextFieldState(); @@ -205,6 +207,7 @@ class InputTextFieldState extends State { height: context.textFieldDialogTheme.spacingXSS, ), TextFormField( + key: widget.textFormFieldKey, focusNode: _focusNode, keyboardType: widget.keyBoardType, onEditingComplete: () => widget.obBlurCallback?.call(), diff --git a/packages/widget_toolkit/lib/src/lib_text_field_dialog/views/text_field_dialog.dart b/packages/widget_toolkit/lib/src/lib_text_field_dialog/views/text_field_dialog.dart index 46012044..20789406 100644 --- a/packages/widget_toolkit/lib/src/lib_text_field_dialog/views/text_field_dialog.dart +++ b/packages/widget_toolkit/lib/src/lib_text_field_dialog/views/text_field_dialog.dart @@ -89,6 +89,8 @@ class TextFieldDialog extends StatefulWidget { this.editFieldType = EditFieldType.editfield, this.modalConfiguration = const TextFieldModalConfiguration(), this.enabled = true, + this.textFormFieldKey, + this.saveButtonKey, super.key, }) : assert(editFieldCustomIcon == null || editFieldCustomIcon is IconData || @@ -114,6 +116,8 @@ class TextFieldDialog extends StatefulWidget { final EditFieldType editFieldType; final bool enabled; final TextFieldModalConfiguration modalConfiguration; + final Key? textFormFieldKey; + final Key? saveButtonKey; @override State> createState() => _TextFieldDialogState(); @@ -187,6 +191,8 @@ class _TextFieldDialogState extends State> { initialValue: _value, ).providers, child: TextFieldDialogPage( + textFormFieldKey: widget.textFormFieldKey, + saveButtonKey: widget.saveButtonKey, translateError: widget.translateError, callback: (value) { setState(() => _value = value); diff --git a/packages/widget_toolkit/lib/src/lib_text_field_dialog/views/text_field_dialog_page.dart b/packages/widget_toolkit/lib/src/lib_text_field_dialog/views/text_field_dialog_page.dart index c882bd9e..8ff8cc68 100644 --- a/packages/widget_toolkit/lib/src/lib_text_field_dialog/views/text_field_dialog_page.dart +++ b/packages/widget_toolkit/lib/src/lib_text_field_dialog/views/text_field_dialog_page.dart @@ -43,6 +43,8 @@ class TextFieldDialogPage extends StatelessWidget { this.isMultiLinedInputField = false, this.dialogHasBottomPadding = false, this.maxLines, + this.textFormFieldKey, + this.saveButtonKey, super.key, }); @@ -56,6 +58,8 @@ class TextFieldDialogPage extends StatelessWidget { final bool dialogHasBottomPadding; final int? maxLines; final Function(Object error) translateError; + final Key? textFormFieldKey; + final Key? saveButtonKey; @override Widget build(BuildContext context) { @@ -96,6 +100,7 @@ class TextFieldDialogPage extends StatelessWidget { onChanged: (bloc, value) => bloc.events.setText(value), cursorBehaviour: RxTextFormFieldCursorBehaviour.end, builder: (fieldState) => InputTextField( + textFormFieldKey: textFormFieldKey, isFocused: true, keyBoardType: keyBoardType, label: label, @@ -118,6 +123,7 @@ class TextFieldDialogPage extends StatelessWidget { child: RxBlocBuilder>( state: (bloc) => bloc.states.submittedValue as Stream>, builder: (context, snapshot, bloc) => GradientFillButton( + buttonKey: saveButtonKey, elevation: 0, text: fillButtonText, state: snapshot.data is ResultLoading diff --git a/packages/widget_toolkit/lib/src/lib_ui_components/buttons/gradient_fill_button.dart b/packages/widget_toolkit/lib/src/lib_ui_components/buttons/gradient_fill_button.dart index 3e8607e8..8b122c30 100644 --- a/packages/widget_toolkit/lib/src/lib_ui_components/buttons/gradient_fill_button.dart +++ b/packages/widget_toolkit/lib/src/lib_ui_components/buttons/gradient_fill_button.dart @@ -25,6 +25,7 @@ class GradientFillButton extends StatelessWidget { this.colorStyle, this.textStyle, this.areIconsClose = false, + this.buttonKey, super.key, }); @@ -61,6 +62,8 @@ class GradientFillButton extends StatelessWidget { /// The style of the [text] final TextStyle? textStyle; + final Key? buttonKey; + @override Widget build(BuildContext context) { final gradient = activeState() @@ -149,6 +152,7 @@ class GradientFillButton extends StatelessWidget { Color primary, ) { return ElevatedButton( + key: buttonKey, style: ElevatedButton.styleFrom( backgroundColor: primary, padding: padding ?? EdgeInsets.zero, From 15d22eded5f45371fd5c2ceab002de5623da739a Mon Sep 17 00:00:00 2001 From: Dimitar Stoyanov Date: Mon, 20 May 2024 18:01:08 +0300 Subject: [PATCH 2/2] *Fix crashes --- .../example/android/app/build.gradle | 6 ++++ .../example/MainActivityTest.java | 33 +++++++++++++++++++ .../tests/edit_fields_test.dart | 3 +- 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 packages/widget_toolkit/example/android/app/src/androidTest/java/com/primeholding/example/MainActivityTest.java diff --git a/packages/widget_toolkit/example/android/app/build.gradle b/packages/widget_toolkit/example/android/app/build.gradle index 8598ca66..beeb105a 100644 --- a/packages/widget_toolkit/example/android/app/build.gradle +++ b/packages/widget_toolkit/example/android/app/build.gradle @@ -51,6 +51,8 @@ android { targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName + testInstrumentationRunner "pl.leancode.patrol.PatrolJUnitRunner" + testInstrumentationRunnerArguments clearPackageData: "true" } buildTypes { @@ -60,6 +62,9 @@ android { signingConfig signingConfigs.debug } } + testOptions { + execution "ANDROIDX_TEST_ORCHESTRATOR" + } } flutter { @@ -68,4 +73,5 @@ flutter { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + androidTestUtil "androidx.test:orchestrator:1.4.2" } diff --git a/packages/widget_toolkit/example/android/app/src/androidTest/java/com/primeholding/example/MainActivityTest.java b/packages/widget_toolkit/example/android/app/src/androidTest/java/com/primeholding/example/MainActivityTest.java new file mode 100644 index 00000000..c7142da9 --- /dev/null +++ b/packages/widget_toolkit/example/android/app/src/androidTest/java/com/primeholding/example/MainActivityTest.java @@ -0,0 +1,33 @@ +package com.primeholding.example; // replace "com.example.myapp" with your app's package + +import androidx.test.platform.app.InstrumentationRegistry; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import pl.leancode.patrol.PatrolJUnitRunner; + +@RunWith(Parameterized.class) +public class MainActivityTest { + @Parameters(name = "{0}") + public static Object[] testCases() { + PatrolJUnitRunner instrumentation = (PatrolJUnitRunner) InstrumentationRegistry.getInstrumentation(); + // replace "MainActivity.class" with "io.flutter.embedding.android.FlutterActivity.class" + // if your AndroidManifest is using: android:name="io.flutter.embedding.android.FlutterActivity" + instrumentation.setUp(MainActivity.class); + instrumentation.waitForPatrolAppService(); + return instrumentation.listDartTests(); + } + + public MainActivityTest(String dartTestName) { + this.dartTestName = dartTestName; + } + + private final String dartTestName; + + @Test + public void runDartTest() { + PatrolJUnitRunner instrumentation = (PatrolJUnitRunner) InstrumentationRegistry.getInstrumentation(); + instrumentation.runDartTest(dartTestName); + } +} \ No newline at end of file diff --git a/packages/widget_toolkit/example/integration_test/tests/edit_fields_test.dart b/packages/widget_toolkit/example/integration_test/tests/edit_fields_test.dart index 7f3ea217..5e60c260 100644 --- a/packages/widget_toolkit/example/integration_test/tests/edit_fields_test.dart +++ b/packages/widget_toolkit/example/integration_test/tests/edit_fields_test.dart @@ -6,7 +6,7 @@ void main() { final patrolBaseConfig = PatrolBaseConfig(); patrolBaseConfig.patrol('TextFieldDialog debug test', ($) async { - const testInput = 'Test Input'; + const testInput = 'Test'; final editFieldsPage = EditFieldsPage($); @@ -17,6 +17,7 @@ void main() { await editFieldsPage.tapFirstNameDialog(); await editFieldsPage.setFirstName(testInput); await editFieldsPage.tapSaveButton(); + //Check that the keyboard is displayed await Future.delayed(const Duration(seconds: 30)); }); }