├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── alexzh │ │ └── testapp │ │ ├── actions │ │ └── RecyclerViewActions.kt │ │ ├── matchers │ │ ├── RecyclerViewMatchers.kt │ │ └── ToolbarMatcher.kt │ │ └── ui │ │ ├── home │ │ └── HomeActivityTest.kt │ │ ├── login │ │ └── LoginActivityTest.kt │ │ └── settings │ │ └── SettingsActivityTest.kt │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── alexzh │ │ └── testapp │ │ ├── data │ │ └── DummyData.kt │ │ ├── ext │ │ └── String.kt │ │ ├── model │ │ └── Task.kt │ │ └── ui │ │ ├── home │ │ ├── HomeActivity.kt │ │ └── adapter │ │ │ ├── TaskAdapter.kt │ │ │ └── TaskViewHolder.kt │ │ ├── login │ │ └── LoginActivity.kt │ │ └── settings │ │ └── SettingsActivity.kt │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ ├── ic_favorite_black_24dp.xml │ ├── ic_favorite_border_black_24dp.xml │ ├── ic_launcher_background.xml │ └── ic_lock_24dp.xml │ ├── layout │ ├── activity_home.xml │ ├── activity_login.xml │ ├── item_task.xml │ └── settings_activity.xml │ ├── menu │ └── general_menu.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── values │ ├── arrays.xml │ ├── colors.xml │ ├── strings.xml │ └── styles.xml │ └── xml │ └── root_preferences.xml ├── art ├── screens-with-views │ ├── home-screen.png │ ├── login-screen.png │ └── settings-screen.png └── screenshots │ ├── home_screen_framed.png │ ├── login_screen_framed.png │ └── settings_screen_framed.png ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the ART/Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | out/ 15 | 16 | # Gradle files 17 | .gradle/ 18 | build/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | 23 | # Proguard folder generated by Eclipse 24 | proguard/ 25 | 26 | # Log Files 27 | *.log 28 | 29 | # Android Studio Navigation editor temp files 30 | .navigation/ 31 | 32 | # Android Studio captures folder 33 | captures/ 34 | 35 | # IntelliJ 36 | *.iml 37 | .idea/ 38 | 39 | # Keystore files 40 | # Uncomment the following line if you do not want to check your keystore files in. 41 | #*.jks 42 | 43 | # External native build folder generated in Android Studio 2.2 and later 44 | .externalNativeBuild 45 | 46 | # Google Services (e.g. APIs or Firebase) 47 | google-services.json 48 | 49 | # Freeline 50 | freeline.py 51 | freeline/ 52 | freeline_project_description.json 53 | 54 | # fastlane 55 | fastlane/report.xml 56 | fastlane/Preview.html 57 | fastlane/screenshots 58 | fastlane/test_output 59 | fastlane/readme.md 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction to Android UI Testing 2 | 3 | The application was created for improving skills (from the very beginning) in testing Android application use Espresso framework. Each screen contains UI elements which are used by many applications. 4 | 5 |

6 | 7 | 8 | 9 |

10 | 11 | This is the first application from the workshop "Creating Robust UI tests in Android" which allows everyone to learn the basics of Espresso: 12 | * How to test separate screen in Android application 13 | * How to interact with Android Views 14 | * How to verify Settings screen in Android application 15 | * How to create custom matchers and work efficiently with Espresso framework 16 | * How to create custom actions which allow interacting with child view 17 | 18 | # Structure of the project 19 | The project has two main branches 20 | * master 21 | * solutions 22 | 23 | The **master** branch has failed test cases which should be created from scratch. 24 | Each test case has a comment with a description of the test case and a hint (what can be used for implementing it). 25 | 26 | The **solutions** branch has solutions for all test cases. 27 | 28 | ***Note:** In case if any of description is not understandable, please create an issue.* 29 | 30 | # App screens with views 31 | **Login screen** 32 |

33 | 34 |

35 | 36 | **Home screen** 37 |

38 | 39 |

40 | 41 | **Settings screen** 42 |

43 | 44 |

45 | 46 | # Resources 47 | * Espresso 48 | * [Official documentation](https://developer.android.com/training/testing/espresso) 49 | * Hamcrest matcher 50 | * [Official documentation](http://hamcrest.org/JavaHamcrest/) 51 | * [Matchers](http://hamcrest.org/JavaHamcrest/javadoc/1.3/org/hamcrest/Matchers.html) 52 | * UI Testing Tools 53 | * **UiAutomator Viewer** 54 | * [Documentation](https://developer.android.com/training/testing/ui-automator#ui-automator-viewer) 55 | * [Video Tutorial](https://www.youtube.com/watch?v=XBhfYAYKZF4) 56 | * **Layout Inspector** 57 | * [Documentation](https://developer.android.com/studio/debug/layout-inspector) 58 | * [Video Tutorial](https://www.youtube.com/watch?v=3out6Eh_DmQ) 59 | * [Comparing *UiAutomator Viewer* and *Layout Inspector*](https://alexzh.com/2018/12/10/efficient-testing-android-app-tools/) -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | 5 | android { 6 | compileSdkVersion 29 7 | buildToolsVersion "29.0.2" 8 | defaultConfig { 9 | applicationId "com.alexzh.testapp" 10 | minSdkVersion 21 11 | targetSdkVersion 29 12 | versionCode 1 13 | versionName "1.0" 14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 15 | } 16 | buildTypes { 17 | release { 18 | minifyEnabled false 19 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 20 | } 21 | } 22 | } 23 | 24 | dependencies { 25 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 26 | implementation 'androidx.appcompat:appcompat:1.1.0' 27 | implementation 'androidx.core:core-ktx:1.1.0' 28 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 29 | implementation 'androidx.preference:preference:1.1.0' 30 | implementation 'androidx.recyclerview:recyclerview:1.0.0' 31 | implementation "com.google.android.material:material:1.0.0" 32 | 33 | testImplementation 'junit:junit:4.12' 34 | androidTestImplementation 'androidx.test.ext:junit:1.1.1' 35 | androidTestImplementation 'androidx.test:core:1.2.0' 36 | androidTestImplementation 'androidx.test:rules:1.2.0' 37 | androidTestImplementation 'androidx.test:runner:1.2.0' 38 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' 39 | androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.2.0' 40 | androidTestImplementation 'androidx.test.espresso:espresso-intents:3.2.0' 41 | androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0' 42 | } 43 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/alexzh/testapp/actions/RecyclerViewActions.kt: -------------------------------------------------------------------------------- 1 | package com.alexzh.testapp.actions 2 | 3 | import android.view.View 4 | import androidx.test.espresso.UiController 5 | import androidx.test.espresso.ViewAction 6 | import org.hamcrest.Matcher 7 | 8 | /** 9 | * Custom actions for [androidx.recyclerview.widget.RecyclerView] class. 10 | */ 11 | object RecyclerViewActions { 12 | 13 | /** 14 | * Returns an action that clicks the child view for a specific id. 15 | * 16 | * @param id the child view id, which should be clicked. 17 | */ 18 | fun clickByChildViewWithId(id: Int): ViewAction { 19 | 20 | /** 21 | * The ViewAction is responsible for performing an interaction on the given View element. 22 | */ 23 | return object : ViewAction { 24 | 25 | /** 26 | * A mechanism for ViewActions to specify what type of views they can operate on. 27 | * ``` 28 | * override fun getConstraints(): Matcher? { 29 | * return allOf( 30 | * withEffectiveVisibility(Visibility.VISIBLE), 31 | * isAssignableFrom(Button.class) 32 | * ) 33 | * } 34 | * ``` 35 | */ 36 | override fun getConstraints(): Matcher? { 37 | // should be implemented 38 | return null 39 | } 40 | 41 | /** 42 | * Returns a description of the view action. The description should not be overly long and should 43 | * fit nicely in a sentence like: "performing %description% action on view with id ..." 44 | */ 45 | override fun getDescription(): String { 46 | // should be implemented 47 | return "" 48 | } 49 | 50 | /** 51 | * Performs the action on the given view. 52 | * 53 | * Hint: 54 | * - the view can contains subviews which can be found using `view.findViewById(ID)` 55 | * method. 56 | * - the view can be clicked use `view.performClick()` method. 57 | */ 58 | override fun perform(uiController: UiController, view: View) { 59 | // should be implemented 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/alexzh/testapp/matchers/RecyclerViewMatchers.kt: -------------------------------------------------------------------------------- 1 | package com.alexzh.testapp.matchers 2 | 3 | import android.view.View 4 | import androidx.recyclerview.widget.RecyclerView 5 | import androidx.test.espresso.matcher.BoundedMatcher 6 | import org.hamcrest.Description 7 | import org.hamcrest.Matcher 8 | 9 | /** 10 | * Custom matchers for [RecyclerView] class. 11 | */ 12 | object RecyclerViewMatchers { 13 | 14 | /** 15 | * Returns a matcher that matches [View] that contains number of items in RecyclerView 16 | * associated with the given id. 17 | * 18 | * @param count the number of items in [RecyclerView]. 19 | */ 20 | fun withItemCount(count: Int): Matcher { 21 | 22 | /** 23 | * BoundedMatcher has a matcher sugar that lets you create a matcher for a given type 24 | * but only process items of a specific subtype of that matcher. 25 | */ 26 | return object : BoundedMatcher(RecyclerView::class.java) { 27 | 28 | /** 29 | * Generate a description of why the matcher has not accepted the item. 30 | * 31 | * @param description the description for the matcher which allows to report evaluation 32 | * matcher issue. 33 | * 34 | * Hint: 35 | * - the description parameter should be used in a proper way to reporting issues 36 | * correctly. It's partially filled by matcher. Usually expected values passed as a 37 | * parameter for the matcher. When parameter is Hamcrest Matcher we can use `toString()` 38 | * method for convert expected value fom matcher to string. 39 | * ``` 40 | * override fun describeTo(description: Description?) { 41 | * description.appendText(EXPECTED-VALUE) 42 | * } 43 | * ``` 44 | * Output: 45 | * > java.lang.AssertionError: 46 | * > Expected: EXPECTED-VALUE 47 | * > got: ACTUAL-VALUE 48 | */ 49 | override fun describeTo(description: Description?) { 50 | // should be implemented 51 | } 52 | 53 | /** 54 | * Evaluates the matcher for [RecyclerView]. 55 | * 56 | * @param recyclerView the [RecyclerView] view to check the title. 57 | * 58 | * Hint: 59 | * - the recyclerView object has access to Adapter use `recyclerView?.adapter` property. 60 | * Under the hood the [RecyclerView.getAdapter] method will be called. 61 | * - the adapter object has access to number of items in Adapter use `adapter?.itemCount` 62 | * property. Under the hood the [RecyclerView.Adapter.getItemCount] method will be called. 63 | * - the count object can be compared with number of items in adapter. 64 | */ 65 | override fun matchesSafely(recyclerView: RecyclerView?): Boolean { 66 | // should be implemented 67 | return false 68 | } 69 | } 70 | } 71 | 72 | /** 73 | * Returns a matcher that matches [View] that contains view at specified position 74 | * in RecyclerView associated with the given id. 75 | */ 76 | fun atPosition(position: Int, itemMatcher: Matcher): Matcher { 77 | 78 | /** 79 | * BoundedMatcher has a matcher sugar that lets you create a matcher for a given type 80 | * but only process items of a specific subtype of that matcher. 81 | */ 82 | return object : BoundedMatcher(RecyclerView::class.java) { 83 | 84 | /** 85 | * Generate a description of why the matcher has not accepted the item. 86 | * 87 | * @param description the description for the matcher which allows to report evaluation 88 | * matcher issue. 89 | * 90 | * Hint: 91 | * - the description parameter should be used in a proper way to reporting issues 92 | * correctly. It's partially filled by matcher. Usually expected values passed as a 93 | * parameter for the matcher. When parameter is Hamcrest Matcher we can use `toString()` 94 | * method for convert expected value fom matcher to string. 95 | * ``` 96 | * override fun describeTo(description: Description?) { 97 | * description.appendText(EXPECTED-VALUE) 98 | * } 99 | * ``` 100 | * Output: 101 | * > java.lang.AssertionError: 102 | * > Expected: EXPECTED-VALUE 103 | * > got: ACTUAL-VALUE 104 | */ 105 | override fun describeTo(description: Description?) { 106 | // should be implemented 107 | } 108 | 109 | /** 110 | * Evaluates the matcher for [RecyclerView]. 111 | * 112 | * @param recyclerView the [RecyclerView] view to check the title. 113 | * 114 | * Hint: 115 | * - the recyclerView item has a [RecyclerView.ViewHolder] object which allows to work with View 116 | * inside each ViewHolder. We can get access to ViewHolder use 117 | * `recyclerView?.findViewHolderForAdapterPosition(POSITION)` method. 118 | * - the ViewHolder object has access to the view inside ViewHolder use `viewHolder?.itemView` 119 | * property. 120 | * - the itemMatcher can be evaluated with an argument using the [Matcher.matches] method 121 | * `itemMatcher.matches(VIEW)`. 122 | */ 123 | override fun matchesSafely(recyclerView: RecyclerView?): Boolean { 124 | // should be implemented 125 | return false 126 | } 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/alexzh/testapp/matchers/ToolbarMatcher.kt: -------------------------------------------------------------------------------- 1 | package com.alexzh.testapp.matchers 2 | 3 | import android.view.View 4 | import androidx.appcompat.widget.Toolbar 5 | import androidx.test.espresso.matcher.BoundedMatcher 6 | import org.hamcrest.Description 7 | import org.hamcrest.Matcher 8 | 9 | /** 10 | * Custom matchers for [Toolbar] class. 11 | */ 12 | object ToolbarMatcher { 13 | 14 | /** 15 | * Returns a matcher that matches [View] that is displaying the string in Toolbar 16 | * associated with the given id. 17 | * 18 | * @param textMatcher the string matcher the toolbar title is expected to hold. 19 | */ 20 | fun withToolbarTitle(textMatcher: Matcher): Matcher { 21 | 22 | /** 23 | * BoundedMatcher has a matcher sugar that lets you create a matcher for a given type 24 | * but only process items of a specific subtype of that matcher. 25 | */ 26 | return object : BoundedMatcher(Toolbar::class.java) { 27 | 28 | /** 29 | * Generate a description of why the matcher has not accepted the item. 30 | * 31 | * @param description the description for the matcher which allows to report evaluation 32 | * matcher issue. 33 | * 34 | * Hint: 35 | * - the description parameter should be used in a proper way to reporting issues 36 | * correctly. It's partially filled by matcher. Usually expected values passed as a 37 | * parameter for the matcher. When parameter is Hamcrest Matcher we can use `toString()` 38 | * method for convert expected value fom matcher to string. 39 | * ``` 40 | * override fun describeTo(description: Description?) { 41 | * description.appendText(EXPECTED-VALUE) 42 | * } 43 | * ``` 44 | * Output: 45 | * > java.lang.AssertionError: 46 | * > Expected: EXPECTED-VALUE 47 | * > got: ACTUAL-VALUE 48 | */ 49 | override fun describeTo(description: Description?) { 50 | // should be implemented 51 | } 52 | 53 | /** 54 | * Evaluates the matcher for toolbar. 55 | * 56 | * @param toolbar the [Toolbar] view to check the title. 57 | * 58 | * Hint: 59 | * - the title of toolbar can be get from the title property: `toolbar?.title`. 60 | * Under the hood the [Toolbar.getTitle] method will be called. 61 | * - the textMatcher can be evaluated with an argument using the [Matcher.matches] method 62 | * `textMatcher.matches(TOOLBAR TITLE)` 63 | */ 64 | override fun matchesSafely(toolbar: Toolbar?): Boolean { 65 | // should be implemented 66 | return false 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/alexzh/testapp/ui/home/HomeActivityTest.kt: -------------------------------------------------------------------------------- 1 | package com.alexzh.testapp.ui.home 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.rule.ActivityTestRule 5 | import org.junit.Assert.fail 6 | import org.junit.Rule 7 | import org.junit.Test 8 | import org.junit.runner.RunWith 9 | 10 | /** 11 | * Test suite for checking the HomeActivity class from a UI perspective. 12 | * 13 | * Note: 14 | * This class can be run without any pre-requirements. 15 | * The HomeActivity class doesn't verify if the User is logged in because the main idea of the project 16 | * is to learn the basics of Espresso framework. 17 | * 18 | * You can check I'mBarista project (https://github.com/AlexZhukovich/ImBarista-App) for more complex 19 | * practices and test cases. 20 | * 21 | * The "art/screens-with-views/home-screen.png" image demonstrates Home screen with all views 22 | * and IDs. 23 | */ 24 | @RunWith(AndroidJUnit4::class) 25 | class HomeActivityTest { 26 | 27 | /** 28 | * The {@link ActivityTestRule} provides functional testing of a single {@link Activity}. 29 | * The Espresso framework requires it for launching Activity for testings. 30 | * 31 | * We can configure activity before running the test in the following ways: 32 | * - Using the {@link #ActivityTestRule(Class, boolean, boolean)} 33 | * {@link #ActivityTestRule(Class, boolean, boolean)} 34 | * - first argument *activityClass* - defines the activity that is under test. 35 | * - second argument *initialTouchMode* - when set to true, activity will be started in "touch mode". 36 | * Touch mode is a state of the UIToolkit that causes the view hierarchy to depend only 37 | * on the user interaction. For more information please check: 38 | * https://android-developers.googleblog.com/2008/12/touch-mode.html 39 | * - third argument *launchActivity* - if this argument is set to true the Activity will be 40 | * launched before the test case. Before the set up marked with @Before and closed after 41 | * tear down marked with @After. This means we cannot configure it before executing the test case. 42 | * If this argument is set to false it is possible to launch the activity with different Intents 43 | * per each test method. This is especially useful if we have extras that we want to put in the intent 44 | * ``` 45 | * @Rule @JvmField 46 | * val activity = ActivityTestRule(HomeActivity::class.java, true, false) 47 | * 48 | * @Before 49 | * fun setUp() { 50 | * val intent = ... 51 | * activity.launchActivity(intent) 52 | * } 53 | * ``` 54 | * - Using the {@link ActivityTestRule#beforeActivityLaunched) method we can do additional 55 | * action before executing test case, like configure Mock objects, clean database, etc.) 56 | * ``` 57 | * @get: Rule 58 | * val activity = object: ActivityTestRule) { 59 | * override fun beforeActivityLaunched() { 60 | * super.beforeActivityLaunched() 61 | * ... 62 | * } 63 | * } 64 | * ``` 65 | * 66 | * Note: 67 | * - a field created for JUnit Rule should be public, not static, and a subtype of TestRule 68 | * or MethodRule (https://junit.org/junit4/javadoc/4.12/org/junit/Rule.html) 69 | * - In Kotlin we can use @get: Rule or @Rule @JvmField annotations for JUnit Rule fields 70 | */ 71 | @get: Rule 72 | val activity = ActivityTestRule(HomeActivity::class.java) 73 | 74 | /** 75 | * Test case: 76 | * - HomeActivity should have options menu 77 | * - "Settings" menu item should be displayed in options menu 78 | * 79 | * Notes: 80 | * - the "Settings" text can be found in strings.xml file as settings_action const 81 | * (R.string.settings_action) 82 | * 83 | * Hint(s): 84 | * - the Espresso#openActionBarOverflowOrOptionsMenu method allows us to open options menu. 85 | */ 86 | @Test 87 | fun shouldOptionsMenuHasSettingsItem() { 88 | fail() 89 | } 90 | 91 | /** 92 | * Test case: 93 | * - HomeActivity should have options menu 94 | * - SettingsActivity should be opened after pressing on "Settings" in options menu 95 | * 96 | * Notes: 97 | * - the "Settings" text can be found in strings.xml file as settings_action const 98 | * (R.string.settings_action) 99 | * 100 | * Hint(s): 101 | * - Intent.intended(IntentMatcher) allows us to verify that Activity was opened. 102 | * The Intents component should be initialized before using with the Intents#init() method 103 | * and released when it's not needed with the Intents#release() method. As an alternative 104 | * we can use IntentsTestRule, which initializes Espresso-Intents before each test annotated 105 | * with Test and releases Espresso-Intents after each test run. 106 | * - the Espresso#openActionBarOverflowOrOptionsMenu method allows us to open options menu. 107 | */ 108 | @Test 109 | fun shouldOpenSettingsScreenWhenSettingsItemClicked() { 110 | fail() 111 | } 112 | 113 | /** 114 | * Test case: 115 | * - the task title should be displayed after click on item from RecyclerView 116 | * 117 | * Notes: 118 | * - the "RecyclerView" has ID: recyclerView (R.id.recyclerView) 119 | * - the "RecyclerView" shows items created in DummyData#getTasks() method 120 | * - the error message with the same text as Task title is displayed 121 | * 122 | * Hint(s): 123 | * - the action on item can be made with RecyclerViewActions#actionOnItem method. 124 | * - the task can be got from DummyData#getTasks() by position use `DummyData.getTasks()[ POSITION ]` 125 | * - the title of the task can be got from title property `task.title` 126 | * - the Snackbar view has error message 127 | * - the Snackbar text message view has ID: snackbar_text (com.google.android.material.R.id.snackbar_text) 128 | */ 129 | @Test 130 | fun shouldBeDisplayedTaskTitleWhenClickOnTask() { 131 | fail() 132 | } 133 | 134 | /** 135 | * Test case: 136 | * - Toolbar should have a text "TestApp" 137 | * 138 | * Notes: 139 | * - the "Toolbar" view has ID: homeToolbar (R.id.homeToolbar) 140 | * - the "TestApp" string can be found in strings.xml file as app_name const (R.string.app_name) 141 | * 142 | * Hint(s): 143 | * - the toolbar title can be checked with ToolbarMatcher#withToolbarTitle ViewMatcher 144 | * (can be not implemented yet). 145 | */ 146 | @Test 147 | fun shouldToolbarContainsTextAppTitle() { 148 | fail() 149 | } 150 | 151 | /** 152 | * Test case: 153 | * - RecyclerView should have 44 items by default 154 | * - Number of tasks can be changed in [com.alexzh.testapp.data.DummyData.TASK_COUNT] const 155 | * 156 | * Notes: 157 | * - the "RecyclerView" has ID: recyclerView (R.id.recyclerView) 158 | * - the "RecyclerView" shows items created in DummyData#getTasks() method 159 | * 160 | * Hint(s): 161 | * - the number of items in RecyclerView can be checked with RecyclerViewMatchers#withItemCount 162 | * ViewMatcher (can be not implemented yet). 163 | * - the number of visible items depends on the screen. If you want to do action with invisible 164 | * on screen item you should scroll to this item first, you can use the following actions: 165 | * - RecyclerViewActions#scrollTo(itemMatcher) 166 | * - RecyclerViewActions#scrollToHolder(viewHolderMatcher) 167 | * - RecyclerViewActions#scrollToPosition(position) 168 | */ 169 | @Test 170 | fun shouldRecyclerViewHas44Items() { 171 | fail() 172 | } 173 | 174 | /** 175 | * Test case: 176 | * - the "The {task title} task is favourite" or "The {task title} task is not favourite" text 177 | * should be displayed in Snackbar after click on favourite icon of the item, depends on 178 | * the status of the item. 179 | * 180 | * Notes: 181 | * - the data displayed in the RecyclerView can found in DummyData#getTasks() method. 182 | * - the "The {task title} task is favourite" string can be found in string.xml file as 183 | * formatted_favourite_task const (R.string.formatted_favourite_task). 184 | * - the "The {task title} task is not favourite" string can be found in string.xml file as 185 | * formatted_not_favourite_task const (R.string.formatted_not_favourite_task). 186 | * - the Snackbar component displays text which can be found in view with ID: snackbar_text 187 | * (com.google.android.material.R.id.snackbar_text). 188 | * 189 | * Hint(s): 190 | * - the click on child item, like favourite icon can be made with 191 | * RecyclerViewActions#clickByChildViewWithId ViewAction (can be not implemented yet). 192 | */ 193 | @Test 194 | fun shouldBeDisplayTaskInfoWhenClickedOnFavouriteIcon() { 195 | fail() 196 | } 197 | 198 | /** 199 | * Test case: 200 | * - the first task should be favourite. We can verify it by verification ViewHolder of the 201 | * task. Each item has two descendants: title and icon. 202 | * 203 | * Notes: 204 | * - the view with ID: favouriteImageView (R.id.favouriteImageView) displays the drawable 205 | * with ID: ic_favorite_black_24dp (R.drawable.ic_favorite_black_24dp) when task is favourite. 206 | * - the data displayed in the RecyclerView can found in DummyData#getTasks() method. 207 | * - comparing rendered image with image from drawable file is very inefficient because image 208 | * can be resized or rendered from vector image. As an alternative we can use the TAG attribute 209 | * and set TAG as a file name of used resource. 210 | * - the "RecyclerView" has ID: recyclerView (R.id.recyclerView). 211 | * 212 | * Hint(s): 213 | * - the checking descendant property can done with RecyclerViewMatchers#atPosition ViewMatcher 214 | * (can be not implemented yet). 215 | */ 216 | @Test 217 | fun shouldFirstTestItemIsFavourite() { 218 | fail() 219 | } 220 | 221 | /** 222 | * Test case: 223 | * - the lat task should be not favourite. We can verify it by verification ViewHolder of the 224 | * task. Each item has two descendants: title and icon. 225 | * 226 | * Note: 227 | * - the view with ID: favouriteImageView (R.id.favouriteImageView) displays the drawable 228 | * with ID: ic_favorite_border_black_24dp (R.drawable.ic_favorite_border_black_24dp) when task 229 | * is not favourite. 230 | * - the data displayed in the RecyclerView can found in DummyData#getTasks() method. 231 | * - comparing rendered image with image from drawable file is very inefficient because image 232 | * can be resized or rendered from vector image. As an alternative we can use the TAG attribute 233 | * and set TAG as a file name of used resource. 234 | * - the "RecyclerView" has ID: recyclerView (R.id.recyclerView). 235 | * 236 | * Hint(s): 237 | * - the checking descendant property can done with RecyclerViewMatchers#atPosition ViewMatcher 238 | * (can be not implemented yet). 239 | * - the number of visible items depends on the screen. If you want to do action with invisible 240 | * on screen item you should scroll to this item first, you can use the following actions: 241 | * - RecyclerViewActions#scrollTo(itemMatcher) 242 | * - RecyclerViewActions#scrollToHolder(viewHolderMatcher) 243 | * - RecyclerViewActions#scrollToPosition(position) 244 | */ 245 | @Test 246 | fun shouldLastTestItemIsNotFavourite() { 247 | fail() 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/alexzh/testapp/ui/login/LoginActivityTest.kt: -------------------------------------------------------------------------------- 1 | package com.alexzh.testapp.ui.login 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.rule.ActivityTestRule 5 | import org.junit.Assert.fail 6 | import org.junit.Rule 7 | import org.junit.Test 8 | import org.junit.runner.RunWith 9 | 10 | /** 11 | * Test suite for checking the LoginActivity class from a UI perspective. 12 | * 13 | * Note: 14 | * This class can be run without any pre-requirements. 15 | * The the main idea of the project is to learn the basics of Espresso framework. 16 | * 17 | * You can check I'mBarista (https://github.com/AlexZhukovich/ImBarista-App) project for more complex 18 | * practices and test cases. 19 | * 20 | * The "art/screens-with-views/login-screen.png" image demonstrates Login screen with all views 21 | * and IDs. 22 | */ 23 | @RunWith(AndroidJUnit4::class) 24 | class LoginActivityTest { 25 | 26 | /** 27 | * The {@link ActivityTestRule} provides functional testing of a single {@link Activity}. 28 | * The Espresso framework requires it for launching Activity for testings. 29 | * 30 | * We can configure activity before running the test in the following ways: 31 | * - Using the {@link #ActivityTestRule(Class, boolean, boolean)} 32 | * {@link #ActivityTestRule(Class, boolean, boolean)} 33 | * - first argument *activityClass* - defines the activity that is under test. 34 | * - second argument *initialTouchMode* - when set to true, activity will be started in "touch mode". 35 | * Touch mode is a state of the UIToolkit that causes the view hierarchy to depend only 36 | * on the user interaction. For more information please check: 37 | * https://android-developers.googleblog.com/2008/12/touch-mode.html 38 | * - third argument *launchActivity* - if this argument is set to true the Activity will be 39 | * launched before the test case. Before the set up marked with @Before and closed after 40 | * tear down marked with @After. This means we cannot configure it before executing the test case. 41 | * If this argument is set to false it is possible to launch the activity with different Intents 42 | * per each test method. This is especially useful if we have extras that we want to put in the intent 43 | * ``` 44 | * @Rule @JvmField 45 | * val activity = ActivityTestRule(HomeActivity::class.java, true, false) 46 | * 47 | * @Before 48 | * fun setUp() { 49 | * val intent = ... 50 | * activity.launchActivity(intent) 51 | * } 52 | * ``` 53 | * - Using the {@link ActivityTestRule#beforeActivityLaunched) method we can do additional 54 | * action before executing test case, like configure Mock objects, clean database, etc.) 55 | * ``` 56 | * @get: Rule 57 | * val activity = object: ActivityTestRule) { 58 | * override fun beforeActivityLaunched() { 59 | * super.beforeActivityLaunched() 60 | * ... 61 | * } 62 | * } 63 | * ``` 64 | * 65 | * Note: 66 | * - a field created for JUnit Rule should be public, not static, and a subtype of TestRule 67 | * or MethodRule (https://junit.org/junit4/javadoc/4.12/org/junit/Rule.html) 68 | * - In Kotlin we can use @get: Rule or @Rule @JvmField annotations for JUnit Rule fields 69 | */ 70 | @get: Rule 71 | val activity = ActivityTestRule(LoginActivity::class.java) 72 | 73 | /** 74 | * Test case: 75 | * - the "Email Input" view is enabled 76 | * - the "Password Input" view is enabled 77 | * - the "I agree" checkbox is enabled 78 | * - the "LOG IN" button is enabled 79 | * 80 | * Notes: 81 | * - the "Email Input" view has ID: emailEditText (R.id.emailEditText) 82 | * - the "Password Input" view has ID: passwordEditText (R.id.passwordEditText) 83 | * - the "I agree" checkbox has ID: agreeCheckBox (R.id.agreeCheckBox) 84 | * - the "LOG IN" button has ID: loginButton (R.id.loginButton) 85 | */ 86 | @Test 87 | fun shouldBeEnabledAllViews() { 88 | fail() 89 | } 90 | 91 | /** 92 | * Test case: 93 | * - the "Email Input" has the "Email" hint 94 | * - the "Password Input" has the "Password" hint 95 | * 96 | * Notes: 97 | * - the "Email Input" view has ID: emailEditText (R.id.emailEditText) 98 | * - the "Password Input" view has ID: passwordEditText (R.id.passwordEditText) 99 | * - the "Email" string can be found in strings.xml file as hint_email const (R.string.hint_email) 100 | * - the "Password" string can be found in strings.xml file as hint_password const (R.string.hint_password) 101 | */ 102 | @Test 103 | fun shouldEmailAndPasswordFieldsDisplayHint() { 104 | fail() 105 | } 106 | 107 | /** 108 | * Test case: 109 | * - the "Email Input" is blank 110 | * - the "Password Input" is blank 111 | * - the error message with "Email and password are blank" text should be displayed 112 | * after click on "LOG IN" button 113 | * 114 | * Notes: 115 | * - the "Email Input" view has ID: emailEditText (R.id.emailEditText) 116 | * - the "Password Input" view has ID: passwordEditText (R.id.passwordEditText) 117 | * - the "LOG IN" button has ID: loginButton (R.id.loginButton) 118 | * - the "Email and password are blank" string can be found in strings.xml file as 119 | * email_and_password_are_blank const (R.string.email_and_password_are_blank) 120 | * - the [androidx.test.espresso.action.ViewActions.typeText] ViewAction can be used for typing 121 | * text into text field character by character. 122 | * - the [androidx.test.espresso.action.ViewActions.replaceText] ViewAction can be used for 123 | * replacing existing ot empty text into text field. 124 | * - the [androidx.test.espresso.action.ViewActions.closeSoftKeyboard] ViewAction allows to hide 125 | * a keyboard from the screen. 126 | * - the Snackbar view has error message 127 | * - the Snackbar text message view has ID: snackbar_text 128 | * (com.google.android.material.R.id.snackbar_text) 129 | * 130 | * Hint(s): 131 | * - error text can be found by text on screen or by text in view 132 | */ 133 | @Test 134 | fun shouldDisplayErrorWhenEmailAndPasswordAreBlank() { 135 | fail() 136 | } 137 | 138 | /** 139 | * Test case: 140 | * - the "Email Input" is blank 141 | * - the "Password Input" is NOT blank 142 | * - the error message with "Email is blank" text should be displayed 143 | * after click on "LOG IN" button 144 | * 145 | * Notes: 146 | * - the "Email Input" view has ID: emailEditText (R.id.emailEditText) 147 | * - the "Password Input" view has ID: passwordEditText (R.id.passwordEditText) 148 | * - the "LOG IN" button has ID: loginButton (R.id.loginButton) 149 | * - the "Email is blank" string can be found in strings.xml file as 150 | * email_is_blank const (R.string.email_is_blank) 151 | * - the [androidx.test.espresso.action.ViewActions.typeText] ViewAction can be used for typing 152 | * text into text field character by character. 153 | * - the [androidx.test.espresso.action.ViewActions.replaceText] ViewAction can be used for 154 | * replacing existing ot empty text into text field. 155 | * - the [androidx.test.espresso.action.ViewActions.closeSoftKeyboard] ViewAction allows to hide 156 | * a keyboard from the screen. 157 | * - the Snackbar view has error message 158 | * - the Snackbar text message view has ID: snackbar_text 159 | * (com.google.android.material.R.id.snackbar_text) 160 | * 161 | * Hint(s): 162 | * - error text can be found by text on screen or by text in view 163 | */ 164 | @Test 165 | fun shouldDisplayErrorWhenEmailIsBlank() { 166 | fail() 167 | } 168 | 169 | /** 170 | * Test case: 171 | * - the "Email Input" is NOT blank 172 | * - the "Password Input" is blank 173 | * - the error message with "Password is blank" text should be displayed 174 | * after click on "LOG IN" button 175 | * 176 | * Notes: 177 | * - the "Email Input" view has ID: emailEditText (R.id.emailEditText) 178 | * - the "Password Input" view has ID: passwordEditText (R.id.passwordEditText) 179 | * - the "LOG IN" button has ID: loginButton (R.id.loginButton) 180 | * - the "Password is blank" string can be found in strings.xml file as 181 | * password_is_blank const (R.string.password_is_blank) 182 | * - the [androidx.test.espresso.action.ViewActions.typeText] ViewAction can be used for typing 183 | * text into text field character by character. 184 | * - the [androidx.test.espresso.action.ViewActions.replaceText] ViewAction can be used for 185 | * replacing existing ot empty text into text field. 186 | * - the [androidx.test.espresso.action.ViewActions.closeSoftKeyboard] ViewAction allows to hide 187 | * a keyboard from the screen. 188 | * - the Snackbar view has error message 189 | * - the Snackbar text message view has ID: snackbar_text 190 | * (com.google.android.material.R.id.snackbar_text) 191 | * 192 | * Hint(s): 193 | * - error text can be found by text on screen or by text in view 194 | */ 195 | @Test 196 | fun shouldDisplayErrorWhenPasswordIsBlank() { 197 | fail() 198 | } 199 | 200 | /** 201 | * Test case: 202 | * - the "Email Input" doesn't match with email template "TEXT@TEXT.TEXT" 203 | * - the "Password Input" is NOT blank 204 | * - the error message with "Text is not email" text should be displayed 205 | * after click on "LOG IN" button 206 | * 207 | * Notes: 208 | * - the "Email Input" view has ID: emailEditText (R.id.emailEditText) 209 | * - the "Password Input" view has ID: passwordEditText (R.id.passwordEditText) 210 | * - the "LOG IN" button has ID: loginButton (R.id.loginButton) 211 | * - the "Text is not email" string can be found in strings.xml file as 212 | * text_is_not_email const (R.string.text_is_not_email) 213 | * - the [androidx.test.espresso.action.ViewActions.typeText] ViewAction can be used for typing 214 | * text into text field character by character. 215 | * - the [androidx.test.espresso.action.ViewActions.replaceText] ViewAction can be used for 216 | * replacing existing ot empty text into text field. 217 | * - the [androidx.test.espresso.action.ViewActions.closeSoftKeyboard] ViewAction allows to hide 218 | * a keyboard from the screen. 219 | * - the Snackbar view has error message 220 | * - the Snackbar text message view has ID: snackbar_text 221 | * (com.google.android.material.R.id.snackbar_text) 222 | * 223 | * Hint(s): 224 | * - error text can be found by text on screen or by text in view 225 | */ 226 | @Test 227 | fun shouldDisplayErrorWhenEmailTextIsNotEmail() { 228 | fail() 229 | } 230 | 231 | /** 232 | * Test case: 233 | * - the "Email Input" has text which not matches with 234 | * [com.alexzh.testapp.data.DummyData.EMAIL] const 235 | * - the "Password Input" has text which not matches with 236 | * [com.alexzh.testapp.data.DummyData.PASSWORD] const 237 | * - the error message with "Check email and password" text should be displayed 238 | * after click on "LOG IN" button 239 | * 240 | * Notes: 241 | * - the "Email Input" view has ID: emailEditText (R.id.emailEditText) 242 | * - the "Password Input" view has ID: passwordEditText (R.id.passwordEditText) 243 | * - the "LOG IN" button has ID: loginButton (R.id.loginButton) 244 | * - the "Check email and password" string can be found in strings.xml file as 245 | * check_email_and_password const (R.string.check_email_and_password) 246 | * - the [androidx.test.espresso.action.ViewActions.typeText] ViewAction can be used for typing 247 | * text into text field character by character. 248 | * - the [androidx.test.espresso.action.ViewActions.replaceText] ViewAction can be used for 249 | * replacing existing ot empty text into text field. 250 | * - the [androidx.test.espresso.action.ViewActions.closeSoftKeyboard] ViewAction allows to hide 251 | * a keyboard from the screen. 252 | * - the Snackbar view has error message 253 | * - the Snackbar text message view has ID: snackbar_text 254 | * (com.google.android.material.R.id.snackbar_text) 255 | * 256 | * Hint(s): 257 | * - error text can be found by text on screen or by text in view 258 | */ 259 | @Test 260 | fun shouldDisplayErrorWhenEmailAndPasswordAreIncorrect() { 261 | fail() 262 | } 263 | 264 | /** 265 | * Test case: 266 | * - the "Email Input" has text which not matches with 267 | * [com.alexzh.testapp.data.DummyData.EMAIL] const 268 | * - the "Password Input" has text which matches with 269 | * [com.alexzh.testapp.data.DummyData.PASSWORD] const 270 | * - the error message with "Check email and password" text should be displayed 271 | * after click on "LOG IN" button 272 | * 273 | * Notes: 274 | * - the "Email Input" view has ID: emailEditText (R.id.emailEditText) 275 | * - the "Password Input" view has ID: passwordEditText (R.id.passwordEditText) 276 | * - the "LOG IN" button has ID: loginButton (R.id.loginButton) 277 | * - the "Check email and password" string can be found in strings.xml file as 278 | * check_email_and_password const (R.string.check_email_and_password) 279 | * - the [androidx.test.espresso.action.ViewActions.typeText] ViewAction can be used for typing 280 | * text into text field character by character. 281 | * - the [androidx.test.espresso.action.ViewActions.replaceText] ViewAction can be used for 282 | * replacing existing ot empty text into text field. 283 | * - the [androidx.test.espresso.action.ViewActions.closeSoftKeyboard] ViewAction allows to hide 284 | * a keyboard from the screen. 285 | * - the Snackbar view has error message 286 | * - the Snackbar text message view has ID: snackbar_text 287 | * (com.google.android.material.R.id.snackbar_text) 288 | * 289 | * Hint(s): 290 | * - error text can be found by text on screen or by text in view 291 | */ 292 | @Test 293 | fun shouldDisplayErrorWhenEmailIsIncorrect() { 294 | fail() 295 | } 296 | 297 | /** 298 | * Test case: 299 | * - the "Email Input" has text which matches with 300 | * [com.alexzh.testapp.data.DummyData.EMAIL] const 301 | * - the "Password Input" has text which not matches with 302 | * [com.alexzh.testapp.data.DummyData.PASSWORD] const 303 | * - the error message with "Check email and password" text should be displayed 304 | * after click on "LOG IN" button 305 | * 306 | * Notes: 307 | * - the "Email Input" view has ID: emailEditText (R.id.emailEditText) 308 | * - the "Password Input" view has ID: passwordEditText (R.id.passwordEditText) 309 | * - the "LOG IN" button has ID: loginButton (R.id.loginButton) 310 | * - the "Check email and password" string can be found in strings.xml file as 311 | * check_email_and_password const (R.string.check_email_and_password) 312 | * - the [androidx.test.espresso.action.ViewActions.typeText] ViewAction can be used for typing 313 | * text into text field character by character. 314 | * - the [androidx.test.espresso.action.ViewActions.replaceText] ViewAction can be used for 315 | * replacing existing ot empty text into text field. 316 | * - the [androidx.test.espresso.action.ViewActions.closeSoftKeyboard] ViewAction allows to hide 317 | * a keyboard from the screen. 318 | * - the Snackbar view has error message 319 | * - the Snackbar text message view has ID: snackbar_text 320 | * (com.google.android.material.R.id.snackbar_text) 321 | * 322 | * Hint(s): 323 | * - error text can be found by text on screen or by text in view 324 | */ 325 | @Test 326 | fun shouldDisplayErrorWhenPasswordIsIncorrect() { 327 | fail() 328 | } 329 | 330 | /** 331 | * Test case: 332 | * - the "Email Input" has text which matches with 333 | * [com.alexzh.testapp.data.DummyData.EMAIL] const 334 | * - the "Password Input" has text which matches with 335 | * [com.alexzh.testapp.data.DummyData.PASSWORD] const 336 | * - the "I agree" checkbox should be not checked 337 | * - the error message with "'I agree' should be checked" text should be displayed 338 | * after click on "LOG IN" button 339 | * 340 | * Notes: 341 | * - the "Email Input" view has ID: emailEditText (R.id.emailEditText) 342 | * - the "Password Input" view has ID: passwordEditText (R.id.passwordEditText) 343 | * - the "LOG IN" button has ID: loginButton (R.id.loginButton) 344 | * - the "'I agree' should be checked" string can be found in strings.xml file as 345 | * i_agree_should_be_checked const (R.string.i_agree_should_be_checked) 346 | * - the [ViewActions.typeText] ViewAction can be used for typing text into text field character 347 | * by character. 348 | * - the [ViewActions.replaceText] ViewAction can be used for replacing existing ot empty text 349 | * into text field. 350 | * - the [ViewActions.closeSoftKeyboard] ViewAction allows to hide a keyboard from the screen. 351 | * - the Snackbar view has error message 352 | * - the Snackbar text message view has ID: snackbar_text 353 | * (com.google.android.material.R.id.snackbar_text) 354 | * 355 | * Hint(s): 356 | * - error text can be found by text on screen or by text in view 357 | */ 358 | @Test 359 | fun shouldDisplayErrorWhenAgreeCheckboxIsNotChecked() { 360 | fail() 361 | } 362 | 363 | /** 364 | * Test case: 365 | * - the "Email Input" has text which matches with 366 | * [com.alexzh.testapp.data.DummyData.EMAIL] const 367 | * - the "Password Input" has text which matches with 368 | * [com.alexzh.testapp.data.DummyData.PASSWORD] const 369 | * - the "I agree" checkbox is checked 370 | * - the error message with "Check email and password" text should be displayed 371 | * after click on "LOG IN" button 372 | * 373 | * Notes: 374 | * - the "Email Input" view has ID: emailEditText (R.id.emailEditText) 375 | * - the "Password Input" view has ID: passwordEditText (R.id.passwordEditText) 376 | * - the "LOG IN" button has ID: loginButton (R.id.loginButton) 377 | * - the "I agree" checkbox has ID: i_agree (R.id.i_agree) 378 | * - the "I agree" string can be found in strings.xml file as i_agree const (R.string.i_agree) 379 | * - the "Check email and password" string can be found in strings.xml file as 380 | * check_email_and_password const (R.string.check_email_and_password) 381 | * - the [androidx.test.espresso.action.ViewActions.typeText] ViewAction can be used for typing 382 | * text into text field character by character. 383 | * - the [androidx.test.espresso.action.ViewActions.replaceText] ViewAction can be used for 384 | * replacing existing ot empty text into text field. 385 | * - the [androidx.test.espresso.action.ViewActions.closeSoftKeyboard] ViewAction allows to hide 386 | * a keyboard from the screen. 387 | * - the Snackbar view has error message 388 | * - the Snackbar text message view has ID: snackbar_text 389 | * (com.google.android.material.R.id.snackbar_text) 390 | * 391 | * Hint(s): 392 | * - error text can be found by text on screen or by text in view 393 | * - Intent.intended(IntentMatcher) allows us to verify that Activity was opened. 394 | * The Intents component should be initialized before using with the Intents#init() method 395 | * and released when it's not needed with the Intents#release() method. As an alternative 396 | * we can use IntentsTestRule, which initializes Espresso-Intents before each test annotated 397 | * with Test and releases Espresso-Intents after each test run. 398 | */ 399 | @Test 400 | fun shouldOpenHomeScreenWhenEmailAndPasswordAreCorrectAndAgreeCheckboxIsChecked() { 401 | fail() 402 | } 403 | } 404 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/alexzh/testapp/ui/settings/SettingsActivityTest.kt: -------------------------------------------------------------------------------- 1 | package com.alexzh.testapp.ui.settings 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.rule.ActivityTestRule 5 | import org.junit.Assert.fail 6 | import org.junit.Rule 7 | import org.junit.Test 8 | import org.junit.runner.RunWith 9 | 10 | /** 11 | * Test suite for checking the SettingsActivity class from a UI perspective. 12 | * 13 | * In Android project often used preferences from "Support Libraries" or "AndroidX". 14 | * Android projects often use preferences from "Support Libraries". Now with the advent of the 15 | * "AndroidX" libraries it is very important to know which ones are used in the tested project. 16 | * Although similar on the first glance they use different technologies under the hood. 17 | * 18 | * The "Support Libraries" implementation use ListView for rendering Preferences. As a result we 19 | * should test them with the onData method. 20 | * 21 | * The "AndroidX" implementation uses RecyclerView for displaying Preferences. As a result, we can 22 | * use the onView method for testing screen with Preferences. Espresso framework has many built-in 23 | * tools that allow one to work with a RecyclerView, like RecyclerViewActions from "espresso-contrib" 24 | * library. 25 | * 26 | * Note: 27 | * This class can be run without any pre-requirements. 28 | * The SettingsActivity class doesn't verify if the User is logged in because the main idea of the 29 | * project is to learn the basics of Espresso framework. 30 | * 31 | * You can check I'mBarista project (https://github.com/AlexZhukovich/ImBarista-App) for more complex 32 | * practices and test cases. 33 | * 34 | * The "art/screens-with-views/settings-screen.png" image demonstrates Settings screen with all views 35 | * and IDs. 36 | */ 37 | @RunWith(AndroidJUnit4::class) 38 | class SettingsActivityTest { 39 | 40 | /** 41 | * The {@link ActivityTestRule} provides functional testing of a single {@link Activity}. 42 | * The Espresso framework requires it for launching Activity for testings. 43 | * 44 | * We can configure activity before running the test in the following ways: 45 | * - Using the {@link #ActivityTestRule(Class, boolean, boolean)} 46 | * - first argument *activityClass* - defines the activity that is under test. 47 | * - second argument *initialTouchMode* - when set to true, activity will be started in "touch mode". 48 | * Touch mode is a state of the UIToolkit that causes the view hierarchy to depend only 49 | * on the user interaction. For more information please check: 50 | * https://android-developers.googleblog.com/2008/12/touch-mode.html 51 | * - third argument *launchActivity* - if this argument is set to true the Activity will be 52 | * launched before the test case. Before the set up marked with @Before and closed after 53 | * tear down marked with @After. This means we cannot configure it before executing the test case. 54 | * If this argument is set to false it is possible to launch the activity with different Intents 55 | * per each test method. This is especially useful if we have extras that we want to put in the intent 56 | * ``` 57 | * @Rule @JvmField 58 | * val activity = ActivityTestRule(HomeActivity::class.java, true, false) 59 | * 60 | * @Before 61 | * fun setUp() { 62 | * val intent = ... 63 | * activity.launchActivity(intent) 64 | * } 65 | * ``` 66 | * - Using the {@link ActivityTestRule#beforeActivityLaunched) method we can do additional 67 | * action before executing test case, like configure Mock objects, clean database, etc.) 68 | * ``` 69 | * @get: Rule 70 | * val activity = object: ActivityTestRule) { 71 | * override fun beforeActivityLaunched() { 72 | * super.beforeActivityLaunched() 73 | * ... 74 | * } 75 | * } 76 | * ``` 77 | * 78 | * Note: 79 | * - a field created for JUnit Rule should be public, not static, and a subtype of TestRule 80 | * or MethodRule (https://junit.org/junit4/javadoc/4.12/org/junit/Rule.html) 81 | * - In Kotlin we can use @get: Rule or @Rule @JvmField annotations for JUnit Rule fields 82 | */ 83 | @get: Rule 84 | val activity = ActivityTestRule(SettingsActivity::class.java) 85 | 86 | /** 87 | * Test case: 88 | * - Toolbar should have a text "Settings" 89 | * 90 | * Notes: 91 | * - the "Toolbar" view has ID: settingsToolbar (R.id.settingsToolbar) 92 | * - the "Settings" string can be found in strings.xml file as app_name const 93 | * (R.string.screen_settings) 94 | * 95 | * Hint(s): 96 | * - the [androidx.test.platform.app.InstrumentationRegistry.getInstrumentation] method allows 97 | * to get an instrumentation (can be not implemented yet). 98 | * object which has access to context of the application. 99 | * - the [android.app.Instrumentation.getTargetContext] allows to get a context of the instrumented 100 | * application. 101 | * - the toolbar title can be checked with [com.alexzh.testapp.matchers.ToolbarMatcher.withToolbarTitle] 102 | * ViewMatcher (can be not implemented yet). 103 | */ 104 | @Test 105 | fun shouldToolbarContainsSettingsTitle() { 106 | fail() 107 | } 108 | 109 | /** 110 | * Test case: 111 | * - the "Messages" category should be displayed 112 | * - the "Sync" category should be displayed 113 | * 114 | * Notes: 115 | * - the "Messages" string can be found in strings.xml file as messages_header const 116 | * (R.string.messages_header) 117 | * - the "Sync" string can be found in strings.xml file as sync_header const 118 | * (R.string.sync_header) 119 | * 120 | * Hint(s): 121 | * - check which preferences are used "Support Libraries" vs "AndroidX" (read more about 122 | * difference in comment for the [SettingsActivityTest] class). You can check the 123 | * /res/xml/root_preferences.xml for understanding which preferences are used in the project. 124 | * - check ID of Preferences container (ListView or RecyclerView). The "UiAutomatorViewer" 125 | * and "Layout Inspector" can help with it. 126 | * - the checking descendant property can done with [com.alexzh.testapp.matchers.RecyclerViewMatchers.atPosition] 127 | * (can be not implemented yet). 128 | * ViewMatcher (can be not implemented yet). 129 | * - the preference item often has 2 descends: title and value. 130 | * - the [androidx.test.espresso.matcher.ViewMatchers.hasDescendant] matcher allows to match 131 | * Matcher to descendants of specified View. 132 | */ 133 | @Test 134 | fun shouldMessagesAndSyncCategoriesDisplayed() { 135 | fail() 136 | } 137 | 138 | /** 139 | * Test case: 140 | * - the "You signature" property has "Not set" default value 141 | * - the "Default reply action" has "Reply" default value 142 | * 143 | * Notes: 144 | * - the "Your signature" string can be found in stings.xml file as signature_title const 145 | * (R.string.signature_title) 146 | * - the "Default reply action" string can be found in stings.xml file as reply_title const 147 | * (R.string.reply_title) 148 | * - the "Reply" action" string can be found in stings.xml file as reply_entity const 149 | * (R.string.reply_entity) 150 | * 151 | * Hint(s): 152 | * - check which preferences are used "Support Libraries" vs "AndroidX" (read more about 153 | * difference in comment for the [SettingsActivityTest] class). You can check the 154 | * /res/xml/root_preferences.xml for understanding which preferences are used in the project. 155 | * - check ID of Preferences container (ListView or RecyclerView). The "UiAutomatorViewer" 156 | * and "Layout Inspector" can help with it. 157 | * - the checking descendant property can done with [com.alexzh.testapp.matchers.RecyclerViewMatchers.atPosition] 158 | * (can be not implemented yet). 159 | * ViewMatcher (can be not implemented yet). 160 | * - the preference item often has 2 descends: title and value. 161 | */ 162 | @Test 163 | fun shouldMessagesPropertiesHaveDefaultValues() { 164 | fail() 165 | } 166 | 167 | /** 168 | * Test case: 169 | * - the "Sync email periodically" has "not checked" default value 170 | * - the "Download incoming attachments" has "not checked" default value 171 | * 172 | * Notes: 173 | * - the "Sync email periodically" string can be found in stings.xml file as sync_title const 174 | * (R.string.sync_title) 175 | * - the "Download incoming attachments" string can be found in strings.xml file as 176 | * attachment_title const (R.string.attachment_title) 177 | * 178 | * Hint(s): 179 | * - check which preferences are used "Support Libraries" vs "AndroidX" (read more about 180 | * difference in comment for the [SettingsActivityTest] class). You can check the 181 | * /res/xml/root_preferences.xml for understanding which preferences are used in the project. 182 | * - check ID of Preferences container (ListView or RecyclerView). The "UiAutomatorViewer" 183 | * and "Layout Inspector" can help with it. 184 | * - the checking descendant property can done with [com.alexzh.testapp.matchers.RecyclerViewMatchers.atPosition] 185 | * (can be not implemented yet). 186 | * ViewMatcher (can be not implemented yet). 187 | * - the preference item often has 2 descends: title and value. 188 | * - the [androidx.test.espresso.matcher.ViewMatchers.isChecked] matcher allows to check that 189 | * CheckBox, RadioButton, Switch, or other Views are checked. 190 | * - the [androidx.test.espresso.matcher.ViewMatchers.hasDescendant] matcher allows to match 191 | * Matcher to descendants of specified View. 192 | * - the [org.hamcrest.CoreMatchers.instanceOf] method allows to check that View of specified 193 | * type. 194 | * - the [org.hamcrest.CoreMatchers.allOf] method allows to create a matcher of matchers. 195 | */ 196 | @Test 197 | fun shouldSyncPropertiesHaveDefaultValues() { 198 | fail() 199 | } 200 | 201 | /** 202 | * Test case: 203 | * - the "Your signature" property has "Not set" default value 204 | * - the "Your signature" property has "test" value after changing it in dialog 205 | * 206 | * Notes: 207 | * - the "Your signature" string can be found in stings.xml file as signature_title const 208 | * (R.string.signature_title) 209 | * 210 | * Hint(s): 211 | * - check which preferences are used "Support Libraries" vs "AndroidX" (read more about 212 | * difference in comment for the [SettingsActivityTest] class). You can check the 213 | * /res/xml/root_preferences.xml for understanding which preferences are used in the project. 214 | * - check ID of Preferences container (ListView or RecyclerView). The "UiAutomatorViewer" 215 | * and "Layout Inspector" can help with it. 216 | * - the checking descendant property can done with [com.alexzh.testapp.matchers.RecyclerViewMatchers.atPosition] 217 | * (can be not implemented yet). 218 | * ViewMatcher (can be not implemented yet). 219 | * - the preference item often has 2 descends: title and value. 220 | * - the [androidx.test.espresso.matcher.ViewMatchers.hasDescendant] matcher allows to match 221 | * Matcher to descendants of specified View. 222 | */ 223 | @Test 224 | fun shouldChangeDefaultValueOfYourSignature() { 225 | fail() 226 | } 227 | 228 | /** 229 | * Test case: 230 | * - the "Default reply action" has "Reply" default value 231 | * - the "Default reply action" has "Reply to all" value after changing it in dialog 232 | * 233 | * Notes: 234 | * - the "Default reply action" string can be found in stings.xml file as reply_title const 235 | * (R.string.reply_title) 236 | * - the "Reply" string can be found in stings.xml file as reply_entity const 237 | * (R.string.reply_entity) 238 | * - the "Reply to all" string can be found in stings.xml file as reply_to_all_entity const 239 | * (R.string.reply_to_all_entity) 240 | * 241 | * Hint(s): 242 | * - check which preferences are used "Support Libraries" vs "AndroidX" (read more about 243 | * difference in comment for the [SettingsActivityTest] class). You can check the 244 | * /res/xml/root_preferences.xml for understanding which preferences are used in the project. 245 | * - check ID of Preferences container (ListView or RecyclerView). The "UiAutomatorViewer" 246 | * and "Layout Inspector" can help with it. 247 | * - the checking descendant property can done with [com.alexzh.testapp.matchers.RecyclerViewMatchers.atPosition] 248 | * (can be not implemented yet). 249 | * ViewMatcher (can be not implemented yet). 250 | * - the preference item often has 2 descends: title and value. 251 | * - the [androidx.test.espresso.matcher.ViewMatchers.hasDescendant] matcher allows to match 252 | * Matcher to descendants of specified View. 253 | */ 254 | @Test 255 | fun shouldChangeDefaultValueOfDefaultReplyAction() { 256 | fail() 257 | } 258 | 259 | /** 260 | * Test case: 261 | * - the "Sync email periodically" has "false" default value 262 | * - the "Sync email periodically" has "true" value after changing property 263 | * 264 | * Notes: 265 | * - the "Sync email periodically" string can be found in stings.xml file as sync_title const 266 | * (R.string.sync_title) 267 | * 268 | * Hint(s): 269 | * - check which preferences are used "Support Libraries" vs "AndroidX" (read more about 270 | * difference in comment for the [SettingsActivityTest] class). You can check the 271 | * /res/xml/root_preferences.xml for understanding which preferences are used in the project. 272 | * - check ID of Preferences container (ListView or RecyclerView). The "UiAutomatorViewer" 273 | * and "Layout Inspector" can help with it. 274 | * - the checking descendant property can done with RecyclerViewMatchers#atPosition ViewMatcher 275 | * (can be not implemented yet). 276 | * - the preference item often has 2 descends: title and value. 277 | * - the [androidx.test.espresso.matcher.ViewMatchers.isChecked] matcher allows to check that 278 | * CheckBox, RadioButton, Switch, or other Views are checked. 279 | * - the [androidx.test.espresso.matcher.ViewMatchers.hasDescendant] matcher allows to match 280 | * Matcher to descendants of specified View. 281 | * - the [org.hamcrest.CoreMatchers.instanceOf] method allows to check that View of specified 282 | * type. 283 | * - the [org.hamcrest.CoreMatchers.allOf] method allows to create a matcher of matchers. 284 | */ 285 | @Test 286 | fun shouldChangeDefaultValueOfSyncEmailPeriodically() { 287 | fail() 288 | } 289 | 290 | /** 291 | * Test case: 292 | * - the "Download incoming attachments" is disabled 293 | * - the "Download incoming attachments" has "false" default value 294 | * - the "Sync email periodically" has "true" value after changing property 295 | * - the "Download incoming attachments" enabled after changing "Sync email periodically" to "true" 296 | * - the "Download incoming attachments" change value after changing property 297 | * 298 | * Notes: 299 | * - the "Download incoming attachments" string can be found in stings.xml file as attachment_title const 300 | * (R.string.attachment_title) 301 | * - the "Sync email periodically" string can be found in stings.xml file as sync_title const 302 | * (R.string.sync_title) 303 | * 304 | * Hint(s): 305 | * - check which preferences are used "Support Libraries" vs "AndroidX" (read more about 306 | * difference in comment for the [SettingsActivityTest] class). You can check the 307 | * /res/xml/root_preferences.xml for understanding which preferences are used in the project. 308 | * - check ID of Preferences container (ListView or RecyclerView). The "UiAutomatorViewer" 309 | * and "Layout Inspector" can help with it. 310 | * - the checking descendant property can done with RecyclerViewMatchers#atPosition ViewMatcher 311 | * (can be not implemented yet). 312 | * - the preference item often has 2 descends: title and value. 313 | * - the [androidx.test.espresso.matcher.ViewMatchers.isEnabled] matcher allow to check that 314 | * View is enabled. 315 | * - the [androidx.test.espresso.matcher.ViewMatchers.isChecked] matcher allows to check that 316 | * CheckBox, RadioButton, Switch, or other Views are checked. 317 | * - the [androidx.test.espresso.matcher.ViewMatchers.hasDescendant] matcher allows to match 318 | * Matcher to descendants of specified View. 319 | * - the [org.hamcrest.CoreMatchers.instanceOf] method allows to check that View of specified 320 | * type. 321 | * - the [org.hamcrest.CoreMatchers.allOf] method allows to create a matcher of matchers. 322 | */ 323 | @Test 324 | fun shouldChangeDefaultValueOfDownloadIncomingAttachments() { 325 | fail() 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/alexzh/testapp/data/DummyData.kt: -------------------------------------------------------------------------------- 1 | package com.alexzh.testapp.data 2 | 3 | import com.alexzh.testapp.model.Task 4 | 5 | object DummyData { 6 | private const val TASK_COUNT = 44 7 | const val EMAIL = "t@t.t" 8 | const val PASSWORD = "t" 9 | 10 | fun getTasks(): List { 11 | return mutableListOf().apply { 12 | repeat(TASK_COUNT) { 13 | this.add( 14 | Task( 15 | title = "Test item ${it + 1}", 16 | isFavourite = it % 2 == 0 17 | ) 18 | ) 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/alexzh/testapp/ext/String.kt: -------------------------------------------------------------------------------- 1 | package com.alexzh.testapp.ext 2 | 3 | import android.util.Patterns 4 | 5 | fun String.isValidEmail(): Boolean { 6 | val matcher = Patterns.EMAIL_ADDRESS.matcher(this) 7 | return matcher.matches() 8 | } 9 | 10 | fun String.isNotValidEmail(): Boolean { 11 | return !this.isValidEmail() 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/alexzh/testapp/model/Task.kt: -------------------------------------------------------------------------------- 1 | package com.alexzh.testapp.model 2 | 3 | data class Task( 4 | val title: String, 5 | val isFavourite: Boolean 6 | ) 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/alexzh/testapp/ui/home/HomeActivity.kt: -------------------------------------------------------------------------------- 1 | package com.alexzh.testapp.ui.home 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import androidx.appcompat.app.AppCompatActivity 6 | import android.os.Bundle 7 | import android.view.Menu 8 | import android.view.MenuItem 9 | import androidx.recyclerview.widget.DividerItemDecoration 10 | import androidx.recyclerview.widget.LinearLayoutManager 11 | import com.alexzh.testapp.R 12 | import com.alexzh.testapp.data.DummyData 13 | import com.alexzh.testapp.model.Task 14 | import com.alexzh.testapp.ui.home.adapter.TaskAdapter 15 | import com.alexzh.testapp.ui.settings.SettingsActivity 16 | import com.google.android.material.snackbar.Snackbar 17 | import kotlinx.android.synthetic.main.activity_home.* 18 | 19 | class HomeActivity : AppCompatActivity() { 20 | 21 | companion object { 22 | 23 | fun start(context: Context) { 24 | context.startActivity(Intent(context, HomeActivity::class.java)) 25 | } 26 | } 27 | 28 | override fun onCreate(savedInstanceState: Bundle?) { 29 | super.onCreate(savedInstanceState) 30 | setContentView(R.layout.activity_home) 31 | setSupportActionBar(homeToolbar) 32 | 33 | val adapter = TaskAdapter() 34 | adapter.setOnItemClick { handleItemClick(it) } 35 | adapter.setOnFavouriteIconClick { handleItemFavouriteIconClick(it) } 36 | adapter.setTasks(DummyData.getTasks()) 37 | 38 | val layoutManager = LinearLayoutManager(this) 39 | recyclerView.layoutManager = layoutManager 40 | recyclerView.adapter = adapter 41 | recyclerView.addItemDecoration(DividerItemDecoration(recyclerView.context, layoutManager.orientation)) 42 | } 43 | 44 | override fun onCreateOptionsMenu(menu: Menu?): Boolean { 45 | menuInflater.inflate(R.menu.general_menu, menu) 46 | return true 47 | } 48 | 49 | override fun onOptionsItemSelected(item: MenuItem?): Boolean { 50 | when (item?.itemId) { 51 | R.id.settings_action -> { 52 | SettingsActivity.start(this@HomeActivity) 53 | return true 54 | } 55 | } 56 | return false 57 | } 58 | 59 | private fun handleItemClick(task: Task) { 60 | displayMessage(task.title) 61 | } 62 | 63 | private fun handleItemFavouriteIconClick(task: Task) { 64 | val text = if (task.isFavourite) { 65 | getString(R.string.formatted_favourite_task, task.title) 66 | } else { 67 | getString(R.string.formatted_not_favourite_task, task.title) 68 | } 69 | displayMessage(text) 70 | } 71 | 72 | private fun displayMessage(text: String) { 73 | Snackbar.make(root, text, Snackbar.LENGTH_LONG).show() 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /app/src/main/java/com/alexzh/testapp/ui/home/adapter/TaskAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.alexzh.testapp.ui.home.adapter 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.recyclerview.widget.RecyclerView 6 | import com.alexzh.testapp.R 7 | import com.alexzh.testapp.model.Task 8 | 9 | class TaskAdapter : RecyclerView.Adapter() { 10 | private var itemClick: ((Task) -> Unit)? = null 11 | private var favouriteIconClick: ((Task) -> Unit)? = null 12 | private val tasks = mutableListOf() 13 | 14 | fun setTasks(tasks: List) { 15 | this.tasks.clear() 16 | this.tasks.addAll(tasks) 17 | notifyDataSetChanged() 18 | } 19 | 20 | fun setOnItemClick(itemClick: (Task) -> Unit) { 21 | this.itemClick = itemClick 22 | } 23 | 24 | fun setOnFavouriteIconClick(favouriteIconClick: (Task) -> Unit) { 25 | this.favouriteIconClick = favouriteIconClick 26 | } 27 | 28 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TaskViewHolder { 29 | return TaskViewHolder( 30 | LayoutInflater.from(parent.context).inflate(R.layout.item_task, parent, false), 31 | favouriteIconClick, 32 | itemClick 33 | ) 34 | } 35 | 36 | override fun onBindViewHolder(holder: TaskViewHolder, position: Int) { 37 | holder.bind(tasks[position]) 38 | } 39 | 40 | override fun getItemCount(): Int = tasks.size 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/alexzh/testapp/ui/home/adapter/TaskViewHolder.kt: -------------------------------------------------------------------------------- 1 | package com.alexzh.testapp.ui.home.adapter 2 | 3 | import android.view.View 4 | import androidx.recyclerview.widget.RecyclerView 5 | import com.alexzh.testapp.R 6 | import com.alexzh.testapp.model.Task 7 | import kotlinx.android.synthetic.main.item_task.view.* 8 | 9 | class TaskViewHolder( 10 | view: View, 11 | private val favouriteIconClick: ((Task) -> Unit)?, 12 | private val itemClick: ((Task) -> Unit)? 13 | ) : RecyclerView.ViewHolder(view) { 14 | 15 | fun bind(task: Task) { 16 | val drawableId = if (task.isFavourite) { 17 | R.drawable.ic_favorite_black_24dp 18 | } else { 19 | R.drawable.ic_favorite_border_black_24dp 20 | } 21 | 22 | itemView.titleTextView.text = task.title 23 | itemView.favouriteImageView.setImageResource(drawableId) 24 | itemView.favouriteImageView.tag = drawableId 25 | 26 | if (favouriteIconClick != null) { 27 | itemView.favouriteImageView.setOnClickListener { favouriteIconClick.invoke(task) } 28 | } 29 | 30 | if (itemClick != null) { 31 | itemView.setOnClickListener { itemClick.invoke(task) } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/alexzh/testapp/ui/login/LoginActivity.kt: -------------------------------------------------------------------------------- 1 | package com.alexzh.testapp.ui.login 2 | 3 | import androidx.appcompat.app.AppCompatActivity 4 | import android.os.Bundle 5 | import com.alexzh.testapp.R 6 | import com.alexzh.testapp.data.DummyData 7 | import com.alexzh.testapp.ext.isNotValidEmail 8 | import com.alexzh.testapp.ui.home.HomeActivity 9 | import com.google.android.material.snackbar.Snackbar 10 | import kotlinx.android.synthetic.main.activity_login.* 11 | 12 | class LoginActivity : AppCompatActivity() { 13 | 14 | override fun onCreate(savedInstanceState: Bundle?) { 15 | super.onCreate(savedInstanceState) 16 | setContentView(R.layout.activity_login) 17 | 18 | logoImageView.tag = R.drawable.ic_lock_24dp 19 | 20 | loginButton.setOnClickListener { 21 | val email = emailEditText.text.toString() 22 | val password = passwordEditText.text.toString() 23 | 24 | if (email.isBlank() && password.isBlank()) { 25 | displayMessage(getString(R.string.email_and_password_are_blank)) 26 | return@setOnClickListener 27 | } 28 | 29 | if (email.isBlank() && password.isNotBlank()) { 30 | displayMessage(getString(R.string.email_is_blank)) 31 | return@setOnClickListener 32 | } 33 | 34 | if (password.isBlank() && email.isNotEmpty()) { 35 | displayMessage(getString(R.string.password_is_blank)) 36 | return@setOnClickListener 37 | } 38 | 39 | if (email.isNotValidEmail()) { 40 | displayMessage(getString(R.string.text_is_not_email)) 41 | return@setOnClickListener 42 | } 43 | 44 | if (!(email == DummyData.EMAIL && password == DummyData.PASSWORD)) { 45 | displayMessage(getString(R.string.check_email_and_password)) 46 | return@setOnClickListener 47 | } 48 | 49 | if (!agreeCheckBox.isChecked) { 50 | displayMessage(getString(R.string.i_agree_should_be_checked)) 51 | return@setOnClickListener 52 | } 53 | 54 | HomeActivity.start(this) 55 | } 56 | } 57 | 58 | private fun displayMessage(text: String) { 59 | Snackbar.make(root, text, Snackbar.LENGTH_LONG).show() 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/java/com/alexzh/testapp/ui/settings/SettingsActivity.kt: -------------------------------------------------------------------------------- 1 | package com.alexzh.testapp.ui.settings 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import android.view.MenuItem 7 | import androidx.appcompat.app.AppCompatActivity 8 | import androidx.preference.PreferenceFragmentCompat 9 | import com.alexzh.testapp.R 10 | import kotlinx.android.synthetic.main.settings_activity.* 11 | 12 | class SettingsActivity : AppCompatActivity() { 13 | 14 | companion object { 15 | fun start(context: Context) { 16 | context.startActivity(Intent(context, SettingsActivity::class.java)) 17 | } 18 | } 19 | 20 | override fun onCreate(savedInstanceState: Bundle?) { 21 | super.onCreate(savedInstanceState) 22 | setContentView(R.layout.settings_activity) 23 | setSupportActionBar(settingsToolbar) 24 | supportFragmentManager 25 | .beginTransaction() 26 | .replace(R.id.settings, SettingsFragment()) 27 | .commit() 28 | supportActionBar?.setDisplayHomeAsUpEnabled(true) 29 | } 30 | 31 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 32 | when (item.itemId) { 33 | android.R.id.home -> { 34 | onBackPressed() 35 | return true 36 | } 37 | } 38 | return false 39 | } 40 | 41 | class SettingsFragment : PreferenceFragmentCompat() { 42 | override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { 43 | setPreferencesFromResource(R.xml.root_preferences, rootKey) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_favorite_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_favorite_border_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 44 | 46 | 48 | 50 | 52 | 54 | 56 | 58 | 60 | 62 | 64 | 66 | 68 | 70 | 72 | 74 | 75 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_lock_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_home.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 18 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_login.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 18 | 21 | 22 | 31 | 42 | 52 | 61 |