├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── checkstyle ├── checkstyle-suppression.xml └── checkstyle.xml ├── pom.xml └── src ├── main └── java │ └── org │ └── example │ ├── constants │ └── FrameworkConstants.java │ ├── customexceptions │ ├── DriverInitializationException.java │ ├── FrameworkException.java │ ├── JsonFileUsageException.java │ └── PropertyFileUsageException.java │ ├── driver │ ├── Drivers.java │ ├── factory │ │ └── DriverFactory.java │ └── manager │ │ ├── AndroidManager.java │ │ ├── DeviceManager.java │ │ ├── DriverManager.java │ │ ├── IosManager.java │ │ └── PlatformManager.java │ ├── enums │ ├── ConfigJson.java │ ├── ConfigProperties.java │ ├── MobileBrowserName.java │ ├── MobileFindBy.java │ ├── MobilePlatformName.java │ └── WaitStrategy.java │ ├── factories │ └── WaitFactory.java │ ├── pageobjects │ ├── LoginPage.java │ ├── ProductPage.java │ └── screen │ │ └── ScreenActions.java │ └── utils │ ├── AppiumServerManager.java │ ├── configloader │ ├── JsonParser.java │ └── PropertyUtils.java │ ├── screenrecording │ ├── ScreenRecordingService.java │ └── ScreenRecordingUtils.java │ └── screenshot │ ├── ScreenshotService.java │ └── ScreenshotUtils.java └── test ├── java └── org │ └── example │ ├── runner │ └── RunnerTest.java │ └── stepdefinitions │ ├── Hooks.java │ └── LoginSteps.java └── resources ├── app └── Android.SauceLabs.Mobile.Sample.app.2.7.1.apk ├── config ├── config.json └── config.properties ├── cucumber.properties └── feature └── Login.feature /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | max_line_length = 120 10 | tab_width = 4 11 | ij_continuation_indent_size = 8 12 | ij_formatter_off_tag = @formatter:off 13 | ij_formatter_on_tag = @formatter:on 14 | ij_formatter_tags_enabled = false 15 | ij_smart_tabs = false 16 | ij_visual_guides = none 17 | ij_wrap_on_typing = false 18 | 19 | [*.java] 20 | indent_size = 2 21 | max_line_length = 136 22 | tab_width = 2 23 | ij_continuation_indent_size = 2 24 | ij_java_align_consecutive_assignments = false 25 | ij_java_align_consecutive_variable_declarations = false 26 | ij_java_align_group_field_declarations = false 27 | ij_java_align_multiline_annotation_parameters = false 28 | ij_java_align_multiline_array_initializer_expression = false 29 | ij_java_align_multiline_assignment = false 30 | ij_java_align_multiline_binary_operation = false 31 | ij_java_align_multiline_chained_methods = false 32 | ij_java_align_multiline_extends_list = false 33 | ij_java_align_multiline_for = true 34 | ij_java_align_multiline_method_parentheses = false 35 | ij_java_align_multiline_parameters = true 36 | ij_java_align_multiline_parameters_in_calls = true 37 | ij_java_align_multiline_parenthesized_expression = false 38 | ij_java_align_multiline_records = true 39 | ij_java_align_multiline_resources = true 40 | ij_java_align_multiline_ternary_operation = false 41 | ij_java_align_multiline_text_blocks = false 42 | ij_java_align_multiline_throws_list = false 43 | ij_java_align_subsequent_simple_methods = false 44 | ij_java_align_throws_keyword = false 45 | ij_java_annotation_parameter_wrap = off 46 | ij_java_array_initializer_new_line_after_left_brace = false 47 | ij_java_array_initializer_right_brace_on_new_line = false 48 | ij_java_array_initializer_wrap = normal 49 | ij_java_assert_statement_colon_on_next_line = false 50 | ij_java_assert_statement_wrap = normal 51 | ij_java_assignment_wrap = normal 52 | ij_java_binary_operation_sign_on_next_line = false 53 | ij_java_binary_operation_wrap = normal 54 | ij_java_blank_lines_after_anonymous_class_header = 0 55 | ij_java_blank_lines_after_class_header = 0 56 | ij_java_blank_lines_after_imports = 1 57 | ij_java_blank_lines_after_package = 1 58 | ij_java_blank_lines_around_class = 1 59 | ij_java_blank_lines_around_field = 0 60 | ij_java_blank_lines_around_field_in_interface = 0 61 | ij_java_blank_lines_around_initializer = 1 62 | ij_java_blank_lines_around_method = 1 63 | ij_java_blank_lines_around_method_in_interface = 1 64 | ij_java_blank_lines_before_class_end = 0 65 | ij_java_blank_lines_before_imports = 1 66 | ij_java_blank_lines_before_method_body = 0 67 | ij_java_blank_lines_before_package = 0 68 | ij_java_block_brace_style = end_of_line 69 | ij_java_block_comment_add_space = false 70 | ij_java_block_comment_at_first_column = true 71 | ij_java_builder_methods = none 72 | ij_java_call_parameters_new_line_after_left_paren = false 73 | ij_java_call_parameters_right_paren_on_new_line = false 74 | ij_java_call_parameters_wrap = normal 75 | ij_java_case_statement_on_separate_line = true 76 | ij_java_catch_on_new_line = false 77 | ij_java_class_annotation_wrap = split_into_lines 78 | ij_java_class_brace_style = end_of_line 79 | ij_java_class_count_to_use_import_on_demand = 999 80 | ij_java_class_names_in_javadoc = 1 81 | ij_java_do_not_indent_top_level_class_members = false 82 | ij_java_do_not_wrap_after_single_annotation = false 83 | ij_java_do_while_brace_force = never 84 | ij_java_doc_add_blank_line_after_description = true 85 | ij_java_doc_add_blank_line_after_param_comments = false 86 | ij_java_doc_add_blank_line_after_return = false 87 | ij_java_doc_add_p_tag_on_empty_lines = true 88 | ij_java_doc_align_exception_comments = true 89 | ij_java_doc_align_param_comments = true 90 | ij_java_doc_do_not_wrap_if_one_line = false 91 | ij_java_doc_enable_formatting = true 92 | ij_java_doc_enable_leading_asterisks = true 93 | ij_java_doc_indent_on_continuation = false 94 | ij_java_doc_keep_empty_lines = true 95 | ij_java_doc_keep_empty_parameter_tag = true 96 | ij_java_doc_keep_empty_return_tag = true 97 | ij_java_doc_keep_empty_throws_tag = true 98 | ij_java_doc_keep_invalid_tags = true 99 | ij_java_doc_param_description_on_new_line = false 100 | ij_java_doc_preserve_line_breaks = false 101 | ij_java_doc_use_throws_not_exception_tag = true 102 | ij_java_else_on_new_line = false 103 | ij_java_enum_constants_wrap = normal 104 | ij_java_extends_keyword_wrap = normal 105 | ij_java_extends_list_wrap = normal 106 | ij_java_field_annotation_wrap = split_into_lines 107 | ij_java_finally_on_new_line = false 108 | ij_java_for_brace_force = never 109 | ij_java_for_statement_new_line_after_left_paren = false 110 | ij_java_for_statement_right_paren_on_new_line = false 111 | ij_java_for_statement_wrap = normal 112 | ij_java_generate_final_locals = false 113 | ij_java_generate_final_parameters = false 114 | ij_java_if_brace_force = never 115 | ij_java_imports_layout = *, |, javax.**, java.**, |, $* 116 | ij_java_indent_case_from_switch = true 117 | ij_java_insert_inner_class_imports = false 118 | ij_java_insert_override_annotation = true 119 | ij_java_keep_blank_lines_before_right_brace = 2 120 | ij_java_keep_blank_lines_between_package_declaration_and_header = 2 121 | ij_java_keep_blank_lines_in_code = 2 122 | ij_java_keep_blank_lines_in_declarations = 2 123 | ij_java_keep_builder_methods_indents = false 124 | ij_java_keep_control_statement_in_one_line = true 125 | ij_java_keep_first_column_comment = true 126 | ij_java_keep_indents_on_empty_lines = false 127 | ij_java_keep_line_breaks = true 128 | ij_java_keep_multiple_expressions_in_one_line = false 129 | ij_java_keep_simple_blocks_in_one_line = false 130 | ij_java_keep_simple_classes_in_one_line = false 131 | ij_java_keep_simple_lambdas_in_one_line = false 132 | ij_java_keep_simple_methods_in_one_line = false 133 | ij_java_label_indent_absolute = false 134 | ij_java_label_indent_size = 0 135 | ij_java_lambda_brace_style = end_of_line 136 | ij_java_layout_static_imports_separately = true 137 | ij_java_line_comment_add_space = false 138 | ij_java_line_comment_at_first_column = true 139 | ij_java_method_annotation_wrap = split_into_lines 140 | ij_java_method_brace_style = end_of_line 141 | ij_java_method_call_chain_wrap = normal 142 | ij_java_method_parameters_new_line_after_left_paren = false 143 | ij_java_method_parameters_right_paren_on_new_line = false 144 | ij_java_method_parameters_wrap = normal 145 | ij_java_modifier_list_wrap = false 146 | ij_java_names_count_to_use_import_on_demand = 999 147 | ij_java_new_line_after_lparen_in_record_header = false 148 | ij_java_parameter_annotation_wrap = normal 149 | ij_java_parentheses_expression_new_line_after_left_paren = false 150 | ij_java_parentheses_expression_right_paren_on_new_line = false 151 | ij_java_place_assignment_sign_on_next_line = false 152 | ij_java_prefer_longer_names = true 153 | ij_java_prefer_parameters_wrap = false 154 | ij_java_record_components_wrap = normal 155 | ij_java_repeat_synchronized = true 156 | ij_java_replace_instanceof_and_cast = false 157 | ij_java_replace_null_check = true 158 | ij_java_replace_sum_lambda_with_method_ref = true 159 | ij_java_resource_list_new_line_after_left_paren = false 160 | ij_java_resource_list_right_paren_on_new_line = false 161 | ij_java_resource_list_wrap = normal 162 | ij_java_rparen_on_new_line_in_record_header = false 163 | ij_java_space_after_closing_angle_bracket_in_type_argument = false 164 | ij_java_space_after_colon = true 165 | ij_java_space_after_comma = true 166 | ij_java_space_after_comma_in_type_arguments = true 167 | ij_java_space_after_for_semicolon = true 168 | ij_java_space_after_quest = true 169 | ij_java_space_after_type_cast = true 170 | ij_java_space_before_annotation_array_initializer_left_brace = false 171 | ij_java_space_before_annotation_parameter_list = false 172 | ij_java_space_before_array_initializer_left_brace = true 173 | ij_java_space_before_catch_keyword = true 174 | ij_java_space_before_catch_left_brace = true 175 | ij_java_space_before_catch_parentheses = true 176 | ij_java_space_before_class_left_brace = true 177 | ij_java_space_before_colon = true 178 | ij_java_space_before_colon_in_foreach = true 179 | ij_java_space_before_comma = false 180 | ij_java_space_before_do_left_brace = true 181 | ij_java_space_before_else_keyword = true 182 | ij_java_space_before_else_left_brace = true 183 | ij_java_space_before_finally_keyword = true 184 | ij_java_space_before_finally_left_brace = true 185 | ij_java_space_before_for_left_brace = true 186 | ij_java_space_before_for_parentheses = true 187 | ij_java_space_before_for_semicolon = false 188 | ij_java_space_before_if_left_brace = true 189 | ij_java_space_before_if_parentheses = true 190 | ij_java_space_before_method_call_parentheses = false 191 | ij_java_space_before_method_left_brace = true 192 | ij_java_space_before_method_parentheses = false 193 | ij_java_space_before_opening_angle_bracket_in_type_parameter = false 194 | ij_java_space_before_quest = true 195 | ij_java_space_before_switch_left_brace = true 196 | ij_java_space_before_switch_parentheses = true 197 | ij_java_space_before_synchronized_left_brace = true 198 | ij_java_space_before_synchronized_parentheses = true 199 | ij_java_space_before_try_left_brace = true 200 | ij_java_space_before_try_parentheses = true 201 | ij_java_space_before_type_parameter_list = false 202 | ij_java_space_before_while_keyword = true 203 | ij_java_space_before_while_left_brace = true 204 | ij_java_space_before_while_parentheses = true 205 | ij_java_space_inside_one_line_enum_braces = false 206 | ij_java_space_within_empty_array_initializer_braces = false 207 | ij_java_space_within_empty_method_call_parentheses = false 208 | ij_java_space_within_empty_method_parentheses = false 209 | ij_java_spaces_around_additive_operators = true 210 | ij_java_spaces_around_assignment_operators = true 211 | ij_java_spaces_around_bitwise_operators = true 212 | ij_java_spaces_around_equality_operators = true 213 | ij_java_spaces_around_lambda_arrow = true 214 | ij_java_spaces_around_logical_operators = true 215 | ij_java_spaces_around_method_ref_dbl_colon = false 216 | ij_java_spaces_around_multiplicative_operators = true 217 | ij_java_spaces_around_relational_operators = true 218 | ij_java_spaces_around_shift_operators = true 219 | ij_java_spaces_around_type_bounds_in_type_parameters = true 220 | ij_java_spaces_around_unary_operator = false 221 | ij_java_spaces_within_angle_brackets = false 222 | ij_java_spaces_within_annotation_parentheses = false 223 | ij_java_spaces_within_array_initializer_braces = false 224 | ij_java_spaces_within_braces = false 225 | ij_java_spaces_within_brackets = false 226 | ij_java_spaces_within_cast_parentheses = false 227 | ij_java_spaces_within_catch_parentheses = false 228 | ij_java_spaces_within_for_parentheses = false 229 | ij_java_spaces_within_if_parentheses = false 230 | ij_java_spaces_within_method_call_parentheses = false 231 | ij_java_spaces_within_method_parentheses = false 232 | ij_java_spaces_within_parentheses = false 233 | ij_java_spaces_within_record_header = false 234 | ij_java_spaces_within_switch_parentheses = false 235 | ij_java_spaces_within_synchronized_parentheses = false 236 | ij_java_spaces_within_try_parentheses = false 237 | ij_java_spaces_within_while_parentheses = false 238 | ij_java_special_else_if_treatment = true 239 | ij_java_subclass_name_suffix = Impl 240 | ij_java_ternary_operation_signs_on_next_line = false 241 | ij_java_ternary_operation_wrap = normal 242 | ij_java_test_name_suffix = Test 243 | ij_java_throws_keyword_wrap = normal 244 | ij_java_throws_list_wrap = normal 245 | ij_java_use_external_annotations = false 246 | ij_java_use_fq_class_names = false 247 | ij_java_use_relative_indents = false 248 | ij_java_use_single_class_imports = true 249 | ij_java_variable_annotation_wrap = normal 250 | ij_java_visibility = public 251 | ij_java_while_brace_force = never 252 | ij_java_while_on_new_line = false 253 | ij_java_wrap_comments = false 254 | ij_java_wrap_first_method_in_call_chain = false 255 | ij_java_wrap_long_lines = false 256 | 257 | [*.properties] 258 | ij_properties_align_group_field_declarations = false 259 | ij_properties_keep_blank_lines = false 260 | ij_properties_key_value_delimiter = equals 261 | ij_properties_spaces_around_key_value_delimiter = false 262 | 263 | [.editorconfig] 264 | ij_editorconfig_align_group_field_declarations = false 265 | ij_editorconfig_space_after_colon = false 266 | ij_editorconfig_space_after_comma = true 267 | ij_editorconfig_space_before_colon = false 268 | ij_editorconfig_space_before_comma = false 269 | ij_editorconfig_spaces_around_assignment_operators = true 270 | 271 | [{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.jspx,*.pom,*.rng,*.tagx,*.tld,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}] 272 | ij_xml_align_attributes = true 273 | ij_xml_align_text = false 274 | ij_xml_attribute_wrap = normal 275 | ij_xml_block_comment_add_space = false 276 | ij_xml_block_comment_at_first_column = true 277 | ij_xml_keep_blank_lines = 2 278 | ij_xml_keep_indents_on_empty_lines = false 279 | ij_xml_keep_line_breaks = true 280 | ij_xml_keep_line_breaks_in_text = true 281 | ij_xml_keep_whitespaces = false 282 | ij_xml_keep_whitespaces_around_cdata = preserve 283 | ij_xml_keep_whitespaces_inside_cdata = false 284 | ij_xml_line_comment_at_first_column = true 285 | ij_xml_space_after_tag_name = false 286 | ij_xml_space_around_equals_in_attribute = false 287 | ij_xml_space_inside_empty_tag = false 288 | ij_xml_text_wrap = normal 289 | ij_xml_use_custom_settings = false 290 | 291 | [{*.bash,*.sh,*.zsh}] 292 | indent_size = 2 293 | tab_width = 2 294 | ij_shell_binary_ops_start_line = false 295 | ij_shell_keep_column_alignment_padding = false 296 | ij_shell_minify_program = false 297 | ij_shell_redirect_followed_by_space = false 298 | ij_shell_switch_cases_indented = false 299 | ij_shell_use_unix_line_separator = true 300 | 301 | [{*.har,*.json}] 302 | indent_size = 2 303 | ij_json_keep_blank_lines_in_code = 0 304 | ij_json_keep_indents_on_empty_lines = false 305 | ij_json_keep_line_breaks = true 306 | ij_json_space_after_colon = true 307 | ij_json_space_after_comma = true 308 | ij_json_space_before_colon = true 309 | ij_json_space_before_comma = false 310 | ij_json_spaces_within_braces = false 311 | ij_json_spaces_within_brackets = false 312 | ij_json_wrap_long_lines = false 313 | 314 | [{*.markdown,*.md}] 315 | ij_markdown_force_one_space_after_blockquote_symbol = true 316 | ij_markdown_force_one_space_after_header_symbol = true 317 | ij_markdown_force_one_space_after_list_bullet = true 318 | ij_markdown_force_one_space_between_words = true 319 | ij_markdown_keep_indents_on_empty_lines = false 320 | ij_markdown_max_lines_around_block_elements = 1 321 | ij_markdown_max_lines_around_header = 1 322 | ij_markdown_max_lines_between_paragraphs = 1 323 | ij_markdown_min_lines_around_block_elements = 1 324 | ij_markdown_min_lines_around_header = 1 325 | ij_markdown_min_lines_between_paragraphs = 1 326 | 327 | [{*.yaml,*.yml}] 328 | indent_size = 2 329 | ij_yaml_align_values_properties = do_not_align 330 | ij_yaml_autoinsert_sequence_marker = true 331 | ij_yaml_block_mapping_on_new_line = false 332 | ij_yaml_indent_sequence_value = true 333 | ij_yaml_keep_indents_on_empty_lines = false 334 | ij_yaml_keep_line_breaks = true 335 | ij_yaml_sequence_on_new_line = false 336 | ij_yaml_space_before_colon = false 337 | ij_yaml_spaces_within_braces = true 338 | ij_yaml_spaces_within_brackets = true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /server-logs 3 | /screen-recordings 4 | /target -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Thangaraj 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # appium-mobile-automation-framework-bdd 2 | 3 | Mobile automation framework using appium - BDD 4 | 5 | 6 | 7 | 8 | ## :rocket: Quick Start - Appium set up on Windows (Android): 9 | 10 | 1) Install [Java JDK8](https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html) 11 | and [IntelliJ IDEA](https://www.jetbrains.com/idea/download/) 12 | 2) Install [NodeJS](https://nodejs.org/en/download/) 13 | 3) Install [Android studio](https://developer.android.com/studio) 14 | 4) Install Appium Server (v2.x) using npm (CLI) command `npm install -g appium@next` 15 | ``` 16 | Appium server version 2.0.0-beta.55 17 | ``` 18 | This will install the Appium server only, but no drivers. Install the latest versions of the XCUITest and UiAutomator2 drivers, after installing Appium you would run the following commands: 19 | ``` 20 | appium driver install uiautomator2 21 | appium driver install xcuitest 22 | ``` 23 | ``` 24 | Command to check the installed appium version: `appium --version` 25 | ``` 26 | 27 | 5) Add below Android SDK path in the environment variable 28 | 29 | ``` 30 | - ANDROID_HOME = 31 | - %ANDROID_HOME%\tools 32 | - %ANDROID_HOME%\tools\bin 33 | - %ANDROID_HOME%\platform-tools 34 | ``` 35 | 36 | 6) Install [Appium desktop](https://github.com/appium/appium-desktop/releases/) 37 | 7) Install [Appium Inspector](https://github.com/appium/appium-inspector/releases) 38 | 39 | ## :rocket: Quick Start - Appium set up on MAC (Android): 40 | 41 | 1) Install Homebrew 42 | 2) Install [NodeJS](https://nodejs.org/en/download/) 43 | 3) Install [Java JDK8](https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html) 44 | and [IntelliJ IDEA](https://www.jetbrains.com/idea/download/) 45 | 4) Install Appium server using npm (CLI) or Appium desktop client 46 | 5) Install [Android studio](https://developer.android.com/studio) 47 | 6) Install [Appium Inspector](https://github.com/appium/appium-inspector/releases) 48 | 7) Set JAVA_HOME and ANDROID_HOME environment variables 49 | 50 | ## :pushpin: Appium Doctor to verify the installations 51 | 52 | 1) Install appium-doctor using command `npm install -g appium-doctor` 53 | 2) To view the list of available options `appium-doctor --help` 54 | 55 | ``` 56 | To check Android set up `appium-doctor --android` 57 | To check ios set up `appium-doctor --ios` 58 | ``` 59 | 60 | ## :pushpin: Creating Android Virtual Device (Emulator) from Android Studio: 61 | 62 | 1) Open Android Studio. 63 | 2) Click on Tools -> AVD Manager -> Create Virtual Device -> Select the device and OS version -> Finish. 64 | 3) Once Virtual device is created, click on Launch this AVD in the emulator. 65 | 4) Command to view the list of devices attached `adb devices` 66 | 67 | ## :pushpin: Android Real Device Set up: 68 | 69 | 1) Connect Android real device to the machine(Desktop/Laptop) 70 | 2) Turn on the developer options in android mobile 71 | 3) Enable USB debugging 72 | 4) Run command `adb devices` in cmd prompt to check whether the device is recognised 73 | 74 | ## :pushpin: Mirror android/ios device to your desktop 75 | 76 | 1) Download [Vysor](https://www.vysor.io/) 77 | 78 | ## :pushpin: Start Android Emulator from Command line 79 | 80 | 1) Open command prompt, go to `` 81 | 82 | ``` 83 | Command to stard AVD: `emulator -avd ` 84 | Command to stop/kill AVD: `adb -e emu kill` 85 | ``` 86 | 87 | ## :pushpin: Pushing the App (.apk file) to Android Emulator: 88 | 89 | 1) Copy the .apk file and paste it in the path - `` 90 | 2) Open the cmd terminal from the directory where APK file is placed and enter command `adb install ` 91 | 92 | ## :pushpin: Android - Finding appPackage and appActivity: 93 | 94 | If the app is already installed on your device then we can make use of appPackage and appActivity to launch the app 95 | 96 | Option 1 : 97 | 1) Open the app on the device, for which appPackage and appActivity is required. 98 | 2) Open powershell and enter command `adb shell dumpsys window | grep -E 'mCurrentFocus|mFocusedApp'` 99 | NOTE: This command may not work for newer Android OS (10 or 11). In that case, use command: 100 | `adb shell "dumpsys activity activities | grep mResumedActivity"` 101 | 102 | Option 2 : 103 | Install APK info app to retrieve appPackage and appActivity for the app installed in your device 104 | 105 | ## :pushpin: Inspecting Elements 106 | 107 | ### uiautomatorviewer 108 | 109 | 1) Go to the path - `\tools\bin\` 110 | 2) click on `uiautomatorviewer` 111 | 3) On the UI Automator Viewer, click on Device Screenshot (uiautomator dump). Ui automator will capture the screenshot 112 | of current open screen in the device. 113 | 114 | UiAutomatorViewer 115 | 116 | ### Appium Inspector 117 | 118 | 1) Start the Appium Server and connect with Real device/Emulator. 119 | 2) Open Appium Inspector app and provide the appium server details and Desired Capabilities. 120 | 121 | Appium Inspector 122 | 123 | 3) Click on Start session which will start the appium inspector with layout shown below. 124 | 125 | Appium 126 | 127 | ## :pushpin: Inspecting Element for mobile web browser 128 | 129 | ``` 130 | Type url `chrome://inspect/#devices` in the desktop chrome browser and start inspecting element 131 | ``` 132 | 133 | Capture 134 | 135 | ## :pushpin: Launching Android Emulator Automatically 136 | 137 | Add below lines in the Desired capabilities 138 | 139 | ``` 140 | capability.setCapability(AndroidMobileCapabilityType.AVD, "Pixel_3a"); 141 | capability.setCapability(AndroidMobileCapabilityType.AVD_LAUNCH_TIMEOUT, "180000"); 142 | ``` 143 | 144 | ## :pushpin: Auto Discovery of compatible ChromeDriver 145 | 146 | Start appium server using command `appium --allow-insecure chromedriver_autodownload` 147 | 148 | ## :pushpin: Auto download of compatible ChromeDriver programmatically 149 | 150 | Add below line in the `AppiumServiceBuilder` 151 | 152 | ``` 153 | AppiumServiceBuilder builder = new AppiumServiceBuilder(); 154 | builder.withArgument(GeneralServerFlag.ALLOW_INSECURE, "chromedriver_autodownload"); 155 | ``` 156 | 157 | ## :pushpin: Start Appium server programmatically 158 | 159 | Use `AppiumServiceBuilder` and `AppiumDriverLocalService` to start the server programmatically Set environment 160 | variable `APPIUM_HOME = \node_modules\appium\build\lib` where `main.js` file is present 161 | 162 | ## :pushpin: Running tests through Test Runner (JUnit) 163 | 164 | :point_right: Run `RunCucumberTest` in the 165 | path `appium-mobile-automation-framework-bdd\src\test\java\org\example\runner\RunCucumberTest.java` 166 | 167 | ## :pushpin: Running tests in parallel on multiple devices through Maven CLI 168 | 169 | :point_right: Run test using following command 170 | 171 | ``` 172 | mvn clean test -DplatformName= -DdeviceName= -Dudid= -DsystemPort= -DchromedriverPort= 173 | 174 | Example: 175 | mvn clean test -DplatformName=android -DdeviceName=Pixel_4 -Dudid=emulator-5554 -DsystemPort=7070 -DchromedriverPort=7071 176 | ``` 177 | 178 | ## :pushpin: Config Json for updating DesiredCapabilities 179 | 180 | Capture 181 | 182 | ## :pushpin: Cucumber-html-reports 183 | 184 | Cucumber-html-report 185 | -------------------------------------------------------------------------------- /checkstyle/checkstyle-suppression.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /checkstyle/checkstyle.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 4.0.0 6 | 7 | org.example 8 | appium-mobile-automation-framework-bdd 9 | 1.0-SNAPSHOT 10 | appium-mobile-automation-framework-bdd 11 | http://maven.apache.org 12 | 13 | 14 | UTF-8 15 | 11 16 | 11 17 | 7.10.1 18 | 8.3.0 19 | 2.14.0 20 | 1.18.24 21 | 2.11.0 22 | 20220924 23 | 25.0.2 24 | 5.7.4 25 | 3.8.1 26 | 3.0.0-M5 27 | 3.2.0 28 | checkstyle/checkstyle.xml 29 | checkstyle/checkstyle-suppression.xml 30 | 31 | 32 | 33 | 34 | 35 | io.cucumber 36 | cucumber-junit 37 | ${cucumber.version} 38 | test 39 | 40 | 41 | 42 | 43 | io.cucumber 44 | cucumber-java 45 | ${cucumber.version} 46 | 47 | 48 | 49 | 50 | io.cucumber 51 | cucumber-picocontainer 52 | ${cucumber.version} 53 | 54 | 55 | 56 | 57 | io.cucumber 58 | gherkin 59 | ${gherkin.version} 60 | 61 | 62 | 63 | 64 | io.appium 65 | java-client 66 | ${appium-java-client.version} 67 | 68 | 69 | 70 | 71 | commons-io 72 | commons-io 73 | ${commons-io.version} 74 | 75 | 76 | 77 | 78 | commons-codec 79 | commons-codec 80 | 1.15 81 | 82 | 83 | 84 | 85 | org.json 86 | json 87 | ${json.version} 88 | 89 | 90 | 91 | 92 | com.fasterxml.jackson.core 93 | jackson-databind 94 | ${jackson.version} 95 | 96 | 97 | 98 | 99 | com.fasterxml.jackson.core 100 | jackson-core 101 | ${jackson.version} 102 | 103 | 104 | 105 | 106 | org.projectlombok 107 | lombok 108 | ${lombok.version} 109 | 110 | 111 | 112 | net.masterthought 113 | maven-cucumber-reporting 114 | ${maven-cucumber-reporting.version} 115 | 116 | 117 | 118 | 119 | org.apache.maven.plugins 120 | maven-checkstyle-plugin 121 | ${maven-checkstyle-plugin.version} 122 | 123 | 124 | org.seleniumhq.selenium 125 | selenium-support 126 | 4.5.0 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | org.apache.maven.plugins 135 | maven-compiler-plugin 136 | ${maven-compiler-plugin.version} 137 | 138 | 11 139 | 11 140 | 141 | 142 | 143 | 144 | org.apache.maven.plugins 145 | maven-surefire-plugin 146 | ${maven-surefire-plugin.version} 147 | 148 | 149 | 150 | org.apache.maven.plugins 151 | maven-checkstyle-plugin 152 | ${maven-checkstyle-plugin.version} 153 | 154 | 155 | verify-style 156 | verify 157 | 158 | check 159 | 160 | 161 | 162 | 163 | ${checkstyle.config} 164 | ${checkstyle.suppress} 165 | UTF-8 166 | UTF-8 167 | true 168 | true 169 | true 170 | true 171 | 172 | 173 | 174 | 175 | net.masterthought 176 | maven-cucumber-reporting 177 | ${maven-cucumber-reporting.version} 178 | 179 | 180 | execution 181 | test 182 | 183 | generate 184 | 185 | 186 | Mobile - Test Automation Framework 187 | ${project.build.directory}/cucumber-maven-html-report 188 | ${project.build.directory}/cucumber-json-output 189 | 190 | **/cucumber.json 191 | 192 | true 193 | 194 | false 195 | 197 | false 198 | 199 | false 200 | 201 | ${project.version} 202 | ${os.name} 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | -------------------------------------------------------------------------------- /src/main/java/org/example/constants/FrameworkConstants.java: -------------------------------------------------------------------------------- 1 | package org.example.constants; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.NoArgsConstructor; 5 | import org.example.enums.ConfigProperties; 6 | import org.example.utils.configloader.PropertyUtils; 7 | 8 | import java.io.File; 9 | import java.time.LocalDateTime; 10 | import java.time.format.DateTimeFormatter; 11 | 12 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 13 | public final class FrameworkConstants { 14 | 15 | public static final String PROJECT_PATH = System.getProperty("user.dir"); 16 | public static final String RESOURCES_PATH = "src/test/resources"; 17 | public static final String APPIUM_SERVER_HOST = "127.0.0.1"; 18 | public static final int APPIUM_SERVER_PORT = 4723; 19 | public static final String APPIUM_JS_PATH = System.getenv("APPIUM_JS"); 20 | public static final String ANDROID_APK_PATH = 21 | PROJECT_PATH + File.separator + RESOURCES_PATH + File.separator + "app" + File.separator + 22 | "Android.SauceLabs.Mobile.Sample.app.2.7.1.apk"; 23 | public static final String CONFIG_PROPERTIES_PATH = 24 | PROJECT_PATH + File.separator + RESOURCES_PATH + File.separator + "config" + File.separator + "config.properties"; 25 | public static final String CONFIG_JSON_PATH = 26 | PROJECT_PATH + File.separator + RESOURCES_PATH + File.separator + "config" + File.separator + "config.json"; 27 | public static final long EXPLICIT_WAIT = 15; 28 | public static final String IOS_APP_PATH = ""; 29 | public static final String SCREENSHOT_PATH = PROJECT_PATH + File.separator + "screenshots"; 30 | public static final String NODEJS_PATH = System.getenv("NODE_HOME") + File.separator + "node.exe"; 31 | 32 | private static final String SERVER_LOGS_PATH = PROJECT_PATH + File.separator + "server-logs"; 33 | private static final String SCREEN_RECORDINGS_PATH = PROJECT_PATH + File.separator + "screen-recordings"; 34 | 35 | public static String getAppiumServerLogsPath() { 36 | if (PropertyUtils.getPropertyValue(ConfigProperties.OVERRIDE_SERVER_LOG).equalsIgnoreCase("yes")) 37 | return SERVER_LOGS_PATH + File.separator + "server.log"; 38 | else return SERVER_LOGS_PATH + File.separator + getCurrentDateTime() + File.separator + "server.log"; 39 | } 40 | 41 | public static String getScreenRecordingsPath() { 42 | var screenRecordingsDir = new File(SCREEN_RECORDINGS_PATH); 43 | if (!screenRecordingsDir.exists()) 44 | screenRecordingsDir.mkdir(); 45 | return SCREEN_RECORDINGS_PATH; 46 | } 47 | 48 | private static String getCurrentDateTime() { 49 | DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy_MM_dd-HH_mm_ss"); 50 | LocalDateTime localDateTime = LocalDateTime.now(); 51 | return dateTimeFormatter.format(localDateTime); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/org/example/customexceptions/DriverInitializationException.java: -------------------------------------------------------------------------------- 1 | package org.example.customexceptions; 2 | 3 | public class DriverInitializationException extends FrameworkException { 4 | 5 | public DriverInitializationException(String message) { 6 | super(message); 7 | } 8 | 9 | public DriverInitializationException(String message, Throwable t) { 10 | super(message, t); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/org/example/customexceptions/FrameworkException.java: -------------------------------------------------------------------------------- 1 | package org.example.customexceptions; 2 | 3 | public class FrameworkException extends RuntimeException { 4 | 5 | public FrameworkException(String message) { 6 | super(message); 7 | } 8 | 9 | public FrameworkException(String message, Throwable t) { 10 | super(message, t); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/org/example/customexceptions/JsonFileUsageException.java: -------------------------------------------------------------------------------- 1 | package org.example.customexceptions; 2 | 3 | public class JsonFileUsageException extends FrameworkException { 4 | 5 | public JsonFileUsageException(String message) { 6 | super(message); 7 | } 8 | 9 | public JsonFileUsageException(String message, Throwable t) { 10 | super(message, t); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/org/example/customexceptions/PropertyFileUsageException.java: -------------------------------------------------------------------------------- 1 | package org.example.customexceptions; 2 | 3 | public class PropertyFileUsageException extends FrameworkException { 4 | 5 | public PropertyFileUsageException(String message) { 6 | super(message); 7 | } 8 | 9 | public PropertyFileUsageException(String message, Throwable t) { 10 | super(message, t); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/org/example/driver/Drivers.java: -------------------------------------------------------------------------------- 1 | package org.example.driver; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.NoArgsConstructor; 5 | import org.example.driver.factory.DriverFactory; 6 | import org.example.driver.manager.DriverManager; 7 | import org.example.enums.MobilePlatformName; 8 | 9 | import java.util.Objects; 10 | 11 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 12 | public final class Drivers { 13 | 14 | public static void initializeDriver(MobilePlatformName mobilePlatformName) { 15 | DriverManager.setAppiumDriver(DriverFactory.getDriver(mobilePlatformName)); 16 | } 17 | 18 | public static void quitDriver() { 19 | if (Objects.nonNull(DriverManager.getDriver())) { 20 | DriverManager.getDriver().quit(); 21 | DriverManager.unload(); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/org/example/driver/factory/DriverFactory.java: -------------------------------------------------------------------------------- 1 | package org.example.driver.factory; 2 | 3 | import io.appium.java_client.AppiumDriver; 4 | import lombok.AccessLevel; 5 | import lombok.NoArgsConstructor; 6 | import org.example.driver.manager.AndroidManager; 7 | import org.example.driver.manager.IosManager; 8 | import org.example.enums.MobilePlatformName; 9 | 10 | import java.util.EnumMap; 11 | import java.util.Map; 12 | import java.util.function.Supplier; 13 | 14 | import static org.example.enums.MobilePlatformName.ANDROID; 15 | import static org.example.enums.MobilePlatformName.IOS; 16 | 17 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 18 | public final class DriverFactory { 19 | 20 | private static final Map> DRIVER_TYPE_MAP = 21 | new EnumMap<>(MobilePlatformName.class); 22 | 23 | static { 24 | DRIVER_TYPE_MAP.put(ANDROID, AndroidManager::createAndroidDriver); 25 | DRIVER_TYPE_MAP.put(IOS, IosManager::createIOSDriver); 26 | } 27 | 28 | public static AppiumDriver getDriver(MobilePlatformName mobilePlatformName) { 29 | return DRIVER_TYPE_MAP.get(mobilePlatformName).get(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/org/example/driver/manager/AndroidManager.java: -------------------------------------------------------------------------------- 1 | package org.example.driver.manager; 2 | 3 | import io.appium.java_client.AppiumDriver; 4 | import io.appium.java_client.android.AndroidDriver; 5 | import io.appium.java_client.android.options.UiAutomator2Options; 6 | import lombok.AccessLevel; 7 | import lombok.NoArgsConstructor; 8 | import org.example.constants.FrameworkConstants; 9 | import org.example.customexceptions.DriverInitializationException; 10 | import org.example.enums.ConfigJson; 11 | 12 | import java.net.URL; 13 | import java.time.Duration; 14 | import java.util.Optional; 15 | 16 | import static org.example.utils.configloader.JsonParser.getConfig; 17 | 18 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 19 | public final class AndroidManager { 20 | 21 | public static AppiumDriver createAndroidDriver() { 22 | try { 23 | UiAutomator2Options options = new UiAutomator2Options(); 24 | options.setDeviceName(Optional.ofNullable(System.getProperty("deviceName")) 25 | .orElse(getConfig(ConfigJson.ANDROID_DEVICE_NAME))) 26 | .setUdid(Optional.ofNullable(System.getProperty("udid")) 27 | .orElse(getConfig(ConfigJson.ANDROID_UDID))) 28 | .setApp(FrameworkConstants.ANDROID_APK_PATH) 29 | .setAppPackage(getConfig(ConfigJson.ANDROID_APP_PACKAGE)) 30 | .setAppActivity(getConfig(ConfigJson.ANDROID_APP_ACTIVITY)) 31 | .setSystemPort(Integer.parseInt(Optional.ofNullable(System.getProperty("systemPort")) 32 | .orElse(getConfig( 33 | ConfigJson.ANDROID_SYSTEM_PORT)))); 34 | if (getConfig(ConfigJson.ANDROID_EMULATOR).equalsIgnoreCase("yes")) { 35 | options.setAvd(Optional.ofNullable(System.getProperty("deviceName")) 36 | .orElse(getConfig(ConfigJson.ANDROID_DEVICE_NAME))) 37 | .setAvdLaunchTimeout(Duration.ofMillis(Long.parseLong(getConfig(ConfigJson.AVD_LAUNCH_TIMEOUT)))); 38 | } 39 | return new AndroidDriver(new URL(getConfig(ConfigJson.APPIUM_URL)), options); 40 | } catch (Exception e) { 41 | throw new DriverInitializationException("Failed to initialize driver. Please check the desired capabilities", e); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/org/example/driver/manager/DeviceManager.java: -------------------------------------------------------------------------------- 1 | package org.example.driver.manager; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.NoArgsConstructor; 5 | 6 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 7 | public final class DeviceManager { 8 | 9 | private static final ThreadLocal DEVICE_NAME = new ThreadLocal<>(); 10 | 11 | public static String getDeviceName() { 12 | return DEVICE_NAME.get(); 13 | } 14 | 15 | public static void setDeviceName(String device) { 16 | DEVICE_NAME.set(device); 17 | } 18 | 19 | public static void removeDeviceName() { 20 | DEVICE_NAME.remove(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/org/example/driver/manager/DriverManager.java: -------------------------------------------------------------------------------- 1 | package org.example.driver.manager; 2 | 3 | import io.appium.java_client.AppiumDriver; 4 | import lombok.AccessLevel; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.util.Objects; 8 | 9 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 10 | public final class DriverManager { 11 | 12 | private static final ThreadLocal DRIVER_THREAD_LOCAL = new ThreadLocal<>(); 13 | 14 | public static AppiumDriver getDriver() { 15 | return DRIVER_THREAD_LOCAL.get(); 16 | } 17 | 18 | public static void setAppiumDriver(AppiumDriver driver) { 19 | if (Objects.nonNull(driver)) 20 | DRIVER_THREAD_LOCAL.set(driver); 21 | } 22 | 23 | public static void unload() { 24 | DRIVER_THREAD_LOCAL.remove(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/org/example/driver/manager/IosManager.java: -------------------------------------------------------------------------------- 1 | package org.example.driver.manager; 2 | 3 | import io.appium.java_client.AppiumDriver; 4 | import io.appium.java_client.ios.IOSDriver; 5 | import io.appium.java_client.remote.AutomationName; 6 | import io.appium.java_client.remote.IOSMobileCapabilityType; 7 | import io.appium.java_client.remote.MobileCapabilityType; 8 | import lombok.AccessLevel; 9 | import lombok.NoArgsConstructor; 10 | import org.example.constants.FrameworkConstants; 11 | import org.example.customexceptions.DriverInitializationException; 12 | import org.example.enums.ConfigJson; 13 | import org.openqa.selenium.Platform; 14 | import org.openqa.selenium.remote.DesiredCapabilities; 15 | 16 | import java.net.URL; 17 | import java.util.Optional; 18 | 19 | import static org.example.utils.configloader.JsonParser.getConfig; 20 | import static org.openqa.selenium.remote.CapabilityType.PLATFORM_NAME; 21 | 22 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 23 | public final class IosManager { 24 | 25 | public static AppiumDriver createIOSDriver() { 26 | try { 27 | var capability = new DesiredCapabilities(); 28 | capability.setCapability(PLATFORM_NAME, Platform.IOS); 29 | capability.setCapability(MobileCapabilityType.AUTOMATION_NAME, AutomationName.IOS_XCUI_TEST); 30 | capability.setCapability(MobileCapabilityType.DEVICE_NAME, Optional.ofNullable(System.getProperty("deviceName")) 31 | .orElse(getConfig(ConfigJson.IOS_DEVICE_NAME))); 32 | capability.setCapability(MobileCapabilityType.UDID, Optional.ofNullable(System.getProperty("udid")) 33 | .orElse(getConfig(ConfigJson.IOS_UDID))); 34 | capability.setCapability(MobileCapabilityType.APP, FrameworkConstants.IOS_APP_PATH); 35 | capability.setCapability(IOSMobileCapabilityType.BUNDLE_ID, getConfig(ConfigJson.IOS_BUNDLE_ID)); 36 | capability.setCapability(IOSMobileCapabilityType.WDA_LOCAL_PORT, Optional.ofNullable(System.getProperty("wdaLocalPort")) 37 | .orElse(getConfig( 38 | ConfigJson.IOS_WDA_LOCAL_PORT))); // To set different port for each thread - This port is used to communicate with WebDriverAgent driver 39 | // capability.setCapability(CapabilityType.BROWSER_NAME, MobileBrowserName.SAFARI); 40 | capability.setCapability("webkitDebugProxyPort", Optional.ofNullable(System.getProperty("webkitDebugProxyPort")) 41 | .orElse(getConfig(ConfigJson.IOS_WEBKIT_DEBUG_PROXY_PORT))); // For web view/Safari browser testing on real device 42 | 43 | return new IOSDriver(new URL(getConfig(ConfigJson.APPIUM_URL)), capability); 44 | } catch (Exception e) { 45 | throw new DriverInitializationException("Failed to initialize driver. Please check the desired capabilities", e); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/org/example/driver/manager/PlatformManager.java: -------------------------------------------------------------------------------- 1 | package org.example.driver.manager; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.NoArgsConstructor; 5 | 6 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 7 | public final class PlatformManager { 8 | 9 | private static final ThreadLocal PLATFORM_NAME = new ThreadLocal<>(); 10 | 11 | public static String getPlatformName() { 12 | return PLATFORM_NAME.get(); 13 | } 14 | 15 | public static void setPlatformName(String platform) { 16 | PLATFORM_NAME.set(platform); 17 | } 18 | 19 | public static void removePlatformName() { 20 | PLATFORM_NAME.remove(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/org/example/enums/ConfigJson.java: -------------------------------------------------------------------------------- 1 | package org.example.enums; 2 | 3 | public enum ConfigJson { 4 | ANDROID_APP_ACTIVITY, ANDROID_APP_PACKAGE, ANDROID_DEVICE_NAME, 5 | ANDROID_UDID, ANDROID_SYSTEM_PORT, ANDROID_CHROMEDRIVER_PORT, ANDROID_EMULATOR, 6 | APPIUM_URL, AVD_LAUNCH_TIMEOUT, 7 | IOS_DEVICE_NAME, IOS_UDID, IOS_WDA_LOCAL_PORT, IOS_WEBKIT_DEBUG_PROXY_PORT, 8 | IOS_BUNDLE_ID, 9 | PLATFORM 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/org/example/enums/ConfigProperties.java: -------------------------------------------------------------------------------- 1 | package org.example.enums; 2 | 3 | public enum ConfigProperties { 4 | RECORD_SCREEN, 5 | START_APPIUM_SERVER, 6 | OVERRIDE_SERVER_LOG 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/org/example/enums/MobileBrowserName.java: -------------------------------------------------------------------------------- 1 | package org.example.enums; 2 | 3 | public enum MobileBrowserName { 4 | CHROME, 5 | SAFARI 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/org/example/enums/MobileFindBy.java: -------------------------------------------------------------------------------- 1 | package org.example.enums; 2 | 3 | public enum MobileFindBy { 4 | XPATH, CSS, ID, NAME, CLASS, ACCESSIBILITY_ID 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/org/example/enums/MobilePlatformName.java: -------------------------------------------------------------------------------- 1 | package org.example.enums; 2 | 3 | public enum MobilePlatformName { 4 | ANDROID, 5 | IOS 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/org/example/enums/WaitStrategy.java: -------------------------------------------------------------------------------- 1 | package org.example.enums; 2 | 3 | public enum WaitStrategy { 4 | CLICKABLE, PRESENCE, VISIBLE, NONE 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/org/example/factories/WaitFactory.java: -------------------------------------------------------------------------------- 1 | package org.example.factories; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.NoArgsConstructor; 5 | import org.example.constants.FrameworkConstants; 6 | import org.example.driver.manager.DriverManager; 7 | import org.example.enums.WaitStrategy; 8 | import org.openqa.selenium.By; 9 | import org.openqa.selenium.WebElement; 10 | import org.openqa.selenium.support.ui.ExpectedConditions; 11 | import org.openqa.selenium.support.ui.WebDriverWait; 12 | 13 | import java.time.Duration; 14 | import java.util.EnumMap; 15 | import java.util.Map; 16 | import java.util.function.Function; 17 | 18 | import static org.example.enums.WaitStrategy.CLICKABLE; 19 | import static org.example.enums.WaitStrategy.NONE; 20 | import static org.example.enums.WaitStrategy.PRESENCE; 21 | import static org.example.enums.WaitStrategy.VISIBLE; 22 | 23 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 24 | public final class WaitFactory { 25 | 26 | private static final Map> WAIT_STRATEGY_FUNCTION_MAP = 27 | new EnumMap<>(WaitStrategy.class); 28 | 29 | private static final Function CLICKABLE_BY_WEB_ELEMENT_FUNCTION = 30 | by -> new WebDriverWait(DriverManager.getDriver(), Duration.ofSeconds(FrameworkConstants.EXPLICIT_WAIT)) 31 | .until(ExpectedConditions.elementToBeClickable(by)); 32 | private static final Function PRESENCE_BY_WEB_ELEMENT_FUNCTION = 33 | by -> new WebDriverWait(DriverManager.getDriver(), Duration.ofSeconds(FrameworkConstants.EXPLICIT_WAIT)) 34 | .until(ExpectedConditions.presenceOfElementLocated(by)); 35 | private static final Function VISIBLE_BY_WEB_ELEMENT_FUNCTION = 36 | by -> new WebDriverWait(DriverManager.getDriver(), Duration.ofSeconds(FrameworkConstants.EXPLICIT_WAIT)) 37 | .until(ExpectedConditions.visibilityOfElementLocated(by)); 38 | private static final Function NONE_BY_WEB_ELEMENT_FUNCTION = by -> DriverManager.getDriver().findElement(by); 39 | 40 | private static final Map> WAIT_STRATEGY_MOBILE_ELEMENT_FUNCTION_MAP = 41 | new EnumMap<>(WaitStrategy.class); 42 | 43 | private static final Function CLICKABLE_MOBILE_ELEMENT_FUNCTION = 44 | mobileElement -> new WebDriverWait(DriverManager.getDriver(), Duration.ofSeconds(FrameworkConstants.EXPLICIT_WAIT)) 45 | .until(ExpectedConditions.elementToBeClickable(mobileElement)); 46 | private static final Function VISIBLE_MOBILE_ELEMENT_FUNCTION = 47 | mobileElement -> new WebDriverWait(DriverManager.getDriver(), Duration.ofSeconds(FrameworkConstants.EXPLICIT_WAIT)) 48 | .until(ExpectedConditions.visibilityOf(mobileElement)); 49 | private static final Function NONE_MOBILE_ELEMENT_FUNCTION = mobileElement -> mobileElement; 50 | 51 | static { 52 | WAIT_STRATEGY_FUNCTION_MAP.put(CLICKABLE, CLICKABLE_BY_WEB_ELEMENT_FUNCTION); 53 | WAIT_STRATEGY_FUNCTION_MAP.put(PRESENCE, PRESENCE_BY_WEB_ELEMENT_FUNCTION); 54 | WAIT_STRATEGY_FUNCTION_MAP.put(VISIBLE, VISIBLE_BY_WEB_ELEMENT_FUNCTION); 55 | WAIT_STRATEGY_FUNCTION_MAP.put(NONE, NONE_BY_WEB_ELEMENT_FUNCTION); 56 | WAIT_STRATEGY_MOBILE_ELEMENT_FUNCTION_MAP.put(CLICKABLE, CLICKABLE_MOBILE_ELEMENT_FUNCTION); 57 | WAIT_STRATEGY_MOBILE_ELEMENT_FUNCTION_MAP.put(VISIBLE, VISIBLE_MOBILE_ELEMENT_FUNCTION); 58 | WAIT_STRATEGY_MOBILE_ELEMENT_FUNCTION_MAP.put(NONE, NONE_MOBILE_ELEMENT_FUNCTION); 59 | } 60 | 61 | public static WebElement explicitlyWaitForElementLocatedBy(WaitStrategy waitStrategy, By by) { 62 | return WAIT_STRATEGY_FUNCTION_MAP.get(waitStrategy).apply(by); 63 | } 64 | 65 | public static WebElement explicitlyWaitForElement(WaitStrategy waitStrategy, WebElement mobileElement) { 66 | return WAIT_STRATEGY_MOBILE_ELEMENT_FUNCTION_MAP.get(waitStrategy).apply(mobileElement); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/org/example/pageobjects/LoginPage.java: -------------------------------------------------------------------------------- 1 | package org.example.pageobjects; 2 | 3 | import io.appium.java_client.pagefactory.AndroidFindBy; 4 | import io.appium.java_client.pagefactory.iOSXCUITFindBy; 5 | import org.example.enums.WaitStrategy; 6 | import org.example.pageobjects.screen.ScreenActions; 7 | import org.openqa.selenium.WebElement; 8 | 9 | public class LoginPage extends ScreenActions { 10 | 11 | @AndroidFindBy(accessibility = "test-Username") 12 | @iOSXCUITFindBy(accessibility = "test-Username") 13 | private static WebElement txtFieldUsername; 14 | 15 | @AndroidFindBy(accessibility = "test-Password") 16 | @iOSXCUITFindBy(accessibility = "test-Password") 17 | private static WebElement txtFieldPassword; 18 | 19 | @AndroidFindBy(accessibility = "test-LOGIN") 20 | @iOSXCUITFindBy(accessibility = "test-LOGIN") 21 | private static WebElement btnLogin; 22 | 23 | @AndroidFindBy(xpath = "//android.view.ViewGroup[@content-desc='test-Error message']/android.widget.TextView") 24 | private static WebElement errorMessage; 25 | 26 | public boolean isLoginPageDisplayed() { 27 | return isElementDisplayed(txtFieldUsername); 28 | } 29 | 30 | public LoginPage setUsername(String username) { 31 | enter(txtFieldUsername, username); 32 | return this; 33 | } 34 | 35 | public LoginPage setPassword(String password) { 36 | enter(txtFieldPassword, password); 37 | return this; 38 | } 39 | 40 | public ProductPage tapOnLogin() { 41 | click(btnLogin); 42 | return new ProductPage(); 43 | } 44 | 45 | public ProductPage login(String username, String password) { 46 | setUsername(username) 47 | .setPassword(password) 48 | .tapOnLogin(); 49 | 50 | return new ProductPage(); 51 | } 52 | 53 | public String getErrorText() { 54 | return getText(errorMessage, WaitStrategy.VISIBLE); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/org/example/pageobjects/ProductPage.java: -------------------------------------------------------------------------------- 1 | package org.example.pageobjects; 2 | 3 | import io.appium.java_client.pagefactory.AndroidFindBy; 4 | import org.example.enums.WaitStrategy; 5 | import org.example.pageobjects.screen.ScreenActions; 6 | import org.openqa.selenium.WebElement; 7 | 8 | public class ProductPage extends ScreenActions { 9 | 10 | @AndroidFindBy(xpath = "//android.view.ViewGroup[@content-desc=\"test-Cart drop zone\"]/android.view.ViewGroup/android.widget.TextView") 11 | private static WebElement productPageTitle; 12 | 13 | public String getProductPageTitle() { 14 | return getText(productPageTitle, WaitStrategy.VISIBLE); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/org/example/pageobjects/screen/ScreenActions.java: -------------------------------------------------------------------------------- 1 | package org.example.pageobjects.screen; 2 | 3 | import com.google.common.collect.ImmutableMap; 4 | import com.google.common.collect.Ordering; 5 | import io.appium.java_client.AppiumBy; 6 | import io.appium.java_client.android.AndroidDriver; 7 | import io.appium.java_client.android.PowerACState; 8 | import io.appium.java_client.pagefactory.AppiumFieldDecorator; 9 | import org.example.driver.manager.DriverManager; 10 | import org.example.enums.MobileFindBy; 11 | import org.example.enums.WaitStrategy; 12 | import org.example.factories.WaitFactory; 13 | import org.openqa.selenium.Keys; 14 | import org.openqa.selenium.WebElement; 15 | import org.openqa.selenium.interactions.Actions; 16 | import org.openqa.selenium.support.PageFactory; 17 | 18 | import java.util.EnumMap; 19 | import java.util.HashMap; 20 | import java.util.List; 21 | import java.util.Map; 22 | import java.util.concurrent.TimeUnit; 23 | import java.util.function.Consumer; 24 | import java.util.function.Function; 25 | 26 | import static org.example.enums.MobileFindBy.ACCESSIBILITY_ID; 27 | import static org.example.enums.MobileFindBy.CLASS; 28 | import static org.example.enums.MobileFindBy.CSS; 29 | import static org.example.enums.MobileFindBy.ID; 30 | import static org.example.enums.MobileFindBy.NAME; 31 | import static org.example.enums.MobileFindBy.XPATH; 32 | 33 | public class ScreenActions { 34 | 35 | private final Map> mobileFindByFunctionMap = new EnumMap(MobileFindBy.class); 36 | private final Function findByXpath = 37 | mobileElement -> DriverManager.getDriver().findElement(AppiumBy.xpath(mobileElement)); 38 | private final Function findByCss = 39 | mobileElement -> DriverManager.getDriver().findElement(AppiumBy.cssSelector(mobileElement)); 40 | private final Function findById = 41 | mobileElement -> DriverManager.getDriver().findElement(AppiumBy.id(mobileElement)); 42 | private final Function findByName = 43 | mobileElement -> DriverManager.getDriver().findElement(AppiumBy.name(mobileElement)); 44 | private final Function findByAccessibilityId = 45 | mobileElement -> DriverManager.getDriver().findElement(AppiumBy.accessibilityId(mobileElement)); 46 | private final Function findByClassName = 47 | mobileElement -> DriverManager.getDriver().findElement(AppiumBy.className(mobileElement)); 48 | 49 | protected ScreenActions() { 50 | PageFactory.initElements(new AppiumFieldDecorator(DriverManager.getDriver()), this); 51 | } 52 | 53 | private WebElement getMobileElement(String mobileElement, MobileFindBy mobileFindBy) { 54 | if (mobileFindByFunctionMap.isEmpty()) { 55 | mobileFindByFunctionMap.put(XPATH, findByXpath); 56 | mobileFindByFunctionMap.put(CSS, findByCss); 57 | mobileFindByFunctionMap.put(ID, findById); 58 | mobileFindByFunctionMap.put(NAME, findByName); 59 | mobileFindByFunctionMap.put(ACCESSIBILITY_ID, findByAccessibilityId); 60 | mobileFindByFunctionMap.put(CLASS, findByClassName); 61 | } 62 | return mobileFindByFunctionMap.get(mobileFindBy).apply(mobileElement); 63 | } 64 | 65 | protected void waitForPageLoad(int waitTime) { 66 | DriverManager.getDriver().manage().timeouts().pageLoadTimeout(waitTime, TimeUnit.SECONDS); 67 | } 68 | 69 | protected String getTextFromAttribute(WaitStrategy waitStrategy, WebElement element) { 70 | return WaitFactory.explicitlyWaitForElement(waitStrategy, element).getAttribute("text"); 71 | } 72 | 73 | protected String getText(WebElement element, WaitStrategy waitStrategy) { 74 | return WaitFactory.explicitlyWaitForElement(waitStrategy, element).getText(); 75 | } 76 | 77 | protected boolean isElementDisplayed(WebElement element) { 78 | return element.isDisplayed(); 79 | } 80 | 81 | protected void doClear(WebElement element) { 82 | element.clear(); 83 | } 84 | 85 | protected void getServerStatus() { 86 | DriverManager.getDriver().getStatus(); 87 | } 88 | 89 | protected String getElementAttribute(WebElement element, String attributeName) { 90 | return element.getAttribute(attributeName); 91 | } 92 | 93 | protected WebElement getActiveElement() { 94 | return DriverManager.getDriver().switchTo().activeElement(); 95 | } 96 | 97 | protected void moveMouseToElement(WebElement element, int x_offset, int y_offset) { 98 | new Actions(DriverManager.getDriver()) 99 | .moveToElement(element, x_offset, y_offset) 100 | .perform(); 101 | } 102 | 103 | protected void doubleClickOnElement(WebElement element) { 104 | new Actions(DriverManager.getDriver()) 105 | .moveToElement(element) 106 | .doubleClick() 107 | .perform(); 108 | } 109 | 110 | protected void click(WebElement element) { 111 | element.click(); 112 | } 113 | 114 | protected void click(String element, MobileFindBy elementType) { 115 | click(getMobileElement(element, elementType)); 116 | } 117 | 118 | protected void enter(WebElement element, String value) { 119 | WaitFactory.explicitlyWaitForElement(WaitStrategy.VISIBLE, element); 120 | doClear(element); 121 | element.sendKeys(value); 122 | } 123 | 124 | protected void enterValueAndPressEnter(WebElement element, String value) { 125 | doClear(element); 126 | element.sendKeys(value, Keys.ENTER); 127 | } 128 | 129 | protected void enter(String element, MobileFindBy elementType, String value) { 130 | enter(getMobileElement(element, elementType), value); 131 | } 132 | 133 | protected boolean isTextPresent(String containsText) { 134 | return DriverManager.getDriver().getPageSource().contains(containsText); 135 | } 136 | 137 | protected void powerStateAndroid(PowerACState powerACState) { 138 | Consumer powerStateConsumer = state -> 139 | ((AndroidDriver) DriverManager.getDriver()).setPowerAC(state); 140 | powerStateConsumer.accept(powerACState); 141 | } 142 | 143 | /** 144 | * Swipe Down 145 | */ 146 | protected void swipeDown() { 147 | DriverManager.getDriver().executeScript("mobile:scroll", ImmutableMap.of("direction", "down")); 148 | } 149 | 150 | /** 151 | * Swipe Up 152 | */ 153 | protected void swipeUP() { 154 | DriverManager.getDriver().executeScript("mobile:scroll", ImmutableMap.of("direction", "up")); 155 | } 156 | 157 | /** 158 | * Accept Alert 159 | */ 160 | protected void acceptAlert() { 161 | DriverManager.getDriver().executeScript("mobile:acceptAlert"); 162 | } 163 | 164 | /** 165 | * Dismiss Alert 166 | */ 167 | protected void dismissAlert() { 168 | DriverManager.getDriver().executeScript("mobile:dismissAlert"); 169 | } 170 | 171 | /** 172 | * Scroll to specific location 173 | * 174 | * @param element element 175 | * @param value location 176 | */ 177 | protected void scrollToLocation(WebElement element, int value) { 178 | HashMap scrollElement = new HashMap<>(); 179 | scrollElement.put("startX", 0.50); 180 | scrollElement.put("startY", 0.95); 181 | scrollElement.put("endX", 0.50); 182 | scrollElement.put("endY", 0.01); 183 | scrollElement.put("duration", 3.0); 184 | DriverManager.getDriver().executeScript("mobile: swipe", scrollElement); 185 | } 186 | 187 | protected boolean checkListIsSorted(List listToSort) { 188 | if (!listToSort.isEmpty()) { 189 | return Ordering.natural().isOrdered(listToSort); 190 | } 191 | return false; 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/main/java/org/example/utils/AppiumServerManager.java: -------------------------------------------------------------------------------- 1 | package org.example.utils; 2 | 3 | import io.appium.java_client.service.local.AppiumDriverLocalService; 4 | import io.appium.java_client.service.local.AppiumServiceBuilder; 5 | import io.appium.java_client.service.local.flags.GeneralServerFlag; 6 | import lombok.AccessLevel; 7 | import lombok.NoArgsConstructor; 8 | import org.example.constants.FrameworkConstants; 9 | import org.example.enums.ConfigProperties; 10 | import org.example.utils.configloader.PropertyUtils; 11 | 12 | import java.io.File; 13 | import java.io.IOException; 14 | import java.net.ServerSocket; 15 | 16 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 17 | public final class AppiumServerManager { 18 | 19 | private static AppiumDriverLocalService service; 20 | 21 | static boolean checkIfServerIsRunning() { 22 | var isServerRunning = false; 23 | try { 24 | ServerSocket serverSocket = new ServerSocket(FrameworkConstants.APPIUM_SERVER_PORT); 25 | serverSocket.close(); 26 | } catch (IOException e) { 27 | isServerRunning = true; 28 | } 29 | return isServerRunning; 30 | } 31 | 32 | public static void startAppiumServer() { 33 | if (PropertyUtils.getPropertyValue(ConfigProperties.START_APPIUM_SERVER).equalsIgnoreCase("yes") && 34 | !AppiumServerManager.checkIfServerIsRunning()) { 35 | //Build the Appium service 36 | AppiumServiceBuilder builder = new AppiumServiceBuilder(); 37 | builder.usingDriverExecutable(new File(FrameworkConstants.NODEJS_PATH)); 38 | builder.withAppiumJS(new File(FrameworkConstants.APPIUM_JS_PATH)); 39 | builder.withIPAddress(FrameworkConstants.APPIUM_SERVER_HOST); 40 | builder.usingPort(FrameworkConstants.APPIUM_SERVER_PORT); 41 | builder.withArgument(GeneralServerFlag.SESSION_OVERRIDE); 42 | builder.withArgument(GeneralServerFlag.ALLOW_INSECURE, "chromedriver_autodownload"); 43 | builder.withLogFile(new File(FrameworkConstants.getAppiumServerLogsPath())); 44 | //Start the server with the builder 45 | service = AppiumDriverLocalService.buildService(builder); 46 | service.start(); 47 | service.clearOutPutStreams(); 48 | } 49 | } 50 | 51 | public static void stopAppiumServer() { 52 | if (PropertyUtils.getPropertyValue(ConfigProperties.START_APPIUM_SERVER).equalsIgnoreCase("yes")) { 53 | service.stop(); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/org/example/utils/configloader/JsonParser.java: -------------------------------------------------------------------------------- 1 | package org.example.utils.configloader; 2 | 3 | import com.fasterxml.jackson.core.type.TypeReference; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import lombok.AccessLevel; 6 | import lombok.NoArgsConstructor; 7 | import org.example.constants.FrameworkConstants; 8 | import org.example.customexceptions.JsonFileUsageException; 9 | import org.example.enums.ConfigJson; 10 | 11 | import java.io.File; 12 | import java.io.IOException; 13 | import java.util.HashMap; 14 | import java.util.Map; 15 | import java.util.Objects; 16 | 17 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 18 | public final class JsonParser { 19 | 20 | private static Map map; 21 | 22 | static void readJson(String jsonPath) { 23 | try { 24 | map = new ObjectMapper().readValue(new File(jsonPath), 25 | new TypeReference>() { 26 | }); 27 | } catch (IOException e) { 28 | throw new JsonFileUsageException("Exception occurred while reading Json file in the specified path"); 29 | } 30 | } 31 | 32 | public static String getConfig(ConfigJson key) { 33 | readJson(FrameworkConstants.CONFIG_JSON_PATH); 34 | if (Objects.isNull(map.get(key.name().toLowerCase()))) { 35 | throw new JsonFileUsageException("Property name - " + key + " is not found. Please check the config.json"); 36 | } 37 | return map.get(key.name().toLowerCase()); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/org/example/utils/configloader/PropertyUtils.java: -------------------------------------------------------------------------------- 1 | package org.example.utils.configloader; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.NoArgsConstructor; 5 | import org.example.constants.FrameworkConstants; 6 | import org.example.customexceptions.PropertyFileUsageException; 7 | import org.example.enums.ConfigProperties; 8 | 9 | import java.io.FileInputStream; 10 | import java.io.IOException; 11 | import java.util.Objects; 12 | import java.util.Properties; 13 | 14 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 15 | public final class PropertyUtils { 16 | 17 | private static final Properties property = new Properties(); 18 | 19 | static void loadProperties() { 20 | try (var input = new FileInputStream(FrameworkConstants.CONFIG_PROPERTIES_PATH)) { 21 | property.load(input); 22 | } catch (IOException e) { 23 | throw new PropertyFileUsageException("IOException occurred while loading Property file in the specified path"); 24 | } 25 | } 26 | 27 | public static String getPropertyValue(ConfigProperties key) { 28 | loadProperties(); 29 | if (Objects.isNull(property.getProperty(key.name().toLowerCase()))) { 30 | throw new PropertyFileUsageException("Property name - " + key + " is not found. Please check the config.properties"); 31 | } 32 | return property.getProperty(key.name().toLowerCase()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/org/example/utils/screenrecording/ScreenRecordingService.java: -------------------------------------------------------------------------------- 1 | package org.example.utils.screenrecording; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.NoArgsConstructor; 5 | import org.example.enums.ConfigProperties; 6 | import org.example.utils.configloader.PropertyUtils; 7 | 8 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 9 | public final class ScreenRecordingService { 10 | 11 | public static void startRecording() { 12 | if (PropertyUtils.getPropertyValue(ConfigProperties.RECORD_SCREEN).equalsIgnoreCase("yes")) { 13 | ScreenRecordingUtils.startScreenRecording(); 14 | } 15 | } 16 | 17 | public static void stopRecording(String methodName) { 18 | if (PropertyUtils.getPropertyValue(ConfigProperties.RECORD_SCREEN).equalsIgnoreCase("yes")) { 19 | ScreenRecordingUtils.stopScreenRecording(methodName); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/org/example/utils/screenrecording/ScreenRecordingUtils.java: -------------------------------------------------------------------------------- 1 | package org.example.utils.screenrecording; 2 | 3 | import io.appium.java_client.screenrecording.CanRecordScreen; 4 | import lombok.AccessLevel; 5 | import lombok.NoArgsConstructor; 6 | import org.apache.commons.codec.binary.Base64; 7 | import org.example.constants.FrameworkConstants; 8 | import org.example.driver.manager.DriverManager; 9 | 10 | import java.io.File; 11 | import java.io.FileOutputStream; 12 | import java.io.IOException; 13 | 14 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 15 | public final class ScreenRecordingUtils { 16 | 17 | public static void startScreenRecording() { 18 | ((CanRecordScreen) DriverManager.getDriver()).startRecordingScreen(); 19 | } 20 | 21 | public static void stopScreenRecording(String methodName) { 22 | var recordedVideoFile = ((CanRecordScreen) DriverManager.getDriver()).stopRecordingScreen(); 23 | var pathToWriteVideoFile = FrameworkConstants.getScreenRecordingsPath() + File.separator + methodName + ".mp4"; 24 | writeToOutputStream(pathToWriteVideoFile, recordedVideoFile); 25 | } 26 | 27 | static void writeToOutputStream(String filePathToWrite, String recordedVideoFile) { 28 | try (var outputStream = new FileOutputStream(filePathToWrite)) { 29 | outputStream.write(Base64.decodeBase64(recordedVideoFile)); 30 | } catch (IOException e) { 31 | e.printStackTrace(); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/org/example/utils/screenshot/ScreenshotService.java: -------------------------------------------------------------------------------- 1 | package org.example.utils.screenshot; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.NoArgsConstructor; 5 | 6 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 7 | public final class ScreenshotService { 8 | 9 | public static byte[] getScreenshotAsBytes() { 10 | return ScreenshotUtils.captureScreenshotAsBytes(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/org/example/utils/screenshot/ScreenshotUtils.java: -------------------------------------------------------------------------------- 1 | package org.example.utils.screenshot; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.NoArgsConstructor; 5 | import lombok.SneakyThrows; 6 | import org.apache.commons.io.FileUtils; 7 | import org.example.constants.FrameworkConstants; 8 | import org.example.driver.manager.DriverManager; 9 | import org.openqa.selenium.OutputType; 10 | import org.openqa.selenium.TakesScreenshot; 11 | 12 | import java.io.File; 13 | 14 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 15 | public final class ScreenshotUtils { 16 | 17 | // This class is to handle the change in third party library 18 | @SneakyThrows 19 | public static void captureScreenshotAsFile(String testName) { 20 | var source = ((TakesScreenshot) DriverManager.getDriver()).getScreenshotAs(OutputType.FILE); 21 | var destination = new File(FrameworkConstants.SCREENSHOT_PATH + File.separator + testName + ".png"); 22 | FileUtils.copyFile(source, destination); 23 | } 24 | 25 | public static byte[] captureScreenshotAsBytes() { 26 | return DriverManager.getDriver().getScreenshotAs(OutputType.BYTES); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/test/java/org/example/runner/RunnerTest.java: -------------------------------------------------------------------------------- 1 | package org.example.runner; 2 | 3 | import io.cucumber.junit.Cucumber; 4 | import io.cucumber.junit.CucumberOptions; 5 | import org.example.driver.Drivers; 6 | import org.example.driver.manager.DriverManager; 7 | import org.example.enums.ConfigJson; 8 | import org.example.enums.MobilePlatformName; 9 | import org.junit.AfterClass; 10 | import org.junit.BeforeClass; 11 | import org.junit.runner.RunWith; 12 | 13 | import java.util.Objects; 14 | import java.util.Optional; 15 | 16 | import static org.example.utils.AppiumServerManager.startAppiumServer; 17 | import static org.example.utils.AppiumServerManager.stopAppiumServer; 18 | import static org.example.utils.configloader.JsonParser.getConfig; 19 | 20 | @RunWith(Cucumber.class) 21 | @CucumberOptions(plugin = {"pretty", 22 | "summary", 23 | "html:target/cucumber-html-output/cucumber-html-report.html", 24 | "json:target/cucumber-json-output/cucumber.json"}, 25 | features = {"src/test/resources/feature"}, 26 | glue = {"org.example.stepdefinitions"}, 27 | monochrome = true, 28 | tags = "@Login" 29 | ) 30 | public class RunnerTest { 31 | 32 | @BeforeClass 33 | public static void setUp() { 34 | startAppiumServer(); 35 | if (Objects.isNull(DriverManager.getDriver())) { 36 | Drivers.initializeDriver(MobilePlatformName.valueOf( 37 | Optional.ofNullable(System.getProperty("platformName")) 38 | .orElse(getConfig(ConfigJson.PLATFORM)).toUpperCase())); 39 | } 40 | } 41 | 42 | @AfterClass 43 | public static void tearDown() { 44 | if (Objects.nonNull(DriverManager.getDriver())) { 45 | Drivers.quitDriver(); 46 | } 47 | stopAppiumServer(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/test/java/org/example/stepdefinitions/Hooks.java: -------------------------------------------------------------------------------- 1 | package org.example.stepdefinitions; 2 | 3 | import io.cucumber.java.After; 4 | import io.cucumber.java.Before; 5 | import io.cucumber.java.Scenario; 6 | import org.example.utils.screenrecording.ScreenRecordingService; 7 | import org.example.utils.screenshot.ScreenshotService; 8 | 9 | public class Hooks { 10 | 11 | @Before 12 | public void setUp() { 13 | ScreenRecordingService.startRecording(); 14 | } 15 | 16 | @After 17 | public void tearDown(Scenario scenario) { 18 | if (scenario.isFailed()) { 19 | scenario.attach(ScreenshotService.getScreenshotAsBytes(), 20 | "image/png", scenario.getName()); 21 | } 22 | ScreenRecordingService.stopRecording(scenario.getName()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/test/java/org/example/stepdefinitions/LoginSteps.java: -------------------------------------------------------------------------------- 1 | package org.example.stepdefinitions; 2 | 3 | import io.cucumber.java.en.Then; 4 | import io.cucumber.java.en.When; 5 | import org.example.pageobjects.LoginPage; 6 | import org.example.pageobjects.ProductPage; 7 | import org.junit.Assert; 8 | 9 | public class LoginSteps { 10 | 11 | @When("^User enter username as \"([^\"]*)\"$") 12 | public void userEnterUsernameAs(String username) { 13 | new LoginPage().setUsername(username); 14 | } 15 | 16 | @When("^User enter password as \"([^\"]*)\"$") 17 | public void userEnterPasswordAs(String password) { 18 | new LoginPage().setPassword(password); 19 | } 20 | 21 | @When("^clicks on login$") 22 | public void clickOnLogin() { 23 | new LoginPage().tapOnLogin(); 24 | } 25 | 26 | @Then("^login should fail with an error \"([^\"]*)\"$") 27 | public void loginShouldFailWithAnError(String err) { 28 | Assert.assertEquals(new LoginPage().getErrorText(), err); 29 | } 30 | 31 | @Then("^User should see Products page with title \"([^\"]*)\"$") 32 | public void userShouldSeeProductsPageWithTitle(String title) { 33 | Assert.assertEquals(new ProductPage().getProductPageTitle(), title); 34 | } 35 | 36 | @When("User enter username: {string} and password: {string}") 37 | public void userEnterUsernameAndPassword(String username, String password) { 38 | new LoginPage().setUsername(username).setPassword(password); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/test/resources/app/Android.SauceLabs.Mobile.Sample.app.2.7.1.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thangarajtk/appium-mobile-automation-framework-bdd/ad317e1002d7d6e72372326eb4e0deea55512b69/src/test/resources/app/Android.SauceLabs.Mobile.Sample.app.2.7.1.apk -------------------------------------------------------------------------------- /src/test/resources/config/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "platform": "android", 3 | "android_app_package": "com.swaglabsmobileapp", 4 | "android_app_activity": "com.swaglabsmobileapp.MainActivity", 5 | "avd_launch_timeout": 180000, 6 | "android_device_name": "Pixel_3a_API_30", 7 | "android_udid": "emulator-5554", 8 | "android_system_port": 7878, 9 | "android_chromedriver_port": 8787, 10 | "android_emulator": "yes", 11 | "ios_device_name": "", 12 | "ios_udid": "", 13 | "ios_bundle_id": "", 14 | "ios_webkit_debug_proxy_port": "", 15 | "ios_wda_local_port": "", 16 | "appium_url": "http://127.0.0.1:4723" 17 | } 18 | -------------------------------------------------------------------------------- /src/test/resources/config/config.properties: -------------------------------------------------------------------------------- 1 | # Provides an option to record screen during execution and save it in .mp4 format 2 | record_screen=yes 3 | # Provides an option to start appium server programmatically 4 | start_appium_server=yes 5 | # Provides an option to override appium server log 6 | override_server_log=yes 7 | -------------------------------------------------------------------------------- /src/test/resources/cucumber.properties: -------------------------------------------------------------------------------- 1 | cucumber.publish.quiet=true 2 | -------------------------------------------------------------------------------- /src/test/resources/feature/Login.feature: -------------------------------------------------------------------------------- 1 | @Login 2 | Feature: Login test 3 | 4 | @InvalidUsername 5 | Scenario Outline: Login with invalid username 6 | When User enter username: "" and password: "" 7 | And clicks on login 8 | Then login should fail with an error "" 9 | Examples: 10 | | username | password | error_message | 11 | | invalidUsername | secret_sauce | Username and password do not match any user in this service. | 12 | 13 | @InvalidPassword 14 | Scenario Outline: Login with invalid password 15 | When User enter username as "" 16 | And User enter password as "" 17 | And clicks on login 18 | Then login should fail with an error "" 19 | Examples: 20 | | username | password | error_message | 21 | | standard_user | invalidPassword | Username and password do not match any user in this service. | 22 | 23 | @ValidLogin 24 | Scenario Outline: Login with valid username and password 25 | When User enter username as "" 26 | And User enter password as "" 27 | And clicks on login 28 | Then User should see Products page with title "" 29 | Examples: 30 | | username | password | title | 31 | | standard_user | secret_sauce | PRODUCTS | 32 | --------------------------------------------------------------------------------