Repository: open-tool/ultron Branch: master Commit: f2b51e76a92a Files: 559 Total size: 1.3 MB Directory structure: gitextract_x04z9exv/ ├── .github/ │ └── workflows/ │ ├── ci-pipeline.yml │ ├── docs.yml │ └── maven_central_publish.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle.kts ├── buildSrc/ │ ├── build.gradle.kts │ └── src/ │ └── main/ │ └── kotlin/ │ └── Versions.kt ├── composeApp/ │ ├── build.gradle.kts │ ├── karma.config.d/ │ │ └── wasm/ │ │ └── config.js │ └── src/ │ ├── androidMain/ │ │ ├── AndroidManifest.xml │ │ ├── kotlin/ │ │ │ ├── Platform.android.kt │ │ │ └── com/ │ │ │ └── atiurin/ │ │ │ └── samplekmp/ │ │ │ └── MainActivity.kt │ │ └── res/ │ │ ├── drawable/ │ │ │ └── ic_launcher_background.xml │ │ ├── drawable-v24/ │ │ │ └── ic_launcher_foreground.xml │ │ ├── mipmap-anydpi-v26/ │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ └── values/ │ │ └── strings.xml │ ├── commonMain/ │ │ ├── composeResources/ │ │ │ └── drawable/ │ │ │ └── compose-multiplatform.xml │ │ └── kotlin/ │ │ ├── App.kt │ │ ├── Greeting.kt │ │ ├── Platform.kt │ │ ├── repositories/ │ │ │ ├── ContactRepository.kt │ │ │ └── Storage.kt │ │ └── ui/ │ │ └── screens/ │ │ └── ContactsListScreen.kt │ ├── commonTest/ │ │ └── kotlin/ │ │ ├── BaseInteractionTest.kt │ │ ├── ExampleTest.kt │ │ ├── ListTest.kt │ │ ├── UltronTestFlowTest.kt │ │ └── UltronTestFlowTest2.kt │ ├── desktopMain/ │ │ └── kotlin/ │ │ ├── Platform.jvm.kt │ │ └── main.kt │ ├── desktopTest/ │ │ └── kotlin/ │ │ └── DesktopSampleTest.kt │ ├── iosMain/ │ │ └── kotlin/ │ │ ├── MainViewController.kt │ │ └── Platform.ios.kt │ ├── iosTest/ │ │ └── kotlin/ │ │ └── IOSSampleTest.kt │ ├── jsMain/ │ │ └── kotlin/ │ │ └── Platform.js.kt │ ├── jsTest/ │ │ └── kotlin/ │ │ └── JsSampleTest.kt │ └── wasmJsMain/ │ ├── kotlin/ │ │ ├── Platform.wasmJs.kt │ │ └── main.kt │ └── resources/ │ ├── index.html │ └── styles.css ├── docs/ │ ├── .gitignore │ ├── README.md │ ├── babel.config.js │ ├── docs/ │ │ ├── android/ │ │ │ ├── _category_.json │ │ │ ├── espress.md │ │ │ ├── recyclerview.md │ │ │ ├── rootview.md │ │ │ ├── testconditions.md │ │ │ ├── uiautomator.md │ │ │ └── webview.md │ │ ├── common/ │ │ │ ├── _category_.json │ │ │ ├── allure.md │ │ │ ├── boolean.md │ │ │ ├── customassertion.md │ │ │ ├── extension.md │ │ │ ├── listeners.md │ │ │ ├── resulthandler.md │ │ │ ├── uiblock.md │ │ │ └── ultrontest.md │ │ ├── compose/ │ │ │ ├── _category_.json │ │ │ ├── android.md │ │ │ ├── api.md │ │ │ ├── index.md │ │ │ ├── lazylist.md │ │ │ └── multiplatform.md │ │ ├── index.md │ │ └── intro/ │ │ ├── _category_.json │ │ ├── configuration.md │ │ ├── connect.md │ │ └── dependencies.md │ ├── docusaurus.config.ts │ ├── package.json │ ├── sidebars.ts │ ├── src/ │ │ ├── components/ │ │ │ └── HomepageFeatures/ │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ │ ├── css/ │ │ │ └── custom.css │ │ └── pages/ │ │ ├── index.module.css │ │ ├── index.tsx │ │ └── markdown-page.md │ ├── static/ │ │ └── .nojekyll │ └── tsconfig.json ├── gradle/ │ ├── libs.versions.toml │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── iosApp/ │ ├── Configuration/ │ │ └── Config.xcconfig │ ├── iosApp/ │ │ ├── Assets.xcassets/ │ │ │ ├── AccentColor.colorset/ │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset/ │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── ContentView.swift │ │ ├── Info.plist │ │ ├── Preview Content/ │ │ │ └── Preview Assets.xcassets/ │ │ │ └── Contents.json │ │ └── iOSApp.swift │ └── iosApp.xcodeproj/ │ └── project.pbxproj ├── prepare-emulator.bat ├── prepare-emulator.sh ├── sample-app/ │ ├── .gitignore │ ├── build.gradle.kts │ ├── proguard-rules.pro │ └── src/ │ ├── androidTest/ │ │ └── java/ │ │ └── com/ │ │ └── atiurin/ │ │ └── sampleapp/ │ │ ├── framework/ │ │ │ ├── CustomTestRunner.kt │ │ │ ├── DummyMetaObject.kt │ │ │ ├── Log.kt │ │ │ ├── ScreenshotLifecycleListener.kt │ │ │ ├── ultronext/ │ │ │ │ ├── UltronComposeExt.kt │ │ │ │ ├── UltronEspressoExt.kt │ │ │ │ ├── UltronEspressoWebExt.kt │ │ │ │ └── UltronUiAutomatorExt.kt │ │ │ └── utils/ │ │ │ ├── AssertUtils.kt │ │ │ ├── EspressoUtil.kt │ │ │ ├── TestDataUtils.kt │ │ │ └── TimeUtils.kt │ │ ├── pages/ │ │ │ ├── ChatPage.kt │ │ │ ├── ComposeElementsPage.kt │ │ │ ├── ComposeListPage.kt │ │ │ ├── ComposeSecondPage.kt │ │ │ ├── FriendsListPage.kt │ │ │ ├── UiElementsPage.kt │ │ │ ├── UiObject2ElementsPage.kt │ │ │ ├── UiObject2FriendsListPage.kt │ │ │ ├── UiObjectElementsPage.kt │ │ │ ├── WebViewPage.kt │ │ │ └── uiblock/ │ │ │ ├── ComposeUiBlockScreen.kt │ │ │ ├── EspressoUiBlockScreen.kt │ │ │ ├── UiObject2UiBlockScreen.kt │ │ │ └── WebElementUiBlockScreen.kt │ │ └── tests/ │ │ ├── BaseTest.kt │ │ ├── UiElementsTest.kt │ │ ├── compose/ │ │ │ ├── CheckboxTest.kt │ │ │ ├── CollectionInteractionTest.kt │ │ │ ├── ComposeConfigTest.kt │ │ │ ├── ComposeCustomAssertionTest.kt │ │ │ ├── ComposeEmptyListTest.kt │ │ │ ├── ComposeListTest.kt │ │ │ ├── ComposeListWithPositionTestTagTest.kt │ │ │ ├── ComposeUIElementsTest.kt │ │ │ ├── DefaultComponentActivityTest.kt │ │ │ ├── RunUltronUiTest.kt │ │ │ ├── SampleClassTest.kt │ │ │ ├── SemNodeInteractionObjectTest.kt │ │ │ ├── TreeTest.kt │ │ │ ├── UltronComposeUiBlockTest.kt │ │ │ └── elements/ │ │ │ └── DataPickerTest.kt │ │ ├── espresso/ │ │ │ ├── CustomClicksTest.kt │ │ │ ├── CustomMatchersTest.kt │ │ │ ├── DemoEspressoTest.kt │ │ │ ├── RecyclerPerfTest.kt │ │ │ ├── RecyclerViewTest.kt │ │ │ ├── UltronActivityRuleTest.kt │ │ │ ├── UltronEspressoConfigTest.kt │ │ │ ├── UltronEspressoUiBlockTest.kt │ │ │ ├── ViewInteractionActionsTest.kt │ │ │ ├── ViewInteractionAssertionsTest.kt │ │ │ ├── ViewTest.kt │ │ │ └── WithSuitableRootTest.kt │ │ ├── espresso_web/ │ │ │ ├── BaseWebViewTest.kt │ │ │ ├── EspressoWebUiElementsTest.kt │ │ │ ├── UltronWebDocumentTest.kt │ │ │ ├── UltronWebElementTest.kt │ │ │ ├── UltronWebElementsTest.kt │ │ │ └── UltronWebUiBlockTest.kt │ │ ├── testlifecycle/ │ │ │ ├── ExceptionsProcessingTest.kt │ │ │ ├── ParametrizedTest.kt │ │ │ ├── SetUpTearDownRuleTest.kt │ │ │ ├── UltronTestFlowTest.kt │ │ │ ├── UltronTestFlowTest2.kt │ │ │ ├── UltronTestPlan.kt │ │ │ └── UltronTestRuleSequenceMergeTest.kt │ │ └── uiautomator/ │ │ ├── UiAutomatorCustomAssertionTest.kt │ │ ├── UltronUiAutomatorPerfTest.kt │ │ ├── UltronUiObject2ActionsTest.kt │ │ ├── UltronUiObject2AssertionsTest.kt │ │ ├── UltronUiObject2ScrollTest.kt │ │ ├── UltronUiObject2UiBlockTest.kt │ │ ├── UltronUiObjectActionsTest.kt │ │ └── UltronUiObjectAssertionsTest.kt │ ├── debug/ │ │ └── AndroidManifest.xml │ └── main/ │ ├── AndroidManifest.xml │ ├── assets/ │ │ ├── webview.html │ │ └── webview_small.html │ ├── java/ │ │ └── com/ │ │ └── atiurin/ │ │ └── sampleapp/ │ │ ├── MyApplication.kt │ │ ├── activity/ │ │ │ ├── BusyActivity.kt │ │ │ ├── ChatActivity.kt │ │ │ ├── ComposeElementsActivity.kt │ │ │ ├── ComposeListActivity.kt │ │ │ ├── ComposeListWithPositionTestTagActivity.kt │ │ │ ├── ComposeRouterActivity.kt │ │ │ ├── ComposeSecondActivity.kt │ │ │ ├── CustomClicksActivity.kt │ │ │ ├── LoginActivity.kt │ │ │ ├── MainActivity.kt │ │ │ ├── ProfileActivity.kt │ │ │ ├── SplashActivity.kt │ │ │ ├── UiBlockActivity.kt │ │ │ ├── UiElementsActivity.kt │ │ │ └── WebViewActivity.kt │ │ ├── adapters/ │ │ │ ├── ContactAdapter.kt │ │ │ └── MessageAdapter.kt │ │ ├── async/ │ │ │ ├── AsyncDataLoading.kt │ │ │ ├── ContactsPresenter.kt │ │ │ ├── Either.kt │ │ │ ├── GetContacts.kt │ │ │ ├── UseCase.kt │ │ │ └── task/ │ │ │ └── CompatAsyncTask.kt │ │ ├── compose/ │ │ │ ├── ContacsList.kt │ │ │ ├── CustomButton.kt │ │ │ ├── DatePicker.kt │ │ │ ├── LinearProgressBar.kt │ │ │ ├── LoadingAnimation.kt │ │ │ ├── RadioGroup.kt │ │ │ ├── RegionsClickListener.kt │ │ │ ├── SimpleOutlinedText.kt │ │ │ ├── SwipeableNode.kt │ │ │ ├── app/ │ │ │ │ ├── App.kt │ │ │ │ ├── AppBar.kt │ │ │ │ └── AppScreen.kt │ │ │ └── screen/ │ │ │ ├── DatePickerScreen.kt │ │ │ └── NavigationScreen.kt │ │ ├── data/ │ │ │ ├── Tags.kt │ │ │ ├── entities/ │ │ │ │ ├── Contact.kt │ │ │ │ ├── Message.kt │ │ │ │ └── User.kt │ │ │ ├── loaders/ │ │ │ │ └── MessageLoader.kt │ │ │ ├── repositories/ │ │ │ │ ├── ContactRepositoty.kt │ │ │ │ ├── MessageRepository.kt │ │ │ │ └── Storage.kt │ │ │ └── viewmodel/ │ │ │ ├── ContactsViewModel.kt │ │ │ └── DataViewModel.kt │ │ ├── idlingresources/ │ │ │ ├── AbstractIdlingResource.kt │ │ │ ├── Holder.kt │ │ │ ├── IdlingHelper.kt │ │ │ └── resources/ │ │ │ ├── ChatIdlingResource.kt │ │ │ └── ContactsIdlingResource.kt │ │ ├── managers/ │ │ │ ├── AccountManager.kt │ │ │ └── PrefsManager.kt │ │ ├── utils/ │ │ │ └── TimeUtils.kt │ │ └── view/ │ │ ├── CircleImageView.java │ │ └── listeners/ │ │ └── OnSwipeTouchListener.kt │ └── res/ │ ├── drawable/ │ │ ├── background_splash.xml │ │ ├── circle.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_menu_camera.xml │ │ ├── ic_menu_gallery.xml │ │ ├── ic_menu_manage.xml │ │ ├── ic_menu_send.xml │ │ ├── ic_menu_share.xml │ │ ├── ic_menu_slideshow.xml │ │ ├── img.xml │ │ └── side_nav_bar.xml │ ├── drawable-anydpi/ │ │ ├── ic_account.xml │ │ ├── ic_attach_file.xml │ │ ├── ic_exit.xml │ │ ├── ic_messages.xml │ │ └── ic_send.xml │ ├── drawable-v24/ │ │ └── ic_launcher_foreground.xml │ ├── layout/ │ │ ├── activity_chat.xml │ │ ├── activity_custom_clicks.xml │ │ ├── activity_login.xml │ │ ├── activity_main.xml │ │ ├── activity_profile.xml │ │ ├── activity_uiblock.xml │ │ ├── activity_uielements.xml │ │ ├── activity_webview.xml │ │ ├── app_bar_main.xml │ │ ├── content_main.xml │ │ ├── list_item.xml │ │ ├── message_item.xml │ │ ├── my_text_view.xml │ │ ├── nav_header_main.xml │ │ └── ui_block_contact_item.xml │ ├── menu/ │ │ ├── activity_main_drawer.xml │ │ └── main.xml │ ├── mipmap-anydpi-v26/ │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ ├── values/ │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── values-v21/ │ └── styles.xml ├── settings.gradle.kts ├── ultron-allure/ │ ├── .gitignore │ ├── build.gradle.kts │ ├── gradle.properties │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── atiurin/ │ │ └── ultron/ │ │ └── allure/ │ │ ├── UltronAllureTestRunner.kt │ │ ├── attachment/ │ │ │ ├── AllureDirectoryUtil.kt │ │ │ └── AttachUtil.kt │ │ ├── condition/ │ │ │ ├── AllureConditionExecutorWrapper.kt │ │ │ └── AllureConditionsExecutor.kt │ │ ├── config/ │ │ │ ├── AllureAttachStrategy.kt │ │ │ ├── AllureConfigParams.kt │ │ │ └── UltronAllureConfig.kt │ │ ├── hierarchy/ │ │ │ └── AllureHierarchyDumper.kt │ │ ├── listeners/ │ │ │ ├── DetailedOperationAllureListener.kt │ │ │ ├── ScreenshotAttachListener.kt │ │ │ └── WindowHierarchyAttachListener.kt │ │ ├── runner/ │ │ │ ├── LogcatAttachRunListener.kt │ │ │ ├── ScreenshotAttachRunListener.kt │ │ │ ├── UltronAllureResultsTransferListener.kt │ │ │ ├── UltronAllureRunInformer.kt │ │ │ ├── UltronLogAttachRunListener.kt │ │ │ ├── UltronLogCleanerRunListener.kt │ │ │ ├── UltronTestRunListener.kt │ │ │ └── WindowHierarchyAttachRunListener.kt │ │ ├── screenshot/ │ │ │ └── AllureScreenshot.kt │ │ └── step/ │ │ └── UltronStep.kt │ └── test/ │ └── java/ │ └── com/ │ └── atiurin/ │ └── ultron/ │ └── allure/ │ └── ExampleUnitTest.kt ├── ultron-android/ │ ├── .gitignore │ ├── build.gradle.kts │ ├── gradle.properties │ └── src/ │ ├── main/ │ │ ├── kotlin/ │ │ │ └── com/ │ │ │ └── atiurin/ │ │ │ └── ultron/ │ │ │ ├── core/ │ │ │ │ ├── config/ │ │ │ │ │ ├── UltronConfig.kt │ │ │ │ │ └── UltronConfigParams.kt │ │ │ │ ├── espresso/ │ │ │ │ │ ├── EspressoOperationExecutor.kt │ │ │ │ │ ├── EspressoOperationResult.kt │ │ │ │ │ ├── UltronEspresso.kt │ │ │ │ │ ├── UltronEspressoInteraction.kt │ │ │ │ │ ├── UltronEspressoOperation.kt │ │ │ │ │ ├── UltronEspressoOperationLifecycle.kt │ │ │ │ │ ├── UltronEspressoUiBlock.kt │ │ │ │ │ ├── action/ │ │ │ │ │ │ ├── EspressoActionExecutor.kt │ │ │ │ │ │ ├── EspressoActionType.kt │ │ │ │ │ │ ├── UltronCustomClickAction.kt │ │ │ │ │ │ ├── UltronEspressoActionParams.kt │ │ │ │ │ │ ├── UltronSwipeAction.kt │ │ │ │ │ │ └── UltronTypeTextAction.kt │ │ │ │ │ ├── assertion/ │ │ │ │ │ │ ├── EspressoAssertionExecutor.kt │ │ │ │ │ │ ├── EspressoAssertionType.kt │ │ │ │ │ │ └── UltronEspressoAssertionParams.kt │ │ │ │ │ └── recyclerview/ │ │ │ │ │ ├── RecyclerViewItemExecutor.kt │ │ │ │ │ ├── RecyclerViewItemMatchingExecutor.kt │ │ │ │ │ ├── RecyclerViewItemPositionalExecutor.kt │ │ │ │ │ ├── RecyclerViewScrollAction.kt │ │ │ │ │ ├── RecyclerViewScrollToPositionViewAction.kt │ │ │ │ │ ├── RecyclerViewUtils.kt │ │ │ │ │ ├── UltronRecyclerView.kt │ │ │ │ │ ├── UltronRecyclerViewImpl.kt │ │ │ │ │ └── UltronRecyclerViewItem.kt │ │ │ │ ├── espressoweb/ │ │ │ │ │ ├── UltronWebLifecycle.kt │ │ │ │ │ ├── operation/ │ │ │ │ │ │ ├── EspressoWebOperationType.kt │ │ │ │ │ │ ├── WebInteractionOperation.kt │ │ │ │ │ │ ├── WebInteractionOperationExecutor.kt │ │ │ │ │ │ ├── WebInteractionOperationIterationResult.kt │ │ │ │ │ │ ├── WebOperationExecutor.kt │ │ │ │ │ │ └── WebOperationResult.kt │ │ │ │ │ └── webelement/ │ │ │ │ │ ├── UltronWebDocument.kt │ │ │ │ │ ├── UltronWebElement.kt │ │ │ │ │ ├── UltronWebElementId.kt │ │ │ │ │ ├── UltronWebElementUiBlock.kt │ │ │ │ │ ├── UltronWebElementXpath.kt │ │ │ │ │ └── UltronWebElements.kt │ │ │ │ └── uiautomator/ │ │ │ │ ├── UiAutomatorActionType.kt │ │ │ │ ├── UiAutomatorAssertionType.kt │ │ │ │ ├── UiAutomatorOperation.kt │ │ │ │ ├── UiAutomatorOperationExecutor.kt │ │ │ │ ├── UiAutomatorOperationResult.kt │ │ │ │ ├── UltronUiAutomatorLifecycle.kt │ │ │ │ ├── uiobject/ │ │ │ │ │ ├── UiAutomatorUiSelectorOperation.kt │ │ │ │ │ ├── UiAutomatorUiSelectorOperationExecutor.kt │ │ │ │ │ └── UltronUiObject.kt │ │ │ │ └── uiobject2/ │ │ │ │ ├── UiAutomatorBySelectorAction.kt │ │ │ │ ├── UiAutomatorBySelectorActionExecutor.kt │ │ │ │ ├── UiAutomatorBySelectorAssertion.kt │ │ │ │ ├── UiAutomatorBySelectorAssertionExecutor.kt │ │ │ │ ├── UltronUiObject2.kt │ │ │ │ └── UltronUiObject2UiBlock.kt │ │ │ ├── custom/ │ │ │ │ └── espresso/ │ │ │ │ ├── action/ │ │ │ │ │ ├── AnonymousViewAction.kt │ │ │ │ │ ├── CustomEspressoActionType.kt │ │ │ │ │ ├── GetContentDescriptionAction.kt │ │ │ │ │ ├── GetDrawableAction.kt │ │ │ │ │ ├── GetTextAction.kt │ │ │ │ │ └── GetViewAction.kt │ │ │ │ ├── assertion/ │ │ │ │ │ ├── AnyRootAssertions.kt │ │ │ │ │ ├── CustomEspressoAssertionType.kt │ │ │ │ │ ├── DrawableAssertion.kt │ │ │ │ │ ├── ExistsEspressoViewAssertion.kt │ │ │ │ │ └── TextColorAssertion.kt │ │ │ │ ├── base/ │ │ │ │ │ ├── Checker.kt │ │ │ │ │ ├── IterableUtils.kt │ │ │ │ │ ├── RootViewPickerCreator.kt │ │ │ │ │ ├── UltronRootViewFinder.kt │ │ │ │ │ └── UltronViewFinder.kt │ │ │ │ └── matcher/ │ │ │ │ ├── AppCompatTextMatcher.kt │ │ │ │ ├── DrawableMatchers.kt │ │ │ │ ├── ElementWithAttributeMatcher.kt │ │ │ │ ├── NotUniqueViewMatchers.kt │ │ │ │ ├── SuitableRootMatcher.kt │ │ │ │ └── TextColorMatchers.kt │ │ │ ├── extensions/ │ │ │ │ ├── BitmapExt.kt │ │ │ │ ├── DataInterationExt.kt │ │ │ │ ├── DrawableExt.kt │ │ │ │ ├── MatcherViewExt.kt │ │ │ │ ├── PerfomOnViewExt.kt │ │ │ │ ├── RecyclerViewExt.kt │ │ │ │ ├── ReflectionExt.kt │ │ │ │ ├── ViewExt.kt │ │ │ │ └── ViewInteractionExt.kt │ │ │ └── utils/ │ │ │ ├── ViewGroupUtils.kt │ │ │ └── ViewUtils.kt │ │ └── res/ │ │ └── values/ │ │ └── strings.xml │ └── test/ │ └── java/ │ └── com/ │ └── atiurin/ │ └── ultron/ │ └── ExampleUnitTest.java ├── ultron-common/ │ ├── .gitignore │ ├── build.gradle.kts │ ├── gradle.properties │ └── src/ │ ├── androidMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── atiurin/ │ │ └── ultron/ │ │ ├── core/ │ │ │ └── config/ │ │ │ └── UltronAndroidCommonConfig.kt │ │ ├── extensions/ │ │ │ ├── AnyExt.android.kt │ │ │ ├── BundleExt.kt │ │ │ ├── DescriptionExt.kt │ │ │ └── FileExt.android.kt │ │ ├── hierarchy/ │ │ │ ├── HierarchyDumpResult.kt │ │ │ ├── HierarchyDumper.kt │ │ │ └── UiDeviceHierarchyDumper.kt │ │ ├── log/ │ │ │ ├── UltronFileLoggerImpl.android.kt │ │ │ ├── UltronLog.android.kt │ │ │ └── UltronLogcatLogger.android.kt │ │ ├── runner/ │ │ │ ├── RunListener.kt │ │ │ ├── UltronLogRunListener.kt │ │ │ ├── UltronRunInformer.kt │ │ │ └── UltronRunListener.kt │ │ ├── screenshot/ │ │ │ ├── ScreenshotResult.kt │ │ │ ├── Screenshoter.kt │ │ │ ├── UiAutomationScreenshoter.kt │ │ │ └── ViewScreenshoter.kt │ │ ├── testlifecycle/ │ │ │ ├── activity/ │ │ │ │ ├── UltronActivityRule.kt │ │ │ │ ├── UltronActivityScenario.kt │ │ │ │ └── UltronInstrumentationActivityInvoker.kt │ │ │ ├── rulesequence/ │ │ │ │ └── RuleSequence.kt │ │ │ └── setupteardown/ │ │ │ ├── Condition.kt │ │ │ ├── ConditionExecutorWrapper.kt │ │ │ ├── ConditionRule.kt │ │ │ ├── ConditionsExecutor.kt │ │ │ ├── DefaultConditionExecutorWrapper.kt │ │ │ ├── DefaultConditionsExecutor.kt │ │ │ ├── RuleSequenceTearDown.kt │ │ │ ├── SetUp.kt │ │ │ ├── SetUpRule.kt │ │ │ ├── TearDown.kt │ │ │ └── TearDownRule.kt │ │ └── utils/ │ │ ├── ActivityUtil.android.kt.kt │ │ ├── InstrumentationUtil.android.kt │ │ └── ThreadUtil.android.kt │ ├── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── atiurin/ │ │ └── ultron/ │ │ ├── annotations/ │ │ │ └── ExperimentalUltronApi.kt │ │ ├── core/ │ │ │ ├── common/ │ │ │ │ ├── AbstractOperationLifecycle.kt │ │ │ │ ├── DefaultElementInfo.kt │ │ │ │ ├── DefaultOperationIterationResult.kt │ │ │ │ ├── ElementInfo.kt │ │ │ │ ├── Operation.kt │ │ │ │ ├── OperationExecutor.kt │ │ │ │ ├── OperationIterationResult.kt │ │ │ │ ├── OperationProcessor.kt │ │ │ │ ├── OperationResult.kt │ │ │ │ ├── ResultDescriptor.kt │ │ │ │ ├── UltronOperationType.kt │ │ │ │ ├── assertion/ │ │ │ │ │ ├── DefaultOperationAssertion.kt │ │ │ │ │ ├── EmptyOperationAssertion.kt │ │ │ │ │ ├── NoListenersOperationAssertion.kt │ │ │ │ │ ├── OperationAssertion.kt │ │ │ │ │ └── SoftAssertion.kt │ │ │ │ ├── options/ │ │ │ │ │ ├── ClickOption.kt │ │ │ │ │ ├── ContentDescriptionContainsOption.kt │ │ │ │ │ ├── DoubleClickOption.kt │ │ │ │ │ ├── LongClickOption.kt │ │ │ │ │ ├── PerformCustomBlockOption.kt │ │ │ │ │ ├── TextContainsOption.kt │ │ │ │ │ └── TextEqualsOption.kt │ │ │ │ └── resultanalyzer/ │ │ │ │ ├── CheckOperationResultAnalyzer.kt │ │ │ │ ├── DefaultSoftAssertionOperationResultAnalyzer.kt │ │ │ │ ├── OperationResultAnalyzer.kt │ │ │ │ ├── SoftAssertionOperationResultAnalyzer.kt │ │ │ │ └── UltronDefaultOperationResultAnalyzer.kt │ │ │ ├── config/ │ │ │ │ └── UltronCommonConfig.kt │ │ │ └── test/ │ │ │ ├── TestMethod.kt │ │ │ ├── UltronTest.kt │ │ │ └── context/ │ │ │ ├── DefaultUltronTestContext.kt │ │ │ ├── DefaultUltronTestContextProvider.kt │ │ │ ├── UltronTestContext.kt │ │ │ └── UltronTestContextProvider.kt │ │ ├── exceptions/ │ │ │ ├── UltronAssertionBlockException.kt │ │ │ ├── UltronAssertionException.kt │ │ │ ├── UltronException.kt │ │ │ ├── UltronOperationException.kt │ │ │ ├── UltronUiAutomatorException.kt │ │ │ └── UltronWrapperException.kt │ │ ├── extensions/ │ │ │ └── AnyCommonExt.kt │ │ ├── file/ │ │ │ └── MimeType.kt │ │ ├── listeners/ │ │ │ ├── AbstractListener.kt │ │ │ ├── AbstractListenersContainer.kt │ │ │ ├── LifecycleListener.kt │ │ │ ├── LogLifecycleListener.kt │ │ │ ├── UltronLifecycleListener.kt │ │ │ └── UltronListenerUtil.kt │ │ ├── log/ │ │ │ ├── LogLevel.kt │ │ │ ├── ULogger.kt │ │ │ ├── UltronFileLogger.kt │ │ │ ├── UltronLog.kt │ │ │ └── UltronLogUtil.kt │ │ ├── page/ │ │ │ ├── Page.kt │ │ │ └── Screen.kt │ │ └── utils/ │ │ ├── AssertUtils.kt │ │ ├── ThreadUtil.kt │ │ └── TimeUtil.kt │ ├── jsWasmMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── atiurin/ │ │ └── ultron/ │ │ └── utils/ │ │ └── ThreadUtil.jsWasm.kt │ ├── jvmMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── atiurin/ │ │ └── ultron/ │ │ └── utils/ │ │ └── ThreadUtil.jvm.kt │ ├── nativeMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── atiurin/ │ │ └── ultron/ │ │ └── utils/ │ │ └── ThreadUtil.native.kt │ └── shared/ │ └── kotlin/ │ └── com/ │ └── atiurin/ │ └── ultron/ │ └── log/ │ └── UltronLog.shared.kt └── ultron-compose/ ├── build.gradle.kts ├── gradle.properties └── src/ ├── androidMain/ │ └── kotlin/ │ └── com/ │ └── atiurin/ │ └── ultron/ │ ├── core/ │ │ └── compose/ │ │ ├── ComposeRuleContainer.android.kt │ │ ├── UltronComposeUiBlockExt.kt │ │ ├── activity/ │ │ │ └── AndroidComposeTestRule.kt │ │ ├── config/ │ │ │ └── UltronComposeConfig.android.kt │ │ ├── list/ │ │ │ ├── ItemChildInteractionProvider.android.kt │ │ │ └── UltronComposeListItem.android.kt │ │ ├── listeners/ │ │ │ └── ComposDebugListener.kt │ │ └── nodeinteraction/ │ │ └── UltronComposeSemanticsNodeInteraction.android.kt │ └── extensions/ │ ├── ReflectionComposeExt.android.kt │ ├── SemanticsMatcherExt.android.kt │ └── SemanticsNodeInteractionExt.android.kt ├── commonMain/ │ └── kotlin/ │ └── com/ │ └── atiurin/ │ └── ultron/ │ ├── core/ │ │ └── compose/ │ │ ├── ComposeTestContainer.kt │ │ ├── ComposeTestEnvironment.kt │ │ ├── UltronUiTest.kt │ │ ├── config/ │ │ │ ├── UltronComposeConfig.kt │ │ │ └── UltronComposeConfigParams.kt │ │ ├── list/ │ │ │ ├── ComposeItemExecutor.kt │ │ │ ├── IndexComposeItemExecutor.kt │ │ │ ├── ItemChildInteractionProvider.kt │ │ │ ├── MatcherComposeItemExecutor.kt │ │ │ ├── PositionComposeItemExecutor.kt │ │ │ ├── UltronComposeList.kt │ │ │ └── UltronComposeListItem.kt │ │ ├── nodeinteraction/ │ │ │ ├── SwipePosition.kt │ │ │ ├── UltronComposeOffsets.kt │ │ │ ├── UltronComposeSemanticsNodeInteraction.kt │ │ │ └── UltronComposeSemanticsNodeInteractionClicks.kt │ │ ├── operation/ │ │ │ ├── ComposeOperationExecutor.kt │ │ │ ├── ComposeOperationResult.kt │ │ │ ├── ComposeOperationType.kt │ │ │ ├── UltronComposeCollectionInteraction.kt │ │ │ ├── UltronComposeOperation.kt │ │ │ ├── UltronComposeOperationLifecycle.kt │ │ │ └── UltronComposeOperationParams.kt │ │ ├── option/ │ │ │ └── ComposeSwipeOption.kt │ │ └── page/ │ │ └── UltronComposeUiBlock.kt │ └── extensions/ │ ├── AssertionsExt.kt │ ├── FiltersExt.kt │ ├── SemanticsMatcherExt.kt │ ├── SemanticsNodeExt.kt │ ├── SemanticsNodeInteractionExt.kt │ ├── SemanticsSelectorExt.kt │ └── TouchInjectionScopeExt.kt ├── jvmMain/ │ └── kotlin/ │ └── com/ │ └── atiurin/ │ └── ultron/ │ └── core/ │ └── compose/ │ └── UltronUiTest.jvm.kt ├── shared/ │ └── kotlin/ │ └── com/ │ └── atiurin/ │ └── ultron/ │ ├── core/ │ │ └── compose/ │ │ ├── config/ │ │ │ └── UltronComposeConfig.shared.kt │ │ └── list/ │ │ ├── ItemChildInteractionProvider.shared.kt │ │ └── UltronComposeListItem.shared.kt │ └── extensions/ │ └── SemanticsNodeInteractionCommonExt.shared.kt └── test/ └── java/ └── com/ └── atiurin/ └── ultron/ └── compose/ └── ExampleUnitTest.kt ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/ci-pipeline.yml ================================================ name: MultiplatformCI on: push: branches: [ master ] pull_request: branches: [ master ] jobs: compileKotlin: runs-on: macos-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-java@v4 with: distribution: 'adopt' java-version: '17' - name: Compile framework run: ./gradlew compileDebugKotlin compileDebugKotlinAndroid compileKotlinDesktop compileKotlinIosArm64 compileKotlinIosSimulatorArm64 compileKotlinJs compileKotlinWasmJs ================================================ FILE: .github/workflows/docs.yml ================================================ name: Build and deploy docs on: push: branches: - master jobs: github-pages: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v4 with: node-version: 22 - run: npm install working-directory: ./docs - run: npm run build working-directory: ./docs - name: Deploy to GitHub Pages uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./docs/build ================================================ FILE: .github/workflows/maven_central_publish.yml ================================================ name: Publish permissions: contents: read on: push: branches: - 'release/*' jobs: publish: name: Publish to Maven Central Portal runs-on: macos-latest strategy: matrix: include: - target: :ultron-common:publishToMavenCentral - target: :ultron-compose:publishToMavenCentral - target: :ultron-android:publishToMavenCentral - target: :ultron-allure:publishToMavenCentral steps: - name: Checkout uses: actions/checkout@v3 - name: Validate Gradle Wrapper uses: gradle/wrapper-validation-action@v1 - name: Setup JDK 17 uses: actions/setup-java@v3 with: java-version: '17' distribution: "zulu" - name: Import GPG key uses: crazy-max/ghaction-import-gpg@v6 with: gpg_private_key: ${{ secrets.OSSRH_GPG_SECRET_KEY }} passphrase: ${{ secrets.OSSRH_GPG_SECRET_KEY_PASSWORD }} - name: Publish to MavenCentral run: ./gradlew "${{ matrix.target }}" --no-configuration-cache env: ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_USER }} ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_TOKEN }} ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.SIGNING_KEY_ID }} ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }} ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_KEY_CONTENTS }} ================================================ FILE: .gitignore ================================================ *.iml .gradle /local.properties /.idea /.idea/caches /.idea/libraries /.idea/modules.xml /.idea/workspace.xml /.idea/navEditor.xml /.idea/assetWizardSettings.xml .DS_Store build/ /captures .externalNativeBuild /allure-results /.kotlin ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================

[![Documentation][documentation-badge]][documentation] [![Releases][releases-badge]][releases] [![Telegram][telegram-badge]][telegram]
Ultron is the simplest framework to develop UI tests for **Android** & **Compose Multiplatform**. It's constructed upon the Espresso, UI Automator and Compose UI testing frameworks. Ultron introduces a range of remarkable new features. Furthermore, Ultron puts you in complete control of your tests! You don't need to learn any new classes or special syntax. All magic actions and assertions are provided from crunch. Ultron can be easially customised and extended. Wish you exclusively stable tests! ## What are the benefits of using the framework? - Page/Screen Object pattern support - Exceptional simplification for [**Compose UI tests**](https://open-tool.github.io/ultron/docs/compose/index) - Out-of-the-box generation of [**Allure report**](https://open-tool.github.io/ultron/docs/common/allure) (Now, for Android UI tests only) - A straightforward and expressive syntax - Ensured **Stability** for all actions and assertions - Complete control over every action and assertion - Incredible interaction with lists: [**RecyclerView**](./android/recyclerview.md) and [**Compose LazyList**](https://open-tool.github.io/ultron/docs/compose/lazylist). - An **Architectural** approach to developing UI tests (search "Best practice") - An incredible mechanism for setups and teardowns (You can even set up preconditions for a single test within a test class, without affecting the others) - [The ability to effortlessly extend the framework with your own operations](https://open-tool.github.io/ultron/docs/common/extension) - Accelerated UI Automator operations - Ability to monitor each stage of operation execution with [Listeners](https://open-tool.github.io/ultron/docs/common/listeners) - [Custom operation assertions](https://open-tool.github.io/ultron/docs/common/customassertion) *** ### Documentation The framework offers an excellent [documentation](https://open-tool.github.io/ultron/docs/) that addresses the majority of significant usage scenarios. ### A few words about syntax The standard syntax provided by Google is intricate and not intuitive. This is especially evident when dealing with **LazyList** and **RecyclerView** interactions. Let's explore some examples: #### 1. Simple compose operation (refer to the doc [here](https://open-tool.github.io/ultron/docs/compose/index)) _Compose framework_ ```kotlin composeTestRule.onNode(hasTestTag("Continue")).performClick() composeTestRule.onNodeWithText("Welcome").assertIsDisplayed() ``` _Ultron_ ```kotlin hasTestTag("Continue").click() hasText("Welcome").assertIsDisplayed() ``` #### 2. Compose list operation (refer to the [doc](https://open-tool.github.io/ultron/docs/compose/lazylist)) _Compose framework_ ```kotlin val itemMatcher = hasText(contact.name) composeRule .onNodeWithTag(contactsListTestTag) .performScrollToNode(itemMatcher) .onChildren() .filterToOne(itemMatcher) .assertTextContains(contact.name) ``` _Ultron_ ```kotlin composeList(hasTestTag(contactsListTestTag)) .item(hasText(contact.name)) .assertTextContains(contact.name) ``` #### 3. Simple Espresso assertion and action. _Espresso_ ```kotlin onView(withId(R.id.send_button)).check(isDisplayed()).perform(click()) ``` _Ultron_ ```kotlin withId(R.id.send_button).isDisplayed().click() ``` This presents a cleaner approach. Ultron's operation names mirror Espresso's, while also providing additional operations. Refer to the [doc](https://open-tool.github.io/ultron/docs/android/espress) for further details. #### 4. Action on RecyclerView list item _Espresso_ ```kotlin onView(withId(R.id.recycler_friends)) .perform( RecyclerViewActions .actionOnItem( hasDescendant(withText("Janice")), click() ) ) ``` _Ultron_ ```kotlin withRecyclerView(R.id.recycler_friends) .item(hasDescendant(withText("Janice"))) .click() ``` Explore the [doc](https://open-tool.github.io/ultron/docs/android/espress) to unveil Ultron's magic with RecyclerView interactions. #### 5. Espresso WebView operations _Espresso_ ```kotlin onWebView() .withElement(findElement(Locator.ID, "text_input")) .perform(webKeys(newTitle)) .withElement(findElement(Locator.ID, "button1")) .perform(webClick()) .withElement(findElement(Locator.ID, "title")) .check(webMatches(getText(), containsString(newTitle))) ``` _Ultron_ ```kotlin id("text_input").webKeys(newTitle) id("button1").webClick() id("title").hasText(newTitle) ``` Refer to the [doc](https://open-tool.github.io/ultron/docs/android/webview) for more details. #### 6. UI Automator operations _UI Automator_ ```kotlin val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) device .findObject(By.res("com.atiurin.sampleapp:id", "button1")) .click() ``` _Ultron_ ```kotlin byResId(R.id.button1).click() ``` Refer to the [doc](https://open-tool.github.io/ultron/docs/android/uiautomator) *** ### Acquiring the result of any operation as Boolean value ```kotlin val isButtonDisplayed = withId(R.id.button).isSuccess { isDisplayed() } if (isButtonDisplayed) { //do some reasonable actions } ``` *** ### Why are all Ultron actions and assertions more stable? The framework captures a list of specified exceptions and attempts to repeat the operation during a timeout period (default is 5 seconds). Of course, you have the ability to customize the list of handled exceptions. You can also set a custom timeout for any operation. ```kotlin withId(R.id.result).withTimeout(10_000).hasText("Passed") ``` *** ## 3 steps to develop a test using Ultron We advocate for a proper test framework architecture, division of responsibilities between layers, and other best practices. Therefore, when using Ultron, we recommend the following approach: 1. Create a Page Object and specify screen UI elements as `Matcher` objects. ```kotlin object ChatPage : Page() { private val messagesList = withId(R.id.messages_list) private val clearHistoryBtn = withText("Clear history") private val inputMessageText = withId(R.id.message_input_text) private val sendMessageBtn = withId(R.id.send_button) } ``` It's recommended to make all Page Objects as `object` and descendants of Page class. This allows for the utilization of convenient Kotlin features. It also helps you to keep Page Objects stateless. 2. Describe user step methods in Page Object. ```kotlin object ChatPage : Page() { fun sendMessage(text: String) = apply { inputMessageText.typeText(text) sendMessageBtn.click() getMessageListItem(text).text .isDisplayed() .hasText(text) } fun clearHistory() = apply { openContextualActionModeOverflowMenu() clearHistoryBtn.click() } } ``` Refer to the full code sample [ChatPage.class](https://github.com/open-tool/ultron/blob/master/sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/ChatPage.kt) 3. Call user steps in test ```kotlin @Test fun friendsItemCheck(){ FriendsListPage { assertName("Janice") assertStatus("Janice","Oh. My. God") } } @Test fun sendMessage(){ FriendsListPage.openChat("Janice") ChatPage { clearHistory() sendMessage("test message") } } ``` Refer to the full code sample [DemoEspressoTest.class](https://github.com/open-tool/ultron/blob/master/sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso/DemoEspressoTest.kt) In essence, your project's architecture will look like this: [acrchitecture](https://github.com/open-tool/ultron/assets/12834123/b0882d34-a18d-4f1f-959b-f75796d11036) *** ## Allure report Ultron has built in support to generate artifacts for Allure reports. Just apply the recommended configuration and set testIntrumentationRunner. For the complete guide, refer to the [Allure description](https://open-tool.github.io/ultron/docs/common/allure) ```kotlin @BeforeClass @JvmStatic fun setConfig() { UltronConfig.applyRecommended() UltronAllureConfig.applyRecommended() UltronComposeConfig.applyRecommended() } ``` ![allure](https://github.com/open-tool/ultron/assets/12834123/c05c813a-ece6-45e6-a04f-e1c92b82ffb1) ![allure compose](https://github.com/open-tool/ultron/assets/12834123/1f751f3d-fc58-4874-a850-acd9181bfb70) ## Add Ultron to your project Gradle ```groovy repositories { mavenCentral() } dependencies { androidTestImplementation 'com.atiurin:ultron-android:' androidTestImplementation 'com.atiurin:ultron-allure:' androidTestImplementation 'com.atiurin:ultron-compose:' } ``` Please, read [gradle dependencies management](https://open-tool.github.io/ultron/docs/intro/dependencies) doc. [telegram-badge]:https://img.shields.io/badge/Chat-Telegram-0088CC?style=for-the-badge [documentation-badge]:https://img.shields.io/badge/Documentation-233a60?style=for-the-badge [releases-badge]:https://img.shields.io/github/release/open-tool/ultron.svg?style=for-the-badge [telegram]:https://t.me/ultron_framework [documentation]:https://open-tool.github.io/ultron/ [releases]:https://github.com/open-tool/ultron/releases ================================================ FILE: build.gradle.kts ================================================ import org.jetbrains.compose.internal.utils.getLocalProperty buildscript { extra.apply { set("RELEASE_REPOSITORY_URL", "https://central.sonatype.com/api/v1/publisher") set("SNAPSHOT_REPOSITORY_URL", "https://central.sonatype.com/api/v1/publisher") } repositories { google() mavenCentral() mavenLocal() } dependencies { classpath(Plugins.kotlinGradle) classpath(Plugins.androidToolsBuildGradle) classpath(Plugins.androidMavenGradle) classpath(Plugins.dokka) } } plugins { //trick: for the same plugin versions in all sub-modules alias(libs.plugins.androidLibrary) apply false alias(libs.plugins.kotlinMultiplatform) apply false alias(libs.plugins.androidApplication) apply false alias(libs.plugins.jetbrainsCompose) apply false alias(libs.plugins.kotlinJvm) apply false alias(libs.plugins.compose.compiler) apply false alias(libs.plugins.kotlinAndroid) apply false alias(libs.plugins.vanniktech.mavenPublish) apply false } allprojects { repositories { google() mavenCentral() mavenLocal() gradlePluginPortal() } } ================================================ FILE: buildSrc/build.gradle.kts ================================================ import org.gradle.kotlin.dsl.`kotlin-dsl` plugins { `kotlin-dsl` } repositories { google() mavenCentral() gradlePluginPortal() } ================================================ FILE: buildSrc/src/main/kotlin/Versions.kt ================================================ object Versions { const val kotlin = "2.1.21" const val androidToolsBuildGradle = "8.3.1" const val androidMavenGradlePlugin = "2.1" const val dokkaPlugin = "1.9.20" const val recyclerView = "1.2.1" const val espresso = "3.6.1" const val uiautomator = "2.2.0" const val accessibility = "4.0.0" const val hamcrestCore = "2.2" const val compose = "1.7.0" const val androidXTest = "1.4.0" const val junit = "4.13.2" const val allure = "2.4.0" //sample-app const val coroutines = "1.4.2" const val ktx = "1.6.0" const val supportV4 = "1.0.0" const val appcompat = "1.3.1" const val material = "1.4.0" const val material3 = "1.3.1" const val constraintlayout = "2.1.4" const val cardview = "1.0.0" const val robolectric = "4.8.1" const val mockito = "3.9.0" const val activityCompose = "1.8.2" const val junitExt = "1.1.2" } object Plugins { val androidToolsBuildGradle = "com.android.tools.build:gradle:${Versions.androidToolsBuildGradle}" val androidMavenGradle = "com.github.dcendents:android-maven-gradle-plugin:${Versions.androidMavenGradlePlugin}" val kotlinGradle = "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.kotlin}" val dokka = "org.jetbrains.dokka:dokka-gradle-plugin:${Versions.dokkaPlugin}" } object Libs { val kotlinStdlib = "org.jetbrains.kotlin:kotlin-stdlib:${Versions.kotlin}" val kotlinReflect = "org.jetbrains.kotlin:kotlin-reflect:${Versions.kotlin}" val espressoCore = "androidx.test.espresso:espresso-core:${Versions.espresso}" val espressoContrib = "androidx.test.espresso:espresso-contrib:${Versions.espresso}" val espressoWeb = "androidx.test.espresso:espresso-web:${Versions.espresso}" val uiautomator = "androidx.test.uiautomator:uiautomator:${Versions.uiautomator}" val accessibility = "com.google.android.apps.common.testing.accessibility.framework:accessibility-test-framework:${Versions.accessibility}" val hamcrestCore = "org.hamcrest:hamcrest-core:${Versions.hamcrestCore}" val recyclerView = "androidx.recyclerview:recyclerview:${Versions.recyclerView}" val androidXRunner = "androidx.test:runner:${Versions.androidXTest}" val composeUiTest = "androidx.compose.ui:ui-test-junit4:${Versions.compose}" val junit = "junit:junit:${Versions.junit}" // allure val allureCommon = "io.qameta.allure:allure-kotlin-commons:${Versions.allure}" val allureModel = "io.qameta.allure:allure-kotlin-model:${Versions.allure}" val allureJunit4 = "io.qameta.allure:allure-kotlin-junit4:${Versions.allure}" val allureAndroid = "io.qameta.allure:allure-kotlin-android:${Versions.allure}" // sample-app val coroutines = "org.jetbrains.kotlinx:kotlinx-coroutines-android:${Versions.coroutines}" val androidXKtx = "androidx.core:core-ktx:${Versions.ktx}" val supportV4 = "androidx.legacy:legacy-support-v4:${Versions.supportV4}" val appcompat = "androidx.appcompat:appcompat:${Versions.appcompat}" val material = "com.google.android.material:material:${Versions.material}" val material3 = "androidx.compose.material3:material3-android:${Versions.material3}" val constraintLayout = "androidx.constraintlayout:constraintlayout:${Versions.constraintlayout}" val cardview = "androidx.cardview:cardview:${Versions.cardview}" // sample-app compose val composeUi = "androidx.compose.ui:ui:${Versions.compose}" val composeUiTooling = "androidx.compose.ui:ui-tooling:${Versions.compose}" // Tooling support (Previews, etc.) val composeFoundation = "androidx.compose.foundation:foundation:${Versions.compose}" // Foundation (Border, Background, Box, Image, Scroll, shapes, animations, etc.) val composeMaterial = "androidx.compose.material:material:${Versions.compose}" val composeMaterialIconsCore = "androidx.compose.material:material-icons-core:${Versions.compose}" // Material design icons val composeMaterialIconsExtend = "androidx.compose.material:material-icons-extended:${Versions.compose}" val activityCompose = "androidx.activity:activity-compose:${Versions.activityCompose}" // sample-app test val robolectric = "org.robolectric:robolectric:${Versions.robolectric}" val mockito = "org.mockito:mockito-core:${Versions.mockito}" val androidXTextCore = "androidx.test:core:${Versions.androidXTest}" //sample-app androidTest val espressoIdlingResource = "androidx.test.espresso:espresso-idling-resource:${Versions.espresso}" val espressoIntents = "androidx.test.espresso:espresso-intents:${Versions.espresso}" val espressoAccessibility = "androidx.test.espresso:espresso-accessibility:${Versions.espresso}" val espressoConcurrent = "androidx.test.espresso.idling:idling-concurrent:${Versions.espresso}" val androidXRules = "androidx.test:rules:${Versions.androidXTest}" val androidXTruth = "androidx.test.ext:truth:${Versions.androidXTest}" val androidXJunit = "androidx.test.ext:junit:${Versions.junitExt}" } ================================================ FILE: composeApp/build.gradle.kts ================================================ import org.jetbrains.compose.ExperimentalComposeLibrary import org.jetbrains.compose.desktop.application.dsl.TargetFormat import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSetTree import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl plugins { alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.androidApplication) alias(libs.plugins.jetbrainsCompose) alias(libs.plugins.compose.compiler) } kotlin { androidTarget { @OptIn(ExperimentalKotlinGradlePluginApi::class) compilerOptions { jvmTarget.set(JvmTarget.JVM_17) } @OptIn(ExperimentalKotlinGradlePluginApi::class) instrumentedTestVariant { sourceSetTree.set(KotlinSourceSetTree.test) dependencies { implementation(libs.androidx.ui.test.junit4.android) debugImplementation(libs.androidx.ui.test.manifest) implementation(project(":ultron-compose")) } } } jvm("desktop") listOf( iosX64(), iosArm64(), iosSimulatorArm64() ).forEach { iosTarget -> iosTarget.binaries.framework { baseName = "ComposeApp" isStatic = true } } js(IR){ browser() } @OptIn(ExperimentalWasmDsl::class) wasmJs(){ browser { testTask(Action { useKarma { useChromeHeadless() useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm")) } }) } } sourceSets { val desktopMain by getting androidMain.dependencies { implementation(compose.preview) implementation(libs.androidx.activity.compose) } commonMain.dependencies { implementation(compose.runtime) implementation(compose.foundation) implementation(compose.material) implementation(compose.ui) implementation(compose.components.resources) implementation(compose.components.uiToolingPreview) implementation(libs.androidx.lifecycle.runtime.compose) implementation(libs.androidx.lifecycle.viewmodel.compose) implementation(libs.androidx.navigation.compose) } commonTest.dependencies { @OptIn(ExperimentalComposeLibrary::class) implementation(compose.uiTest) implementation(kotlin("test")) implementation(project(":ultron-compose")) } desktopMain.dependencies { implementation(compose.desktop.currentOs) } val desktopTest by getting { dependencies { implementation(compose.desktop.uiTestJUnit4) implementation(compose.desktop.currentOs) } } @OptIn(ExperimentalWasmDsl::class) wasmJs() val wasmJsTest by getting } } android { namespace = "com.atiurin.samplekmp" compileSdk = libs.versions.android.compileSdk.get().toInt() sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") sourceSets["main"].res.srcDirs("src/androidMain/res") sourceSets["main"].resources.srcDirs("src/commonMain/resources") defaultConfig { applicationId = "com.atiurin.samplekmp" minSdk = libs.versions.android.minSdk.get().toInt() targetSdk = libs.versions.android.targetSdk.get().toInt() versionCode = 1 versionName = "1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } } buildTypes { getByName("release") { isMinifyEnabled = false } } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } buildFeatures { compose = true } // dependencies { // debugImplementation(compose.uiTooling) // } } compose.desktop { application { mainClass = "MainKt" nativeDistributions { targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) packageName = "com.atiurin.samplekmp" packageVersion = "1.0.0" } } } ================================================ FILE: composeApp/karma.config.d/wasm/config.js ================================================ // see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file // This file provides karma.config.d configuration to run tests with k/wasm const path = require("path"); config.browserConsoleLogOptions.level = "debug"; const basePath = config.basePath; const projectPath = path.resolve(basePath, "..", "..", "..", ".."); const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out") const debug = message => console.log(`[karma-config] ${message}`); debug(`karma basePath: ${basePath}`); debug(`karma generatedAssetsPath: ${generatedAssetsPath}`); config.proxies["/"] = path.resolve(basePath, "kotlin"); config.files = [ {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false}, {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false}, {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false}, {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false}, {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false}, {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false}, {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false}, ].concat(config.files); function KarmaWebpackOutputFramework(config) { // This controller is instantiated and set during the preprocessor phase. const controller = config.__karmaWebpackController; // only if webpack has instantiated its controller if (!controller) { console.warn( "Webpack has not instantiated controller yet.\n" + "Check if you have enabled webpack preprocessor and framework before this framework" ) return } config.files.push({ pattern: `${controller.outputPath}/**/*`, included: false, served: true, watched: false }) } const KarmaWebpackOutputPlugin = { 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework], }; config.plugins.push(KarmaWebpackOutputPlugin); config.frameworks.push("webpack-output"); ================================================ FILE: composeApp/src/androidMain/AndroidManifest.xml ================================================ ================================================ FILE: composeApp/src/androidMain/kotlin/Platform.android.kt ================================================ import android.os.Build class AndroidPlatform : Platform { override val name: String = "Android ${Build.VERSION.SDK_INT}" } actual fun getPlatform(): Platform = AndroidPlatform() ================================================ FILE: composeApp/src/androidMain/kotlin/com/atiurin/samplekmp/MainActivity.kt ================================================ package com.atiurin.samplekmp import App import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState) setContent { App() } } } @Preview @Composable fun AppAndroidPreview() { App() } ================================================ FILE: composeApp/src/androidMain/res/drawable/ic_launcher_background.xml ================================================ ================================================ FILE: composeApp/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml ================================================ ================================================ FILE: composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml ================================================ ================================================ FILE: composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml ================================================ ================================================ FILE: composeApp/src/androidMain/res/values/strings.xml ================================================ sample-kmp ================================================ FILE: composeApp/src/commonMain/composeResources/drawable/compose-multiplatform.xml ================================================ ================================================ FILE: composeApp/src/commonMain/kotlin/App.kt ================================================ import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.material.Button import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.ui.tooling.preview.Preview import ui.screens.ContactsListScreen import ultron.composeapp.generated.resources.Res import ultron.composeapp.generated.resources.compose_multiplatform @Composable @Preview fun App() { MaterialTheme { var showContent by remember { mutableStateOf(false) } Column(Modifier.fillMaxWidth().navigationBarsPadding().statusBarsPadding(), horizontalAlignment = Alignment.CenterHorizontally) { Button(onClick = { showContent = !showContent }) { Text("Click me!") } AnimatedVisibility(showContent) { val greeting = remember { Greeting().greet() } Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { Image(painterResource(Res.drawable.compose_multiplatform), null) Text("Compose: $greeting", modifier = Modifier.semantics { testTag = "greeting" }) } } ContactsListScreen() } } } ================================================ FILE: composeApp/src/commonMain/kotlin/Greeting.kt ================================================ class Greeting { private val platform = getPlatform() fun greet(): String { return "Hello, ${platform.name}!" } } ================================================ FILE: composeApp/src/commonMain/kotlin/Platform.kt ================================================ interface Platform { val name: String } expect fun getPlatform(): Platform ================================================ FILE: composeApp/src/commonMain/kotlin/repositories/ContactRepository.kt ================================================ package repositories object ContactRepository { fun getContact(id: Int) : Contact { return contacts.find { it.id == id }!! } fun getFirst(): Contact { return contacts.first() } fun getLast() : Contact { return contacts.last() } fun all() = contacts.toList() private val contacts = CONTACTS } ================================================ FILE: composeApp/src/commonMain/kotlin/repositories/Storage.kt ================================================ package repositories data class Contact( val id: Int,val name: String, val status: String, val avatar: Int) data class User( val id: Int,val name: String, val avatar: Int, val login: String, val password: String) val CURRENT_USER = User(1, "Joey Tribbiani", Avatars.JOEY.drawable, "joey", "1234") val CONTACTS = arrayListOf( Contact(2, "Chandler Bing", "Joey doesn't share food!", Avatars.CHANDLER.drawable), Contact(3, "Ross Geller", "UNAGI", Avatars.ROSS.drawable), Contact(4, "Rachel Green", "I got off the plane!", Avatars.RACHEL.drawable), Contact(5, "Phoebe Buffay", "Smelly cat, smelly cat..", Avatars.PHOEBE.drawable), Contact(6, "Monica Geller", "I need to clean up..", Avatars.MONICA.drawable), Contact(7, "Gunther", "They were on break :(", Avatars.GUNTHER.drawable), Contact(8, "Janice", "Oh. My. God", Avatars.JANICE.drawable), Contact(9, "Bob", "I wanna drink", Avatars.DEFAULT.drawable), Contact(10, "Marty McFly", "Back to the ...", Avatars.DEFAULT.drawable), Contact(12, "Emmet Brown", "Time fluid capacitor", Avatars.DEFAULT.drawable), Contact(13, "Friend1", "Time fluid capacitor", Avatars.DEFAULT.drawable), Contact(14, "Friend2", "Time fluid capacitor", Avatars.DEFAULT.drawable), Contact(15, "Friend3", "Time fluid capacitor", Avatars.DEFAULT.drawable), Contact(16, "Friend4", "Time fluid capacitor", Avatars.DEFAULT.drawable), Contact(17, "Friend5", "Time fluid capacitor", Avatars.DEFAULT.drawable), Contact(18, "Friend6", "Time fluid capacitor", Avatars.DEFAULT.drawable), Contact(19, "Friend7", "Time fluid capacitor", Avatars.DEFAULT.drawable), Contact(20, "Friend8", "Time fluid capacitor", Avatars.DEFAULT.drawable), Contact(21, "Friend9", "Time fluid capacitor", Avatars.DEFAULT.drawable), Contact(22, "Friend10", "Time fluid capacitor", Avatars.DEFAULT.drawable), Contact(23, "Friend11", "Time fluid capacitor", Avatars.DEFAULT.drawable), Contact(24, "Friend12", "Time fluid capacitor", Avatars.DEFAULT.drawable), Contact(25, "Friend13", "Time fluid capacitor", Avatars.DEFAULT.drawable), Contact(26, "Friend14", "Time fluid capacitor", Avatars.DEFAULT.drawable), Contact(27, "Friend15", "Time fluid capacitor", Avatars.DEFAULT.drawable), Contact(28, "Friend16", "Time fluid capacitor", Avatars.DEFAULT.drawable), Contact(29, "Friend17", "Time fluid capacitor", Avatars.DEFAULT.drawable), Contact(30, "Friend18", "Time fluid capacitor", Avatars.DEFAULT.drawable), Contact(31, "Friend19", "Time fluid capacitor", Avatars.DEFAULT.drawable), Contact(32, "Friend20", "Time fluid capacitor", Avatars.DEFAULT.drawable) ) enum class Avatars(val drawable: Int) { CHANDLER(0), ROSS(1), MONICA(2), RACHEL(3), PHOEBE(4), GUNTHER(5), JOEY(6), JANICE(7), DEFAULT(8) } ================================================ FILE: composeApp/src/commonMain/kotlin/ui/screens/ContactsListScreen.kt ================================================ package ui.screens import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.Divider import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.TextUnitType import androidx.compose.ui.unit.dp import repositories.ContactRepository import kotlinx.coroutines.async import repositories.Contact @Composable fun ContactsListScreen() { Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { val scope = rememberCoroutineScope() var contactItems by remember { mutableStateOf(emptyList()) } var text by remember { mutableStateOf("Loading ...") } scope.async { contactItems = loadContacts() text = "Contacts loaded" } Text(text) LazyColumn( contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp), verticalArrangement = Arrangement.spacedBy(4.dp), modifier = Modifier.semantics { contentDescription = "contactsListContentDesc" testTag = "contactsListTestTag" } ) { items(contactItems) { contact -> ContactItem(contact) } } } } @Composable fun ContactItem(contact: Contact) { Box(modifier = Modifier.testTag("contactItem=${contact.id}")) { Column { Row { Column { Text(contact.name, Modifier.semantics { testTag = "contactNameTestTag" }, fontSize = TextUnit(20f, TextUnitType.Sp)) Spacer(modifier = Modifier.height(8.dp)) Text(text = contact.status, Modifier.semantics { testTag = "contactStatusTestTag" }, fontSize = TextUnit(16f, TextUnitType.Sp)) Spacer(modifier = Modifier.height(8.dp)) } } } Spacer(modifier = Modifier.height(8.dp)) Divider(color = Color.Black) } } suspend fun loadContacts(): List { // delay(1000) return ContactRepository.all() } ================================================ FILE: composeApp/src/commonTest/kotlin/BaseInteractionTest.kt ================================================ import androidx.compose.foundation.layout.Column import androidx.compose.material.Button import androidx.compose.material.Text import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.hasText import com.atiurin.ultron.core.common.options.TextContainsOption import com.atiurin.ultron.core.compose.config.UltronComposeConfig import com.atiurin.ultron.core.compose.nodeinteraction.click import com.atiurin.ultron.core.compose.runUltronUiTest import com.atiurin.ultron.extensions.assertIsDisplayed import com.atiurin.ultron.extensions.isSuccess import com.atiurin.ultron.extensions.withAssertion import com.atiurin.ultron.extensions.withTimeout import com.atiurin.ultron.extensions.withUseUnmergedTree import kotlin.test.AfterTest import kotlin.test.Test import kotlin.test.assertFalse import kotlin.test.assertTrue @OptIn(ExperimentalTestApi::class) class BaseInteractionTest { @Test fun test() = runUltronUiTest { setContent { App() } hasText("Click me!").withAssertion() { hasTestTag("greeting") .assertIsDisplayed() .assertTextContains("Compose: Hello,", option = TextContainsOption(substring = true)) }.click() } @Test fun useUnmergedTreeConfigTest() = runUltronUiTest { val testTag = "element" setContent { Column { Button(onClick = {}, modifier = Modifier.testTag(testTag)) { Text("Text1") Text("Text2") } } } UltronComposeConfig.params.useUnmergedTree = true assertFalse("Ultron operation success should be false") { hasTestTag(testTag).isSuccess { withTimeout(1000).assertTextContains("Text1") } } assertTrue ("Ultron operation success should be true") { hasTestTag(testTag).withUseUnmergedTree(false).isSuccess { assertTextContains("Text1") } } } @AfterTest fun disableUseUnmergedTree(){ UltronComposeConfig.params.useUnmergedTree = false } } ================================================ FILE: composeApp/src/commonTest/kotlin/ExampleTest.kt ================================================ import androidx.compose.material.Button import androidx.compose.material.Text import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.assertTextEquals import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick import androidx.compose.ui.test.runComposeUiTest import kotlin.test.Test class ExampleTest { @OptIn(ExperimentalTestApi::class) @Test fun myTest() = runComposeUiTest { setContent { var text by remember { mutableStateOf("Hello") } Text( text = text, modifier = Modifier.testTag("text") ) Button( onClick = { text = "Compose" }, modifier = Modifier.testTag("button") ) { Text("Click me") } } // Tests the declared UI with assertions and actions of the Compose Multiplatform testing API onNodeWithTag("text").assertTextEquals("Hello") onNodeWithTag("button").performClick() onNodeWithTag("text").assertTextEquals("Compose") } } ================================================ FILE: composeApp/src/commonTest/kotlin/ListTest.kt ================================================ import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.hasTestTag import com.atiurin.ultron.core.compose.list.UltronComposeListItem import com.atiurin.ultron.core.compose.list.composeList import com.atiurin.ultron.core.compose.runUltronUiTest import com.atiurin.ultron.page.Screen import repositories.ContactRepository import kotlin.test.Test @OptIn(ExperimentalTestApi::class) class ListTest { @Test fun testList() = runUltronUiTest { setContent { App() } composeList(hasTestTag("contactsListTestTag")) .assertIsDisplayed().assertNotEmpty() .firstVisibleItem().assertIsDisplayed() } @Test fun testListItemChildElements() = runUltronUiTest { setContent { App() } val contact = ContactRepository.getFirst() ListScreen { list.assertContentDescriptionEquals(contactsListContentDesc) list.getFirstVisibleItem().apply { name.assertIsDisplayed().assertTextContains(contact.name) status.assertIsDisplayed().assertTextContains(contact.status) } } } } object ListScreen : Screen() { const val contactsListTestTag = "contactsListTestTag" const val contactsListContentDesc = "contactsListContentDesc" val list = composeList( listMatcher = hasTestTag(contactsListTestTag), initBlock = { registerItem { ListItem() } } ) class ListItem : UltronComposeListItem() { val name by child { hasTestTag("contactNameTestTag") } val status by child { hasTestTag("contactStatusTestTag") } } } ================================================ FILE: composeApp/src/commonTest/kotlin/UltronTestFlowTest.kt ================================================ import com.atiurin.ultron.annotations.ExperimentalUltronApi import com.atiurin.ultron.core.test.UltronTest import com.atiurin.ultron.log.UltronLog import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue class UltronTestFlowTest : UltronTest() { companion object { var order = 0 var beforeFirstTestCounter = 0 var commonBeforeOrder = -1 var commonAfterOrder = -1 var afterOrder = -1 } @OptIn(ExperimentalUltronApi::class) override val beforeFirstTest = { beforeFirstTestCounter++ UltronLog.info("Before Class") } override val beforeTest = { commonBeforeOrder = order order++ UltronLog.info("Before test common") } override val afterTest = { commonAfterOrder = order order++ assertTrue(afterOrder < commonAfterOrder, message = "CommonAfter block should run after 'after' test block") UltronLog.info("After test common") } @Test fun someTest1() = test { var beforeOrder = -1 var goOrder = -1 order++ before { assertTrue(beforeFirstTestCounter == 1, message = "beforeFirstTest block should run before all test") beforeOrder = order order++ UltronLog.info("Before TestMethod 1") }.go { goOrder = order order++ UltronLog.info("Run TestMethod 1") }.after { afterOrder = order order++ assertTrue(commonBeforeOrder < beforeOrder, message = "beforeOrder block should run after commonBefore block") assertTrue(beforeOrder < goOrder, message = "Before block should run before 'go'") assertTrue(goOrder < afterOrder, message = "After block should run after 'go'") } } @Test fun someTest2() = test(suppressCommonBefore = true) { before { UltronLog.info("Before TestMethod 2") }.after { UltronLog.info("After TestMethod 2") }.go { assertTrue(beforeFirstTestCounter == 1, message = "beforeFirstTest block should run only once") UltronLog.info("Run TestMethod 2") } } @Test fun simpleTest() = test { assertTrue(beforeFirstTestCounter == 1, message = "beforeFirstTest block should run only once") UltronLog.info("UltronTest simpleTest") } @Test fun afterBlockExecutedOnFailedTest() { var isAfterExecuted = false runCatching { test { go { throw RuntimeException("test exception") } after { isAfterExecuted = true } } } assertTrue(isAfterExecuted) } @Test fun testExceptionMessageThrownOnFailedTest() { val testExceptionMessage = "test exception" runCatching { test { go { throw RuntimeException(testExceptionMessage) } after { throw RuntimeException("Another after exception") } } }.onFailure { ex -> assertEquals(ex.message, testExceptionMessage) } } } ================================================ FILE: composeApp/src/commonTest/kotlin/UltronTestFlowTest2.kt ================================================ import com.atiurin.ultron.annotations.ExperimentalUltronApi import com.atiurin.ultron.core.test.UltronTest import com.atiurin.ultron.log.UltronLog import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue class UltronTestFlowTest2 : UltronTest() { companion object { var order = 0 var beforeFirstTestCounter = 0 } @OptIn(ExperimentalUltronApi::class) override val beforeFirstTest = { beforeFirstTestCounter++ order++ UltronLog.info("Before Class") } @Test fun someTest1() = test { var beforeOrder = -1 var afterOrder = -1 var goOrder = -1 order++ before { assertEquals(1, beforeFirstTestCounter, message = "beforeFirstTest block should run before all test") beforeOrder = order order++ UltronLog.info("Before TestMethod 1") }.go { goOrder = order order++ UltronLog.info("Run TestMethod 1") }.after { afterOrder = order assertTrue(beforeOrder < goOrder, message = "Before block should run before 'go'") assertTrue(goOrder < afterOrder, message = "After block should run after 'go'") } } @Test fun someTest2() = test(suppressCommonBefore = true) { before { UltronLog.info("Before TestMethod 2") }.after { UltronLog.info("After TestMethod 2") }.go { assertEquals(1, beforeFirstTestCounter, message = "beforeFirstTest block should run before all test") UltronLog.info("Run TestMethod 2") } } } ================================================ FILE: composeApp/src/desktopMain/kotlin/Platform.jvm.kt ================================================ class JVMPlatform: Platform { override val name: String = "Java ${System.getProperty("java.version")}" } actual fun getPlatform(): Platform = JVMPlatform() ================================================ FILE: composeApp/src/desktopMain/kotlin/main.kt ================================================ import androidx.compose.ui.window.Window import androidx.compose.ui.window.application fun main() = application { Window( onCloseRequest = ::exitApplication, title = "sample-kmp", ) { App() } } ================================================ FILE: composeApp/src/desktopTest/kotlin/DesktopSampleTest.kt ================================================ import androidx.compose.material.Button import androidx.compose.material.Text import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.hasTestTag import com.atiurin.ultron.core.compose.runDesktopUltronUiTest import com.atiurin.ultron.core.test.UltronTest import com.atiurin.ultron.extensions.assertTextEquals import com.atiurin.ultron.extensions.click import com.atiurin.ultron.page.Page import org.junit.Test class DesktopSampleTest : UltronTest() { @OptIn(ExperimentalTestApi::class) @Test fun myTest() = test { runDesktopUltronUiTest { setContent { var text by remember { mutableStateOf("Hello") } Text( text = text, modifier = Modifier.testTag("text") ) Button( onClick = { text = "Compose" }, modifier = Modifier.testTag("button") ) { Text("Click me") } } SamplePage { someStep() } } } } object SamplePage : Page() { private val text = hasTestTag("text") private val button = hasTestTag("button") fun someStep(){ text.assertTextEquals("Hello") button.click() text.assertTextEquals("Compose") } } ================================================ FILE: composeApp/src/iosMain/kotlin/MainViewController.kt ================================================ import androidx.compose.ui.window.ComposeUIViewController fun MainViewController() = ComposeUIViewController { App() } ================================================ FILE: composeApp/src/iosMain/kotlin/Platform.ios.kt ================================================ import platform.UIKit.UIDevice class IOSPlatform: Platform { override val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion } actual fun getPlatform(): Platform = IOSPlatform() ================================================ FILE: composeApp/src/iosTest/kotlin/IOSSampleTest.kt ================================================ import androidx.compose.material.Button import androidx.compose.material.Text import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.hasTestTag import com.atiurin.ultron.core.compose.runUltronUiTest import com.atiurin.ultron.extensions.assertTextEquals import com.atiurin.ultron.extensions.click import com.atiurin.ultron.page.Page import kotlin.test.Test class IOSSampleTest { @OptIn(ExperimentalTestApi::class) @Test fun sampleTest() = runUltronUiTest { setContent { var text by remember { mutableStateOf("Hello") } Text( text = text, modifier = Modifier.testTag("text") ) Button( onClick = { text = "Compose" }, modifier = Modifier.testTag("button") ) { Text("Click me") } } SamplePage { someStep() } } } object SamplePage : Page() { private val text = hasTestTag("text") private val button = hasTestTag("button") fun someStep(){ text.assertTextEquals("Hello") button.click() text.assertTextEquals("Compose") } } ================================================ FILE: composeApp/src/jsMain/kotlin/Platform.js.kt ================================================ actual fun getPlatform(): Platform { TODO("Not yet implemented") } ================================================ FILE: composeApp/src/jsTest/kotlin/JsSampleTest.kt ================================================ import androidx.compose.material.Button import androidx.compose.material.Text import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.assertTextEquals import androidx.compose.ui.test.hasText import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick import androidx.compose.ui.test.runComposeUiTest import com.atiurin.ultron.core.compose.runUltronUiTest import com.atiurin.ultron.extensions.assertTextEquals import com.atiurin.ultron.extensions.click import kotlin.test.Test class JsSampleTest { @OptIn(ExperimentalTestApi::class) @Test fun myTest() = runUltronUiTest { setContent { var text by remember { mutableStateOf("Hello") } Text( text = text, modifier = Modifier.testTag("text") ) Button( onClick = { text = "Compose" }, modifier = Modifier.testTag("button") ) { Text("Click me") } } hasText("text").assertTextEquals("Hello") // Tests the declared UI with assertions and actions of the Compose Multiplatform testing API onNodeWithTag("text").assertTextEquals("Hello") onNodeWithTag("button").performClick() onNodeWithTag("text").assertTextEquals("Compose123123") } } ================================================ FILE: composeApp/src/wasmJsMain/kotlin/Platform.wasmJs.kt ================================================ class WasmPlatform: Platform { override val name: String = "Web with Kotlin/Wasm" } actual fun getPlatform(): Platform = WasmPlatform() ================================================ FILE: composeApp/src/wasmJsMain/kotlin/main.kt ================================================ import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.window.ComposeViewport import kotlinx.browser.document @OptIn(ExperimentalComposeUiApi::class) fun main() { ComposeViewport(document.body!!) { App() } } ================================================ FILE: composeApp/src/wasmJsMain/resources/index.html ================================================ sample-kmp ================================================ FILE: composeApp/src/wasmJsMain/resources/styles.css ================================================ html, body { width: 100%; height: 100%; margin: 0; padding: 0; overflow: hidden; } ================================================ FILE: docs/.gitignore ================================================ # Dependencies /node_modules # Production /build # Generated files .docusaurus .cache-loader # Misc .DS_Store .env.local .env.development.local .env.test.local .env.production.local npm-debug.log* yarn-debug.log* yarn-error.log* ================================================ FILE: docs/README.md ================================================ # Website This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator. ### Installation ``` $ yarn ``` ### Local Development ``` $ yarn start ``` This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. ### Build ``` $ yarn build ``` This command generates static content into the `build` directory and can be served using any static contents hosting service. ### Deployment Using SSH: ``` $ USE_SSH=true yarn deploy ``` Not using SSH: ``` $ GIT_USER= yarn deploy ``` If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. ================================================ FILE: docs/babel.config.js ================================================ module.exports = { presets: [require.resolve('@docusaurus/core/lib/babel/preset')], }; ================================================ FILE: docs/docs/android/_category_.json ================================================ { "label": "Android", "position": 3, "collapsed": false } ================================================ FILE: docs/docs/android/espress.md ================================================ --- sidebar_position: 1 --- # Espresso ## How to use? Simple espresso operation looks like this ```kotlin onView(withId(R.id.send_button)).check(isDisplayed()).perform(click()) ``` the same with **Ultron** ```kotlin withId(R.id.send_button).isDisplayed().click() ``` Names of all Ultron operations are the same as espresso one. There are a lot of additional operations those simplifies test development. ```kotlin //------ actions ------ click() doubleClick() longClick() typeText(text: String) replaceText(text: String) clearText() pressKey(keyCode: Int) pressKey(key: EspressoKey) closeSoftKeyboard() swipeLeft() swipeRight() swipeUp() swipeDown() scrollTo() perform(viewAction: ViewAction) // execute custom espresso action as Ultron one perform(params: UltronEspressoActionParams? = null, block: (uiController: UiController, view: View) -> Unit) execute(params: UltronEspressoActionParams? = null, block: (uiController: UiController, view: View) -> T): T //------ get View property actions ------ getText() : String? getContentDescription() : String? getDrawable() : Drawable? //------ assertions ------ exists() doesNotExist() isDisplayed() isNotDisplayed() isCompletelyDisplayed() isDisplayingAtLeast(percentage: Int) doesNotExist() isEnabled() isNotEnabled() isSelected() isNotSelected() isClickable() isNotClickable() isChecked() isNotChecked() isFocusable() isNotFocusable() hasFocus() isJavascriptEnabled() hasText(text: String) hasText(resourceId: Int) hasText(stringMatcher: Matcher) textContains(text: String) hasContentDescription(text: String) hasContentDescription(resourceId: Int) hasContentDescription(charSequenceMatcher: Matcher) contentDescriptionContains(text: String) assertMatches(condition: Matcher) // execute custom espresso assertion as Ultron one hasDrawable(@DrawableRes resourceId: Int) hasAnyDrawable() hasCurrentTextColor(@ColorRes colorRes: Int) hasCurrentHintTextColor(@ColorRes colorRes: Int) hasShadowColor(@ColorRes colorRes: Int) hasHighlightColor(@ColorRes colorRes: Int) assertMatches(params: UltronEspressoAssertionParams? = null, block: (view: View) -> Boolean) //------ general ------ withTimeout(timeoutMs: Long) // set custom timeout for operations withResultHandler(resultHandlerBlock) // set custom result handler and process operation result withAssertion(assertion: OperationAssertion) // define custom assertion of action success withAssertion(name: String = "", isListened: Boolean = false, block: () -> Unit) //------ custom clicks ------ clickTopLeft(offsetX: Int, offsetY: Int) clickTopCenter(offsetY: Int) clickTopRight(offsetX: Int, offsetY: Int) clickCenterRight(offsetX: Int) clickBottomRight(offsetX: Int, offsetY: Int) clickBottomCenter(offsetY: Int) clickBottomLeft(offsetX: Int, offsetY: Int) clickCenterLeft(offsetX: Int) ``` ## Best practice Specify page elements as properties of PageObject class. ```kotlin object SomePage : Page() { private val button = withId(R.id.button1) private val eventStatus = withId(R.id.last_event_status) } ``` Use this properties in page steps ```kotlin object SomePage : Page() { //page elements fun someUserStepOnPage(expectedEventText: String){ button.click() eventStatus.hasText(expectedEventText) } } ``` ## Custom timeout for any operation ```kotlin withId(R.id.last_event_status).withTimeout(10_000).isDisplayed() ``` There are 2 ways of using custom timeout: - Specify it for page property and it will be applied for all operations with this element ```kotlin object SomePage : Page() { private val eventStatus = withId(R.id.last_event_status).withTimeout(10_000) } ``` - Specify it inside special step there the element operation could take more time. This timeout value will be applied only once for single operation. ```kotlin object SomePage : Page() { fun someLongUserStep(expectedEventText: String){ longRequestButton.click() eventStatus.withTimeout(20_000).hasText(expectedEventText) } } ``` ## Boolean operation result There is `isSuccess` method that allows us to get the result of any operation as boolean value. In case of false it could be executed to long (5 sec by default). So it reasonable to specify custom timeout for some operations. ```kotlin val isButtonDisplayed = withId(R.id.button).isSuccess { withTimeout(2_000).isDisplayed() } if (isButtonDisplayed) { //do some reasonable actions } ``` ## Dialog and popup To execute any operation inside dialog or popup with espresso you have to specify correct root element ```kotlin onView(withText("OK"))).inRoot(isDialog()).perform(click()) onView(withText("Cancel")).inRoot(isPlatformPopup()).perform(click()) ``` Here is a point we need to put our minds on. **Ultron extends not only `Matcher` object but also `ViewInteraction` and `DataInteraction` objects** `onView(withText("OK"))).inRoot(isDialog())` returns _ViewInteraction_. Therefore it's possible to use Ultron operations with dialogs. So the best way would be a following ```kotlin object DialogPage : Page() { val okButton = onView(withText(R.string.ok_button))).inRoot(isDialog()) val cancelButton = onView(withText(R.string.cancel_button))).inRoot(isDialog()) } ... fun someUserStepInsideSomePage(){ DialogPage.okButton.click() somePageElement.isDisplayed() } ``` ## Extend framework with your own ViewActions and ViewAssertions Under the hood all espresso Ultron operations are described in `UltronEspressoInteraction` class. That is why you just need to extend this class using [kotlin extension function](https://kotlinlang.org/docs/extensions.html), e.g. ```kotlin fun UltronEspressoInteraction.appendText(text: String) = apply { executeAction( operationBlock = getInteractionActionBlock(AppendTextAction(text)), name = "Append text '$text' to ${getInteractionMatcher()}", description = "${interaction!!::class.simpleName} APPEND_TEXT to ${getInteractionMatcher()} during $timeoutMs ms", ) } ``` `AppendTextAction` is a custom ViewAction, smth like that ```kotlin class AppendTextAction(private val value: String) : ViewAction { override fun getConstraints() = allOf(isDisplayed(), isAssignableFrom(TextView::class.java)) override fun perform(uiController: UiController, view: View) { (view as TextView).apply { this.text = "$text$value" } uiController.loopMainThreadUntilIdle() } ... } ``` To make your custom operation 100% native for Ultron framework it's required to add 3 lines more ```kotlin //support action for all Matcher fun Matcher.appendText(text: String) = UltronEspressoInteraction(onView(this)).appendText(text) //support action for all ViewInteractions fun ViewInteraction.appendText(text: String) = UltronEspressoInteraction(this).appendText(text) //support action for all DataInteractions fun DataInteraction.appendText(text: String) = UltronEspressoInteraction(this).appendText(text) ``` Finally you are able to use this custom operation ```kotlin withId(R.id.text_input).appendText("some text to append") ``` View sample code [UltronEspressoExt](https://github.com/open-tool/ultron/blob/master/sample-app/src/androidTest/java/com/atiurin/sampleapp/framework/ultronext/UltronEspressoExt.kt) ## Get any property of any View There are several build in methods that extends `Matcher, ViewInteraction, DataInteraction`: ```kotlin getText() : String? getContentDescription() : String? getDrawable() : Drawable? ``` And you are able to get any other property. There is an example how it could be done - [GetTextAction](https://github.com/alex-tiurin/ultron/blob/master/ultron/src/main/java/com/atiurin/ultron/custom/espresso/action/GetTextAction.kt) ================================================ FILE: docs/docs/android/recyclerview.md ================================================ --- sidebar_position: 3 --- # RecyclerView ## Terms Before we go forward we need to define some terms: - RecyclerView - list of some items (a standard Android framework class). Ultron has a class that wraps an interaction with RecyclerView - `UltronRecyclerView`. - RecyclerViewItem - single item of RecyclerView list (there is a class `UltronRecyclerViewItem`) - RecyclerViewItem.child - child element of RecyclerViewItem (just a term, there is no special class to work with child elements). So _RecyclerViewItem.child_ could be considered as a simple android View. ![Terms](https://user-images.githubusercontent.com/12834123/107883156-4008d000-6efe-11eb-9764-8c57e767e5e2.png) ## UltronRecyclerView Create an instance of `UltronRecyclerView` using the method `withRecyclerView(..)` method: ```kotlin withRecyclerView(R.id.recycler_friends).assertSize(CONTACTS.size) ``` ### Parameters for `withRecyclerView` method The withRecyclerView method allows creating an instance of UltronRecyclerView with customizable parameters: - `recyclerViewMatcher: Matcher`/ `resourceId: Int`, - A Matcher / @IntegerRes that identifies the target RecyclerView in the layout. - `loadTimeout: Long` - The maximum time (in milliseconds) to wait for RecyclerView items to load. The default value is defined by `UltronConfig.Espresso.RECYCLER_VIEW_LOAD_TIMEOUT`. - `itemSearchLimit: Int` - The maximum number of items to search through when locating an item in the RecyclerView. The default value is defined by `UltronConfig.Espresso.RECYCLER_VIEW_ITEM_SEARCH_LIMIT`. - `operationsTimeoutMs: Long` - The maximum time (in milliseconds) to wait for operations on RecyclerView to complete. The default value is defined by `UltronConfig.Espresso.RECYCLER_VIEW_OPERATIONS_TIMEOUT`. - `implementation: UltronRecyclerViewImpl`- Specifies the implementation of UltronRecyclerView to use. The default value is `UltronConfig.Espresso.RECYCLER_VIEW_IMPLEMENTATION`, which is set in the configuration. ### UltronRecyclerViewImpl `UltronRecyclerViewImpl` has two available modes: - `STANDARD`: This is the default implementation. It is already 4 times faster than the previous version. When multiple identical child elements are found within an UltronRecyclerViewItem, the first matching element is selected without throwing an AmbiguousViewMatcherException. - `PERFORMANCE`: Optimized for higher performance. However, if multiple child elements matching the same criteria are found, an exception (AmbiguousViewMatcherException) will be thrown. The choice of implementation affects not only performance but also how child elements of `UltronRecyclerViewItem` are handled. For now, the actual difference in performance between these modes is minimal, but it could be highly valuable if your RecyclerView item contains many child elements. ### _Best practice_ - save `UltronRecyclerView` as page class properties ```kotlin object FriendsListPage : Page() { // param loadTimeout in ms specifies a time of waiting while RecyclerView items will be loaded val recycler = withRecyclerView(R.id.recycler_friends, loadTimeout = 10_000L) fun someStep(){ recycler.assertEmpty() recycler.hasContentDescription("Description") } } ``` `UltronRecyclerView` api ```kotlin // ----- assertions ----- assertEmpty() // Asserts RecyclerView has no item assertSize(expected: Int) // Asserts RecyclerView list has [expected] items count during assertHasItemAtPosition(position: Int) // Asserts RecyclerView list has item at [position] assertMatches(matcher: Matcher) // Assert RecyclerView matches custom condition assertItemNotExist(matcher: Matcher, timeoutMs: Long) // watch java doc to understand how it works assertItemNotExistImmediately(matcher: Matcher, timeoutMs: Long) isDisplayed() isNotDisplayed() doesNotExist() isEnabled() isNotEnabled() hasContentDescription(contentDescription: String) hasContentDescription(resourceId: Int) hasContentDescription(charSequenceMatcher: Matcher) contentDescriptionContains(text: String) // ----- item providers for simple UltronRecyclerViewItem ----- // all item provider methods has params [autoScroll: Boolean = true, scrollOffset: Int = 0]. It's shown only once but all of them has it item(matcher: Matcher, autoScroll: Boolean = true, scrollOffset: Int = 0): UltronRecyclerViewItem item(position: Int, ..): UltronRecyclerViewItem firstItem(..): UltronRecyclerViewItem lastItem(..): UltronRecyclerViewItem // Sometimes it is impossible to provide unique matcher for RecyclerView item // There is a set of methods to access not unique items by matcher and index // index is a value from 0 to lastIndex of matched items itemMatched(matcher: Matcher, index: Int): UltronRecyclerViewItem firstItemMatched(matcher: Matcher, ..): UltronRecyclerViewItem lastItemMatched(matcher: Matcher, ..): UltronRecyclerViewItem // ----- item providers for UltronRecyclerViewItem subclasses ----- // following methods return a generic type T which is a subclass of UltronRecyclerViewItem getItem(matcher: Matcher, autoScroll: Boolean = true, scrollOffset: Int = 0): T getItem(position: Int, ..): T getFirstItem(..): T getLastItem(..): T // ----- in case it's impossible to define unique matcher for `UltronRecyclerViewItem` ----- getItemMatched(matcher: Matcher, index: Int, ..): T getFirstItemMatched(matcher: Matcher, ..): T getLastItemMatched(matcher: Matcher, ..): T ``` ## UltronRecyclerViewItem `UltronRecyclerView` provides an access to `UltronRecyclerViewItem`. ### Simple Item If you don't need to interact with item child just use methods like `item`, `firstItem`, `lastItem`, `itemMatched` and etc ```kotlin recycler.item(position = 10, autoScroll = true).click() // find item at position 10 and scroll to this item recycler.item(matcher = hasDescendant(withText("Janice"))).isDisplayed() recycler.firstItem().click() //take first RecyclerView item recycler.lastItem().isCompletelyDisplayed() // if it's impossible to specify unique matcher for target item val matcher = hasDescendant(withText("Friend")) recycler.itemMatched(matcher, index = 2).click() //return 3rd matched item, because index starts from zero recycler.firstItemMatched(matcher).isDisplayed() recycler.lastItemMatched(matcher).isDisplayed() recycler.getItemsAdapterPositionList(matcher) // return positions of all matched items ``` You don't need to worry about scroll to item. By default autoscroll in all item accessor method equals true. ### Complex item with children It's often required to interact with item child. The best solution will be to describe children as properties of `UltronRecyclerViewItem` subclass. ```kotlin class FriendRecyclerItem : UltronRecyclerViewItem() { val avatar by child { withId(R.id.avatar) } val name by child { withId(R.id.tv_name) } val status by child { withId(R.id.tv_status) } } ``` **Note: you have to use delegated initialisation with `by child`.** Now you're able to get `FriendRecyclerItem` object using methods `getItem`, `getFirstItem`, `getLastItem` etc ```kotlin recycler.getItem(position = 10, autoScroll = true).status.hasText("UNAGI") recycler.getItem(matcher = hasDescendant(withText("Janice"))).status.textContains("Oh. My") recycler.getFirstItem().avatar.click() //take first RecyclerView item recycler.getLastItem().isCompletelyDisplayed() // if it's impossible to specify unique matcher for target item val matcher = hasDescendant(withText(containsString("Friend"))) recycler.getItemMatched(matcher, index = 2).name.click() //return 3rd matched item, because index starts from zero recycler.getFirstItemMatched(matcher).name.hasText("Friend1") recycler.getLastItemMatched(matcher).avatar.isDisplayed() ``` ### _Best practice_ - add a method to Page class that returns `FriendRecyclerItem` ```kotlin object FriendsListPage : Page() { val recycler = withRecyclerView(R.id.recycler_friends) fun getListItem(contactName: String): FriendRecyclerItem { return recycler.getItem(hasDescendant(allOf(withId(R.id.tv_name), withText(contactName)))) } fun getListItem(positions: Int): FriendRecyclerItem { return recycler.getItem(positions) } } ``` use `getListItem` inside `FriendsListPage` steps ```kotlin object FriendsListPage : Page() { ... fun assertStatus(name: String, status: String) = apply { getListItem(name).status.hasText(status).isDisplayed() } } ``` `UltronRecyclerViewItem` api ```kotlin //actions scrollToItem(offset: Int = 0) click() longClick() doubleClick() swipeUp() swipeDown() swipeLeft() swipeRight() perform(viewAction: ViewAction) //assertions isDisplayed() isNotDisplayed() isCompletelyDisplayed() isDisplayingAtLeast(percentage: Int) isClickable() isNotClickable() isEnabled() isNotEnabled() assertMatches(condition: Matcher) hasContentDescription(contentDescription: String) hasContentDescription(resourceId: Int) hasContentDescription(charSequenceMatcher: Matcher) contentDescriptionContains(text: String) //general getViewHolder(): RecyclerView.ViewHolder? getChild(childMatcher: Matcher): Matcher //return matcher to a child element withTimeout(timeoutMs: Long) //set custom timeout for the next operation withResultHandler(..) // allows you to process action on item by your own way // click options clickTopLeft(offsetX: Int = 0, offsetY: Int = 0) clickTopCenter(offsetY: Int) clickTopRight(offsetX: Int = 0, offsetY: Int = 0) clickCenterRight(offsetX: Int = 0) clickBottomRight(offsetX: Int = 0, offsetY: Int = 0) clickBottomCenter(offsetY: Int = 0) clickBottomLeft(offsetX: Int = 0, offsetY: Int = 0) clickCenterLeft(offsetX: Int = 0) ``` ================================================ FILE: docs/docs/android/rootview.md ================================================ --- sidebar_position: 5 --- # withSuitableRoot Method allows to avoiding nontrivial element lookup exceptions In some cases, we encounter non-trivial exceptions in finding elements that are part of the Espresso framework. Such problems and their solution will be considered. # Waited for the root of the view hierarchy to have window focus and not request layout for 10 seconds. If you observe such an exception, then this indicates a complex problem for testing the user interface. One of the well-known reasons is that programmers add their views to the application context, and not to the activity or fragment. At phase of view interaction creation, Espresso assigns a root view where your matcher will be matched. Unfortunately, the views attached to the application context may not have the same root view that was set at the time view interaction was created. To solve this problem, the following solution was created: ```kotlin val toolbarTitle = withId(R.id.toolbar_title) fun assertToolbarTitleWithSuitableRoot(text: String) { toolbarTitle.withSuitableRoot().hasText(text) } ``` withSuitableRoot() extension returns a view interaction with the correct root view in which the element you are looking for will be located. If the root view is not found, the test will be interrupted with espresso exception - NoMatchingRootException: Matcher ...did not match any of the following roots... You can also use the root matcher to set the root for Espresso view interaction. ```kotlin val toolbarTitle = withId(R.id.toolbar_title) onView(toolbarTitle).inRoot(withSuitableRoot(toolbarTitle)).check { // Your checks here } ``` The same works for UltronRecyclerViewItem: ```kotlin val recycler = withRecyclerView(R.id.recycler_friends) class FriendRecyclerItem : UltronRecyclerViewItem() { val name by child { withId(R.id.tv_name) } val status by child { withId(R.id.tv_status) } val avatar by child { withId(R.id.avatar) } } fun getListItem(positions: Int): FriendRecyclerItem { return recycler.getItem(positions) } // Usage: getListItem(0).withSuitableRoot().isDisplayed() getListItem(0).name.withSuitableRoot().isDisplayed().click() ``` ================================================ FILE: docs/docs/android/testconditions.md ================================================ --- sidebar_position: 6 --- # Test Conditions Management It is a feature that includes 3 parts - RuleSequence - SetUpRule & TearDownRule - @SetUp @TearDown annotations Additional feature - UltronActivityRule for launch Activity before test and finish after RuleSequence + SetUps & TearDowns for tests = full control of your tests - control the execution of pre- and postconditions of each test - control the moment of activity launching. It is one of the most important point in android automation. - don't write @Before and @After methods by changing it to the lambdas of SetUpRule or TearDownRule object - combine conditions of your test using annotations ## RuleSequence This rule is a modern replacement of JUnit 4 *RuleChain*. It allows to control an order of rules execution. The RuleChain is not flexible. It is unpleasant to use RuleChain especially with class inheritance. That's why [RuleSequence](https://github.com/alex-tiurin/ultron/blob/master/ultron/src/main/java/com/atiurin/ultron/testlifecycle/rulesequence/RuleSequence.kt) has been created. The order of rules execution depends on its addition order. RuleSequence contains three rules lists with their own priority. - first - rules from this list will be executed first of all - normal - rules will be added to this list by default - last - rules from this list will be executed last It is recommended to create `RuleSequence` in `BaseTest`. You will be able to add rules to `RuleSequence` in `BaseTest` and in `BaseTest` subclasses. ```kotlin abstract class BaseTest { val setupRule = SetUpRule(name = "some name").add { // some resonable precondition for all tests, eg login or smth like that } @get:Rule open val ruleSequence = RuleSequence(setupRule) } ``` It's better to add rules in subclasses inside `init` section. ```kotlin class DemoTest : BaseTest() { private val activityRule = ActivityScenarioRule(MainActivity::class.java) init { ruleSequence.addLast(activityRule) } } ``` **Note**: while using `RuleSequence`(as it was with `RuleChain`) you don't need to specify `@get:Rule` annotation for other rules. Full code sample: - [BaseTest](https://github.com/alex-tiurin/ultron/blob/master/sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/BaseTest.kt) - [DemoEspressoTest](https://github.com/alex-tiurin/ultron/blob/master/sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso/DemoEspressoTest.kt) To learn more about order of rules execution see [Deep dive into rules order with RuleSequence](https://github.com/alex-tiurin/ultron/wiki/Deep-dive-into-rules-order-with-RuleSequence) ## SetUpRule This rule allows you to specify lambdas which will be definitely invoked before a test is started. Moreover in combination with **RuleSequence** setup lambdas could be invoked before an activity is launched. ### Precondition for each tests Add lambda to `SetUpRule` without any string key and it will be executed before each test in class. ```kotlin open val setupRule = SetUpRule("Login user rule") .add(name = "Login valid user $CURRENT_USER") { Log.info("Login valid user will be executed before any test is started") AccountManager(InstrumentationRegistry.getInstrumentation().targetContext).login( CURRENT_USER.login, CURRENT_USER.password ) } ``` ### Precondition for specific test 1. add lambda with string key to `SetUpRule` 2. add `@SetUp` annotation with specified key to desired test ```kotlin setupRule.add(FIRST_CONDITION){ Log.info("$FIRST_CONDITION setup, executed for test with annotation @SetUp(FIRST_CONDITION)") } @SetUp(FIRST_CONDITION) @Test fun someTest() { // some test steps } ``` **Attention**: dont forget to add `SetUpRule` to `RuleSequence` ```kotlin ruleSequence.add(setupRule) ``` ## TearDownRule This rule allows you to specify lambdas which will be definitely invoked after a test is finished. ### Postcondition for all tests Add lambda to `TearDownRule` without any string key and it will be executed after each test in class. ```kotlin open val tearDownRule = TearDownRule(name = "Logout user from app") .add { AccountManager(InstrumentationRegistry.getInstrumentation().targetContext).logout() } ``` ### Postcondition for specific test 1. add lambda with string key to `TearDownRule` 2. add `@TearDown` annotation with specified key to desired test ```kotlin tearDownRule.add (LAST_CONDITION){ Log.info("$LAST_CONDITION tearDown, executed for test with annotation @TearDown(LAST_CONDITION)") } @TearDown(LAST_CONDITION) @Test fun someTest() { // some test steps } ``` **Attention**: dont forget to add `TearDownRule` to `RuleSequence` ```kotlin ruleSequence.addLast(tearDownRule) ``` ## Add your SetUps and TearDowns to Allure report Lets clearly define a term **condition**. It's any code block that you've `add` for`SetUpRule` or `TearDownRule`. For example: ```kotlin SetUpRule(name = "sample set up").add { //codition code } ``` It's possible to add all SetUps and TearDowns to Allure report with applying a recommended config: ```kotlin UltronAllureConfig.applyRecommended() ``` You can read about Allure configuration [here](../common/allure.md) What it gives us: - Rule `name` param will be used as name of Allure step. ```kotlin SetUpRule(name = "External step name").add {...} ``` - Condition `name` param will be used as a name of inner step ```kotlin SetUpRule(name = "External step name").add(name = "Internal step name") { //condition code } ``` ## UltronActivityRule To start the activity you can use UltronActivityRule instead of `androidx.test.ext.junit.rules.ActivityScenarioRule` The rule has the following advantages: - finish all activities in RESUMED, PAUSED and STOPPED stage after test - does not await idle state for finish activity (fix infinity test execution in case AppNotIdleException) - has setup and teardown step in allure report ```kotlin val activityRule = UltronActivityRule(YourActivity::class.java) ruleSequence.add(activityRule) ``` ================================================ FILE: docs/docs/android/uiautomator.md ================================================ --- sidebar_position: 4 --- # UI Automator **Ultron** makes UI Automator actions and assertions much more stable and simple. It wraps both UiObject and UiObject2. # Speed up all UI Automator operations **Ultron** operation could be significantly faster then UI Automator one. To accelerate all operations add single line of code in tests precondition. ```kotlin @BeforeClass @JvmStatic fun speedUpAutomator() { UltronConfig.UiAutomator.speedUp() //or apply the config UltronConfig.apply { accelerateUiAutomator = true } } ``` # How to use? Compare following code snippets. _UI Automator_ ```kotlin val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) device.wait( Until.findObject( By.res("com.atiurin.sampleapp:id", "button1") ), 5_000 ).click() val uiObject2 = device.wait( Until.findObject( By.res("com.atiurin.sampleapp:id", "last_event_status") ), 5_000 ) uiObject2.text = "Ultron" Assert.assertEquals("Ultron", uiObject2.text) ``` _Ultron_ ```kotlin byResId(R.id.button1).click() byResId(R.id.last_event_status).replaceText("Ultron").hasText("Ultron") ``` The last one looks a little bit better :) `byResId(R.id.button1)` actually returns `UltronUiObject2`. While the framework tries to execute UI Automator operation, it catches a list of specified exceptions and tries to repeat the operation during the timeout (5 seconds by default). Of course, you are able to customize the list of processed exceptions. It is also possible to specify a custom timeout for any operation. The configuration process for this part of the framework is explained below. ## `UltronUiObject2` api There are factory methods to create `UltronUiObject2`. ```kotlin byResId(@IntegerRes resourceId: Int): UltronUiObject2 // specify element with target application resourceId by(bySelector: BySelector): UltronUiObject2 // eg by(By.res("com.android.camera2","shutter_button")) ``` To describe UI element with text or content description use following approach ```kotlin val textElement = by(By.text("some text")) val contentDescElement = by(By.desc("Content desc")) ``` `UltronUiObject2` has all methods of standart UiObject2 and also provide a lot of new features. ```kotlin // data providers getParent(): UltronUiObject2? // return this object's parent, or null if it has no parent getChildren(): List // return a collection of the child elements directly under this object. Empty list if no child exist getChildCount(): Int findObject(bySelector: BySelector): UltronUiObject2? // searches all elements under this object and returns the first object to match the criteria findObjects(bySelector: BySelector): List // searches all elements under this object and returns all objects that match the criteria getApplicationPackage(): String? // return the package name of the app that this object belongs to getText(): String? // return view.text or null if view has no text getClassName(): String // return the class name of the view represented by this object getVisibleBounds(): Rect? // return the visible bounds of this object in screen coordinates getVisibleCenter(): Point? // return a point in the center of the visible bounds of this object getResourceName(): String? // return the fully qualified resource name for this object's id getContentDescription(): String? // return the content description for this object //actions click(duration: Long = 0) // A basic click is a touch down and touch up over the same point with no delay. longClick() clear() // Clears the text content if object is an editable field addText(text: String) // Add the text content if object is an editable field legacySetText(text: String) // Set the text content by sending individual key codes replaceText(text: String) // Set the text content if object is an editable field drag(dest: Point, speed: Int = DEFAULT_DRAG_SPEED) // Drags object to the specified location pinchClose(percent: Float, speed: Int = DEFAULT_PINCH_SPEED) // Performs a pinch close gesture on this object swipe(direction: Direction, percent: Float, speed: Int = DEFAULT_SWIPE_SPEED) // Performs a swipe gesture on this object swipeUp() swipeDown() swipeLeft() swipeRight() scroll(direction: Direction, percent: Float, speed: Int = DEFAULT_SCROLL_SPEED) // Performs a scroll gesture on this object scrollUp() scrollDown() scrollLeft() scrollRight() fling(direction: Direction, speed: Int = DEFAULT_FLING_SPEED) // Performs a fling gesture on this object perform(actionBlock: UiObject2.() -> Unit, actionDescription: String) // custom action on UiObject2 //asserts hasText(textMatcher: Matcher) hasText(text: String) textContains(textSubstring: String) textIsNullOrEmpty() textIsNotNullOrEmpty() hasContentDescription(contentDescMatcher: Matcher) hasContentDescription(contentDesc: String) contentDescriptionContains(contentDescSubstring: String) contentDescriptionIsNullOrEmpty() contentDescriptionIsNotNullOrEmpty() isCheckable() isNotCheckable() isChecked() isNotChecked() isClickable() isNotClickable() isEnabled() isNotEnabled() isFocusable() isNotFocusable() isFocused() isNotFocused() isLongClickable() isNotLongClickable() isScrollable() isNotScrollable() isSelected() isNotSelected() isDisplayed() isNotDisplayed() assertThat(assertBlock: UiObject2.() -> Boolean, assertionDescription: String) // custom assertion of UiObject2 //------ general ------ withTimeout(timeoutMs: Long) // set custom timeout for operations withResultHandler(resultHandlerBlock) // set custom result handler and process operation result withAssertion(assertion: OperationAssertion) // define custom assertion of action success withAssertion(name: String = "", isListened: Boolean = false, block: () -> Unit) ``` ## `UltronUiObject` api As it was mentioned before **Ultron** wraps UiObject too. There is a set of static methods to create `UltronUiObject`. ```kotlin uiResId(@IntegerRes resourceId: Int): UltronUiObject // specify element with target application resourceId ui(uiSelector: UiSelector): UltronUiObject ``` It has all methods of standart UiObject and also provide a lot of new features. As `UltronUiObject` has almost the same api as `UltronUiObject2` we don't list it. ## Best practice Specify page elements as properties of PageObject class. ```kotlin object SomePage : Page() { private val button = byResId(R.id.button1) private val eventStatus = byResId(R.id.last_event_status) } ``` Use this properties in page steps ```kotlin object SomePage : Page() { //page elements fun someUserStepOnPage(expectedEventText: String){ button.click() eventStatus.hasText(expectedEventText) } } ``` ## Custom timeout for any operation ```kotlin byResId(R.id.last_event_status).withTimeout(10_000).isDisplayed() ``` There are 2 ways of using custom timeout: - Specify it for page property and it will be applied for all operations with this element ```kotlin object SomePage : Page() { private val eventStatus = byResId(R.id.last_event_status).withTimeout(10_000) } ``` - Specify it inside special step there the element operation could take more time. This timeout value will be applied only once for single operation. ```kotlin object SomePage : Page() { fun someLongUserStep(expectedEventText: String){ longRequestButton.click() eventStatus.withTimeout(20_000).hasText(expectedEventText) } } ``` ## Boolean operation result There is `isSuccess` method that allows us to get the result of any operation as boolean value. In case of false it could be executed to long (5 sec by default). So it's resonable to specify custom timeout for some operations. ```kotlin val isButtonDisplayed = byResId(R.id.button).isSuccess { withTimeout(2_000).isDisplayed() } if (isButtonDisplayed) { //do some reasonable actions } ``` ## Extend framework with your own action and assertion It's described in another page [here](../common/extension.md#ui-automator) ================================================ FILE: docs/docs/android/webview.md ================================================ --- sidebar_position: 2 --- # WebView There are 3 different objects to interact with. * `UltronWebDocument` - wraps operations with WebView DOM document (execute JS script and etc). * `UltronWebElement` - represents a DOM element. Provides operations with element (`webClick`, `replaceText`, `exists` etc) * `UltronWebElements` - represents a list of similar WebElements. ## How to use? ### UltronWebDocument It contains a set of static methods. For example ```kotlin UltronWebDocument.evalJS("document.getElementById(\"title\").innerHTML = '$title';") UltronWebDocument.assertThat( webContent( elementById( "apple_link", withTextContent("Apple") ) ) ) ``` Full list: ```kotlin forceJavascriptEnabled(webViewMatcher, timeoutMs, ..) // performs a force enable of Javascript on a WebView evalJS(script: String, webViewMatcher, timeoutMs, ..) // evaluate JS on webView assertThat(WebAssertion, webViewMatcher, ..) // use any webAssertion to assert it safely selectActiveElement(..): ElementReference // finds the currently active element in the document selectFrameByIndex(index: Int, ..): WindowReference // selects a subframe of the currently selected window by it's index selectFrameByIdOrName(idOrName: String, ..): WindowReference // selects a subframe of the current window by it's name or id ``` ### UltronWebElement `UltronWebElement` has a list of factory methods that help us to create an instance of UltronWebElement. Full list is here - [UltronWebElement](https://github.com/open-tool/ultron/blob/603150ab12a703a19245ad08a48b036ce562dfd8/ultron/src/main/java/com/atiurin/ultron/core/espressoweb/webelement/UltronWebElement.kt#L311) ```kotlin import com.atiurin.ultron.core.espressoweb.webelement.UltronWebElement.Companion.id //other imports id("text_input").webKeys("Ultron") className("css_button").webClick() xpath("some_xpath_link").hasAttribute("href", "https://github.com/alex-tiurin/ultron") ``` It's preferable to use `id` or `xpath` to create `UltronWebElement` instance because they provide very profitable method `hasAttribute` Full operations list ```kotlin //actions clearElement() // clears content from an editable element replaceText(String) // simulates javascript clear and key events sent to a certain element webKeys(String) // simulates javascript key events sent to a certain element getText() // returns the visible text beneath a given DOM element webScrollIntoView() // executes scroll to view webScrollIntoViewBoolean() // returns if the desired element is in view after scrolling webClick() // simulates the javascript events to click on a particular element //assertions containsText(String) // asserts that DOM element contains visible text beneath it self exists() // asserts that element exists in webView hasText(String) // asserts that DOM element has visible text beneath it self hasAttribute(String, Matcher) // assert any html attribute value assertThat(WebAssertion) // use any webAssertion to assert it safely isSuccess(block: UltronWebElement.() -> Unit) // transforms any action or assertion to Boolean value reset() // removes the Element and Window references from this interaction //------ general ------ withTimeout(timeoutMs: Long) // set custom timeout withResultHandler(resultHandlerBlock) // provides the ability to process operation result in custom way withContextual(UltronWebElement) // set a parent element withAssertion(assertion: OperationAssertion) // define custom assertion of action success withAssertion(name: String = "", isListened: Boolean = false, block: () -> Unit) ``` ### UltronWebElements It helps to find similar elements. ```kotlin classNames("link").getElements() .find { ultronWebElement -> ultronWebElement.isSuccess { withTimeout(100).hasText("Apple") } }?.webClick() ``` It has only 2 usable methods ```kotlin getElements(): List getSize(): Int ``` ## Boolean operation result There is `isSuccess` method that allows us to get the result of any operation as boolean value. In case of false it could be executed to long (5 sec by default). So it reasonable to specify custom timeout for some operations. ```kotlin val isWebElementExist = xpath("some_xpath").isSuccess { withTimeout(2_000).exists() } if (isWebElementExist) { //do some reasonable actions } ``` ## Best practice Specify web elements as properties of PageObject class. ```kotlin object WebViewPage : Page() { private val button = id("button") private val textInput = id("text_input") private val title = xpath("some_xpath") } ``` Use this properties in page steps ```kotlin object WebViewPage : Page() { //page elements fun someUserStepOnWebView(expectedEventText: String){ textInput.replaceText(expectedEventText) button.webClick() title.hasText(expectedEventText) } } ``` ## Extend framework with your own Web operations It's described in another page [here](../common/extension.md#espresso-web) ================================================ FILE: docs/docs/common/_category_.json ================================================ { "label": "Common", "position": 4, "collapsed": false } ================================================ FILE: docs/docs/common/allure.md ================================================ --- sidebar_position: 1 --- # Allure Ultron can generate artifacts for Allure report only for Android UI tests. Just set Ultron `testInstrumentationRunner` in your app build.gradle file ([example build.gradle.kts](https://github.com/open-tool/ultron/blob/master/sample-app/build.gradle.kts#L14)) ```kotlin android { defaultConfig { testInstrumentationRunner = "com.atiurin.ultron.allure.UltronAllureTestRunner" ... } ``` and apply recommended config in your BaseTest class ([example BaseTest](https://github.com/open-tool/ultron/blob/master/sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/BaseTest.kt#L31)). ```kotlin @BeforeClass @JvmStatic fun setConfig() { UltronConfig.applyRecommended() UltronAllureConfig.applyRecommended() } ``` ## Custom results directory Ultron allows you to specify the directory where the Allure results will be stored. By default, the results are stored in the `/files/allure-results` directory in the root of the project. You can change this directory by calling `UltronAllureConfig.setAllureResultsDirectory()` ```kotlin @BeforeClass @JvmStatic fun setConfig() { ... UltronAllureConfig.applyRecommended() UltronAllureConfig.setAllureResultsDirectory(Environment.DIRECTORY_DOWNLOADS) } ``` ## Ultron Allure report contains: - Detailed report about all operations in your test - Logcat file (in case of failure) - Screenshot (in case of failure) - Ultron log file (in case of failure) You also can add any artifact you need. It will be described later. ![allure](https://github.com/open-tool/ultron/assets/12834123/c05c813a-ece6-45e6-a04f-e1c92b82ffb1) *** ## Ultron `step` Ultron wraps Allure `step` method into it's own one. It's recommended to use Ultron method cause it will provide more info to report in future releases. ### Best practice Wraps all steps with Ultron `step` method e.g. ```kotlin object ChatPage: Page(){ ... fun sendMessage(text: String) = apply { step("Send message with text '$text") { inputMessageText.typeText(text) sendMessageBtn.click() this.getMessageListItem(text).text .isDisplayed() .hasText(text) } } fun assertMessageTextAtPosition(position: Int, text: String) = apply { step("Assert item at position $position has text '$text'"){ this.getListItemAtPosition(position).text.isDisplayed().hasText(text) } } } ``` ## Custom config ```kotlin UltronConfig.apply { this.operationTimeoutMs = 10_000 this.logToFile = false this.accelerateUiAutomator = false } UltronAllureConfig.apply { this.attachUltronLog = false this.attachLogcat = false this.detailedAllureReport = false this.addConditionsToReport = false this.addScreenshotPolicy = mutableSetOf( AllureAttachStrategy.TEST_FAILURE, // attach screenshot at the end of failed test AllureAttachStrategy.OPERATION_FAILURE, // attach screenshot once operation failed AllureAttachStrategy.OPERATION_SUCCESS // attach screenshot for each operation ) } UltronComposeConfig.apply { this.operationTimeoutMs = 7_000 ... } ``` ## Add detailed info about your conditions to report Ultron provides cool feature called [Test condition management](../android/testconditions.md) With recommended config all conditions will be added to Allure report automatically. The `name` of rule and condition is used as Allure `step` name. For example this code ```kotlin val setupRule = SetUpRule("Login user rule") .add(name = "Login valid user $CURRENT_USER") { AccountManager(InstrumentationRegistry.getInstrumentation().targetContext).login( CURRENT_USER.login, CURRENT_USER.password ) } ``` generate following marked steps ![conditions](https://user-images.githubusercontent.com/12834123/232789449-1b6a0bc8-5c68-4dd3-836c-8d39696ce8dd.png) ## How to add custom artifacts to Allure report? ### Write artifact to report The framework has special methods to write your artifacts into report. `createCacheFile` - creates temp file to write the content ([see InstrumentationUtil.kt](https://github.com/open-tool/ultron/blob/master/ultron/src/main/java/com/atiurin/ultron/utils/InstrumentationUtil.kt))\ `AttachUtil.attachFile(...)` - to attach file to report [see AttachUtil](https://github.com/open-tool/ultron/blob/master/ultron-allure/src/main/java/com/atiurin/ultron/allure/attachment/AttachUtil.kt) You method can looks like ```kotlin fun addMyArtifactToAllure(){ val tempFile = createCacheFile() val result = writeContentToFile(tempFile) val fileName = AttachUtil.attachFile( name = "file_name.xml", file = tempFile, mimeType = "text/xml" ) } ``` `writeContentToFile(tempFile)` - you should implement it. ### Manage artifact creation You can attach artifact using 2 types of Ultron listeners: - [UltronLifecycleListener](https://github.com/open-tool/ultron/blob/master/ultron/src/main/java/com/atiurin/ultron/listeners/UltronLifecycleListener.kt) - once Ultron operation finished with any result. Sample - [ScreenshotAttachListener.kt](https://github.com/open-tool/ultron/blob/master/ultron-allure/src/main/java/com/atiurin/ultron/allure/listeners/ScreenshotAttachListener.kt) - [UltronRunListener](https://github.com/open-tool/ultron/blob/master/ultron/src/main/java/com/atiurin/ultron/runner/UltronRunListener.kt) which is inherited from [RunListener](https://github.com/open-tool/ultron/blob/master/ultron/src/main/java/com/atiurin/ultron/runner/RunListener.kt). This type can be used to add artifact in different test lifecycle state. Sample - [WindowHierarchyAttachRunListener.kt](https://github.com/open-tool/ultron/blob/master/ultron-allure/src/main/java/com/atiurin/ultron/allure/runner/WindowHierarchyAttachRunListener.kt) Refer to the [Listeners doc page](../common/listeners.md) for details. ================================================ FILE: docs/docs/common/boolean.md ================================================ --- sidebar_position: 5 --- # Boolean result While using the **Ultron** framework you always can get the result of any operation as boolean value. ```kotlin object SomePage : Page{ private val composeElement = hasTestTag("some_tag") private val espressoElement = withId(R.id.espressoId) private val espressoWebViewElement = xpath("some_xpath") private val uiautomatorElement = byResId(R.id.uiatomatorId) } ``` All these elements have `isSuccess` method that allows us to get boolean result. In case of false it could be executed to long (5 sec by default). So it reasonable to specify custom timeout for some operations. ```kotlin composeElement.isSuccess { withTimeout(1_000).assertIsDisplayed() } espressoElement.isSuccess { withTimeout(2_000).isDisplayed() } uiautomatorElement.isSuccess { withTimeout(2_000).isDisplayed() } espressoWebViewElement.isSuccess { withTimeout(2_000).exists() } ``` ================================================ FILE: docs/docs/common/customassertion.md ================================================ --- sidebar_position: 6 --- # Custom assertions Our applications are not perfect. It's often happens, that some action has no result. Mostly, this problem connected with bad app design and test device freeze. All Ultron operations (Espresso, Web, UiAutomator and Compose) has an ability to be asserted by custom logic. For example, you need to assert that some element appears after click. If it's not you need to repeat the click action. You can do it like: ```kotlin button.withAssertion("Assert smth is displayed") { title.isDisplayed() }.click() ``` `"Assert smth is displayed"` - is the name of assertion an you will see it in case of exception. You can skip it and write shorter: ```kotlin button.withAssertion { title.isDisplayed() }.click() ``` By default all Ultron operations inside assertion block are not logged in logcat, but don't worry! You will see it result in case of exception. If you want to have everything in logcat, use `isListened` param ```kotlin button.withAssertion(isListened = true) { .. } ``` ### Few words about timeouts **Please note: it is really important to understand the timeouts of operation and assertion.** * `withAssertion {..}` may double the time of failure. This happens because an operation is executed at least twice. And the assertion block is also executed twice. It's required to make sure that the failure is a proper failure. * In case an operation is executed successfully but an assertion fails you may have several interactions of the operation and the assertion. * You may restrict the assertion time by using Ultron `withTimeout()` method, e.g. ```kotlin button.withAssertion { title.withTimeout(3_000L).isDisplayed() }.click() ``` * You still can extend operation timeout ```kotlin button.withTimeout(10_000L).withAssertion { title.withTimeout(2_000L).isDisplayed() }.click() ``` ================================================ FILE: docs/docs/common/extension.md ================================================ --- sidebar_position: 3 --- # Ultron Extension Ultron leverages the power of [Kotlin extension functions](https://kotlinlang.org/docs/extensions.html). You can extend the framework by using its native approach along with your custom operations. ## Compose *** To enhance the Compose part of the framework, follow these steps: - Create an extension method for `UltronComposeSemanticsNodeInteraction`. This method should encapsulate the logic of the operation. - Create `SemanticsMatcher` extension method to invoke the method with the operation logic. Two methods facilitate this process: - `perform`: This evaluates the operation and returns updated `UltronComposeSemanticsNodeInteraction` object. ```kotlin fun UltronComposeSemanticsNodeInteraction.hasAnyChildren() = perform { Assert.assertTrue("SemanticsNode has any children", it.fetchSemanticsNode().children.isNotEmpty()) } fun SemanticsMatcher.hasAnyChildren() = UltronComposeSemanticsNodeInteraction(this).hasAnyChildren() ``` - `execute`: This evaluates the operation and returns the operation's result. ```kotlin fun UltronComposeSemanticsNodeInteraction.getWidth(): Int = execute { it.fetchSemanticsNode().size.width } fun SemanticsMatcher.getWidth(): Int = UltronComposeSemanticsNodeInteraction(this).getWidth() ``` ### Customize operation info You can provide additional information to the framework using `UltronComposeOperationParams` for both the `perform` and `execute` methods. ```kotlin fun UltronComposeSemanticsNodeInteraction.getWidth(): Int = execute( UltronComposeOperationParams( operationName = "Get width of '${semanticsNodeInteraction.getDescription()}'", operationDescription = "Compose get width of '${semanticsNodeInteraction.getDescription()}' during $timeoutMs ms", operationType = CustomComposeOperationType.GET_WIDTH ) ) { it.fetchSemanticsNode().size.width } ``` ## Espresso *** For Espresso operations, extend `UltronEspressoInteraction` class. There are 3 methods that help us: - `perform`: This evaluates the action and returns an updated `UltronEspressoInteraction` object. ```kotlin fun UltronEspressoInteraction.appendText(value: String) = perform { _, view -> val textView = (view as TextView) textView.text = "${textView.text}$value" } ``` - `execute`: This evaluates the action and returns the result of the operation. ```kotlin fun UltronEspressoInteraction.getText(): String = execute { _, view -> (view as TextView).text.toString() } ``` - `assertMatches`: This evaluates the assertion and returns an updated `UltronEspressoInteraction` object. ```kotlin fun UltronEspressoInteraction.assertChecked(expectedState: Boolean) = assertMatches { view -> // block returns Boolean defining whether assertion failed or succeded (view as CheckBox).isChecked == expectedState } ``` To make your custom operation fully native, extend `Matcher`, `ViewInteraction`, `DataInteraction`: ```kotlin //support action for all Matcher fun Matcher.appendText(text: String) = UltronEspressoInteraction(onView(this)).appendText(text) //support action for all ViewInteractions fun ViewInteraction.appendText(text: String) = UltronEspressoInteraction(this).appendText(text) //support action for all DataInteractions fun DataInteraction.appendText(text: String) = UltronEspressoInteraction(this).appendText(text) ``` You are able to use this custom operation ```kotlin withId(R.id.text_input).appendText("some text to append") ``` ### Customize action info You can provide additional information to the framework using `UltronEspressoActionParams` for both the `perform` and `execute` methods. ```kotlin fun UltronEspressoInteraction.getText(): String = execute( UltronEspressoActionParams( operationName = "GetText from TextView with '${getInteractionMatcher()}'", operationDescription = "${interaction.simpleClassName()} action '${CustomEspressoActionType.GET_TEXT}' of '${getInteractionMatcher()}' with root '${getInteractionRootMatcher()}' during ${getActionTimeout()} ms", operationType = CustomEspressoActionType.GET_TEXT, viewActionDescription = "getting text from TextView", viewActionConstraints = isAssignableFrom(TextView::class.java) ) ) { _, view -> (view as TextView).text.toString() } ``` ### Customize assertion info You can provide additional information to the framework using `UltronEspressoAssertionParams` for the `assertChecked` method. ```kotlin fun UltronEspressoInteraction.assertChecked(expectedState: Boolean) = assertMatches ( UltronEspressoAssertionParams( operationName = "Assert CheckBox isChecked = '$expectedState'", operationDescription = "Assert CheckBox isChecked = '$expectedState' during $timeoutMs ms", operationType = EspressoAssertionType.IS_CHECKED, ) ){ view -> (view as CheckBox).isChecked == expectedState } ``` ## Espresso Web *** For Espresso Web operations, extend the `UltronWebElement` class. ```kotlin // add action on wenView fun UltronWebElement.appendText(text: String) = apply { executeOperation( getUltronWebActionOperation ( webInteractionBlock = { webInteractionBlock().perform(DriverAtoms.webKeys(text)) }, name = "WebElement(${locator.type} = '$value') appendText '$text'", description = "WebElement(${locator.type} = '$value') appendText '$text' during $timeoutMs ms" ) ) } ``` Use it like ```kotlin id("text_input").appendText("some text") ``` In case you need to add an assertion, use `getUltronWebAssertionOperation()` instead of `getUltronWebActionOperation()` ```kotlin // add assertion on webView fun UltronWebElement.appendText(text: String) = apply { executeOperation( getUltronWebAssertionOperation (...) ) } ``` ## UI Automator *** For UI Automator operations, extend either `UltronUiObject2` or `UltronUiObject` class. ```kotlin //actually, UltronUiObject2 already has the same method addText // this is just an example of how to extend UltronUiObject2 fun UltronUiObject2.appendText(appendText: String) = apply { executeAction( actionBlock = { uiObject2ProviderBlock()!!.text += appendText }, name = "AppendText '$appendText' to $selectorDesc", description = "UiObject2 action '${UiAutomatorActionType.ADD_TEXT}' $selectorDesc appendText '$appendText' during $timeoutMs ms" ) } ``` Use this new ability like: ```kotlin object SomePage : Page() { private val search = byResId(R.id.search) fun someUserStep(prefixText: String){ search.addPrefixText(prefix) } } ``` The same approach applies to adding custom assertions: ```kotlin // actually it is not required to create custom UltronOperationType, but could be useful later enum class CustomUltronOperations : UltronOperationType { ASSERT_HAS_ANY_CHILD } // add extension function to UltronUiObject2 that calls `executeAssertion` fun UltronUiObject2.assertHasAnyChild() = apply { executeAssertion( assertionBlock = { uiObject2ProviderBlock()!!.childCount > 0 }, name = "Assert $selectorDesc has any child", type = CustomUltronOperations.ASSERT_HAS_ANY_CHILD, description = "UiObject2 assertion '${CustomUltronOperations.ASSERT_HAS_ANY_CHILD}' of $selectorDesc during $timeoutMs ms" ) } ``` Use this new ability like: ```kotlin object SomePage : Page() { private val searchResult = byResId(R.id.search_result) fun someUserStep(prefixText: String){ search.addPrefixText(prefix) searchResult.assertHasAnyChild() } } ``` ================================================ FILE: docs/docs/common/listeners.md ================================================ --- sidebar_position: 4 --- # Listeners The framework has 2 types of listeners: UltronLifecycleListener & UltronRunListener ## UltronLifecycleListener This one allows you to listen all stages of **Operation execution**. ```kotlin abstract class UltronLifecycleListener { /** * executed before any action or assertion */ override fun before(operation: Operation) = Unit /** * called when action or assertion failed */ override fun afterFailure(operationResult: OperationResult) = Unit /** * called when action or assertion has been executed successfully */ override fun afterSuccess(operationResult: OperationResult) = Unit /** * called in any case of action or assertion result */ override fun after(operationResult: OperationResult) = Unit } ``` `Operation` object contains all info about operation (name, description, type, timeout) `OperationResult` object contains all info about operation result (success, all exceptions that occured and exception that was thrown, description etc) and also has a reference to `Operation`. All listener methods will be executed before an exception will be thrown. It gives you a guarantee that all exceptions in your tests will be processed as you want. ### Log operation example For instance, here is a listener that logs everything to Ultron log. ```kotlin class LogLifecycleListener : UltronLifecycleListener() { override fun before(operation: Operation) { UltronLog.info("Start execution of ${operation.name}") } override fun afterSuccess(operationResult: OperationResult) { UltronLog.info("Successfully executed ${operationResult.operation.name}") } override fun afterFailure(operationResult: OperationResult) { UltronLog.error("Failed ${operationResult.operation.name} with description: \n" + "${operationResult.description} ") } } ``` You can create you own custom listener in the same way. ```kotlin class CustomLifecycleListener : UltronLifecycleListener() {...} ``` Add new listener for Ultron operations using `UltronCommonConfig.addListener()`. ```kotlin abstract class BaseTest { companion object { @BeforeClass @JvmStatic fun configureUltron() { UltronCommonConfig.addListener(CustomLifecycleListener()) } } } ``` ### Configuration Basically we already know how to add new listener. But there are other options to configure Ultron listeners. First of all Ultron by default already has [LogLifecycleListener](https://github.com/alex-tiurin/ultron/blob/master/ultron/src/main/java/com/atiurin/ultron/listeners/LogLifecycleListener.kt) that writes some usable info to logcat. ### Lifecycles Ultron has 4 different lifecycles that watch for different operations. - UltronEspressoOperationLifecycle - UltronWebLifecycle (WebView operations) - UltronUiAutomatorLifecycle - UltronComposeOperationLifecycle It is possible to add listener for any of these lifecycles. `UltronUiAutomatorLifecycle.addListener(CustomLifecycleListener())` In this case `CustomLifecycleListener` will be applied only for UI Automator operations. ### Exclude operation from listeners monitor Ultron allows it to exclude operation from all listeners. This option is based on operation type. For example, you've created a new operation ```kotlin enum class CustomUltronOperations : UltronOperationType { ASSERT_HAS_ANY_CHILD } fun UltronUiObject2.assertHasAnyChild() = apply { executeAssertion( assertionBlock = { uiObject2ProviderBlock()!!.childCount > 0 }, name = "Assert $selectorDesc has any child", type = CustomUltronOperations.ASSERT_HAS_ANY_CHILD, description = "UiObject2 assertion '${CustomUltronOperations.ASSERT_HAS_ANY_CHILD}' of $selectorDesc during $timeoutMs ms", timeoutMs = timeoutMs, resultHandler = resultHandler ) } ``` And you would like to exclude it from listeners for any reason no matter why. Add single line to Ultron configuration function. ```kotlin abstract class BaseTest { companion object { @BeforeClass @JvmStatic fun configureUltron() { ... UltronCommonConfig.operationsExcludedFromListeners.add(CustomUltronOperations.ASSERT_HAS_ANY_CHILD) } } } ``` ## UltronRunListener Allows you to add listener for Test Lifecycle. See [RunListener](https://github.com/open-tool/ultron/blob/master/ultron/src/main/java/com/atiurin/ultron/runner/RunListener.kt). It is available in case you use `ultron-allure` and set `testInstrumentationRunner`. ```kotlin testInstrumentationRunner = "com.atiurin.ultron.allure.UltronAllureTestRunner" ``` It could be used, for instance, to attach your custom application log to Allure Report. ```kotlin class AppLogAttachRunListener() : UltronRunListener() { override fun testFailure(failure: Failure) { val logFile: File = AppLogProvider.provide() val fileName = AttachUtil.attachFile( name = "app_log_file", file = logFile, mimeType = MimeType.PLAIN_TEXT ) } } ``` Add custom RunListener to Allure config. ```kotlin @BeforeClass @JvmStatic fun configureUltron() { ... UltronAllureConfig.addRunListener(AppLogAttachRunListener()) } ``` ## ComposDebugListener If you have had issues debugging Compose tests, such as not seeing UI changes on the screen immediately, Ultron can help you fix this. Simply add ComposeDebugListener to the framework configuration. ```kotlin @BeforeClass @JvmStatic fun configureUltron() { ... UltronCommonConfig.addListener(ComposDebugListener()) } ``` ================================================ FILE: docs/docs/common/resulthandler.md ================================================ --- sidebar_position: 7 --- # Result handler **Ultron** allows you to process the result of any operation in your own custom way. It provides full info to do that. Let's loot at the example ```kotlin object SomePage : Page{ private val espressoElement = withId(R.id.espressoId) private val espressoWebViewElement = xpath("some_xpath") private val uiautomatorElement = byResId(R.id.uiatomatorId) private val composeElement = hasTestTag("some_tag") } ``` Now, we want to catch the result of operation and do smth reasonable. There is a method that opens the door - `withResultHandler` ```kotlin espressoElement.withResultHandler { operationResult -> // smth that make sense } ``` What it gives to us? ![resultHandler](https://user-images.githubusercontent.com/12834123/113351564-bc872f00-9343-11eb-925a-432dbc191b32.png) **_A little explanation in case you would like to be more familiar with **Ultron** framework_** There is an entity which we call `ResultHandler`. By default all Ultron operations has the same `ResultHandler`. It catches the result of operation and asks `OperationResultAnalyzer` to analyze the result. In case `operationResult.success` is `false` the result analyzer throws catched exception. ## How to use? There are 2 ways of using custom ResultHandler: - Specify it for page property and it will be applied for all operations with this element ```kotlin object SomePage : Page() { private val eventStatus = withId(R.id.last_event_status).withResultHandler { operationResult -> // smth that make sense } } ``` - Specify it inside special step there the element operation should be processed in different way. This ResultHandler will be applied only once for single operation. ```kotlin object SomePage : Page() { fun someSpecificUserStep(expectedEventText: String){ eventStatus.withResultHandler { operationResult -> // smth that make sense }.hasText(expectedEventText) } } ``` ================================================ FILE: docs/docs/common/uiblock.md ================================================ --- sidebar_position: 2 --- # UI Block UI blocks are a powerful tool for describing and interacting with user interface elements. They allow you to define UI elements within the context of their parent blocks, rather than the entire screen, which makes tests more readable, maintainable, and reliable. For example, consider a UI block that represents a user’s name and status. We can define this block once and reuse it across different screens. ![UI Block](/img/uiblock.png) We can describe this block and use it on different screens. _Supported: Compose (CMP & Android), Espresso, Espresso Web, UiAutomator (UiObject2)_ ## Compose *** Create a class that inherits from `UltronComposeUiBlock`. ```kotlin class ContactCard(blockMatcher: SemanticsMatcher, blockDescription: String) : UltronComposeUiBlock(blockMatcher, blockDescription) { val name = child(hasTestTag(contactNameTag)).withName("Name in '$blockDescription'") val status = child(hasTestTag(contactStatusTag)) } ``` `UltronComposeUiBlock` accepts two parameters: - `blockMatcher` – describes how to locate this block in the Compose element tree. This is a required parameter and must always be provided. - `blockDescription` – a description of the block that clearly identifies the UI container. This parameter is optional, with a default value of `blockDescription = ""`. **Note**: To describe child elements of a UI block, you need to use the `child()` method. As shown in the example above, we added a custom name to the `name` field. If an error occurs, this name will appear in the description of the element we tried to interact with. We recommend including the value of `blockDescription` in the element description. This provides better context about the specific element being checked (or any other operation performed). The next step is to integrate the block into a screen. ```kotlin object SomeComposeScreen : Screen(){ val card = ContactCard(hasTestTag(contactCardTag), "SomeComposeScreen contact card") fun assertContactCard(contact: Contact){ softAssertion { card.name.assertTextEquals(contact.name) card.status.assertTextEquals(contact.status) } } } ``` As seen in `SomeComposeScreen`, we no longer need to know how to locate `name` and `status`. It's enough to describe how to locate the parent UI block – `ContactCard`. In addition to individual UI elements, child blocks can also represent other UI blocks. To describe a child UI block, you can use one of the overloaded `child` methods. - In Multiplatform, only the method requiring an explicit approach to creating the child block is available. - In Android, you can simplify this further using reflection. ### Compose Multiplatform ```kotlin class ProfileBlock(blockMatcher: SemanticsMatcher, blockDescription: String) : UltronComposeUiBlock(blockMatcher, blockDescription) { val card = child( childMatcher = hasTestTag(contactCardTag), uiBlockFactory = { updatedMatcher -> ContactCard( blockMatcher = updatedMatcher, blockDescription = "Contact card '$blockDescription'" ) } ) } ``` This method offers greater flexibility for creating child UI blocks. `updatedMatcher` – an updated matcher used to locate the `ContactCard` only within the `ProfileBlock`. ### Compose Android Only Reflection capabilities in Android are more advanced than in Multiplatform, allowing for simpler descriptions of child UI blocks. ```kotlin class ProfileBlock(blockMatcher: SemanticsMatcher, blockDescription: String) : UltronComposeUiBlock(blockMatcher, blockDescription) { val card = child( ContactCard( blockMatcher = hasTestTag(contactCardTag), blockDescription = "Contact card '$blockDescription'" ) ) } ``` There are limitations to using this method: The class must meet the following conditions to be instantiated: 1. It must not be a nested or inner class. It should be defined at the top level or as a file-level class. 2. It must have one of the following constructors: - A constructor with one parameter of type *SemanticsMatcher*. - A constructor with two parameters: `blockMatcher` of type *SemanticsMatcher* and `blockDescription` of type *String*. We can use the `ProfileBlock` on the screen. ```kotlin object SomeComposeScreen : Screen(){ val profile = ProfileBlock(hasTestTag(profileTag), "SomeComposeScreen profile card") fun assertContactCardInProfile(contact: Contact){ softAssertion { profile.card.name.assertTextEquals(contact.name) profile.card.status.assertTextEquals(contact.status) } } } ``` The `UltronComposeUiBlock` class has a `uiBlock` property, which facilitates proper interaction with block elements. ```kotlin object SomeComposeScreen : Screen(){ val profile = ProfileBlock(hasTestTag(profileTag), "SomeComposeScreen profile block") fun assertProfileContactIsDisplayed(){ profile.card.uiBlock.assertIsDisplayed() } } ``` ## Espresso *** Create a class that inherits from `UltronEspressoUiBlock` ```kotlin class ContactCard(blockMatcher: Matcher, blockDescription: String) : UltronEspressoUiBlock(blockMatcher, blockDescription) { val name = child(withId(R.id.name)).withName("Name in '$blockDescription'") val status = child(withId(R.id.name)) } ``` Add the block to the screen. ```kotlin object SomeEspressoScreen : Screen(){ val card = ContactCard(withId(R.id.card), "SomeComposeScreen contact card") fun assertContactCard(contact: Contact){ softAssertion { card.name.hasText(contact.name) card.status.hasText(contact.status) } } } ``` Using reflection simplifies the implementation of child UI blocks by automating instantiation under specific conditions. Child UI block with reflection. ```kotlin class ProfileBlock(blockMatcher: Matcher, blockDescription: String) : UltronEspressoUiBlock(blockMatcher, blockDescription) { val card = child( ContactCard( blockMatcher = withId(R.id.contactCard), blockDescription = "Contact card of '$blockDescription'" ) ) } ``` Child UI block with factory method ```kotlin class ProfileBlock(blockMatcher: Matcher, blockDescription: String) : UltronEspressoUiBlock(blockMatcher, blockDescription) { val card = child( childMatcher = withId(R.id.contactCard), uiBlockFactory = { updatedMatcher -> ContactCard( blockMatcher = updatedMatcher, blockDescription = "Contact card '$blockDescription'" ) } ) } ``` Define block on screen ```kotlin object SomeEspressoScreen : Screen(){ val profile = ProfileBlock(withId(R.id.profileBlock), "SomeEspressoScreen profile block") fun assertContactCardInProfile(contact: Contact){ softAssertion { profile.uiBlock.isDisplayed() profile.card.uiBlock.isDisplayed() profile.card.name.hasText(contact.name) profile.card.status.hasText(contact.status) } } } ``` ## Espresso Web *** Create a class that inherits from `UltronWebElementUiBlock` ```kotlin class WebContactCard(blockElement: UltronWebElement, blockDescription: String) : UltronWebElementUiBlock(blockElement, blockDescription){ val name = child(id("name")).withName("Name in '$blockDescription'") val status = child(className("status")) } ``` Add the block to the screen. ```kotlin object SomeWebScreen : Screen(){ val card = WebContactCard(id("card"), "SomeWebScreen contact card") fun assertContactCard(contact: Contact){ softAssertion { card.name.hasText(contact.name) card.status.hasText(contact.status) } } } ``` Child UI block with reflection ```kotlin class WebProfileBlock(blockMatcher: UltronWebElement, blockDescription: String) : UltronWebElementUiBlock(blockMatcher, blockDescription) { val card = child( WebContactCard( blockElement = id("card"), blockDescription = "Contact card of '$blockDescription'" ) ) } ``` Child UI block with factory method ```kotlin class WebProfileBlock(blockMatcher: UltronWebElement, blockDescription: String) : UltronWebElementUiBlock(blockMatcher, blockDescription) { val card = child( childMatcher = id("card"), uiBlockFactory = { updatedElement -> ContactCard( blockElement = updatedElement, blockDescription = "Contact card '$blockDescription'" ) } ) } ``` ## UiAutomator *** Only **UiObject2** is supported. Create a class that inherits from `UltronUiObject2UiBlock` ```kotlin class UiAutomatorContactCard(blockDesc: String, blockSelector: () -> BySelector) : UltronUiObject2UiBlock(blockDesc, blockSelector){ val name = child(bySelector(R.id.name)).withName("Name in '$blockDesc'") val status = child(By.desc("status content desc")) } ``` Add the block to the screen. ```kotlin object SomeUiAutomatorScreen : Screen(){ val card = UiAutomatorContactCard( blockDesc="SomeUiAutomatorScreen contact card", blockSelector=bySelector(R.id.card) ) fun assertContactCard(contact: Contact){ softAssertion { card.name.hasText(contact.name) card.status.hasText(contact.status) } } } ``` Child UI block with reflection ```kotlin class UiAutomatorProfileBlock(blockDesc: String, blockSelector: () -> BySelector) : UltronUiObject2UiBlock(blockDesc, blockSelector){ val card = child( UiAutomatorContactCard( blockDesc = "Contact card of '$blockDesc'", blockSelector = { bySelector(R.id.card) } ) ) } ``` Child UI block with factory method ```kotlin class UiAutomatorProfileBlock(blockDesc: String, blockSelector: () -> BySelector) : UltronUiObject2UiBlock(blockDesc, blockSelector){ val card = child( selector = bySelector(R.id.card), description = "Contact card of '$desc'", uiBlockFactory = { desc, selector -> UiAutomatorContactCard(desc, selector) } ) } ``` ================================================ FILE: docs/docs/common/ultrontest.md ================================================ --- sidebar_position: 2 --- # UltronTest `UltronTest` is a powerful base class provided by the Ultron framework that enables the definition of common preconditions and postconditions for tests. By extending this class, you can streamline test setup and teardown, ensuring consistent execution across your test suite. ## Features of `UltronTest` - **Pre-Test Actions:** Define actions to be executed before each test. - **Post-Test Actions:** Define actions to be executed after each test. - **Lifecycle Management:** Execute code once before all tests in a class using `beforeFirstTest`. - **Customizable Test Execution:** Suppress pre-test or post-test actions when needed. ### Example Here is an example of using `UltronTest`: ```kotlin class SampleUltronFlowTest : UltronTest() { @OptIn(ExperimentalUltronApi::class) override val beforeFirstTest = { UltronLog.info("Before Class") } override val beforeTest = { UltronLog.info("Before test common") } override val afterTest = { UltronLog.info("After test common") } /** * The order of method execution is as follows:: * beforeFirstTest, beforeTest, before, go, after, afterTest */ @Test fun someTest1() = test { before { UltronLog.info("Before TestMethod 1") }.go { UltronLog.info("Run TestMethod 1") }.after { UltronLog.info("After TestMethod 1") } } /** * An order of methods execution is follow: before, go, after * `beforeFirstTest` - Not executed, as it is only run once and was already executed before `someTest1`. * `beforeTest` - Not executed because it was suppressed using `suppressCommonBefore`. * `afterTest` - Not executed because it was suppressed using `suppressCommonAfter`. */ @Test fun someTest2() = test( suppressCommonBefore = true, suppressCommonAfter = true ) { before { UltronLog.info("Before TestMethod 2") }.go { UltronLog.info("Run TestMethod 2") }.after { UltronLog.info("After TestMethod 2") } } /** * An order of methods execution is follow: beforeTest, test, afterTest * `beforeFirstTest` - Not executed, since it was executed before `someTest1` */ @Test fun someTest3() = test { UltronLog.info("UltronTest simpleTest") } } ``` ### Key Methods - **`beforeFirstTest`**: Code executed once before all tests in a class. - **`beforeTest`**: Code executed before each test. - **`afterTest`**: Code executed after each test. - **`test`**: Executes a test with options to suppress pre-test or post-test actions. ### Key Features of the `test` Method - **Test Context Recreation:** The `test` method automatically recreates the `UltronTestContext` for each test execution, ensuring a clean and isolated state for the test context. - **Soft Assertion Reset:** Any exceptions captured during `softAssertions` in the previous test are cleared at the start of each new `test` execution, maintaining a clean state. - **Lifecycle Management:** It invokes `beforeTest` and `afterTest` methods around your test logic unless explicitly suppressed. --- ### Purpose of `before`, `go`, and `after` - **`before`:** Defines preconditions or setup actions that must be performed before the main test logic is executed. These actions might include preparing data, navigating to a specific screen, or setting up the environment. ```kotlin before { UltronLog.info("Setting up preconditions for TestMethod 2") } ``` - **`go`:** Encapsulates the core logic or actions of the test. This is where the actual operations being tested are performed, such as interacting with UI elements or executing specific functionality. ```kotlin go { UltronLog.info("Executing the main logic of TestMethod 2") } ``` - **`after`:** Block is used for postconditions or cleanup actions that need to occur after the main test logic has executed. This might include verifying results, resetting the environment, or clearing resources. ```kotlin after { UltronLog.info("Cleaning up after TestMethod 2") } ``` These methods help clearly separate test phases, making tests easier to read and maintain. ## Using `softAssertion` for Flexible Error Handling The `softAssertion` mechanism in Ultron allows tests to catch and verify multiple exceptions during their execution without failing immediately. This feature is particularly useful for validating multiple conditions within a single test. ### Example of `softAssertion` ```kotlin class SampleTest : UltronTest() { @Test fun softAssertionTest() { softAssertion(failOnException = false) { hasText("NotExistText").withTimeout(100).assertIsDisplayed() hasTestTag("NotExistTestTag").withTimeout(100).assertHasClickAction() } verifySoftAssertions() } } ``` The `softAssertion` mechanism does not inherently depend on `UltronTest`. You can use `softAssertion` independently of the `UltronTest` base class. However, in such cases, you must manually clear exceptions between tests to ensure they do not persist across test executions. ```kotlin class SampleTest { @Test fun softAssertionTest() { UltronCommonConfig.testContext.softAnalyzer.clear() softAssertion() { //assert smth } } } ``` ### Explanation - **Fail on Exception:** By default (`failOnException = true`), `softAssertion` will throw an exception after completing all operations within its block if any failures occur. - **Manual Verification:** If `failOnException` is set to `false`, you can explicitly verify all caught exceptions at the end of the test using `verifySoftAssertions()`. This approach ensures granular control over how exceptions are handled and reported, making it easier to analyze and debug test failures. --- ## Benefits of `UltronTest` usage - Simplifies test setup and teardown with consistent preconditions and postconditions. - Enhances error handling by allowing multiple assertions within a single test. - Improves test readability and maintainability. By leveraging `UltronTest` and `softAssertion`, you can build robust and flexible UI tests for your applications. ================================================ FILE: docs/docs/compose/_category_.json ================================================ { "label": "Compose", "position": 2, "collapsed": false } ================================================ FILE: docs/docs/compose/android.md ================================================ --- sidebar_position: 2 --- # Android Note: it's possible to use Multiplatform approach using methods `runComposeUiTest` and `runUltronUiTest` for Android UI tests. You can read about it in [multiplatform description](multiplatform.md) ## Android Compose testing API Typical Android test looks smth like this: ```kotlin class ComposeContentTest { @get:Rule val composeTestRule = createComposeRule() @Test fun myTest() { composeTestRule.setContent { .. } composeTestRule.onNode(hasTestTag("Continue")).performClick() composeTestRule.onNodeWithText("Welcome").assertIsDisplayed() } } ``` You can read more about it in [official documentation](https://developer.android.com/jetpack/compose/testing) So, all compose testing APIs are provided by `composeTestRule`. It's definitely uncomfortable. Moreover, in case your UI loading takes some time, e.g. in integration test, an assertion or an action fails. If you need to launch an Activity it's required to use another factory method to create Compose TestRule - `createAndroidComposeRule` ```kotlin class ActivityComposeTest { @get:Rule val composeTestRule = createAndroidComposeRule() @Test fun myTest() { composeTestRule.onNode(hasTestTag("Continue")).performClick() composeTestRule.onNodeWithText("Welcome").assertIsDisplayed() } } ``` _**Ultron**_ framework solves all these problems and do a lot more. ## Ultron Compose Just create compose rule using Ultron static method ```kotlin @get:Rule val composeTestRule = createDefaultUltronComposeRule() ``` After that you're able to perform stable compose operations in **ANY** class. Just create a `SemanticsMatcher`(like `hasTestTag("smth")`) and call an operation on it. e.g. ```kotlin hasTestTag("Continue").click() hasText("Welcome").assertIsDisplayed() ``` `SemanticsMatcher` object is used in Android Compose testing framework to find a target node to interact with. To launch an Activity use `createUltronComposeRule` or `createSimpleUltronComposeRule` ```kotlin @get:Rule val composeTestRule = createUltronComposeRule() ``` `createSimpleUltronComposeRule` used `UltronActivityRule` for launch and finish activity. You can read more in testconditions chapter ================================================ FILE: docs/docs/compose/api.md ================================================ --- sidebar_position: 4 --- # Ultron Compose API The framework provides an extended API for Compose UI testing. Basically, it's available for `SemanticsMatcher` object. It could be created by functions like `hasTestTag()`, `hasText()` and etc. ```kotlin //config fun withTimeout(timeoutMs: Long) // to change an operation timeout from default one fun withResultHandler(resultHandler: (ComposeOperationResult) -> Unit) // provide a scope to modify operation result processing fun isSuccess(action: UltronComposeSemanticsNodeInteraction.() -> T): Boolean fun withAssertion(assertion: OperationAssertion) fun withAssertion(name: String = "", isListened: Boolean = false, block: () -> Unit) fun withUseUnmergedTree(value: Boolean) fun withName(name: String) // specify custom name for UI element, it'll be visible in log, exception, and step name for detailed allure report fun withDescription(description: String) // analog of fun withName(name: String) for matchers of UltronComposeList, UltronComposeListItem, and child of UltronComposeListItem fun withMetaInfo(meta: Any) // allows association of custom info with UI element //actions fun click(option: ClickOption? = null) fun clickCenterLeft(option: ClickOption? = null) fun clickCenterRight(option: ClickOption? = null) fun clickTopCenter(option: ClickOption? = null) fun clickTopLeft(option: ClickOption? = null) fun clickTopRight(option: ClickOption? = null) fun clickBottomCenter(option: ClickOption? = null) fun clickBottomLeft(option: ClickOption? = null) fun clickBottomRight(option: ClickOption? = null) fun longClick(option: LongClickOption? = null) fun longClickCenterLeft(option: LongClickOption? = null) fun longClickCenterRight(option: LongClickOption? = null) fun longClickTopCenter(option: LongClickOption? = null) fun longClickTopLeft(option: LongClickOption? = null) fun longClickTopRight(option: LongClickOption? = null) fun longClickBottomCenter(option: LongClickOption? = null) fun longClickBottomLeft(option: LongClickOption? = null) fun longClickBottomRight(option: LongClickOption? = null) fun doubleClick(option: DoubleClickOption? = null) fun doubleClickCenterLeft(option: DoubleClickOption? = null) fun doubleClickCenterRight(option: DoubleClickOption? = null) fun doubleClickTopCenter(option: DoubleClickOption? = null) fun doubleClickTopLeft(option: DoubleClickOption? = null) fun doubleClickTopRight(option: DoubleClickOption? = null) fun doubleClickBottomCenter(option: DoubleClickOption? = null) fun doubleClickBottomLeft(option: DoubleClickOption? = null) fun doubleClickBottomRight(option: DoubleClickOption? = null) fun swipeDown(option: ComposeSwipeOption? = null) fun swipeUp(option: ComposeSwipeOption? = null) fun swipeLeft(option: ComposeSwipeOption? = null) fun swipeRight(option: ComposeSwipeOption? = null) fun scrollTo() fun scrollToIndex(index: Int) fun scrollToKey(key: String) fun scrollToNode(matcher: SemanticsMatcher) fun imeAction() fun pressKey(keyEvent: KeyEvent) fun getText(): String? fun inputText(text: String) fun typeText(text: String) fun inputTextSelection(selection: TextRange) fun setSelection(startIndex: Int = 0, endIndex: Int = 0, traversalMode: Boolean) fun selectText(range: TextRange) fun clearText() fun replaceText(text: String) fun copyText() fun pasteText() fun cutText() fun setText(text: String) fun setText(text: AnnotatedString) fun collapse() fun expand() fun dismiss() fun setProgress(value: Float) fun captureToImage(): ImageBitmap fun performMouseInput(block: MouseInjectionScope.() -> Unit) fun performSemanticsAction(key: SemanticsPropertyKey Boolean>>) fun perform(params: UltronComposeOperationParams? = null, block: (SemanticsNodeInteraction) -> Unit) fun execute(params: UltronComposeOperationParams? = null, block: (SemanticsNodeInteraction) -> T): T fun getNode(): SemanticsNode fun getNodeConfigProperty(key: SemanticsPropertyKey): T //asserts fun assertIsDisplayed() fun assertIsNotDisplayed() fun assertExists() fun assertDoesNotExist() fun assertIsEnabled() fun assertIsNotEnabled() fun assertIsFocused() fun assertIsNotFocused() fun assertIsSelected() fun assertIsNotSelected() fun assertIsSelectable() fun assertIsOn() fun assertIsOff() fun assertIsToggleable() fun assertHasClickAction() fun assertHasNoClickAction() fun assertTextEquals(vararg expected: String, option: TextEqualsOption? = null) fun assertTextContains(expected: String, option: TextContainsOption? = null) fun assertContentDescriptionEquals(vararg expected: String) fun assertContentDescriptionContains(expected: String, option: ContentDescriptionContainsOption? = null) fun assertValueEquals(expected: String) fun assertRangeInfoEquals(range: ProgressBarRangeInfo) fun assertHeightIsAtLeast(minHeight: Dp) fun assertHeightIsEqualTo(expectedHeight: Dp) fun assertWidthIsAtLeast(minWidth: Dp) fun assertWidthIsEqualTo(expectedWidth: Dp) fun assertMatches(matcher: SemanticsMatcher, messagePrefixOnError: (() -> String)? = null) ``` ### _Best practice_ > Use Page Object pattern. Specify page elements as properties of Page class ```kotlin object SomePage : Page() { private val button = hasTestTag(ComposeTestTags.button) private val eventStatus = hasTestTag(ComposeTestTags.eventStatus) } ``` Here `ComposeTestTags` could be an object that stores testTag constants. Use this properties in page steps ```kotlin object SomePage : Page() { //page elements fun someUserStepOnPage(expectedEventText: String) = apply { button.click() eventStatus.assertTextContains(expectedEventText) } } ``` It's possible to use term `Screen` instead of `Page`. They are equals. ```kotlin object SomeScreen : Screen() { ... } ``` ## Extend framework with your own compose operations Under the hood all Ultron compose operations are described in `UltronComposeSemanticsNodeInteraction` class. That is why you just need to extend this class using [kotlin extension function](https://kotlinlang.org/docs/extensions.html), e.g. ```kotlin //new semantic matcher for assertion fun hasProgress(value: Float): SemanticsMatcher = SemanticsMatcher.expectValue(GetProgress, value) //add new operation fun UltronComposeSemanticsNodeInteraction.assertProgress(expected: Float) = apply { executeOperation( operationBlock = { semanticsNodeInteraction.assert(hasProgress(expected)) }, name = "Assert '${semanticsNodeInteraction.getDescription()}' has progress $expected", description = "Compose assertProgress = $expected in '${semanticsNodeInteraction.getDescription()}' during $timeoutMs ms", ) } //extend SemanticsMatcher with your new operation fun SemanticsMatcher.assertProgress(expected: Float) = UltronComposeSemanticsNodeInteraction(this).assertProgress(expected) ``` How to use ```kotlin val progress = 0.7f hasTestTag(ComposeElementsActivity.progressBar).setProgress(progress).assertProgress(progress) ``` You may ask what is `GetProgress`? This is a feature of Compose framework. It's available to extend you app with custom SemanticsPropertyKey. Define it in app and assert it in tests. ```kotlin //application code @Composable fun LinearProgressBar(statusState: MutableState){ val progressState = remember { mutableStateOf(0f) } LinearProgressIndicator(progress = progressState.value, modifier = Modifier .semantics { testTag = ComposeElementsActivity.progressBar setProgress { value -> progressState.value = value statusState.value = "set progress $value" true } progressBarRangeInfo = ProgressBarRangeInfo(progressState.value, 0f..progressState.value, 100) } .getProgress(progressState.value) .progressSemantics() ) } val GetProgress = SemanticsPropertyKey("ProgressValue") var SemanticsPropertyReceiver.getProgress by GetProgress fun Modifier.getProgress(progress: Float): Modifier { return semantics { getProgress = progress } } ``` ================================================ FILE: docs/docs/compose/index.md ================================================ # Compose There are two types of UI tests you can write with Compose. 1. Kotlin Multiplatform UI test ([Kotlin documentation](https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-test.html)) 2. Platform Specific JUnit-based tests ([Android documentation](https://developer.android.com/develop/ui/compose/testing)) Ultron supports both types of UI tests and make it`s development much easier. ================================================ FILE: docs/docs/compose/lazylist.md ================================================ --- sidebar_position: 5 --- # LazyList ## Ultron LazyColumn/LazyRow It's pretty much familiar with `UltronRecyclerView` approach. The difference is in internal structure of `RecyclerView `and `LazyColumn/LazyRow`. Due to implementation features of LazyColumn/LazyRow we can't predict where matched item is located in list without scrolling (actually we can but it takes additional efforts from development) Before we go forward we need to clarify some terms: - ComposeList - list of some items. It's typically implemented in application as LazyColumnt or LazyRow. Ultron has a class that wraps an interaction with list - `UltronComposeList`. - ComposeListItem - single item of ComposeList (there is a class `UltronComposeListItem`) - ComposeListItemChild - child element of ComposeListItem (just a term, there is no special class to work with child elements). So _ComposeListItemChild_ could be considered as a simple compose node. ![lazyColumn](https://user-images.githubusercontent.com/12834123/188237127-32e501ca-ae8b-4cd4-8114-e3e17843dc55.PNG) *** ## UltronComposeList Create an instance of `UltronComposeList` by calling a method `composeList(..)` ```kotlin composeList(hasTestTag(contactsListTestTag)).assertNotEmpty() ``` ### _Best practice_ - define `UltronComposeList` object as page class property ```kotlin object ContactsListPage : Page() { val lazyList = composeList(hasContentDescription(contactsListContentDesc)) fun someStep(){ lazyList.assertNotEmpty() lazyList.assertContentDescriptionEquals(contactsListContentDesc) } } ``` ### `UltronComposeList` API ```kotlin withTimeout(timeoutMs: Long) // defines a timeout for all operations //assertions fun assertIsDisplayed() fun assertIsNotDisplayed() fun assertExists() fun assertDoesNotExist() fun assertContentDescriptionEquals(vararg expected: String) fun assertContentDescriptionContains(expected: String, option: ContentDescriptionContainsOption? = null) fun assertNotEmpty() fun assertEmpty() fun assertVisibleItemsCount(expected: Int) //item providers for simple UltronComposeListItem fun item(matcher: SemanticsMatcher): UltronComposeListItem fun visibleItem(index: Int): UltronComposeListItem fun firstVisibleItem(): UltronComposeListItem fun lastVisibleItem(): UltronComposeListItem // ----- item providers for UltronComposeListItem subclasses ----- // following methods return a generic type T which is a subclass of UltronComposeListItem fun getItem(matcher: SemanticsMatcher): T fun getVisibleItem(index: Int): T fun getFirstVisibleItem(): T fun getLastVisibleItem(): T //interaction provider visibleChild(matcher: SemanticsMatcher) // provides an interaction on visible matched item //actions fun getVisibleItemsCount(): Int fun scrollToNode(itemMatcher: SemanticsMatcher) fun scrollToIndex(index: Int) fun scrollToKey(key: Any) /** * Provide a scope with references to list SemanticsNode and SemanticsNodeInteraction. * It is possible to evaluate any action or assertion on this node. */ fun performOnList(block: (SemanticsNode, SemanticsNodeInteraction) -> T): T ``` ### useUnmergedTree It is really important to understand the difference btwn merged and unmerged tree. There is a property `useUnmergedTree` that defines a behaviour. ```kotlin composeList(hasTestTag(contactsListTestTag), useUnmergedTree = false) ``` - By default `UltronComposeList` uses unmerged tree (`useUnmergedTree = true`). All child elements contain info in seperate nodes. - In case we use merged tree (`useUnmergedTree = false`) all child elements of item is merged to single node. So you're not able to identify a text value of concrete child. Why it's important? Cause you need to use different SemanticsMatchers to find appropriate child. ```kotlin mergedTreeList.item(hasText(contact.name)) // contact.name could be placed in wrong child unmergedList.item(hasAnyDescendant(hasText(contact.name) and hasTestTag(contactNameTestTag))) //it's longer but certainly provides target node ``` *** ## UltronComposeListItem `UltronComposeList` provides an access to `UltronComposeListItem` There is a set of methods to create `UltronComposeListItem`. It's listed upper in `UltronComposeList` api. ### Simple `UltronComposeListItem` If you don't need to interact with item child just use methods like `item`, `firstItem`, `visibleItem`, `firstVisibleItem`, `lastVisibleItem` ```kotlin listWithMergedTree.item(hasText(contact.name)).assertTextContains(contact.name) listWithMergedTree.firstVisibleItem() .assertIsDisplayed() .assertTextContains(contact.name) .assertTextContains(contact.status) ``` You don't need to worry about scroll to item. It's executed automatically. ### Complex `UltronComposeListItem` with children It's often required to interact with item child. The best solution will be to describe children as properties of UltronComposeListItem subclass. ```kotlin class ComposeFriendListItem : UltronComposeListItem(){ val name by child { hasTestTag(contactNameTestTag) } val status by child { hasTestTag(contactStatusTestTag) } } ``` **Note: you have to use delegated initialisation with `by child`.** For Compose Multiplatform project you need to register Item class instances with `initBlock` param: ```kotlin composeList(.., initBlock = { registerItem { ComposeFriendListItem() } registerItem { AnotherListItem() } }) ``` It is required cause Kotlin Multiplatfor Project has limited reflation API for different platforms. You don't need to register Items for Android UI tests. Now you're able to get `ComposeFriendListItem` object using methods `getItem`, `getVisibleItem`, `getFirstVisibleItem`, `getLastVisibleItem` ```kotlin lazyList.getFirstVisibleItem() lazyList.getVisibleItem(index) lazyList.getItem(hasTestTag(..)) ``` ### _Best practice_ > Add a method to `Page` class that returns `UltronComposeListItem` subclass Mark such methods with `private` visibility modifier. e.g. `getContactItem` ```kotlin object ComposeListPage : Page() { private val lazyList = composeList(hasContentDescription(contactsListContentDesc), ..) private fun getContactItem(contact: Contact): ComposeFriendListItem = lazyList.getItem(hasTestTag(contact.id)) class ComposeFriendListItem : UltronComposeListItem(){ val name by lazy { getChild(hasTestTag(contactNameTestTag)) } val status by lazy { getChild(hasTestTag(contactStatusTestTag)) } } } ``` Use `getContactItem` in `Page` steps like `assertContactStatus` ```kotlin object ComposeListPage : Page() { private fun getContactItem(contact: Contact): ComposeFriendListItem = lazyList.getItem(hasTestTag(contact.id)) ... fun assertContactStatus(contact: Contact) = apply { getContactItem(contact).status.assertTextEquals(contact.status) } } ``` ## `UltronComposeListItem` API It's pretty much the same as [simple node api](../compose/api.md), but extends it mostly for internal features. *** ## Efficient Strategies for Locating Items in Compose LazyList Let's start with approaches that you can use without additional efforts. For example, you have identified `LazyList` in your tests code like ```kotlin val lazyList = composeList(listMatcher = hasTestTag("listTestTag").withDescription(description = "List of contacts"), ..) class ComposeListItem : UltronComposeListItem() { val name by lazy { getChild(hasTestTag(contactNameTestTag).withDescription(description = "Contact name")) } val status by lazy { getChild(hasTestTag(contactStatusTestTag).withDescription(description = "Contact status")) } } ``` ### 1. `..visibleItem` This is probably the most unstable approach. It's only suitable in case you didn't interact with `LazyList` and would like to reach an item that is on the screen. Use the following methods: ```kotlin lazyList.firstVisibleItem() lazyList.visibleItem(index = 3) lazyList.lastVisibleItem() lazyList.getFirstVisibleItem() lazyList.getVisibleItem(index = 3) lazyList.getLastVisibleItem() ``` ### 2. Item by unique `SemanticsMatcher` A more stable way to find the item is to use `SemanticsMatcher`. It allows you to find the item not only on the screen. ```kotlin val someText = "Some unique text" lazyList.item(hasAnyDescendant(hasText(someText).withDescription(description = someText)) lazyList.getItem(hasAnyDescendant(hasText("Some unique text")) ``` *** The next two approaches require additional code in the application. These are the most stable and preferable ways. ### 3. Set up `positionPropertyKey` By default, a compose list item doesn't have a property that stores its position in the list. We can add this property in a really simple way. Here is the application code: ```kotlin // create custom SemanticsPropertyKey val ListItemPositionPropertyKey = SemanticsPropertyKey("ListItemPosition") var SemanticsPropertyReceiver.listItemPosition by ListItemPositionPropertyKey // specify it for item and store item index in this property @Composable fun ContactsListWithPosition(contacts: List ) { LazyColumn( modifier = Modifier.semantics { testTag = "listTestTag" } ) { itemsIndexed(contacts) { index, contact -> Column( modifier = Modifier.semantics { listItemPosition = index } ) { // item content } } } } ``` After that, you need to specify the custom `SemanticsPropertyKey` in the test code: ```kotlin val lazyList = composeList( listMatcher = hasTestTag("listTestTag"), positionPropertyKey = ListItemPositionPropertyKey ) ``` It allows you to reach the item by its position in the list: ```kotlin lazyList.firstItem() lazyList.item(position = 25) lazyList.getFirstItem() lazyList.getItem(position = 7) ``` ### 4. Set up item `testTag` It is recommended to build `testTag` in a separate function based on data object. For example, let's assume we have a `Contact` data class that stores data to be presented in the item. ```kotlin data class Contact(val id: Int, val name: String, val status: String, val avatar: String) ``` We can create function to build `testTag` based on `contact.id` ```kotlin fun getContactItemTestTag(contact: Contact) = "contactId=${contact.id}" ``` We can use this function in the application code to specify `testTag` and in the test code to find the item by `testTag`: ```kotlin // application code @Composable fun ContactsListWithPosition(contacts: List ) { LazyColumn( modifier = Modifier.semantics { testTag = "listTestTag" } ) { itemsIndexed(contacts) { index, contact -> Column( modifier = Modifier.semantics { listItemPosition = index testTag = getContactItemTestTag(contact) } ) { // item content } } } } //test code val lazyList = composeList(listMatcher = hasTestTag("listTestTag")) lazyList.item(hasTestTag(getContactItemTestTag(contact))) lazyList.getItem(hasTestTag(getContactItemTestTag(contact))) ``` ================================================ FILE: docs/docs/compose/multiplatform.md ================================================ --- sidebar_position: 1 --- # Multiplatform > Multiplatform support is in Alpha state. Compose Multiplatform provides robust tools for building and testing UI components across various platforms. One significant aspect of this is the ability to write and run common tests for your UI elements ([official doc sample](https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-test.html#write-and-run-common-tests)). ### `runComposeUiTest` vs `runUltronUiTest` With standart Compose Testing framework you have to use `runComposeUiTest` method to interact with UI elements. Here is simplified basic test sample with Compose Multiplatform. Typically it's placed in common app module, like `composeApp/src/commonTest/kotlin` ```kotlin class ComposeExampleTest { @Test fun myTest() = runComposeUiTest { setContent { // reasonable UI content } onNode(hasTestTag("text")).assertTextEquals("Hello") onNode(hasTestTag("button")).performClick() onNode(hasTestTag("text")).assertTextEquals("Compose") } } ``` Usage of `runUltronUiTest` function simplifies the interaction syntax. ```kotlin class UltronExampleTest { @Test fun myTest() = runUltronUiTest { setContent { // reasonable UI content } hasTestTag("text").assertTextEquals("Hello") hasTestTag("button").click() hasTestTag("text").assertTextEquals("Compose") } } ``` More over it makes interactions more reliable and stable. Additionally, it becomes possible to call these interactions **EVERYWHERE** you want, e.g. in **Page Objects** ### Compose Page Object Everyone knows that **Page Object** pattern is a good pattern. But how to use it for multiplatform tests? While `runComposeUiTest` provides the context for interaction with UI elements, like calling `onNodeWithTag()`, moving this logic into a Page Object or any other class/method can lead to issues, as these don’t have direct access to the testing API. This is because the testing API is provided by an object called `SemanticsNodeInteractionProvider`, which needs to be passed into each object to call the testing API. Here’s an example of a modified test using the Page Object pattern: ```kotlin class PageObjectMultiplatformTest { @Test fun myTest() = runComposeUiTest { setContent { // reasonable UI content } ExamplePage(provider = this).someStep() } } class ExamplePage(val provider: SemanticsNodeInteractionsProvider){ fun someStep(){ provider.onNodeWithTag("text").assertTextEquals("Hello") provider.onNodeWithTag("button").performClick() provider.onNodeWithTag("text").assertTextEquals("Compose") } } ``` ### Ultron Page Object Ultron eliminates the need to pass `SemanticsNodeInteractionProvider` into each object. You only need to replace the `runComposeUiTest` method with `runUltronUiTest`. ```kotlin class UltronMultiplatformTest { @Test fun myTest() = runUltronUiTest { setContent { // reasonable UI content } UltronPage.someStep() } } object UltronPage { fun someStep(){ hasTestTag("text").assertTextEquals("Hello") hasTestTag("button").click() hasTestTag("text").assertTextEquals("Compose") } } ``` ================================================ FILE: docs/docs/index.md ================================================ --- sidebar_position: 1 --- # Introduction ![Docusaurus themed image](/img/ultron_banner_light.png#gh-light-mode-only)![Docusaurus themed image](/img/ultron_banner_dark.png#gh-dark-mode-only) Ultron is the simplest framework to develop UI tests for **Android** & **Compose Multiplatform**. It's constructed upon the Espresso, UI Automator and Compose UI testing frameworks. Ultron introduces a range of remarkable new features. Furthermore, Ultron puts you in complete control of your tests! You don't need to learn any new classes or special syntax. All magic actions and assertions are provided from crunch. Ultron can be easially customised and extended. ## What are the benefits of using the framework? - Page/Screen Object pattern support - Exceptional simplification for [**Compose UI tests**](compose/index.md) - Out-of-the-box generation of [**Allure report**](./common/allure.md) (Now, for Android UI tests only) - A straightforward and expressive syntax - Ensured **Stability** for all actions and assertions - Complete control over every action and assertion - Incredible interaction with lists: [**RecyclerView**](./android/recyclerview.md) and [**Compose LazyList**](compose/lazylist.md). - An **Architectural** approach to developing UI tests (search "Best practice") - An incredible mechanism for setups and teardowns (You can even set up preconditions for a single test within a test class, without affecting the others) - [The ability to effortlessly extend the framework with your own operations](common/extension.md) - Accelerated UI Automator operations - Ability to monitor each stage of operation execution with [Listeners](common/listeners.md) - [Custom operation assertions](common/customassertion.md) *** ### A few words about syntax The standard syntax provided by Google is intricate and not intuitive. This is especially evident when dealing with **LazyList** and **RecyclerView** interactions. Let's explore some examples: #### 1. Simple compose operation (refer to the doc [here](./compose/index.md)) _Compose framework_ ```kotlin composeTestRule.onNode(hasTestTag("Continue")).performClick() composeTestRule.onNodeWithText("Welcome").assertIsDisplayed() ``` _Ultron_ ```kotlin hasTestTag("Continue").click() hasText("Welcome").assertIsDisplayed() ``` #### 2. Compose list operation (refer to the [doc](./compose/lazylist.md)) _Compose framework_ ```kotlin val itemMatcher = hasText(contact.name) composeRule .onNodeWithTag(contactsListTestTag) .performScrollToNode(itemMatcher) .onChildren() .filterToOne(itemMatcher) .assertTextContains(contact.name) ``` _Ultron_ ```kotlin composeList(hasTestTag(contactsListTestTag)) .item(hasText(contact.name)) .assertTextContains(contact.name) ``` #### 3. Simple Espresso assertion and action. _Espresso_ ```kotlin onView(withId(R.id.send_button)).check(isDisplayed()).perform(click()) ``` _Ultron_ ```kotlin withId(R.id.send_button).isDisplayed().click() ``` This presents a cleaner approach. Ultron's operation names mirror Espresso's, while also providing additional operations. Refer to the [doc](./android/espress.md) for further details. #### 4. Action on RecyclerView list item _Espresso_ ```kotlin onView(withId(R.id.recycler_friends)) .perform( RecyclerViewActions .actionOnItem( hasDescendant(withText("Janice")), click() ) ) ``` _Ultron_ ```kotlin withRecyclerView(R.id.recycler_friends) .item(hasDescendant(withText("Janice"))) .click() ``` Explore the [doc](./android/espress.md) to unveil Ultron's magic with RecyclerView interactions. #### 5. Espresso WebView operations _Espresso_ ```kotlin onWebView() .withElement(findElement(Locator.ID, "text_input")) .perform(webKeys(newTitle)) .withElement(findElement(Locator.ID, "button1")) .perform(webClick()) .withElement(findElement(Locator.ID, "title")) .check(webMatches(getText(), containsString(newTitle))) ``` _Ultron_ ```kotlin id("text_input").webKeys(newTitle) id("button1").webClick() id("title").hasText(newTitle) ``` Refer to the [doc](./android/webview.md) for more details. #### 6. UI Automator operations _UI Automator_ ```kotlin val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) device .findObject(By.res("com.atiurin.sampleapp:id", "button1")) .click() ``` _Ultron_ ```kotlin byResId(R.id.button1).click() ``` Refer to the [doc](./android/uiautomator.md) *** ### Acquiring the result of any operation as Boolean value ```kotlin val isButtonDisplayed = withId(R.id.button).isSuccess { isDisplayed() } if (isButtonDisplayed) { //do some reasonable actions } ``` *** ### Why are all Ultron actions and assertions more stable? The framework captures a list of specified exceptions and attempts to repeat the operation during a timeout period (default is 5 seconds). Of course, you have the ability to customize the list of handled exceptions. You can also set a custom timeout for any operation. ```kotlin withId(R.id.result).withTimeout(10_000).hasText("Passed") ``` *** ## 3 steps to develop a test using Ultron We advocate for a proper test framework architecture, division of responsibilities between layers, and other best practices. Therefore, when using Ultron, we recommend the following approach: 1. Create a Page Object and specify screen UI elements as `Matcher` objects. ```kotlin object ChatPage : Page() { private val messagesList = withId(R.id.messages_list) private val clearHistoryBtn = withText("Clear history") private val inputMessageText = withId(R.id.message_input_text) private val sendMessageBtn = withId(R.id.send_button) } ``` It's recommended to make all Page Objects as `object` and descendants of Page class. This allows for the utilization of convenient Kotlin features. It also helps you to keep Page Objects stateless. 2. Describe user step methods in Page Object. ```kotlin object ChatPage : Page() { fun sendMessage(text: String) = apply { inputMessageText.typeText(text) sendMessageBtn.click() getMessageListItem(text).text .isDisplayed() .hasText(text) } fun clearHistory() = apply { openContextualActionModeOverflowMenu() clearHistoryBtn.click() } } ``` Refer to the full code sample [ChatPage.class](https://github.com/open-tool/ultron/blob/master/sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/ChatPage.kt) 3. Call user steps in test ```kotlin @Test fun friendsItemCheck(){ FriendsListPage { assertName("Janice") assertStatus("Janice","Oh. My. God") } } @Test fun sendMessage(){ FriendsListPage.openChat("Janice") ChatPage { clearHistory() sendMessage("test message") } } ``` Refer to the full code sample [DemoEspressoTest.class](https://github.com/open-tool/ultron/blob/master/sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso/DemoEspressoTest.kt) In essence, your project's architecture will look like this: [acrchitecture](https://github.com/open-tool/ultron/assets/12834123/b0882d34-a18d-4f1f-959b-f75796d11036) *** ## Allure report Ultron has built in support to generate artifacts for Allure reports. Just apply the recommended configuration and set testIntrumentationRunner. For the complete guide, refer to the [Allure description](./common/allure.md) ```kotlin @BeforeClass @JvmStatic fun setConfig() { UltronConfig.applyRecommended() UltronAllureConfig.applyRecommended() UltronComposeConfig.applyRecommended() } ``` ![allure](https://github.com/open-tool/ultron/assets/12834123/c05c813a-ece6-45e6-a04f-e1c92b82ffb1) ![allure compose](https://github.com/open-tool/ultron/assets/12834123/1f751f3d-fc58-4874-a850-acd9181bfb70) ================================================ FILE: docs/docs/intro/_category_.json ================================================ { "label": "Getting started", "position": 1, "collapsed": false } ================================================ FILE: docs/docs/intro/configuration.md ================================================ --- sidebar_position: 4 --- # Configuration Each library of the framework has it's own config onject. - `UltronComposeConfig` - ultron-compose - `UltronConfig` - ultron-android - `UltronAllureConfig` - ultron-allure - `UltronCommonConfig` - inside each library You can use recommended configuration and just apply it in **BaseTest** class ([sample](https://github.com/open-tool/ultron/blob/master/sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/BaseTest.kt#L29)) : ```kotlin @BeforeClass @JvmStatic fun config() { UltronConfig.applyRecommended() UltronAllureConfig.applyRecommended() UltronComposeConfig.applyRecommended() } ``` ### UltronComposeConfig *** Manages configurations for Compose part of the framework ```kotlin UltronComposeConfig.apply { operationTimeoutMs = 10_000 lazyColumnOperationTimeoutMs = 15_000 operationPollingTimeoutMs = 100 lazyColumnItemSearchLimit = 100 useUnmergedTree = true // set up this value as a default for all SemanticNodeInteractions } ``` ### UltronCommonConfig *** Provides an ability to config common parameters for your testing framework. ```kotlin UltronCommonConfig.apply { logToFile = true operationTimeoutMs = 10_000 logDateFormat = "MM-dd HH:mm:ss.SSS" } ``` It also gives an API to add/remove operations listeners ```kotlin UltronCommonConfig.addListener(CustomListener()) ``` ### UltronConfig *** `UltronConfig` object is responsible for configuring and managing settings related to the Espresso, EspressoWeb, and UiAutomator. You can set custom main settings using `apply` method. ```kotlin UltronConfig.apply { accelerateUiAutomator = true operationTimeoutMs = 10_000 } ``` - `UltronConfig.Espresso` nested Object: Manages configurations specific to the Espresso part of the framework. Provides settings related to timeouts, view matchers, result analyzers, and action/assertion configurations. ```kotlin UltronConfig.Espresso.RECYCLER_VIEW_LOAD_TIMEOUT = 20_000 UltronConfig.Espresso.RECYCLER_VIEW_OPERATIONS_TIMEOUT = 10_000 UltronConfig.Espresso.RECYCLER_VIEW_ITEM_SEARCH_LIMIT = 100 UltronConfig.Espresso.INCLUDE_VIEW_HIERARCHY_TO_EXCEPTION = true // false by default UltronConfig.Espresso.setResultAnalyzer { operationResult -> // set custom operations result analyzer } ``` - `UltronConfig.Espresso.ViewActionConfig` and `UltronConfig.Espresso.ViewAssertionConfig` nested Objects: Manage configurations for Espresso view actions and view assertions, respectively. Provide settings for allowed exceptions and result handlers. ```kotlin UltronConfig.Espresso.ViewActionConfig.allowedExceptions.add(CustomViewException::class.java) UltronConfig.Espresso.ViewAssertionConfig.allowedExceptions.add(CustomViewException::class.java) ``` - `UltronConfig.Espresso.WebInteractionOperationConfig` nested Object: Manages configurations for Espresso web interaction operations. Provides settings for allowed exceptions and result handlers. ```kotlin UltronConfig.Espresso.WebInteractionOperationConfig.allowedExceptions.add(CustomJSException::class.java) ``` - `UltronConfig.UiAutomator` nested Object: Manages configurations specific to the UiAutomator part of the framework. Provides settings related to timeouts, result analyzers, and UiDevice configurations. ```kotlin UltronConfig.UiAutomator.OPERATION_TIMEOUT = 15_000 val device = UltronConfig.UiAutomator.uiDevice UltronConfig.UiAutomator.UiObject2Config.allowedExceptions.add(CustomViewException::class.java) ``` - `UltronConfig.UiAutomator.UiObjectConfig` and `UltronConfig.UiAutomator.UiObject2Config` nested Objects: Manage configurations for UiAutomator operations using UiSelector and BySelector, respectively. Provide settings for allowed exceptions and result handlers. ### UltronAllureConfig *** Help us to configure Allure report. ```kotlin UltronAllureConfig.apply { addScreenshotPolicy = mutableSetOf( AllureAttachStrategy.TEST_FAILURE, AllureAttachStrategy.OPERATION_FAILURE, AllureAttachStrategy.OPERATION_SUCCESS ) addHierarchyPolicy = mutableSetOf( AllureAttachStrategy.TEST_FAILURE ) attachLogcat = false attachUltronLog = true addConditionsToReport = true detailedAllureReport = true } ``` It also allow us to add or remove RunListener. ```kotlin UltronAllureConfig.addRunListener(LogcatAttachRunListener()) UltronAllureConfig.removeRunListener(LogcatAttachRunListener::class.java) ``` ================================================ FILE: docs/docs/intro/connect.md ================================================ --- sidebar_position: 2 --- # Connect to project The framework has three libraries that could be added as dependencies. - `com.atiurin:ultron-compose` - could be used both for Android application and Compose Multiplatform UI tests - `com.atiurin:ultron-android` - native Android UI tests based on Espresso(including web part) and UI Automator - `com.atiurin:ultron-allure` - Allure report support for Android application UI tests You need **mavenCentral** repository. ```kotlin repositories { mavenCentral() } ``` ### Android application instrumented UI tests ```kotlin dependencies { androidTestImplementation("com.atiurin:ultron-compose:") androidTestImplementation("com.atiurin:ultron-android:") androidTestImplementation("com.atiurin:ultron-allure:") } ``` ### Compose Multiplatform UI tests ```kotlin kotlin { sourceSets { commonTest.dependencies { implementation("com.atiurin:ultron-compose:") } } } ``` Since Multiplatform support in alpha state it's possible to have some problems with `commonTest` usage. In this case you can specify dependencies in relevant part. ```kotlin kotlin { androidTarget { @OptIn(ExperimentalKotlinGradlePluginApi::class) instrumentedTestVariant { ... dependencies { implementation("com.atiurin:ultron-compose:") } } } sourceSets { val desktopTest by getting { dependencies { implementation("com.atiurin:ultron-compose:") } } } } ``` ================================================ FILE: docs/docs/intro/dependencies.md ================================================ --- sidebar_position: 3 --- # Dependencies Management Ultron provides all the required dependencies in a transitive manner. That means you don't need to specify the Espresso or UI Automator library in your dependencies section in most cases. You can find all Ultron dependencies in [Versions.kt](https://github.com/open-tool/ultron/blob/master/buildSrc/src/main/kotlin/Versions.kt). ## Android Dependencies The `com.atiurin:ultron-android:` library provides: ```kotlin dependencies { api(Libs.espressoCore) api(Libs.espressoContrib) api(Libs.espressoWeb) api(Libs.accessibility) api(Libs.hamcrestCore) api(Libs.uiautomator) } ``` If you need another Espresso library in dependencies. It's better to use the same Espresso version as Ultron. Now - [Ultron Espresso verion](https://github.com/open-tool/ultron/blob/master/buildSrc/src/main/kotlin/Versions.kt#L9) is `3.6.1`. ## Allure Dependencies The `com.atiurin:ultron-allure:` library provides all Allure dependencies. ```kotlin dependencies { api(Libs.allureAndroid) api(Libs.allureCommon) api(Libs.allureModel) api(Libs.allureJunit4) api(Libs.espressoCore) } ``` ## Compose Dependencies The `com.atiurin:ultron-compose:` library provides `androidx.compose.ui:ui-test-junit4` ```kotlin dependencies { api(Libs.composeUiTest) } ``` ================================================ FILE: docs/docusaurus.config.ts ================================================ import {themes as prismThemes} from 'prism-react-renderer'; import type {Config} from '@docusaurus/types'; import type * as Preset from '@docusaurus/preset-classic'; const config: Config = { title: 'Ultron', tagline: 'Compose Multiplatform and Android UI testing framework', favicon: 'img/favicon.ico', url: 'https://ultron.github.io', // Set the // pathname under which your site is served // For GitHub pages deployment, it is often '//' baseUrl: '/ultron/', // GitHub pages deployment config. // If you aren't using GitHub pages, you don't need these. organizationName: 'Open-tool', // Usually your GitHub org/user name. projectName: 'ultron', // Usually your repo name. onBrokenLinks: 'throw', onBrokenMarkdownLinks: 'warn', // Even if you don't use internationalization, you can use this field to set // useful metadata like html lang. For example, if your site is Chinese, you // may want to replace "en" with "zh-Hans". i18n: { defaultLocale: 'en', locales: ['en'], }, presets: [ [ 'classic', { docs: { sidebarPath: './sidebars.ts', }, theme: { customCss: './src/css/custom.css', }, } satisfies Preset.Options, ], ], themeConfig: { image: 'img/docusaurus-social-card.jpg', navbar: { title: 'Ultron', logo: { alt: 'Ultron Logo', src: 'img/ultron_full_light.png', }, items: [ { type: 'docSidebar', sidebarId: 'tutorialSidebar', position: 'left', label: 'Docs', }, { href: 'https://t.me/ultron_framework', position: 'right', className: 'header-telegram-link', 'aria-label': 'Telegram', }, { href: 'https://github.com/open-tool/ultron', position: 'right', className: 'header-github-link', 'aria-label': 'GitHub repository', }, { type: 'search', position: 'right', }, ], }, prism: { theme: prismThemes.github, darkTheme: prismThemes.dracula, }, algolia: { appId: 'TLB3E9OO68', apiKey: '06f26f943a74848657b1e5bec4c85aaf', indexName: 'open-toolio', contextualSearch: true, searchParameters: {}, insights: false, }, prism: { theme: prismThemes.github, darkTheme: prismThemes.dracula, }, } satisfies Preset.ThemeConfig, plugins: [ [ '@docusaurus/plugin-client-redirects', { fromExtensions: ['html', 'htm'], // /myPage.html -> /myPage toExtensions: ['exe', 'zip'], // /myAsset -> /myAsset.zip (if latter exists) redirects: [ { to: '/docs/', from: '/', }, ], }, ], ], }; export default config; ================================================ FILE: docs/package.json ================================================ { "name": "my-website", "version": "0.0.0", "private": true, "scripts": { "docusaurus": "docusaurus", "start": "docusaurus start", "build": "docusaurus build", "swizzle": "docusaurus swizzle", "deploy": "docusaurus deploy", "clear": "docusaurus clear", "serve": "docusaurus serve", "write-translations": "docusaurus write-translations", "write-heading-ids": "docusaurus write-heading-ids", "typecheck": "tsc" }, "dependencies": { "@docusaurus/core": "3.4.0", "@docusaurus/plugin-client-redirects": "^3.4.0", "@docusaurus/preset-classic": "3.4.0", "@mdx-js/react": "^3.0.0", "clsx": "^2.0.0", "prism-react-renderer": "^2.3.0", "react": "^18.0.0", "react-dom": "^18.0.0" }, "devDependencies": { "@docusaurus/module-type-aliases": "3.4.0", "@docusaurus/tsconfig": "3.4.0", "@docusaurus/types": "3.4.0", "typescript": "~5.2.2" }, "browserslist": { "production": [ ">0.5%", "not dead", "not op_mini all" ], "development": [ "last 3 chrome version", "last 3 firefox version", "last 5 safari version" ] }, "engines": { "node": ">=18.0" } } ================================================ FILE: docs/sidebars.ts ================================================ import type {SidebarsConfig} from '@docusaurus/plugin-content-docs'; /** * Creating a sidebar enables you to: - create an ordered group of docs - render a sidebar for each doc of that group - provide next/previous navigation The sidebars can be generated from the filesystem, or explicitly defined here. Create as many sidebars as you want. */ const sidebars: SidebarsConfig = { // By default, Docusaurus generates a sidebar from the docs folder structure tutorialSidebar: [{type: 'autogenerated', dirName: '.'}], // But you can create a sidebar manually /* tutorialSidebar: [ 'intro', 'hello', { type: 'category', label: 'Tutorial', items: ['tutorial-basics/create-a-document'], }, ], */ }; export default sidebars; ================================================ FILE: docs/src/components/HomepageFeatures/index.tsx ================================================ import clsx from 'clsx'; import Heading from '@theme/Heading'; import styles from './styles.module.css'; type FeatureItem = { title: string; pict: JSX.Element; description: JSX.Element; }; const FeatureList: FeatureItem[] = [ { title: 'Simple', pict: ( simplicity ), description: ( <>

The simplest syntax for UI tests.

hasTestTag("elementId").click() ), }, { title: 'Stable', pict: ( stability ), description: ( <>

No flaky tests

Auto-waits for UI elements

Automatic retries of failed interactions

Custom assertions of executed action

), }, { title: 'Maintainable', pict: ( maintainability ), description: ( <>

Page Object support

Allure support

Detailed logs

Extendable API

), }, ]; function Feature({ title, pict, description }: FeatureItem) { const imageStyle = `text--center padding-horiz--md ${styles.imageContainer} ` return (
{pict}
{title}
{description}
); } export default function HomepageFeatures(): JSX.Element { return (
{FeatureList.map((props, idx) => ( ))}
); } ================================================ FILE: docs/src/components/HomepageFeatures/styles.module.css ================================================ .features { display: flex; align-items: center; padding: 2rem 0; width: 100%; } .featureSvg { height: 150px; width: 150px; padding: 10px; overflow: 'hidden'; display: 'flex'; justify-content: center; } .imageContainer { width: 150px; height: 150px; /* display: flex; */ padding: 10px; margin-left: auto; margin-right: auto; /* overflow: hidden; */ } .imageContainer img { width: 100%; height: 100%; object-fit: cover; border-radius: 10px; } ================================================ FILE: docs/src/css/custom.css ================================================ /** * Any CSS included here will be global. The classic template * bundles Infima by default. Infima is a CSS framework designed to * work well for content-centric websites. */ /* You can override the default Infima variables here. */ /* :root { --ifm-color-primary: #2e8555; --ifm-color-primary-dark: #29784c; --ifm-color-primary-darker: #277148; --ifm-color-primary-darkest: #205d3b; --ifm-color-primary-light: #33925d; --ifm-color-primary-lighter: #359962; --ifm-color-primary-lightest: #3cad6e; --ifm-code-font-size: 95%; --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); } */ :root { --ifm-color-primary: #233a60; --ifm-color-primary-dark: #29784c; --ifm-color-primary-darker: #277148; --ifm-color-primary-darkest: #205d3b; --ifm-color-primary-light: #33925d; --ifm-color-primary-lighter: #359962; --ifm-color-primary-lightest: #3cad6e; --ifm-code-font-size: 95%; --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); } /* For readability concerns, you should choose a lighter palette in dark mode. */ [data-theme='dark'] { --ifm-color-primary: #5a85cf; --ifm-color-primary-dark: #21af90; --ifm-color-primary-darker: #1fa588; --ifm-color-primary-darkest: #1a8870; --ifm-color-primary-light: #29d5b0; --ifm-color-primary-lighter: #32d8b4; --ifm-color-primary-lightest: #4fddbf; --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); } [data-theme='light'] img[src$='#gh-dark-mode-only'], [data-theme='dark'] img[src$='#gh-light-mode-only'] { display: none; } .header-github-link::before { content: ''; width: 24px; height: 24px; display: flex; background-color: var(--ifm-navbar-link-color); mask-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E"); transition: background-color var(--ifm-transition-fast) var(--ifm-transition-timing-default); } .header-github-link:hover::before { background-color: var(--ifm-navbar-link-hover-color); } .header-telegram-link { display: flex; align-items: center; } .header-telegram-link::before { content: ''; display: inline-block; width: 24px; height: 24px; background-image: url('/img/telegram-icon.svg'); background-size: contain; background-repeat: no-repeat; margin-right: 8px; } ================================================ FILE: docs/src/pages/index.module.css ================================================ /** * CSS files with the .module.css suffix will be treated as CSS modules * and scoped locally. */ .heroBanner { padding: 4rem 0; text-align: center; position: relative; overflow: hidden; } @media screen and (max-width: 996px) { .heroBanner { padding: 2rem; } } .buttons { display: flex; align-items: center; justify-content: center; } ================================================ FILE: docs/src/pages/index.tsx ================================================ import clsx from 'clsx'; import Link from '@docusaurus/Link'; import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import Layout from '@theme/Layout'; import HomepageFeatures from '@site/src/components/HomepageFeatures'; import Heading from '@theme/Heading'; import styles from './index.module.css'; function HomepageHeader() { const {siteConfig} = useDocusaurusContext(); return (
U L T R O N

{siteConfig.tagline}

GET STARTED
); } export default function Home(): JSX.Element { const {siteConfig} = useDocusaurusContext(); return (
); } ================================================ FILE: docs/src/pages/markdown-page.md ================================================ --- title: Markdown page example --- # Markdown page example You don't need React to write simple standalone pages. ================================================ FILE: docs/static/.nojekyll ================================================ ================================================ FILE: docs/tsconfig.json ================================================ { // This file is not used in compilation. It is here just for a nice editor experience. "extends": "@docusaurus/tsconfig", "compilerOptions": { "baseUrl": "." } } ================================================ FILE: gradle/libs.versions.toml ================================================ [versions] agp = "8.9.3" atomicfu = "0.27.0" kotlin = "2.1.21" androidx-activityCompose = "1.10.1" kotlinxCoroutinesCore = "1.10.2" kotlinxDatetime = "0.7.1" compose-plugin = "1.8.2" okio = "3.11.0" android-compileSdk = "35" android-minSdk = "24" android-targetSdk = "35" androidx-lifecycle = "2.8.4" androidx-navigation = "2.7.0-alpha06" monitor = "1.7.2" uiTestJunit4Android = "1.8.1" lifecycleCommonJvm = "2.9.2" coreKtx = "1.6.1" [libraries] androidx-ui-test-junit4-android = { module = "androidx.compose.ui:ui-test-junit4-android", version.ref = "uiTestJunit4Android" } androidx-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "uiTestJunit4Android" } atomicfu = { module = "org.jetbrains.kotlinx:atomicfu", version.ref = "atomicfu" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutinesCore" } kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" } okio = { module = "com.squareup.okio:okio", version.ref = "okio" } androidx-monitor = { group = "androidx.test", name = "monitor", version.ref = "monitor" } androidx-lifecycle-runtime-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" } androidx-lifecycle-viewmodel-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" } androidx-navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "androidx-navigation" } androidx-lifecycle-common-jvm = { group = "androidx.lifecycle", name = "lifecycle-common-jvm", version.ref = "lifecycleCommonJvm" } androidx-core-ktx = { group = "androidx.test", name = "core-ktx", version.ref = "coreKtx" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } androidLibrary = { id = "com.android.library", version.ref = "agp" } kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } vanniktech-mavenPublish = { id = "com.vanniktech.maven.publish", version = "0.30.0" } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ #Tue May 20 16:16:07 MSK 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: gradle.properties ================================================ org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M" org.gradle.caching=true org.gradle.configuration-cache=true kotlin.code.style=official android.useAndroidX=true android.nonTransitiveRClass=true org.jetbrains.compose.experimental.wasm.enabled=true org.jetbrains.compose.experimental.jscanvas.enabled=true org.jetbrains.compose.experimental.macos.enabled=true kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.mpp.enableCInteropCommonization=true kotlin.native.cacheKind=none GROUP=com.atiurin POM_ARTIFACT_ID=ultron VERSION_NAME=2.6.2 ================================================ FILE: gradlew ================================================ #!/bin/sh # # Copyright 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ############################################################################## # # Gradle start up script for POSIX generated by Gradle. # # Important for running: # # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is # noncompliant, but you have some other compliant shell such as ksh or # bash, then to run this script, type that shell name before the whole # command line, like: # # ksh Gradle # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: # * functions; # * expansions $var, ${var}, ${var:-default}, ${var+SET}, # ${var#prefix}, ${var%suffix}, and $( cmd ); # * compound commands having a testable exit status, especially case; # * various built-in commands including command, set, and ulimit. # # Important for patching: # # (2) This script targets any POSIX shell, so it avoids extensions provided # by Bash, Ksh, etc; in particular arrays are avoided. # # The "traditional" practice of packing multiple parameters into a # space-separated string is a well documented source of bugs and security # problems, so this is (mostly) avoided, by progressively accumulating # options in "$@", and eventually passing that to Java. # # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; # see the in-line comments for details. # # There are tweaks for specific operating systems such as AIX, CygWin, # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. # ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link app_path=$0 # Need this for daisy-chained symlinks. while APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path [ -h "$app_path" ] do ls=$( ls -ld "$app_path" ) link=${ls#*' -> '} case $link in #( /*) app_path=$link ;; #( *) app_path=$APP_HOME$link ;; esac done APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit APP_NAME="Gradle" APP_BASE_NAME=${0##*/} # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum warn () { echo "$*" } >&2 die () { echo echo "$*" echo exit 1 } >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "$( uname )" in #( CYGWIN* ) cygwin=true ;; #( Darwin* ) darwin=true ;; #( MSYS* | MINGW* ) msys=true ;; #( NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD=$JAVA_HOME/jre/sh/java else JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi # Collect all arguments for the java command, stacking in reverse order: # * args from the command line # * the main class name # * -classpath # * -D...appname settings # * --module-path (only if needed) # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) # Now convert the arguments - kludge to limit ourselves to /bin/sh for arg do if case $arg in #( -*) false ;; # don't mess with options #( /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath [ -e "$t" ] ;; #( *) false ;; esac then arg=$( cygpath --path --ignore --mixed "$arg" ) fi # Roll the args list around exactly as many times as the number of # args, so each arg winds up back in the position where it started, but # possibly modified. # # NB: a `for` loop captures its iteration list before it begins, so # changing the positional parameters here affects neither the number of # iterations, nor the values presented in `arg`. shift # remove old arg set -- "$@" "$arg" # push replacement arg done fi # Collect all arguments for the java command; # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of # shell script including quotes and variable substitutions, so put them in # double quotes to make sure that they get re-expanded; and # * put everything else in single quotes, so that it's not re-expanded. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ org.gradle.wrapper.GradleWrapperMain \ "$@" # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. # # In Bash we could simply go: # # readarray ARGS < <( xargs -n1 <<<"$var" ) && # set -- "${ARGS[@]}" "$@" # # but POSIX shell has neither arrays nor command substitution, so instead we # post-process each arg (as a line of input to sed) to backslash-escape any # character that might be a shell metacharacter, then use eval to reverse # that process (while maintaining the separation between arguments), and wrap # the whole thing up as a single "set" statement. # # This will of course break if any of these variables contains a newline or # an unmatched quote. # eval "set -- $( printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | xargs -n1 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | tr '\n' ' ' )" '"$@"' exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Resolve any "." and ".." in APP_HOME to make it shorter. for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: iosApp/Configuration/Config.xcconfig ================================================ TEAM_ID= BUNDLE_ID=com.atiurin.samplekmp.sample-kmp APP_NAME=sample-kmp ================================================ FILE: iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json ================================================ { "colors" : [ { "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "filename" : "app-icon-1024.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: iosApp/iosApp/Assets.xcassets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: iosApp/iosApp/ContentView.swift ================================================ import UIKit import SwiftUI import ComposeApp struct ComposeView: UIViewControllerRepresentable { func makeUIViewController(context: Context) -> UIViewController { MainViewControllerKt.MainViewController() } func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} } struct ContentView: View { var body: some View { ComposeView() .ignoresSafeArea(.keyboard) // Compose has own keyboard handler } } ================================================ FILE: iosApp/iosApp/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString 1.0 CFBundleVersion 1 LSRequiresIPhoneOS CADisableMinimumFrameDurationOnPhone UIApplicationSceneManifest UIApplicationSupportsMultipleScenes UILaunchScreen UIRequiredDeviceCapabilities armv7 UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight ================================================ FILE: iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: iosApp/iosApp/iOSApp.swift ================================================ import SwiftUI @main struct iOSApp: App { var body: some Scene { WindowGroup { ContentView() } } } ================================================ FILE: iosApp/iosApp.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 60; objects = { /* Begin PBXBuildFile section */ 058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557BA273AAA24004C7B11 /* Assets.xcassets */; }; 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */; }; 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iOSApp.swift */; }; 7555FF83242A565900829871 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* ContentView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 058557BA273AAA24004C7B11 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 2152FB032600AC8F00CF470E /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = ""; }; 7555FF7B242A565900829871 /* sample-kmp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "sample-kmp.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 7555FF82242A565900829871 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; AB3632DC29227652001CCB65 /* Config.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ B92378962B6B1156000C7307 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 058557D7273AAEEB004C7B11 /* Preview Content */ = { isa = PBXGroup; children = ( 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */, ); path = "Preview Content"; sourceTree = ""; }; 42799AB246E5F90AF97AA0EF /* Frameworks */ = { isa = PBXGroup; children = ( ); name = Frameworks; sourceTree = ""; }; 7555FF72242A565900829871 = { isa = PBXGroup; children = ( AB1DB47929225F7C00F7AF9C /* Configuration */, 7555FF7D242A565900829871 /* iosApp */, 7555FF7C242A565900829871 /* Products */, 42799AB246E5F90AF97AA0EF /* Frameworks */, ); sourceTree = ""; }; 7555FF7C242A565900829871 /* Products */ = { isa = PBXGroup; children = ( 7555FF7B242A565900829871 /* sample-kmp.app */, ); name = Products; sourceTree = ""; }; 7555FF7D242A565900829871 /* iosApp */ = { isa = PBXGroup; children = ( 058557BA273AAA24004C7B11 /* Assets.xcassets */, 7555FF82242A565900829871 /* ContentView.swift */, 7555FF8C242A565B00829871 /* Info.plist */, 2152FB032600AC8F00CF470E /* iOSApp.swift */, 058557D7273AAEEB004C7B11 /* Preview Content */, ); path = iosApp; sourceTree = ""; }; AB1DB47929225F7C00F7AF9C /* Configuration */ = { isa = PBXGroup; children = ( AB3632DC29227652001CCB65 /* Config.xcconfig */, ); path = Configuration; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 7555FF7A242A565900829871 /* iosApp */ = { isa = PBXNativeTarget; buildConfigurationList = 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */; buildPhases = ( F36B1CEB2AD83DDC00CB74D5 /* Compile Kotlin Framework */, 7555FF77242A565900829871 /* Sources */, B92378962B6B1156000C7307 /* Frameworks */, 7555FF79242A565900829871 /* Resources */, ); buildRules = ( ); dependencies = ( ); name = iosApp; packageProductDependencies = ( ); productName = iosApp; productReference = 7555FF7B242A565900829871 /* sample-kmp.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 7555FF73242A565900829871 /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 1130; LastUpgradeCheck = 1540; ORGANIZATIONNAME = orgName; TargetAttributes = { 7555FF7A242A565900829871 = { CreatedOnToolsVersion = 11.3.1; }; }; }; buildConfigurationList = 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */; compatibilityVersion = "Xcode 15.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 7555FF72242A565900829871; packageReferences = ( ); productRefGroup = 7555FF7C242A565900829871 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 7555FF7A242A565900829871 /* iosApp */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 7555FF79242A565900829871 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */, 058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ F36B1CEB2AD83DDC00CB74D5 /* Compile Kotlin Framework */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( ); name = "Compile Kotlin Framework"; outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "if [ \"YES\" = \"$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED\" ]; then\n echo \"Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \\\"YES\\\"\"\n exit 0\nfi\ncd \"$SRCROOT/..\"\n./gradlew :composeApp:embedAndSignAppleFrameworkForXcode\n"; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 7555FF77242A565900829871 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */, 7555FF83242A565900829871 /* ContentView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin XCBuildConfiguration section */ 7555FFA3242A565B00829871 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.3; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; 7555FFA4242A565B00829871 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.3; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; VALIDATE_PRODUCT = YES; }; name = Release; }; 7555FFA6242A565B00829871 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; DEVELOPMENT_TEAM = "${TEAM_ID}"; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\n$(SRCROOT)/../composeApp/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)", ); INFOPLIST_FILE = iosApp/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); OTHER_LDFLAGS = ( "$(inherited)", "-framework", ComposeApp, ); PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}"; PRODUCT_NAME = "${APP_NAME}"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; 7555FFA7242A565B00829871 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; DEVELOPMENT_TEAM = "${TEAM_ID}"; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\n$(SRCROOT)/../composeApp/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)", ); INFOPLIST_FILE = iosApp/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); OTHER_LDFLAGS = ( "$(inherited)", "-framework", ComposeApp, ); PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}"; PRODUCT_NAME = "${APP_NAME}"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */ = { isa = XCConfigurationList; buildConfigurations = ( 7555FFA3242A565B00829871 /* Debug */, 7555FFA4242A565B00829871 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */ = { isa = XCConfigurationList; buildConfigurations = ( 7555FFA6242A565B00829871 /* Debug */, 7555FFA7242A565B00829871 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 7555FF73242A565900829871 /* Project object */; } ================================================ FILE: prepare-emulator.bat ================================================ adb shell settings put global development_settings_enabled 1 adb shell settings put global window_animation_scale 0.0 adb shell settings put global transition_animation_scale 0.0 adb shell settings put global animator_duration_scale 0.0 ================================================ FILE: prepare-emulator.sh ================================================ adb shell settings put global development_settings_enabled 1 adb shell settings put global window_animation_scale 0.0 adb shell settings put global transition_animation_scale 0.0 adb shell settings put global animator_duration_scale 0.0 ================================================ FILE: sample-app/.gitignore ================================================ /build ================================================ FILE: sample-app/build.gradle.kts ================================================ plugins { id("com.android.application") id("kotlin-android") alias(libs.plugins.compose.compiler) } android { namespace = "com.atiurin.sampleapp" compileSdk = 35 defaultConfig { applicationId = "com.atiurin.sampleapp" minSdk = 21 targetSdk = 35 multiDexEnabled = true testInstrumentationRunner = "com.atiurin.sampleapp.framework.CustomTestRunner" } compileOptions { targetCompatibility = JavaVersion.VERSION_17 sourceCompatibility = JavaVersion.VERSION_17 } buildFeatures { compose = true buildConfig = true } kotlinOptions { jvmTarget = "17" } buildTypes { debug { isMinifyEnabled = false } } packagingOptions { resources.excludes.add("META-INF/DEPENDENCIES") resources.excludes.add("META-INF/LICENSE") resources.excludes.add("META-INF/LICENSE.txt") resources.excludes.add("META-INF/license.txt") resources.excludes.add("META-INF/NOTICE") resources.excludes.add("META-INF/NOTICE.txt") resources.excludes.add("META-INF/notice.txt") resources.excludes.add("META-INF/AL2.0") resources.excludes.add("META-INF/LGPL2.1") resources.excludes.add("META-INF/*.kotlin_module") } } dependencies { implementation(project(":ultron-compose")) implementation(project(":ultron-allure")) implementation(project(":ultron-android")) implementation(Libs.kotlinStdlib) implementation(Libs.coroutines) implementation(Libs.appcompat) implementation(Libs.androidXKtx) implementation(Libs.supportV4) implementation(Libs.material) implementation(Libs.material3) implementation(Libs.constraintLayout) implementation(Libs.recyclerView) implementation(Libs.cardview) implementation(Libs.espressoIdlingResource) implementation(libs.androidx.navigation.compose) //compose implementation(Libs.composeUi) implementation(Libs.composeUiTooling) implementation(Libs.composeFoundation) implementation(Libs.composeMaterial) implementation(Libs.composeMaterialIconsCore) implementation(Libs.composeMaterialIconsExtend) implementation(Libs.activityCompose) // AndroidJUnitRunner and JUnit Rules testImplementation(Libs.junit) testImplementation(Libs.robolectric) testImplementation(Libs.mockito) testImplementation(Libs.androidXTextCore) androidTestImplementation(Libs.androidXRules) androidTestImplementation(Libs.androidXTruth) androidTestImplementation(Libs.androidXJunit) // Espresso dependencies androidTestImplementation(Libs.espressoIntents) androidTestImplementation(Libs.espressoAccessibility) androidTestImplementation(Libs.espressoConcurrent) } ================================================ FILE: sample-app/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/framework/CustomTestRunner.kt ================================================ package com.atiurin.sampleapp.framework import com.atiurin.ultron.allure.UltronAllureTestRunner class CustomTestRunner : UltronAllureTestRunner() {} ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/framework/DummyMetaObject.kt ================================================ package com.atiurin.sampleapp.framework data class DummyMetaObject(val value: String) ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/framework/Log.kt ================================================ package com.atiurin.sampleapp.framework import android.os.SystemClock import android.util.Log object Log { const val LOG_TAG = "Ultron" fun info(message: String) = Log.i(LOG_TAG, message) fun debug(message: String) = Log.d(LOG_TAG, message) fun error(message: String, name: String) = Log.e(LOG_TAG, message) fun warn(message: String) = Log.w(LOG_TAG, message) fun time(desc: String, block: () -> R) : R{ val startTime = SystemClock.elapsedRealtime() val result = block() debug("$desc duration ${SystemClock.elapsedRealtime() - startTime} ms") return result } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/framework/ScreenshotLifecycleListener.kt ================================================ package com.atiurin.sampleapp.framework import com.atiurin.ultron.core.common.Operation import com.atiurin.ultron.core.common.OperationResult import com.atiurin.ultron.listeners.UltronLifecycleListener class ScreenshotLifecycleListener : UltronLifecycleListener(){ override fun before(operation: Operation) { } override fun after(operationResult: OperationResult) { operationResult.operation } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/framework/ultronext/UltronComposeExt.kt ================================================ package com.atiurin.sampleapp.framework.ultronext import androidx.compose.ui.semantics.SemanticsProperties import androidx.compose.ui.test.SemanticsMatcher import com.atiurin.ultron.core.compose.nodeinteraction.UltronComposeSemanticsNodeInteraction import com.atiurin.ultron.core.compose.operation.UltronComposeOperationParams import com.atiurin.ultron.extensions.assertMatches // ------ custom action definition ------ // step 1: extend base class [UltronComposeSemanticsNodeInteraction] with custom action logic fun UltronComposeSemanticsNodeInteraction.getProgress() = execute( UltronComposeOperationParams( operationName = "Get '${elementInfo.name}' current progress", operationDescription = "Compose get current progress of '${elementInfo.name}' during $timeoutMs ms" ) ) { getNodeConfigProperty(SemanticsProperties.ProgressBarRangeInfo).current } // step 2: extend [SemanticsMatcher] class with new action method fun SemanticsMatcher.getProgress(): Float = UltronComposeSemanticsNodeInteraction(this).getProgress() // ------ custom assertion definition ------ // step 1: define custom matcher logic - use function, not a subclass (it's a compose way) fun hasProgress(expected: Float) = SemanticsMatcher( description = "ProgressBarRangeInfo.current = [$expected]" ) { node -> val current = node.config[SemanticsProperties.ProgressBarRangeInfo].current current == expected } // step 2: extend [UltronComposeSemanticsNodeInteraction] class with extension function and [assertMatches] for easier validation //fun UltronComposeSemanticsNodeInteraction.assertProgress(expected: Float) = assertMatches(hasProgress(expected)) // step 3: extend [SemanticsMatcher] class with new assertion method //fun SemanticsMatcher.assertProgress(expected: Float) = UltronComposeSemanticsNodeInteraction(this).assertProgress(expected) // ------ custom ui element definition ------ // just make a subclass of [UltronComposeSemanticsNodeInteraction] and add new method open class ProgressBar(val matcher: SemanticsMatcher) : UltronComposeSemanticsNodeInteraction(matcher) { fun assertProgress(expected: Float) = matcher.assertMatches(hasProgress(expected)) } fun progressBar(block: () -> SemanticsMatcher): Lazy = lazy { ProgressBar(block()) } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/framework/ultronext/UltronEspressoExt.kt ================================================ package com.atiurin.sampleapp.framework.ultronext import android.view.View import android.widget.CheckBox import android.widget.TextView import androidx.test.espresso.DataInteraction import androidx.test.espresso.Espresso.onView import androidx.test.espresso.ViewInteraction import com.atiurin.ultron.core.espresso.UltronEspressoInteraction import com.atiurin.ultron.core.espresso.action.UltronEspressoActionParams import com.atiurin.ultron.core.espresso.recyclerview.UltronRecyclerView import com.atiurin.ultron.core.espresso.recyclerview.UltronRecyclerViewItem import org.hamcrest.Matcher fun UltronEspressoInteraction.appendText(value: String) = perform( params = UltronEspressoActionParams( operationName = "Append text '$value' to ${getInteractionMatcher()}", operationDescription = "Awesome description" ) ) { _, view -> val textView = (view as TextView) textView.text = "${textView.text}$value" } //support action for all Matcher fun Matcher.appendText(value: String) = UltronEspressoInteraction(onView(this)).appendText(value) //support action for all ViewInteractions fun ViewInteraction.appendText(text: String) = UltronEspressoInteraction(this).appendText(text) //support action for all DataInteractions fun DataInteraction.appendText(text: String) = UltronEspressoInteraction(this).appendText(text) //support action for RecyclerView list. appendText action is useless for RecyclerView. // This is just an example of how to add new behaviour for UltronRecyclerView fun UltronRecyclerView.appendText(text: String) = apply { recyclerViewMatcher.appendText(text) } //support action for RecyclerView item fun UltronRecyclerViewItem.appendText(text: String) = apply { getMatcher().appendText(text) } // assertion example fun UltronEspressoInteraction.assertChecked(expectedState: Boolean) = assertMatches { view -> (view as CheckBox).isChecked == expectedState } fun Matcher.assertChecked(expectedState: Boolean) = UltronEspressoInteraction(onView(this)).assertChecked(expectedState) fun ViewInteraction.assertChecked(expectedState: Boolean) = UltronEspressoInteraction(this).assertChecked(expectedState) fun DataInteraction.assertChecked(expectedState: Boolean) = UltronEspressoInteraction(this).assertChecked(expectedState) fun UltronEspressoInteraction.getViewSimple(): View = execute { _, view -> view } fun Matcher.getViewSimple() = UltronEspressoInteraction(onView(this)).getViewSimple() ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/framework/ultronext/UltronEspressoWebExt.kt ================================================ package com.atiurin.sampleapp.framework.ultronext import androidx.test.espresso.web.webdriver.DriverAtoms import com.atiurin.ultron.core.espressoweb.webelement.UltronWebElement // add action on wenView fun UltronWebElement.appendText(text: String) = apply { executeOperation( getUltronWebActionOperation( webInteractionBlock = { webInteractionBlock().perform(DriverAtoms.webKeys(text)) }, name = "${elementInfo.name} appendText '$text'", description = "${elementInfo.name} appendText '$text' during $timeoutMs ms" ) ) } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/framework/ultronext/UltronUiAutomatorExt.kt ================================================ package com.atiurin.sampleapp.framework.ultronext import com.atiurin.ultron.core.common.UltronOperationType import com.atiurin.ultron.core.uiautomator.UiAutomatorActionType import com.atiurin.ultron.core.uiautomator.uiobject2.UltronUiObject2 //actually, UltronUiObject2 already has the same method addText // this is just an example of how to extend UltronUiObject2 fun UltronUiObject2.appendText(appendText: String) = apply { executeAction( actionBlock = { uiObject2ProviderBlock()!!.text += appendText }, name = "AppendText '$appendText' to ${elementInfo.name}", description = "UiObject2 action '${UiAutomatorActionType.ADD_TEXT}' ${elementInfo.name} appendText '$appendText' during $timeoutMs ms" ) } enum class CustomUltronOperations : UltronOperationType { ADD_TEXT_PREFIX, ASSERT_HAS_ANY_CHILD } // add extension function to UltronUiObject2 that calls `executeAssertion` fun UltronUiObject2.assertHasAnyChild() = apply { executeAssertion( assertionBlock = { uiObject2ProviderBlock()!!.childCount > 0 }, name = "Assert ${elementInfo.name} has any child", type = CustomUltronOperations.ASSERT_HAS_ANY_CHILD, description = "UiObject2 assertion '${CustomUltronOperations.ASSERT_HAS_ANY_CHILD}' of ${elementInfo.name} during $timeoutMs ms" ) } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/framework/utils/AssertUtils.kt ================================================ package com.atiurin.sampleapp.framework.utils import org.junit.Assert object AssertUtils { fun assertException(expected: Boolean = true, block: () -> Unit) { var exceptionOccurs = false try { block() } catch (ex: Throwable) { // throw ex exceptionOccurs = true } Assert.assertEquals(expected, exceptionOccurs) } fun assertExecTimeMoreThen(time: Long, block: () -> Unit) { val startTime = System.currentTimeMillis() try { block() } catch (ex: Throwable) { } Assert.assertTrue(System.currentTimeMillis() - startTime >= time) } fun assertExecTimeLessThen(time: Long, block: () -> Unit) { val startTime = System.currentTimeMillis() try { block() } catch (ex: Throwable) { } Assert.assertTrue(System.currentTimeMillis() - startTime <= time) } fun assertExecTimeBetween(minTime: Long, maxTime: Long, block: () -> Unit) { val startTime = System.currentTimeMillis() try { block() } catch (ex: Throwable) { } val execTime = System.currentTimeMillis() - startTime Assert.assertTrue("ExecTime in $minTime .. $maxTime, but actual $execTime", execTime in minTime..maxTime) } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/framework/utils/EspressoUtil.kt ================================================ package com.atiurin.sampleapp.framework.utils import androidx.test.espresso.Espresso import androidx.test.platform.app.InstrumentationRegistry object EspressoUtil { // fun openOptionsMenu() = apply { // Espresso.openActionBarOverflowOrOptionsMenu(InstrumentationRegistry.getInstrumentation().context) // } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/framework/utils/TestDataUtils.kt ================================================ package com.atiurin.sampleapp.framework.utils import androidx.test.platform.app.InstrumentationRegistry object TestDataUtils { fun getResourceString(resourceId: Int): String { return InstrumentationRegistry.getInstrumentation().targetContext.resources.getString(resourceId) } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/framework/utils/TimeUtils.kt ================================================ package com.atiurin.sampleapp.framework.utils import android.annotation.SuppressLint import java.time.Clock import java.time.Instant import java.time.LocalDate import java.time.ZoneId import java.time.ZoneOffset import java.time.format.DateTimeFormatter object TimeUtils { @SuppressLint("NewApi") fun formatTimestamp(timestamp: Long): String { val instant = Instant.ofEpochMilli(timestamp) val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") .withZone(ZoneId.systemDefault()) return formatter.format(instant) } fun getTimestampStartOfDay(): Long { return LocalDate.now(Clock.systemUTC()) .atStartOfDay() .toInstant(ZoneOffset.UTC) .toEpochMilli() } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/ChatPage.kt ================================================ package com.atiurin.sampleapp.pages import android.view.View import androidx.test.espresso.matcher.ViewMatchers.* import com.atiurin.sampleapp.R import com.atiurin.ultron.allure.step.step import com.atiurin.ultron.page.Page import com.atiurin.ultron.core.espresso.recyclerview.UltronRecyclerViewItem import com.atiurin.ultron.core.espresso.recyclerview.withRecyclerView import com.atiurin.ultron.core.espresso.UltronEspresso import com.atiurin.ultron.custom.espresso.matcher.withSuitableRoot import com.atiurin.ultron.extensions.* import org.hamcrest.Matcher import org.hamcrest.Matchers.allOf object ChatPage : Page() { fun assertPageDisplayed() = apply { step("Assert friends list page displayed") { messagesList.isDisplayed() } } val messagesList = withRecyclerView(R.id.messages_list).withName("Messages list") val clearHistoryBtn = withText("Clear history") val inputMessageText = withId(R.id.message_input_text) val sendMessageBtn = withId(R.id.send_button) val toolbarTitle = withId(R.id.toolbar_title) fun getMessageListItem(text: String): ChatRecyclerItem { return messagesList.getItem(hasDescendant( allOf( withId(R.id.message_text), withText(text) ) ) ) } private fun getListItemAtPosition(position: Int): ChatRecyclerItem { return messagesList.getItem(position) } fun getTitle(title: String): Matcher { return allOf(withId(R.id.toolbar_title), withText(title)) } class ChatRecyclerItem : UltronRecyclerViewItem(){ val text by lazy { getChild(withId(R.id.message_text)) } } fun sendMessage(text: String) = apply { step("Send message with text '$text") { inputMessageText.typeText(text) sendMessageBtn.click() this.getMessageListItem(text).text .isDisplayed() .hasText(text) } } fun assertMessageTextAtPosition(position: Int, text: String) = apply { step("Assert item at position $position has text '$text'") { this.getListItemAtPosition(position).text.isDisplayed().hasText(text) } } fun assertToolbarTitle(text: String){ toolbarTitle.withTimeout(1000).withName("Toolbar title").hasText(text) } fun assertToolbarTitleWithSuitableRoot(text: String){ toolbarTitle.withSuitableRoot().withName("Toolbar title").hasText(text) } fun clearHistory() = apply { step("Clear chat history") { UltronEspresso.openContextualActionModeOverflowMenu() clearHistoryBtn.click() messagesList.assertEmpty() } } fun assertMessageDisplayed(text: String) { getMessageListItem(text).text .isDisplayed() .hasText(text) } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/ComposeElementsPage.kt ================================================ package com.atiurin.sampleapp.pages import androidx.compose.ui.test.hasTestTag import com.atiurin.sampleapp.activity.ComposeElementsActivity import com.atiurin.sampleapp.activity.ComposeElementsActivity.Constants.clickListenerButton import com.atiurin.sampleapp.activity.ComposeElementsActivity.Constants.likesCounterButton import com.atiurin.sampleapp.activity.ComposeElementsActivity.Constants.radioButtonFemaleTestTag import com.atiurin.sampleapp.activity.ComposeElementsActivity.Constants.radioButtonMaleTestTag import com.atiurin.sampleapp.compose.RegionsClickListenerTestTags import com.atiurin.sampleapp.framework.ultronext.progressBar import com.atiurin.ultron.page.Page object ComposeElementsPage : Page() { val status = hasTestTag(ComposeElementsActivity.Constants.statusText) val likesCounter = hasTestTag(likesCounterButton) val longAndDoubleClickButton = hasTestTag(clickListenerButton) val regionsNode = hasTestTag(RegionsClickListenerTestTags.regionsNode) val clickedRegion = hasTestTag(RegionsClickListenerTestTags.regionsClickedText) val editableText = hasTestTag(ComposeElementsActivity.editableText) val swipeableNode = hasTestTag(ComposeElementsActivity.swipeableNode) val disabledButton = hasTestTag(ComposeElementsActivity.disabledButton) val simpleCheckbox = hasTestTag(ComposeElementsActivity.simpleCheckbox) val progressBar by progressBar { hasTestTag(ComposeElementsActivity.progressBar) } val maleRadioButton = hasTestTag(radioButtonMaleTestTag) val femaleRadioButton = hasTestTag(radioButtonFemaleTestTag) val notExistedElement = hasTestTag("NotExistedTestTag") } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/ComposeListPage.kt ================================================ package com.atiurin.sampleapp.pages import androidx.compose.ui.test.hasAnyDescendant import androidx.compose.ui.test.hasContentDescription import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.hasText import com.atiurin.sampleapp.compose.ListItemPositionPropertyKey import com.atiurin.sampleapp.compose.contactNameTestTag import com.atiurin.sampleapp.compose.contactStatusTestTag import com.atiurin.sampleapp.compose.contactsListContentDesc import com.atiurin.sampleapp.compose.getContactItemTestTagById import com.atiurin.sampleapp.data.entities.Contact import com.atiurin.ultron.core.compose.list.UltronComposeListItem import com.atiurin.ultron.core.compose.list.composeList import com.atiurin.ultron.extensions.withDescription import com.atiurin.ultron.page.Page object ComposeListPage : Page() { val lazyList = composeList( listMatcher = hasContentDescription(contactsListContentDesc), positionPropertyKey = ListItemPositionPropertyKey ).withDescription("Contacts list") fun assertContactStatus(contact: Contact) = apply { getContactItemByTestTag(contact).status.assertTextEquals(contact.status) } fun getItemByPosition(position: Int): ComposeFriendListItem { return lazyList.getItem(position) } fun getFirstVisibleItem(): ComposeFriendListItem = lazyList.getFirstVisibleItem() fun getItemByIndex(index: Int): ComposeFriendListItem = lazyList.getVisibleItem(index) fun getContactItemByTestTag(contact: Contact): ComposeFriendListItem = lazyList.getItem(hasTestTag(getContactItemTestTagById(contact))) fun getContactItemByName(contact: Contact): ComposeFriendListItem = lazyList .getItem(hasAnyDescendant(hasText(contact.name) and hasTestTag(contactNameTestTag)).withDescription("Contact '${contact.name}'")) class ComposeFriendListItem : UltronComposeListItem() { val name by child { hasTestTag(contactNameTestTag).withDescription("Contact name") } val status by lazy { getChild(hasTestTag(contactStatusTestTag).withDescription("Contact status")) } val notExisted by child { hasTestTag("NotExistedChild").withDescription("Not existed child") } } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/ComposeSecondPage.kt ================================================ package com.atiurin.sampleapp.pages import androidx.compose.ui.test.hasTestTag import com.atiurin.ultron.page.Page object ComposeSecondPage : Page() { val name = hasTestTag("name") val status = hasTestTag("status") } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/FriendsListPage.kt ================================================ package com.atiurin.sampleapp.pages import androidx.test.espresso.matcher.ViewMatchers.hasDescendant import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import com.atiurin.sampleapp.R import com.atiurin.sampleapp.data.entities.Contact import com.atiurin.ultron.allure.step.step import com.atiurin.ultron.core.espresso.recyclerview.UltronRecyclerViewImpl import com.atiurin.ultron.core.espresso.recyclerview.UltronRecyclerViewItem import com.atiurin.ultron.core.espresso.recyclerview.withRecyclerView import com.atiurin.ultron.custom.espresso.matcher.withSuitableRoot import com.atiurin.ultron.extensions.isDisplayed import com.atiurin.ultron.extensions.withName import com.atiurin.ultron.page.Page import org.hamcrest.Matchers.allOf import org.hamcrest.Matchers.containsString import org.junit.Assert object FriendsListPage : Page() { val recycler = withRecyclerView(R.id.recycler_friends, implementation = UltronRecyclerViewImpl.PERFORMANCE).withName("Friends list") fun assertPageDisplayed() = apply { step("Assert friends list page displayed") { recycler.recyclerViewMatcher.isDisplayed() } } fun assertPageDisplayedWithSuitableRoot() = apply { step("Assert friends list page displayed with suitable root") { recycler.withSuitableRoot().isDisplayed() } } class FriendRecyclerItem : UltronRecyclerViewItem() { val name by child(withId(R.id.tv_name).withName("Friend name")) val status by lazy { getChild(withId(R.id.tv_status)).withName("Status") } val avatar by lazy { getChild(withId(R.id.avatar)).withName("Avatar") } } fun getListItem(contactName: String): FriendRecyclerItem { return recycler.getItem( hasDescendant(allOf(withId(R.id.tv_name), withText(contactName))).withName("Friend '$contactName") ) } fun getListItem(positions: Int): FriendRecyclerItem { return recycler.getItem(positions) } fun openChat(name: String) = apply { step("Open chat with friend '$name'") { this.getListItem(name).click() ChatPage { assertPageDisplayed() } } } fun assertStatus(name: String, status: String) = apply { step("Assert friend with name '$name' has status '$status'") { getListItem(name).status.hasText(status).isDisplayed() } } fun assertName(nameText: String) = apply { step("Assert friend name '$nameText' in the right place") { getListItem(nameText).name.hasText(nameText).isDisplayed() } } fun assertFriendsListSize(size: Int) { Assert.assertEquals(size, recycler.getSize()) } fun getItemMatcher(contact: Contact) = hasDescendant(allOf(withId(R.id.tv_name), withText(containsString(contact.name)))) } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/UiElementsPage.kt ================================================ package com.atiurin.sampleapp.pages import androidx.test.espresso.Espresso.onView import androidx.test.espresso.matcher.RootMatchers.isDialog import androidx.test.espresso.matcher.RootMatchers.isPlatformPopup import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import com.atiurin.sampleapp.R import com.atiurin.ultron.page.Page object UiElementsPage : Page() { val notExistElement = withText("Some not existed text element") val button = withId(R.id.button1) val eventStatus = withId(R.id.last_event_status) val radioVisibleButton = withId(R.id.radio_visible) val radioInvisibleButton = withId(R.id.radio_invisible) val radioGoneButton = withId(R.id.radio_gone) val checkBoxClickable = withId(R.id.checkbox_clickable) val checkBoxEnabled = withId(R.id.checkbox_enable) val checkBoxSelected = withId(R.id.checkbox_selected) val checkBoxFocusable = withId(R.id.checkbox_focusable) val checkBoxJsEnabled = withId(R.id.checkbox_js_enabled) val editTextContentDesc = withId(R.id.et_contentDesc) val webView = withId(R.id.webview) val appCompatTextView = withId(R.id.app_compat_text) val imageView = withId(R.id.swipe_image_view) val imageView2 = withId(R.id.image_view2) val emptyNotClickableImageView = withId(R.id.empty_image_view) val dialogButtonOk = onView(withText("OK")).inRoot(isDialog()) val popupButtonCancel = onView(withText("Cancel")).inRoot(isPlatformPopup()) val hiddenButton = withId(R.id.exist_hidden_button) } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/UiObject2ElementsPage.kt ================================================ package com.atiurin.sampleapp.pages import androidx.test.uiautomator.By import com.atiurin.sampleapp.R import com.atiurin.ultron.core.uiautomator.uiobject2.UltronUiObject2.Companion.by import com.atiurin.ultron.core.uiautomator.uiobject2.UltronUiObject2.Companion.byResId class UiObject2ElementsPage { val notExistedObject = by(By.res("com.atiurin.sampleapp","123123123123")) val button = byResId(R.id.button1) val eventStatus = byResId(R.id.last_event_status) val radioGroup = byResId(R.id.radio_group_visibility) val radioVisibleButton = byResId(R.id.radio_visible) val radioInvisibleButton = byResId(R.id.radio_invisible) val radioGoneButton = byResId(R.id.radio_gone) val checkBoxClickable = byResId(R.id.checkbox_clickable) val checkBoxEnabled = byResId(R.id.checkbox_enable) val checkBoxSelected = byResId(R.id.checkbox_selected) val checkBoxFocusable = byResId(R.id.checkbox_focusable) val checkBoxJsEnabled = byResId(R.id.checkbox_js_enabled) val editTextContentDesc = byResId(R.id.et_contentDesc) val textElement = by(By.text("some text")) val contentDescElement = by(By.desc("Content desc")) val webView = byResId(R.id.webview) val appCompatTextView = byResId(R.id.app_compat_text) val swipableImageView = byResId(R.id.swipe_image_view) val emptyImageView = byResId(R.id.empty_image_view) } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/UiObject2FriendsListPage.kt ================================================ package com.atiurin.sampleapp.pages import androidx.test.uiautomator.By import com.atiurin.sampleapp.R import com.atiurin.sampleapp.data.repositories.ContactRepositoty import com.atiurin.ultron.core.uiautomator.uiobject2.UltronUiObject2.Companion.by import com.atiurin.ultron.core.uiautomator.uiobject2.UltronUiObject2.Companion.byResId import com.atiurin.ultron.page.Page object UiObject2FriendsListPage : Page() { val list = byResId(R.id.recycler_friends) val topElement = by(By.text(ContactRepositoty.getFirst().name)) val bottomElement = by(By.text(ContactRepositoty.getLast().name)) } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/UiObjectElementsPage.kt ================================================ package com.atiurin.sampleapp.pages import com.atiurin.sampleapp.R import com.atiurin.ultron.core.uiautomator.uiobject.UltronUiObject.Companion.uiResId class UiObjectElementsPage { val notExistedObject = uiResId(R.id.send_button) val button = uiResId(R.id.button1) val eventStatus = uiResId(R.id.last_event_status) val radioGroup = uiResId(R.id.radio_group_visibility) val radioVisibleButton = uiResId(R.id.radio_visible) val radioInvisibleButton = uiResId(R.id.radio_invisible) val radioGoneButton = uiResId(R.id.radio_gone) val checkBoxClickable = uiResId(R.id.checkbox_clickable) val checkBoxEnabled = uiResId(R.id.checkbox_enable) val checkBoxSelected = uiResId(R.id.checkbox_selected) val checkBoxFocusable = uiResId(R.id.checkbox_focusable) val checkBoxJsEnabled = uiResId(R.id.checkbox_js_enabled) val editTextContentDesc = uiResId(R.id.et_contentDesc) val webView = uiResId(R.id.webview) val appCompatTextView = uiResId(R.id.app_compat_text) val swipableImageView = uiResId(R.id.swipe_image_view) val emptyImageView = uiResId(R.id.empty_image_view) } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/WebViewPage.kt ================================================ package com.atiurin.sampleapp.pages import com.atiurin.ultron.core.espressoweb.webelement.UltronWebElements.Companion.classNames import com.atiurin.ultron.core.espressoweb.webelement.UltronWebElement.Companion.className import com.atiurin.ultron.core.espressoweb.webelement.UltronWebElement.Companion.id import com.atiurin.ultron.page.Page class WebViewPage : Page() { companion object{ const val BUTTON2_TITLE = "button2 clicked" const val APPLE_LINK_TEXT = "Apple" const val APPLE_LINK_HREF = "fake_link.html" } val textInput = id("text_input") val buttonUpdTitle = id("button1") val buttonSetTitle2 = id("button2") val buttonSetTitleActive = id("button3") val title = id("title") val titleWithCss = className("css_title") val appleLink = id("apple_link") val buttons = classNames("button") val notExistedElement = id("Not existed element") } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/uiblock/ComposeUiBlockScreen.kt ================================================ package com.atiurin.sampleapp.pages.uiblock import androidx.compose.ui.test.SemanticsMatcher import androidx.compose.ui.test.hasTestTag import com.atiurin.sampleapp.activity.ComposeElementsActivity.Constants.contactBlock1Tag import com.atiurin.sampleapp.activity.ComposeElementsActivity.Constants.contactBlock2Tag import com.atiurin.sampleapp.activity.ComposeElementsActivity.Constants.contactNameTag import com.atiurin.sampleapp.activity.ComposeElementsActivity.Constants.contactStatusTag import com.atiurin.sampleapp.activity.ComposeElementsActivity.Constants.contactsListTag import com.atiurin.sampleapp.pages.uiblock.ComposeListUiBlock.Companion.listBlockDesc import com.atiurin.ultron.core.compose.child import com.atiurin.ultron.core.compose.page.UltronComposeUiBlock import com.atiurin.ultron.extensions.withName import com.atiurin.ultron.page.Screen object ComposeUiBlockScreen : Screen(){ val contactBlock1 = ContactUiBlockWithoutDesc(hasTestTag(contactBlock1Tag)) val contactBlock2 = ContactUiBlockWithDesc(hasTestTag(contactBlock2Tag), "Block parent") val contactListBlock = ComposeListUiBlock(hasTestTag(contactsListTag), listBlockDesc) } class ContactUiBlockWithoutDesc(blockMatcher: SemanticsMatcher) : UltronComposeUiBlock(blockMatcher) { val nameWithoutDeepSearch = child(hasTestTag(contactNameTag), descendantSearch = false).withName("No deep search element") val statusDeepSearchText = child(hasTestTag(contactStatusTag)) } class ContactUiBlockWithDesc(blockMatcher: SemanticsMatcher, blockDescription: String) : UltronComposeUiBlock(blockMatcher, blockDescription) { val name = child(hasTestTag(contactNameTag)).withName("$сhildNameDesc $blockDescription") val status = child(hasTestTag(contactStatusTag)) companion object { const val сhildNameDesc = "NamE" } } class ComposeListUiBlock(parent: SemanticsMatcher, blockDescription: String) : UltronComposeUiBlock(parent) { val itemWithoutDesc = child(uiBlock = ContactUiBlockWithoutDesc(hasTestTag(contactBlock1Tag))) val item1BlockWithDesc = child(ContactUiBlockWithDesc(hasTestTag(contactBlock1Tag), "1 $descriptionPrefix $blockDescription")) val item2BlockFactory = child( childMatcher = hasTestTag(contactBlock2Tag), uiBlockFactory = { updatedMatcher -> ContactUiBlockWithDesc(updatedMatcher, blockDescription = "2 $descriptionPrefix $blockDescription") } ) companion object { const val descriptionPrefix = "Item with parent" val listBlockDesc = "ListBlock" } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/uiblock/EspressoUiBlockScreen.kt ================================================ package com.atiurin.sampleapp.pages.uiblock import android.view.View import androidx.test.espresso.matcher.ViewMatchers.withId import com.atiurin.sampleapp.R import com.atiurin.ultron.core.espresso.UltronEspressoUiBlock import com.atiurin.ultron.extensions.withName import com.atiurin.ultron.page.Screen import org.hamcrest.Matcher object EspressoUiBlockScreen : Screen() { val contactItem1 = ContactItemUiBlock(withId(R.id.contact_item_1), "Item 1") val blockWithoutDeepSearch = ContactItemUiBlockWithoutDeepSearch(withId(R.id.contact_item_2)) val contactsListBlock = ContactsListUiBlock(withId(R.id.contact_items), "Items list") } class ContactItemUiBlock(blockMatcher: Matcher, blockDescription: String) : UltronEspressoUiBlock(blockMatcher) { val name = child(withId(R.id.name)).withName("Contact name with parent $blockDescription") val status = child(withId(R.id.status)).withName("Contact item status") val deepSearchChild = child(withId(R.id.deep_search_child)) } class ContactItemUiBlockWithoutDeepSearch(parent: Matcher) : UltronEspressoUiBlock(parent) { val deepSearchFalse = child(withId(R.id.deep_search_child), descendantSearch = false) } class ContactsListUiBlock(blockMatcher: Matcher, blockDescription: String) : UltronEspressoUiBlock(blockMatcher, blockDescription) { val item1 = child(ContactItemUiBlock(withId(R.id.contact_item_1), "Item 1")) val item2 = child( childMatcher = withId(R.id.contact_item_2), uiBlockFactory = { updatedMatcher -> ContactItemUiBlock(updatedMatcher, blockDescription = "Contact Item 2 with parent $blockDescription") } ) } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/uiblock/UiObject2UiBlockScreen.kt ================================================ package com.atiurin.sampleapp.pages.uiblock import androidx.test.uiautomator.BySelector import com.atiurin.sampleapp.R import com.atiurin.ultron.core.uiautomator.uiobject2.UltronUiObject2.Companion.bySelector import com.atiurin.ultron.core.uiautomator.uiobject2.UltronUiObject2UiBlock import com.atiurin.ultron.page.Screen object UiObject2UiBlockScreen : Screen() { val block1 = ContactItemUiObject2Block { bySelector(R.id.contact_item_1) } val block2 = ContactItemUiObject2Block("Block 2") { bySelector(R.id.contact_item_2) } val blocks = UiObject2ListUiBlock("Item blocks") { bySelector(R.id.contact_items) } } class ContactItemUiObject2Block(blockDesc: String = "", blockSelector: () -> BySelector) : UltronUiObject2UiBlock(blockDesc, blockSelector) { val name = child(bySelector(R.id.name)).withName("Name in block $blockDesc") val status = child(bySelector(R.id.status)) val deepSearchChild = child(bySelector(R.id.deep_search_child)) val notExisted = child(bySelector(R.id.recycler_friends)).withTimeout(100) } class UiObject2ListUiBlock(desc: String = "", parent: () -> BySelector) : UltronUiObject2UiBlock(desc, parent) { val item1 = child(ContactItemUiObject2Block { bySelector(R.id.contact_item_1) }) val item2 = child( selector = bySelector(R.id.contact_item_2), description = "Item 2 in block $desc", uiBlockFactory = { desc, selector -> ContactItemUiObject2Block(desc, selector) } ) } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/uiblock/WebElementUiBlockScreen.kt ================================================ package com.atiurin.sampleapp.pages.uiblock import com.atiurin.ultron.core.espressoweb.webelement.UltronWebElement import com.atiurin.ultron.core.espressoweb.webelement.UltronWebElement.Companion.className import com.atiurin.ultron.core.espressoweb.webelement.UltronWebElement.Companion.id import com.atiurin.ultron.core.espressoweb.webelement.UltronWebElementUiBlock import com.atiurin.ultron.page.Screen object WebElementUiBlockScreen : Screen() { val teacherBlock = WebBlock(id("teacher"), "Teacher block") val studentWithoutDesc = WebBlockWithoutDesc(id("student")) val persons = WebPersonsBlock(id("persons"), "persons block") } class WebBlock(blockElement: UltronWebElement, blockDescription: String): UltronWebElementUiBlock(blockElement, blockDescription){ val name = child(className("person_name")).withName("Name of $blockDescription") } class WebBlockWithoutDesc(blockElement: UltronWebElement): UltronWebElementUiBlock(blockElement){ val name = child(className("person_name")) } class WebPersonsBlock(blockElement: UltronWebElement, blockDescription: String): UltronWebElementUiBlock(blockElement, blockDescription){ val teacher = child(WebBlock(id("teacher"), "Teacher child of $blockDescription")) val studentWithoutDesc = child(WebBlockWithoutDesc(id("student"))) val student = child(id("student")){ modifiedElement -> WebBlock(modifiedElement, "student child of $blockDescription") } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/BaseTest.kt ================================================ package com.atiurin.sampleapp.tests import android.os.Environment import androidx.test.platform.app.InstrumentationRegistry import com.atiurin.sampleapp.data.repositories.CURRENT_USER import com.atiurin.sampleapp.managers.AccountManager import com.atiurin.ultron.allure.config.UltronAllureConfig import com.atiurin.ultron.core.compose.config.UltronComposeConfig import com.atiurin.ultron.core.compose.listeners.ComposDebugListener import com.atiurin.ultron.core.config.UltronCommonConfig import com.atiurin.ultron.core.config.UltronConfig import com.atiurin.ultron.core.espresso.recyclerview.UltronRecyclerViewImpl import com.atiurin.ultron.core.test.UltronTest import com.atiurin.ultron.testlifecycle.rulesequence.RuleSequence import com.atiurin.ultron.testlifecycle.setupteardown.SetUpRule import org.junit.BeforeClass import org.junit.Rule abstract class BaseTest : UltronTest(){ val setupRule = SetUpRule("Login user rule") .add(name = "Login valid user $CURRENT_USER") { AccountManager(InstrumentationRegistry.getInstrumentation().targetContext).login( CURRENT_USER.login, CURRENT_USER.password ) } @get:Rule open val ruleSequence = RuleSequence(setupRule) companion object { @BeforeClass @JvmStatic fun config() { UltronConfig.Espresso.RECYCLER_VIEW_IMPLEMENTATION = UltronRecyclerViewImpl.PERFORMANCE UltronConfig.Espresso.ViewActionConfig.autoScroll = true UltronConfig.applyRecommended() UltronComposeConfig.applyRecommended() UltronCommonConfig.addListener(ComposDebugListener()) UltronAllureConfig.applyRecommended() UltronAllureConfig.setAllureResultsDirectory(Environment.DIRECTORY_DOWNLOADS) } } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/UiElementsTest.kt ================================================ package com.atiurin.sampleapp.tests import com.atiurin.sampleapp.activity.UiElementsActivity import com.atiurin.ultron.testlifecycle.activity.UltronActivityRule abstract class UiElementsTest : BaseTest() { val activityRule = UltronActivityRule(UiElementsActivity::class.java) init { ruleSequence.add(activityRule) } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/CheckboxTest.kt ================================================ package com.atiurin.sampleapp.tests.compose import androidx.compose.material.TriStateCheckbox import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.state.ToggleableState import androidx.compose.ui.test.hasContentDescription import com.atiurin.ultron.core.compose.createDefaultUltronComposeRule import com.atiurin.ultron.core.compose.nodeinteraction.click import com.atiurin.ultron.extensions.assertIsIndeterminate import org.junit.Rule import org.junit.Test class CheckboxTest { @get:Rule val composeRule = createDefaultUltronComposeRule() @Test fun checkboxStates() { val testTag = "checkBox" composeRule.setContent { val checkedState = remember { mutableStateOf(ToggleableState.Indeterminate) } TriStateCheckbox( state = checkedState.value, onClick = { if (checkedState.value == ToggleableState.Indeterminate || checkedState.value == ToggleableState.Off) checkedState.value = ToggleableState.On else checkedState.value = ToggleableState.Off }, modifier = Modifier.semantics { contentDescription = testTag } ) } hasContentDescription(testTag).assertIsIndeterminate() .click().assertIsOn() .click().assertIsOff() } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/CollectionInteractionTest.kt ================================================ package com.atiurin.sampleapp.tests.compose import androidx.compose.ui.test.hasTestTag import com.atiurin.sampleapp.activity.ComposeListActivity import com.atiurin.sampleapp.compose.contactNameTestTag import com.atiurin.sampleapp.compose.contactsListTestTag import com.atiurin.sampleapp.data.repositories.CONTACTS import com.atiurin.sampleapp.tests.BaseTest import com.atiurin.ultron.core.compose.createSimpleUltronComposeRule import com.atiurin.ultron.core.compose.operation.UltronComposeCollectionInteraction.Companion.allNodes import org.junit.Rule import org.junit.Test class CollectionInteractionTest: BaseTest() { @get:Rule val composeRule = createSimpleUltronComposeRule() val list = hasTestTag(contactsListTestTag) @Test fun allNodes_getByIndex(){ val index = 4 val contact = CONTACTS[index] allNodes(hasTestTag(contactNameTestTag), true).get(index).assertTextContains(contact.name) } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/ComposeConfigTest.kt ================================================ package com.atiurin.sampleapp.tests.compose import com.atiurin.sampleapp.activity.ComposeElementsActivity import com.atiurin.sampleapp.framework.DummyMetaObject import com.atiurin.sampleapp.framework.utils.AssertUtils import com.atiurin.sampleapp.pages.ComposeElementsPage import com.atiurin.ultron.core.compose.config.UltronComposeConfig import com.atiurin.ultron.core.compose.createSimpleUltronComposeRule import com.atiurin.ultron.extensions.assertExists import com.atiurin.ultron.extensions.isSuccess import com.atiurin.ultron.extensions.withResultHandler import com.atiurin.ultron.extensions.withTimeout import com.atiurin.ultron.core.compose.operation.ComposeOperationResult import com.atiurin.ultron.core.compose.operation.ComposeOperationType import com.atiurin.ultron.core.compose.operation.UltronComposeOperation import com.atiurin.ultron.extensions.* import com.atiurin.ultron.testlifecycle.rulesequence.RuleSequence import com.atiurin.ultron.testlifecycle.setupteardown.SetUp import com.atiurin.ultron.testlifecycle.setupteardown.SetUpRule import com.atiurin.ultron.testlifecycle.setupteardown.TearDown import com.atiurin.ultron.testlifecycle.setupteardown.TearDownRule import org.junit.Assert import org.junit.Rule import org.junit.Test class ComposeConfigTest { companion object { const val setCustomTimeout = "Set custom timeout" const val dropCustomTimeout = "Drop custom timeout" const val customTimeout = 1100L } val page = ComposeElementsPage val composeRule = createSimpleUltronComposeRule() val setUpRule = SetUpRule().add(setCustomTimeout) { UltronComposeConfig.params.operationTimeoutMs = customTimeout } val tearDownRule = TearDownRule().add(dropCustomTimeout) { UltronComposeConfig.params.operationTimeoutMs = UltronComposeConfig.DEFAULT_OPERATION_TIMEOUT } @get:Rule val ruleSequence = RuleSequence().add(composeRule, setUpRule, tearDownRule) @Test fun resultHandler_successOperation() { lateinit var result: ComposeOperationResult page.editableText.withResultHandler { result = it }.assertTextContains("") Assert.assertTrue(result.success) Assert.assertTrue(result.operation.type == ComposeOperationType.CONTAINS_TEXT) } @Test fun resultHandler_failedOperation() { lateinit var result: ComposeOperationResult page.editableText.withResultHandler { result = it }.withTimeout(100).assertTextContains("invalid") Assert.assertFalse(result.success) Assert.assertTrue(result.operation.type == ComposeOperationType.CONTAINS_TEXT) Assert.assertTrue(result.exceptions.isNotEmpty()) Assert.assertTrue(result.description.isNotEmpty()) } @Test @SetUp(setCustomTimeout) @TearDown(dropCustomTimeout) fun operationTimeout() { page.likesCounter.assertIsDisplayed() AssertUtils.assertExecTimeBetween(customTimeout, 4900) { page.likesCounter.assertTextContains("asdqw3213") } } @Test fun isSuccess_false() { Assert.assertFalse(page.editableText.isSuccess { withTimeout(100).assertDoesNotExist() }) } @Test fun isSuccess_true() { Assert.assertTrue(page.editableText.isSuccess { assertExists() }) } @Test fun withName_inOperationProps_ultronInteraction() { val name = "ElementName" page.notExistedElement.withTimeout(100).withName(name).withResultHandler { result -> Assert.assertEquals(name, result.operation.elementInfo.name) }.assertIsDisplayed() } @Test fun withName_inOperationProps_matcherExt() { val name = "ElementName" page.notExistedElement.withName(name).withTimeout(100).withResultHandler { result -> Assert.assertEquals(name, result.operation.elementInfo.name) }.assertIsDisplayed() } @Test fun withName_inExceptionMessage() { val name = "ElementNameToBeInException" runCatching { page.notExistedElement.withTimeout(100).withName(name).assertIsDisplayed() }.onFailure { exception -> Assert.assertTrue(exception.message!!.contains(name)) } } @Test fun withMeta() { val meta = DummyMetaObject("ElementMetaInfo") page.notExistedElement.withTimeout(100).withMetaInfo(meta).withResultHandler { result -> Assert.assertEquals(meta, result.operation.elementInfo.meta) }.assertIsDisplayed() } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/ComposeCustomAssertionTest.kt ================================================ package com.atiurin.sampleapp.tests.compose import com.atiurin.sampleapp.activity.ComposeElementsActivity import com.atiurin.sampleapp.framework.utils.AssertUtils import com.atiurin.sampleapp.pages.ComposeElementsPage import com.atiurin.sampleapp.tests.BaseTest import com.atiurin.ultron.core.compose.createSimpleUltronComposeRule import com.atiurin.ultron.extensions.withAssertion import com.atiurin.ultron.extensions.withTimeout import com.atiurin.ultron.core.compose.nodeinteraction.click import org.junit.Rule import org.junit.Test class ComposeCustomAssertionTest : BaseTest() { val page = ComposeElementsPage @get:Rule val composeRule = createSimpleUltronComposeRule() @Test fun validAssertion(){ page.likesCounter.withAssertion { page.likesCounter.withTimeout(100).assertTextEquals("Like count = 3") }.click() } @Test fun invalidAssertion(){ AssertUtils.assertException { page.likesCounter.withTimeout(1000).withAssertion { page.likesCounter.withTimeout(500).assertTextEquals("some invalid text") }.click() } } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/ComposeEmptyListTest.kt ================================================ package com.atiurin.sampleapp.tests.compose import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag import androidx.compose.ui.test.hasTestTag import com.atiurin.sampleapp.framework.utils.AssertUtils import com.atiurin.sampleapp.tests.BaseTest import com.atiurin.ultron.core.compose.createDefaultUltronComposeRule import com.atiurin.ultron.core.compose.list.composeList import org.junit.Rule import org.junit.Test class ComposeEmptyListTest : BaseTest() { @get:Rule val composeRule = createDefaultUltronComposeRule() private val emptyListTestTag = "emptyList" @Test fun assertNotEmpty_emptyList() { setEmptyListContent() AssertUtils.assertException { composeList(hasTestTag(emptyListTestTag)).withTimeout(100).assertNotEmpty() } } @Test fun assertEmpty_emptyList() { setEmptyListContent() composeList(hasTestTag(emptyListTestTag)).assertEmpty() } private fun setEmptyListContent() { composeRule.setContent { LazyColumn( modifier = Modifier.semantics { testTag = emptyListTestTag } ) {} } } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/ComposeListTest.kt ================================================ package com.atiurin.sampleapp.tests.compose import androidx.compose.ui.test.hasAnyDescendant import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.hasText import androidx.compose.ui.test.performScrollToIndex import com.atiurin.sampleapp.activity.ComposeListActivity import com.atiurin.sampleapp.compose.contactStatusTestTag import com.atiurin.sampleapp.compose.contactsListContentDesc import com.atiurin.sampleapp.compose.contactsListHeaderTag import com.atiurin.sampleapp.compose.contactsListTestTag import com.atiurin.sampleapp.compose.getContactItemTestTagById import com.atiurin.sampleapp.data.repositories.CONTACTS import com.atiurin.sampleapp.data.repositories.ContactRepositoty import com.atiurin.sampleapp.framework.utils.AssertUtils import com.atiurin.sampleapp.pages.ComposeListPage import com.atiurin.sampleapp.pages.ComposeSecondPage import com.atiurin.sampleapp.tests.BaseTest import com.atiurin.ultron.core.common.options.ContentDescriptionContainsOption import com.atiurin.ultron.core.common.options.TextContainsOption import com.atiurin.ultron.core.compose.createSimpleUltronComposeRule import com.atiurin.ultron.core.compose.list.composeList import com.atiurin.ultron.extensions.assertIsDisplayed import com.atiurin.ultron.extensions.assertTextEquals import com.atiurin.ultron.extensions.click import com.atiurin.ultron.extensions.findNodeInTree import com.atiurin.ultron.extensions.withDescription import org.junit.Assert import org.junit.Rule import org.junit.Test class ComposeListTest : BaseTest() { @get:Rule val composeRule = createSimpleUltronComposeRule() private val listWithMergedTree = composeList(hasTestTag(contactsListTestTag), false).withDescription("Contacts list") private val listPage = ComposeListPage private val notExistedList = composeList(hasTestTag("askjhsalk jdhas dlqk ")) private val emptyListTestTag = "emptyList" @Test fun item_existItem() { val index = 20 val contact = CONTACTS[index] listWithMergedTree.assertIsDisplayed() listWithMergedTree.item(hasAnyDescendant(hasText(contact.name)).withDescription("Contact '${contact.name}'")) .assertIsDisplayed() .assertMatches(hasAnyDescendant(hasText(contact.name)).withDescription("Contact '${contact.name}'")) } @Test fun item_notExistItem() { AssertUtils.assertException { listWithMergedTree.item(hasText("123gakshgdasl kgas")).assertIsDisplayed() } } @Test fun visibleItem_indexInScope() { val index = 2 val contact = CONTACTS[index] listWithMergedTree.visibleItem(index).printToLog("ULTRON") .assertMatches(hasAnyDescendant(hasText(contact.name))) } @Test fun visibleItem_indexOutOfScope() { AssertUtils.assertException { listWithMergedTree.visibleItem(20).assertIsDisplayed() } } @Test fun firstVisibleItem() { val contact = CONTACTS[0] listWithMergedTree.firstVisibleItem() .assertIsDisplayed() .assertMatches(hasAnyDescendant(hasText(contact.name))) .assertMatches(hasAnyDescendant(hasText(contact.status))) } @Test fun getVisibleItemByIndex() { val index = 3 val contact = CONTACTS[index] listPage.getItemByIndex(index).apply { name.assertTextEquals(contact.name) status.assertTextEquals(contact.status) } } @Test fun getFirstVisibleItem() { val contact = CONTACTS[0] listPage.getFirstVisibleItem().apply { name.assertTextEquals(contact.name) status.assertTextEquals(contact.status) } } @Test fun scrollToIndex() { val index = 20 val contact = CONTACTS[index] listWithMergedTree.scrollToIndex(index) hasText(contact.name).assertIsDisplayed() } @Test fun scrollToKey() { val index = 20 val contact = CONTACTS[index] listWithMergedTree.scrollToKey(contact.name) hasText(contact.name).assertIsDisplayed() } @Test fun customPerformOnLazyList() { val index = 20 val contact = CONTACTS[index] val children = listWithMergedTree.performOnList { node, interactoion -> interactoion.performScrollToIndex(index) node.children } hasText(contact.name).assertIsDisplayed() Assert.assertTrue(children.size > 10) val child = children.findNodeInTree(hasText(contact.name)) Assert.assertNotNull(child) } @Test fun moveToAnotherComposeActivityPageTest() { val contact = CONTACTS.first() hasText(contact.name).click() ComposeSecondPage.name.assertTextEquals(contact.name) ComposeSecondPage.status.assertTextEquals(contact.status) } @Test fun getItem_ByTestTag_assertNameAndStatusOfContact() { val index = 20 val contact = CONTACTS[index] listPage.getContactItemByTestTag(contact).apply { name.assertTextEquals(contact.name) status.assertTextContains(contact.status) } } @Test fun getItem_ByMatcher_assertNameAndStatusOfContact() { val index = 20 val contact = CONTACTS[index] listPage.getContactItemByName(contact).apply { name.assertTextEquals(contact.name) status.assertTextContains(contact.status) } } @Test fun listHeader_asChild_TextContains() { listPage.lazyList.visibleChild(hasTestTag(contactsListHeaderTag)) .assertTextContains("header", option = TextContainsOption(substring = true)) } @Test fun listHeader_asChild_TextEquals() { listPage.lazyList.visibleChild(hasTestTag(contactsListHeaderTag).withDescription("header")) .assertTextEquals("Lazy column header") } @Test fun listHeader_asChild_TextEquals_mergedTree() { listWithMergedTree.visibleChild(hasTestTag(contactsListHeaderTag).withDescription("header")) .assertTextEquals("Lazy column header") } @Test fun visibleItemChild() { val index = 3 val contact = CONTACTS[index] listPage.lazyList.onVisibleItemChild(index, hasTestTag(contactStatusTestTag)).assertTextEquals(contact.status) } @Test fun assertIsDisplayed_visibleList() { listWithMergedTree.assertIsDisplayed() } @Test fun assertIsDisplayed_invisibleList() { AssertUtils.assertException { notExistedList.withTimeout(1000).assertIsDisplayed() } } @Test fun assertIsNotDisplayed_visibleList() { AssertUtils.assertException { listWithMergedTree.withTimeout(1000).assertIsNotDisplayed() } } @Test fun assertExists_existedList() { listWithMergedTree.assertExists() } @Test fun assertExists_notExistedList() { AssertUtils.assertException { notExistedList.withTimeout(1000).assertExists() } } @Test fun assertDoesNotExist_notExistedList() { notExistedList.assertDoesNotExist() } @Test fun assertDoesNotExist_existedList() { AssertUtils.assertException { listWithMergedTree.withTimeout(1000).assertDoesNotExist() } } @Test fun assertContentDescriptionEquals_properContentDescription() { listWithMergedTree.assertContentDescriptionEquals(contactsListContentDesc) } @Test fun assertContentDescriptionEquals_invalidContentDescription() { AssertUtils.assertException { listWithMergedTree.withTimeout(1000).assertContentDescriptionEquals("some invalid desc") } } @Test fun assertContentDescriptionContains_properContentDescription() { listWithMergedTree.assertContentDescriptionContains(contactsListContentDesc.substring(0, 5), ContentDescriptionContainsOption(substring = true)) } @Test fun assertContentDescriptionContains_invalidContentDescription() { AssertUtils.assertException { listWithMergedTree.withTimeout(1000).assertContentDescriptionContains("some invalid") } } @Test fun assertVisibleItemsCount_properCountProvided() { val count = listWithMergedTree.getVisibleItemsCount() listWithMergedTree.assertVisibleItemsCount(count) } @Test fun assertVisibleItemsCount_invalidCountProvided() { AssertUtils.assertException { listWithMergedTree.withTimeout(1000).assertVisibleItemsCount(100) } } @Test fun itemByPosition_propertyConfiguredTest() { val index = 20 val contact = CONTACTS[index] val item = listPage.lazyList.item(20).assertIsDisplayed() item.assertMatches(hasTestTag(getContactItemTestTagById(contact))) } @Test fun getItemByPosition_propertyConfiguredTest() { val index = 20 val contact = CONTACTS[index] listPage.getItemByPosition(index).apply { name.assertTextEquals(contact.name) status.assertTextEquals(contact.status) assertIsDisplayed() } } @Test fun assertItemDoesNotExistWithSearch_NotExistedItem() { listWithMergedTree.assertItemDoesNotExist(hasText("NOT EXISTED TeXT")) } @Test fun assertItemDoesNotExistWithSearch_ExistedItem() { val contact = ContactRepositoty.getLast() AssertUtils.assertException { listWithMergedTree.withTimeout(2000).assertItemDoesNotExist(hasText(contact.name)) } } @Test fun getItem_NotExistedItemChild() { val index = 20 val contact = CONTACTS[index] listPage.getContactItemByName(contact).apply { AssertUtils.assertException { notExisted.withTimeout(1000).assertIsDisplayed() } } } @Test fun assertNotEmpty_notEmptyList() { listWithMergedTree.assertNotEmpty() } @Test fun assertEmpty_notEmptyList() { AssertUtils.assertException { listWithMergedTree.withTimeout(100).assertEmpty() } } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/ComposeListWithPositionTestTagTest.kt ================================================ package com.atiurin.sampleapp.tests.compose import androidx.compose.ui.test.hasAnyDescendant import androidx.compose.ui.test.hasContentDescription import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.hasText import com.atiurin.sampleapp.activity.ComposeListWithPositionTestTagActivity import com.atiurin.sampleapp.compose.ListItemPositionPropertyKey import com.atiurin.sampleapp.compose.contactsListContentDesc import com.atiurin.sampleapp.compose.getContactItemTestTagByPosition import com.atiurin.sampleapp.data.repositories.CONTACTS import com.atiurin.sampleapp.framework.utils.AssertUtils import com.atiurin.sampleapp.pages.ComposeListPage import com.atiurin.ultron.core.compose.createSimpleUltronComposeRule import com.atiurin.ultron.core.compose.list.composeList import org.junit.Rule import org.junit.Test class ComposeListWithPositionTestTagTest { @get:Rule val composeRule = createSimpleUltronComposeRule() val list = composeList(hasContentDescription(contactsListContentDesc), false) val composeListWithProperty = composeList(hasContentDescription(contactsListContentDesc), false, ListItemPositionPropertyKey) @Test fun itemOutOfVisibleScope() { val index = 20 val contact = CONTACTS[index] list.item(hasTestTag(getContactItemTestTagByPosition(index))) .assertIsDisplayed() .assertMatches(hasAnyDescendant(hasText(contact.name))) } @Test fun lastVisibleItem() { val count = list.getVisibleItemsCount() - 1 val contact = CONTACTS[count] list.lastVisibleItem() .assertIsDisplayed() .assertMatches(hasAnyDescendant(hasText(contact.name))) .assertMatches(hasAnyDescendant(hasText(contact.status))) } @Test fun itemByPosition_propertyNOTConfiguredInTest() { AssertUtils.assertException { list.item(20).assertIsDisplayed() } } @Test fun itemByPosition_propertyNOTConfiguredInApplication() { AssertUtils.assertException { composeListWithProperty.withTimeout(1000).item(20).assertIsDisplayed() } } @Test fun getItemByPosition_propertyNOTConfiguredInTest() { AssertUtils.assertException { list.getItem(20).assertIsDisplayed() } } @Test fun getItemByPosition_propertyNOTConfiguredInApplication() { AssertUtils.assertException { composeListWithProperty.withTimeout(1000).getItem(20).assertIsDisplayed() } } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/ComposeUIElementsTest.kt ================================================ package com.atiurin.sampleapp.tests.compose import androidx.compose.ui.semantics.ProgressBarRangeInfo import androidx.compose.ui.semantics.SemanticsActions import androidx.compose.ui.semantics.SemanticsProperties import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.assertTextContains import androidx.compose.ui.test.hasContentDescription import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.hasText import androidx.compose.ui.test.performSemanticsAction import androidx.compose.ui.text.TextRange import androidx.compose.ui.unit.dp import com.atiurin.sampleapp.activity.ActionsStatus import com.atiurin.sampleapp.activity.ComposeElementsActivity import com.atiurin.sampleapp.activity.ComposeElementsActivity.Constants.likesCounterButton import com.atiurin.sampleapp.activity.ComposeElementsActivity.Constants.likesCounterContentDesc import com.atiurin.sampleapp.activity.ComposeElementsActivity.Constants.likesCounterTextContainerContentDesc import com.atiurin.sampleapp.activity.ComposeElementsActivity.Constants.simpleCheckbox import com.atiurin.sampleapp.activity.ComposeElementsActivity.Constants.statusText import com.atiurin.sampleapp.compose.RegionName import com.atiurin.sampleapp.framework.ultronext.ProgressBar import com.atiurin.sampleapp.framework.ultronext.getProgress import com.atiurin.sampleapp.framework.utils.AssertUtils import com.atiurin.sampleapp.pages.ComposeElementsPage import com.atiurin.sampleapp.tests.BaseTest import com.atiurin.ultron.core.common.assertion.softAssertion import com.atiurin.ultron.core.common.assertion.verifySoftAssertions import com.atiurin.ultron.core.common.options.ClickOption import com.atiurin.ultron.core.common.options.ContentDescriptionContainsOption import com.atiurin.ultron.core.common.options.PerformCustomBlockOption import com.atiurin.ultron.core.common.options.TextContainsOption import com.atiurin.ultron.core.common.options.TextEqualsOption import com.atiurin.ultron.core.compose.createSimpleUltronComposeRule import com.atiurin.ultron.core.compose.nodeinteraction.UltronComposeSemanticsNodeInteraction import com.atiurin.ultron.core.compose.nodeinteraction.click import com.atiurin.ultron.core.compose.operation.ComposeOperationType import com.atiurin.ultron.core.compose.operation.UltronComposeCollectionInteraction.Companion.allNodes import com.atiurin.ultron.core.compose.operation.UltronComposeOperationParams import com.atiurin.ultron.core.compose.operation.assertSize import com.atiurin.ultron.core.compose.option.ComposeSwipeOption import com.atiurin.ultron.core.config.UltronCommonConfig import com.atiurin.ultron.extensions.assertContentDescriptionContains import com.atiurin.ultron.extensions.assertContentDescriptionEquals import com.atiurin.ultron.extensions.assertDoesNotExist import com.atiurin.ultron.extensions.assertExists import com.atiurin.ultron.extensions.assertHasClickAction import com.atiurin.ultron.extensions.assertHasNoClickAction import com.atiurin.ultron.extensions.assertHeightIsAtLeast import com.atiurin.ultron.extensions.assertHeightIsEqualTo import com.atiurin.ultron.extensions.assertIsDisplayed import com.atiurin.ultron.extensions.assertIsEnabled import com.atiurin.ultron.extensions.assertIsNotEnabled import com.atiurin.ultron.extensions.assertIsNotFocused import com.atiurin.ultron.extensions.assertIsNotSelected import com.atiurin.ultron.extensions.assertIsOff import com.atiurin.ultron.extensions.assertIsSelectable import com.atiurin.ultron.extensions.assertIsToggleable import com.atiurin.ultron.extensions.assertTextContains import com.atiurin.ultron.extensions.assertTextEquals import com.atiurin.ultron.extensions.assertValueEquals import com.atiurin.ultron.extensions.assertWidthIsAtLeast import com.atiurin.ultron.extensions.assertWidthIsEqualTo import com.atiurin.ultron.extensions.captureToImage import com.atiurin.ultron.extensions.clearText import com.atiurin.ultron.extensions.click import com.atiurin.ultron.extensions.clickBottomCenter import com.atiurin.ultron.extensions.clickBottomLeft import com.atiurin.ultron.extensions.clickBottomRight import com.atiurin.ultron.extensions.clickCenterLeft import com.atiurin.ultron.extensions.clickCenterRight import com.atiurin.ultron.extensions.clickTopCenter import com.atiurin.ultron.extensions.clickTopLeft import com.atiurin.ultron.extensions.clickTopRight import com.atiurin.ultron.extensions.copyText import com.atiurin.ultron.extensions.doubleClick import com.atiurin.ultron.extensions.execute import com.atiurin.ultron.extensions.getNode import com.atiurin.ultron.extensions.getNodeConfigProperty import com.atiurin.ultron.extensions.getText import com.atiurin.ultron.extensions.inputText import com.atiurin.ultron.extensions.longClick import com.atiurin.ultron.extensions.pasteText import com.atiurin.ultron.extensions.perform import com.atiurin.ultron.extensions.performMouseInput import com.atiurin.ultron.extensions.replaceText import com.atiurin.ultron.extensions.selectText import com.atiurin.ultron.extensions.swipe import com.atiurin.ultron.extensions.swipeDown import com.atiurin.ultron.extensions.swipeLeft import com.atiurin.ultron.extensions.swipeRight import com.atiurin.ultron.extensions.swipeUp import com.atiurin.ultron.extensions.typeText import com.atiurin.ultron.extensions.withTimeout import org.junit.Assert import org.junit.Ignore import org.junit.Rule import org.junit.Test @OptIn(ExperimentalTestApi::class) class ComposeUIElementsTest : BaseTest() { val page = ComposeElementsPage @get:Rule val composeRule = createSimpleUltronComposeRule() val initialText = "Like count = 0" val expectedText = "Like count = 1" @Test fun simpleClick() { hasText(initialText).assertIsDisplayed().click() hasText(expectedText).assertIsDisplayed() } @Test fun contentDescTest() { hasContentDescription(likesCounterContentDesc).click() hasText(expectedText).assertIsDisplayed() } @Test fun testTagTest() { hasText(initialText).click() page.likesCounter.assertTextEquals(expectedText).assertIsDisplayed() } @Test fun getTextTest() { hasTestTag(likesCounterButton).click() val text = hasTestTag(likesCounterButton).getText() Assert.assertEquals(expectedText, text) } @Test fun clickCheckBox() { hasTestTag(simpleCheckbox).assertIsOff().click().assertIsOn() } @Test fun longClick_longClickable() { page.longAndDoubleClickButton.longClick() page.status.assertTextEquals(ActionsStatus.LongClicked.name) } @Test fun doubleClick_doubleClickable() { page.longAndDoubleClickButton.doubleClick() page.status.assertTextEquals(ActionsStatus.DoubleClicked.name) } @Test fun regionsClickTopLeft() { page.regionsNode.clickTopLeft(ClickOption(xOffset = 20, yOffset = 20)) page.status.assertTextEquals(RegionName.TopLeft.name) } @Test fun regionsClickTopCenter() { page.regionsNode.clickTopCenter(ClickOption(yOffset = 20)) page.status.assertTextEquals(RegionName.TopCenter.name) } @Test fun regionsClickTopRight() { page.regionsNode.clickTopRight(ClickOption(yOffset = 20)) page.status.assertTextEquals(RegionName.TopRight.name) } @Test fun regionsClickCenterLeft() { page.regionsNode.clickCenterLeft() page.status.assertTextEquals(RegionName.CenterLeft.name) } @Test fun regionsClickCenterRight() { page.regionsNode.clickCenterRight() page.status.assertTextEquals(RegionName.CenterRight.name) } @Test fun regionsClickBottomLeft() { page.regionsNode.clickBottomLeft(ClickOption(xOffset = 16)) page.status.assertTextEquals(RegionName.BottomLeft.name) } @Test fun regionsClickBottomCenter() { page.regionsNode.clickBottomCenter() page.status.assertTextEquals(RegionName.BottomCenter.name) } @Test fun regionsClickBottomRight() { page.regionsNode.clickBottomRight() page.status.assertTextEquals(RegionName.BottomRight.name) } //flaky on emulator @Test @Ignore fun copyText() { val startText = "begin" page.editableText.apply { replaceText(startText) selectText(TextRange(0, 2)) assertIsDisplayed() copyText() assertIsDisplayed() clearText() click() pasteText() assertTextContains(startText) } } @Test fun swipeDown() { page.swipeableNode.swipeDown() page.status.assertTextEquals(ActionsStatus.SwipeDown.name) } @Test fun swipeUp() { page.swipeableNode.swipeUp() page.status.assertTextEquals(ActionsStatus.SwipeUp.name) } @Test fun swipeRight() { page.swipeableNode.swipeRight() page.status.assertTextEquals(ActionsStatus.SwipeRight.name) } @Test fun swipeLeft() { page.swipeableNode.swipeLeft() page.status.assertTextEquals(ActionsStatus.SwipeLeft.name) } @Test fun swipe_option() { page.swipeableNode.swipeLeft(ComposeSwipeOption(durationMs = 1000L)) page.status.assertTextEquals(ActionsStatus.SwipeLeft.name) } @Test fun swipe_general() { page.swipeableNode.swipe(ComposeSwipeOption( startXOffset = 0.1f, startYOffset = 0.1f, endXOffset = 0.9f, endYOffset = 0.1f, durationMs = 1000L )) page.status.assertTextEquals(ActionsStatus.SwipeRight.name) } @Test fun inputText() { val text = "some text" page.editableText.inputText(text).assertTextContains(text) } @Test fun typeText() { val text = "some text" page.editableText.typeText(text).assertTextContains(text) } @Test fun setSelection() { page.editableText.replaceText("qwerty").setSelection(0, 3, true).assertIsDisplayed().cutText().assertTextContains("rty") } @Test fun captureToImage() { val image = page.longAndDoubleClickButton.captureToImage() Assert.assertNotNull(image) } @Test fun setProgress() { page.progressBar.setProgress(0.7f).assertIsDisplayed() page.status.assertTextEquals("set progress 0.7") } @Test fun performCustomSemanticsAction() { val progress = 0.7f val progressBar = ProgressBar(hasTestTag(ComposeElementsActivity.progressBar)) progressBar.setProgress(progress) val current = progressBar.getProgress() Assert.assertEquals(current, progress ) } @Test fun performCustomSemanticsAssertion() { val progress = 0.7f val progressBar = ProgressBar(hasTestTag(ComposeElementsActivity.progressBar)) progressBar.setProgress(progress) progressBar.assertProgress(progress) } @Test fun performExtendedAssertion() { val progress = 0.7f page.progressBar.apply { setProgress(progress) assertProgress(progress) } } @Test fun performWithLambda() { val progress = 0.7f val result = page.progressBar.perform { it.performSemanticsAction(SemanticsActions.SetProgress) { it.invoke(progress) } } Assert.assertTrue(result is UltronComposeSemanticsNodeInteraction) page.progressBar.assertProgress(progress) } @Test fun semanticsMatcher_performDeprecated() { val text = page.status.perform({ it.fetchSemanticsNode().config[SemanticsProperties.Text].first().text }, option = PerformCustomBlockOption(ComposeOperationType.CUSTOM, "")) Assert.assertTrue(text.isNotBlank()) } @Test fun ultronComposeSemanticsNodeInteraction_performDeprecated() { val text = page.status.assertExists().perform(option = PerformCustomBlockOption(ComposeOperationType.CUSTOM, "")) { it.fetchSemanticsNode().config[SemanticsProperties.Text].first().text } Assert.assertTrue(text.isNotBlank()) } @Test fun ultronComposeSemanticsNodeInteraction_execute() { val text = page.status.assertExists().execute { it.fetchSemanticsNode().config[SemanticsProperties.Text].first().text } Assert.assertTrue(text.isNotBlank()) } @Test fun semanticsMatcher_execute() { val text = page.status.execute { it.fetchSemanticsNode().config[SemanticsProperties.Text].first().text } Assert.assertTrue(text.isNotBlank()) } @Test fun performMouseInput() { page.swipeableNode.performMouseInput { swipeUp() } page.status.assertTextEquals(ActionsStatus.SwipeUp.name) } @Test fun getNode_exits() { val node = page.status.getNode() Assert.assertEquals(ComposeElementsActivity.Constants.statusText, node.config[SemanticsProperties.TestTag]) } @Test fun getNodeConfigProperty_exist() { val testTag = page.status.getNodeConfigProperty(SemanticsProperties.TestTag) Assert.assertEquals(ComposeElementsActivity.Constants.statusText, testTag) } @Test fun assertIsDisplayed() { page.status.assertIsDisplayed() } @Test fun assertExists() { page.status.assertExists() } @Test fun assertExists_notExisted() { AssertUtils.assertException { hasText("some not existed node").withTimeout(100).assertExists() } } @Test fun assertDoesNotExist() { hasText("some not existed node").assertDoesNotExist() } @Test fun assertDoesNotExist_existed() { AssertUtils.assertException { page.editableText.withTimeout(100).assertDoesNotExist() } } @Test fun assertIsEnabled() { page.editableText.assertIsEnabled() } @Test fun assertIsEnabled_disabledButton() { AssertUtils.assertException { page.disabledButton.withTimeout(100).assertIsEnabled() } } @Test fun assertIsNotEnabled() { page.disabledButton.assertIsNotEnabled() } @Test fun assertIsNotEnabled_enabledButton() { AssertUtils.assertException { page.longAndDoubleClickButton.withTimeout(100).assertIsNotEnabled() } } @Test fun assertIsFocused() { page.editableText.click().assertIsFocused() } @Test fun assertIsFocused_notFocused() { AssertUtils.assertException { page.editableText.withTimeout(100).assertIsFocused() } } @Test fun assertIsNotFocused() { page.editableText.assertIsNotFocused() } @Test fun assertIsNotFocused_focused() { AssertUtils.assertException { page.editableText.click().withTimeout(100).assertIsNotFocused() } } @Test fun assertIsSelected() { page.maleRadioButton.click().assertIsSelected() } @Test fun assertIsSelected_notSelected() { page.maleRadioButton.click() AssertUtils.assertException { page.femaleRadioButton.withTimeout(100).assertIsSelected() } } @Test fun assertIsNotSelected_notSelected() { page.maleRadioButton.click() page.femaleRadioButton.assertIsNotSelected() } @Test fun assertIsNotSelected_selected() { AssertUtils.assertException { page.maleRadioButton.click().withTimeout(100).assertIsNotSelected() } } @Test fun assertIsSelectable() { page.femaleRadioButton.assertIsSelectable() } @Test fun assertIsSelectable_notSelectable() { AssertUtils.assertException { page.status.withTimeout(100).assertIsSelectable() } } @Test fun assertIsToggleable() { page.simpleCheckbox.assertIsToggleable() } @Test fun assertIsToggleable_notToggleable() { AssertUtils.assertException { page.editableText.withTimeout(100).assertIsToggleable() } } @Test fun assertIsOn() { page.simpleCheckbox.click().assertIsOn() } @Test fun assertIsOn_checkboxIsOff() { AssertUtils.assertException { page.simpleCheckbox.withTimeout(100).assertIsOn() } } @Test fun assertIsOff() { page.simpleCheckbox.assertIsOff() } @Test fun assertIsOff_checkboxIsOn() { AssertUtils.assertException { page.simpleCheckbox.click().withTimeout(100).assertIsOff() } } @Test fun assertHasClickAction() { page.longAndDoubleClickButton.assertHasClickAction() } @Test fun assertHasClickAction_noClickAction() { AssertUtils.assertException { page.status.withTimeout(100).assertHasClickAction() } } @Test fun assertHasNoClickAction() { page.status.assertHasNoClickAction() } @Test fun assertHasNoClickAction_hasClickAction() { AssertUtils.assertException { page.longAndDoubleClickButton.withTimeout(100).assertHasNoClickAction() } } @Test fun assertTextEquals() { page.editableText.assertTextEquals("Label", "") } @Test fun assertTextEquals_includeEditableFalse() { page.editableText.assertTextEquals("Label", option = TextEqualsOption(false)) } @Test fun assertTextEquals_includeEditableFalse_editableProvided() { AssertUtils.assertException { page.editableText.withTimeout(100).assertTextEquals("Label", "", option = TextEqualsOption(false)) } } @Test fun assertTextEquals_wrongTextProvided() { AssertUtils.assertException { page.editableText.withTimeout(100).assertTextEquals("some invalid text", "") } } @Test fun assertTextEquals_editableNotEmpty_ValidText() { val text = "editable text" page.editableText.replaceText(text).assertTextEquals("Label", text) } @Test fun assertTextEquals_editableNotEmpty_ValidText_mixedOrder() { val text = "editable text" page.editableText.replaceText(text).assertTextEquals(text, "Label") } @Test fun assertTextEquals_editableNotEmpty_includeEditableFalse() { val text = "editable text" AssertUtils.assertException { page.editableText.withTimeout(100).replaceText(text).assertTextEquals("Label", text, option = TextEqualsOption(false)) } } @Test fun assertTextContains_label() { page.editableText.assertTextContains("Label") } @Test fun assertTextContains_editable() { val text = "some text" page.editableText.replaceText(text).assertTextContains(text) } @Test fun assertTextContains_wrongText() { val text = "some text" AssertUtils.assertException { page.editableText.withTimeout(100).assertTextContains(text) } } @Test fun assertTextContains_emptyText() { page.editableText.assertTextContains("") } @Test fun assertTextContains_substringTrue_validSubstringProvided() { val text = "some text" page.editableText.replaceText(text).assertTextContains(text.substring(0, 4), TextContainsOption(substring = true)) } @Test fun assertTextContains_substringTrue_wrongSubstringProvided() { AssertUtils.assertException { page.editableText.replaceText("valid text").withTimeout(100).assertTextContains("wrong text", TextContainsOption(substring = true)) } } @Test fun assertTextContains_substringFalse_validSubstringProvided() { val text = "some text" AssertUtils.assertException { page.editableText.replaceText(text).withTimeout(100).assertTextContains(text.substring(0, 4), TextContainsOption(substring = false)) } } @Test fun assertTextContains_ignoreCase_lowercase() { val text = "SoMe TexT" page.editableText.replaceText(text).assertTextContains(text.lowercase(), TextContainsOption(ignoreCase = true)) } @Test fun assertTextContains_ignoreCase_uppercase() { val text = "SoMe TexT" page.editableText.replaceText(text).assertTextContains(text.uppercase(), TextContainsOption(ignoreCase = true)) } @Test fun assertTextContains_ignoreCase_and_substring() { val text = "SoMe TexT" page.editableText.replaceText(text).assertTextContains(text.substring(0, 4).lowercase(), TextContainsOption(substring = true, ignoreCase = true)) } @Test fun assertTextContains_ignoreCaseFalse() { val text = "SoMe TexT" AssertUtils.assertException { page.editableText.replaceText(text).withTimeout(100).assertTextContains(text.lowercase(), TextContainsOption(ignoreCase = false)) } } @Test fun assertContentDescriptionEquals() { page.likesCounter.assertContentDescriptionEquals(likesCounterContentDesc, likesCounterTextContainerContentDesc) } @Test fun assertContentDescriptionEquals_notEnoughElements() { AssertUtils.assertException { page.likesCounter.withTimeout(100).assertContentDescriptionEquals(likesCounterContentDesc) } } @Test fun assertContentDescriptionContains() { page.likesCounter.assertContentDescriptionContains(likesCounterContentDesc) } @Test fun assertContentDescriptionContains_substringTrue_validSubstringProvided() { page.likesCounter.assertContentDescriptionContains(likesCounterContentDesc.substring(1, 5), ContentDescriptionContainsOption(substring = true)) } @Test fun assertContentDescriptionContains_substringTrue_wrongSubstringProvided() { AssertUtils.assertException { page.likesCounter.withTimeout(100).assertContentDescriptionContains("wrong substring", ContentDescriptionContainsOption(substring = true)) } } @Test fun assertContentDescriptionContains_ignoreCaseTrue_lowercase() { page.likesCounter.assertContentDescriptionContains(likesCounterContentDesc.lowercase(), ContentDescriptionContainsOption(ignoreCase = true)) } @Test fun assertContentDescriptionContains_ignoreCaseTrue_uppercase() { page.likesCounter.assertContentDescriptionContains(likesCounterContentDesc.uppercase(), ContentDescriptionContainsOption(ignoreCase = true)) } @Test fun assertContentDescriptionContains_ignoreCaseFalse() { AssertUtils.assertException { page.likesCounter.withTimeout(100).assertContentDescriptionContains(likesCounterContentDesc.lowercase(), ContentDescriptionContainsOption(ignoreCase = false)) } } @Test fun assertValueEquals() { page.simpleCheckbox.assertValueEquals("default") } @Test fun assertValueEquals_invalidValue() { AssertUtils.assertException { page.simpleCheckbox.withTimeout(100).assertValueEquals("invalid") } } @Test fun assertRangeInfoEquals() { page.progressBar.setProgress(0.7f).assertRangeInfoEquals(ProgressBarRangeInfo(0.7f, range = 0f..0.7f, 100)) } @Test fun assertRangeInfoEquals_invalidInfo() { AssertUtils.assertException { page.progressBar.setProgress(0.7f).withTimeout(100).assertRangeInfoEquals(ProgressBarRangeInfo(0.0f, range = 0f..0.0f, 100)) } } @Test fun assertHeightIsEqualTo() { page.swipeableNode.assertHeightIsEqualTo(100.dp) } @Test fun assertHeightIsEqualTo_invalidValue() { AssertUtils.assertException { page.swipeableNode.withTimeout(100).assertHeightIsEqualTo(50.dp) } } @Test fun assertWidthIsEqualTo() { page.swipeableNode.assertWidthIsEqualTo(100.dp) } @Test fun assertWidthIsEqualTo_invalidValue() { AssertUtils.assertException { page.swipeableNode.withTimeout(100).assertWidthIsEqualTo(50.dp) } } @Test fun assertHeightIsAtLeast() { page.swipeableNode.assertHeightIsAtLeast(10.dp) } @Test fun assertHeightIsAtLeast_invalidValue() { AssertUtils.assertException { page.swipeableNode.withTimeout(100).assertHeightIsAtLeast(500.dp) } } @Test fun assertWidthIsAtLeast() { page.swipeableNode.assertWidthIsAtLeast(10.dp) } @Test fun assertWidthIsAtLeast_invalidValue() { AssertUtils.assertException { page.swipeableNode.withTimeout(100).assertWidthIsAtLeast(500.dp) } } @Test fun assertMatches() { val text = "some text" page.editableText.replaceText(text).assertMatches(hasText(text)) } @Test fun assertMatches_invalid() { AssertUtils.assertException { page.editableText.replaceText("some text").withTimeout(100) .assertMatches(hasText("invalid text")) } } @Test fun customPerformParamsMapping() { val params = UltronComposeOperationParams( operationName = "operationName", operationDescription = "operationDescription", operationType = ComposeOperationType.ASSERT_MATCHES ) page.status.withTimeout(100).withResultHandler { val op = it.operation Assert.assertEquals(params.operationName, op.name) Assert.assertEquals(params.operationDescription, op.description) Assert.assertEquals(params.operationType, op.type) }.perform(params) { it.assertTextContains("Some invalid text") } } @Test fun softAssertionTest() { UltronCommonConfig.testContext.softAnalyzer.clear() softAssertion(false) { hasText("NotExistText").withTimeout(100).assertIsDisplayed() hasTestTag("NotExistTestTag").withTimeout(100).assertHasClickAction() } runCatching { verifySoftAssertions() }.onFailure { exception -> val message = exception.message ?: throw RuntimeException("Empty exception message: $exception") Assert.assertTrue(message.contains("NotExistText")) Assert.assertTrue(message.contains("NotExistTestTag")) } } @Test fun allNodesTest_invalidExpectedValue(){ AssertUtils.assertException { allNodes(hasTestTag(statusText)).assertSize(2, operationTimeoutMs = 1000) } } @Test fun allNodesTest_correctExpectedValue(){ allNodes(hasTestTag(statusText)).assertSize(1) } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/DefaultComponentActivityTest.kt ================================================ package com.atiurin.sampleapp.tests.compose import androidx.compose.material.Text import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag import androidx.compose.ui.test.hasTestTag import com.atiurin.ultron.core.compose.createDefaultUltronComposeRule import com.atiurin.ultron.extensions.assertIsDisplayed import org.junit.Rule import org.junit.Test class DefaultComponentActivityTest { @get:Rule val composeRule = createDefaultUltronComposeRule() @Test fun setContent() { val testTagValue = "testTag" composeRule.setContent { Text(text = "Hello, world!", modifier = Modifier.semantics { testTag = testTagValue }) } hasTestTag(testTagValue) .assertIsDisplayed() .assertTextEquals("Hello, world!") } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/RunUltronUiTest.kt ================================================ package com.atiurin.sampleapp.tests.compose import androidx.compose.foundation.layout.Column import androidx.compose.material.Button import androidx.compose.material.Text import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.hasTestTag import com.atiurin.ultron.core.compose.runUltronUiTest import com.atiurin.ultron.extensions.assertTextContains import com.atiurin.ultron.extensions.isSuccess import org.junit.Test import kotlin.test.assertTrue @OptIn(ExperimentalTestApi::class) class RunUltronUiTest { @Test fun useUnmergedTreeConfigTest() = runUltronUiTest { val testTag = "element" setContent { Column { Button(onClick = {}, modifier = Modifier.testTag(testTag)) { Text("Text1") Text("Text2") } } } assertTrue ("Ultron operation success should be true") { hasTestTag(testTag).isSuccess { assertTextContains("Text1") } } } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/SampleClassTest.kt ================================================ package com.atiurin.sampleapp.tests.compose import androidx.compose.ui.test.hasTestTag import com.atiurin.sampleapp.activity.ComposeElementsActivity import com.atiurin.sampleapp.tests.BaseTest import com.atiurin.ultron.core.compose.createSimpleUltronComposeRule import com.atiurin.ultron.extensions.assertIsDisplayed import com.atiurin.ultron.extensions.withName import com.atiurin.ultron.extensions.withTimeout import com.atiurin.ultron.page.Page import org.junit.Rule import org.junit.Test class SampleClassTest : BaseTest() { @get:Rule val composeRuleBase = createSimpleUltronComposeRule() @Test fun test() { SomePage{ elementWithName.assertIsDisplayed() elementWithTimeout.assertIsDisplayed() elementMatcher.assertIsDisplayed() } } @Test fun test2() { SomePage{ elementWithName.assertIsDisplayed() elementWithTimeout.assertIsDisplayed() elementMatcher.assertIsDisplayed() } } } object SomePage : Page() { val elementWithName = hasTestTag("statusText").withName("sample element name") val elementWithTimeout = hasTestTag("statusText").withTimeout(4000) val elementMatcher = hasTestTag("statusText") } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/SemNodeInteractionObjectTest.kt ================================================ package com.atiurin.sampleapp.tests.compose import com.atiurin.sampleapp.activity.ActionsStatus import com.atiurin.sampleapp.activity.ComposeElementsActivity import com.atiurin.sampleapp.pages.ComposeElementsPage import com.atiurin.sampleapp.tests.BaseTest import com.atiurin.ultron.core.common.options.TextContainsOption import com.atiurin.ultron.core.compose.createSimpleUltronComposeRule import com.atiurin.ultron.core.compose.nodeinteraction.click import com.atiurin.ultron.core.compose.nodeinteraction.doubleClick import com.atiurin.ultron.core.compose.nodeinteraction.longClick import com.atiurin.ultron.core.compose.option.ComposeSwipeOption import com.atiurin.ultron.extensions.assertTextEquals import com.atiurin.ultron.extensions.withMetaInfo import com.atiurin.ultron.extensions.withName import org.junit.Rule import org.junit.Test class SemNodeInteractionObjectTest : BaseTest() { @get:Rule val composeRule = createSimpleUltronComposeRule() val page = ComposeElementsPage @Test fun clickTest(){ page.likesCounter.withMetaInfo("likesCounter") .click() .assertTextContains(option = TextContainsOption(substring = true), expected = "= 1") } @Test fun longClickTest(){ page.longAndDoubleClickButton.withName("longAndDoubleClickButton").longClick() page.status.assertTextEquals(ActionsStatus.LongClicked.name) } @Test fun doubleClick_doubleClickable() { page.longAndDoubleClickButton.withName("longAndDoubleClickButton").doubleClick() page.status.assertTextEquals(ActionsStatus.DoubleClicked.name) } @Test fun swipeDownTest(){ page.swipeableNode.withName("swipeableNode").swipeDown() page.status.assertTextEquals(ActionsStatus.SwipeDown.name) } @Test fun swipeUp() { page.swipeableNode.withName("swipeableNode").swipeUp() page.status.assertTextEquals(ActionsStatus.SwipeUp.name) } @Test fun swipeRight() { page.swipeableNode.withName("swipeableNode").swipeRight() page.status.assertTextEquals(ActionsStatus.SwipeRight.name) } @Test fun swipeLeft() { page.swipeableNode.withName("swipeableNode").swipeLeft() page.status.assertTextEquals(ActionsStatus.SwipeLeft.name) } @Test fun swipe_option() { page.swipeableNode.withName("swipeableNode").swipeLeft(ComposeSwipeOption(durationMs = 1000L)) page.status.assertTextEquals(ActionsStatus.SwipeLeft.name) } @Test fun swipe_general() { page.swipeableNode.withName("Swipeable Node").swipe(ComposeSwipeOption( startXOffset = 0.1f, startYOffset = 0.1f, endXOffset = 0.9f, endYOffset = 0.1f, durationMs = 1000L )) page.status.assertTextEquals(ActionsStatus.SwipeRight.name) } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/TreeTest.kt ================================================ package com.atiurin.sampleapp.tests.compose import androidx.compose.ui.test.onRoot import androidx.compose.ui.test.printToString import com.atiurin.sampleapp.activity.ComposeElementsActivity import com.atiurin.sampleapp.pages.ComposeElementsPage import com.atiurin.sampleapp.tests.BaseTest import com.atiurin.ultron.allure.attachment.AttachUtil import com.atiurin.ultron.core.compose.createSimpleUltronComposeRule import com.atiurin.ultron.file.MimeType import com.atiurin.ultron.log.UltronLog import com.atiurin.ultron.utils.createCacheFile import org.junit.Test class TreeTest : BaseTest() { val page = ComposeElementsPage val composeRule = createSimpleUltronComposeRule() init { ruleSequence.add(composeRule) } @Test fun generateSemanticsTreeTest(){ val node = composeRule.onRoot(useUnmergedTree = true).printToString() val file = createCacheFile("tree_", ".log") file.writeText(node) val fileName = AttachUtil.attachFile(file, MimeType.PLAIN_TEXT) UltronLog.error(node) } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/UltronComposeUiBlockTest.kt ================================================ package com.atiurin.sampleapp.tests.compose import androidx.compose.ui.test.hasTestTag import com.atiurin.sampleapp.activity.ComposeElementsActivity import com.atiurin.sampleapp.activity.ComposeElementsActivity.Constants.contactBlock1Tag import com.atiurin.sampleapp.data.repositories.CONTACTS import com.atiurin.sampleapp.framework.utils.AssertUtils import com.atiurin.sampleapp.pages.uiblock.ComposeListUiBlock.Companion.listBlockDesc import com.atiurin.sampleapp.pages.uiblock.ComposeUiBlockScreen import com.atiurin.sampleapp.pages.uiblock.ContactUiBlockWithDesc import com.atiurin.sampleapp.tests.BaseTest import com.atiurin.sampleapp.tests.espresso.descriptionPrefix import com.atiurin.sampleapp.tests.espresso.сhildNameDesc import com.atiurin.ultron.core.common.assertion.softAssertion import com.atiurin.ultron.core.compose.createSimpleUltronComposeRule import com.atiurin.ultron.extensions.assertIsDisplayed import com.atiurin.ultron.extensions.assertTextContains import com.atiurin.ultron.extensions.withUseUnmergedTree import org.junit.Assert import org.junit.Rule import org.junit.Test class UltronComposeUiBlockTest : BaseTest() { @get:Rule val composeRule = createSimpleUltronComposeRule() @Test fun noUniqueElementTest() { ComposeUiBlockScreen { contactBlock1.blockMatcher.withUseUnmergedTree(true).printToLog("tree") contactBlock1.statusDeepSearchText.assertIsDisplayed() AssertUtils.assertException { contactBlock1.nameWithoutDeepSearch.withTimeout(100).assertIsDisplayed() } contactBlock2.name.assertIsDisplayed() } } @Test fun uiBlockInBlock() { ComposeUiBlockScreen { contactListBlock.blockMatcher.withUseUnmergedTree(true).printToLog("tree") contactListBlock.itemWithoutDesc.statusDeepSearchText.assertIsDisplayed() contactListBlock.itemWithoutDesc.uiBlock.assertIsDisplayed() contactListBlock.item1BlockWithDesc.name.assertIsDisplayed() AssertUtils.assertException { contactListBlock.itemWithoutDesc.nameWithoutDeepSearch.withTimeout(100).assertIsDisplayed() } } } @Test fun childElementDescription() { val blockDesc = "Parent_Name" val expectedChildName = "${ContactUiBlockWithDesc.сhildNameDesc} $blockDesc" ContactUiBlockWithDesc(hasTestTag(contactBlock1Tag), blockDesc).name.assertIsDisplayed().withResultHandler { Assert.assertEquals(expectedChildName, it.operation.elementInfo.name) }.withTimeout(100).assertTextContains("Invalid text") } @Test fun childBlockDescriptionTest() { val expectedItem1Description = "1 $descriptionPrefix $listBlockDesc" val expectedItem2Description = "2 $descriptionPrefix $listBlockDesc" val expectedChildNameDescInBlock1 = "$сhildNameDesc $expectedItem1Description" val expectedChildNameDescInBlock2 = "$сhildNameDesc $expectedItem2Description" ComposeUiBlockScreen { softAssertion { contactListBlock.item1BlockWithDesc.uiBlock.withTimeout(100).withResultHandler { Assert.assertEquals(expectedItem1Description, it.operation.elementInfo.name) }.assertTextEquals("Invalid") contactListBlock.item1BlockWithDesc.name.withTimeout(100).withResultHandler { Assert.assertEquals(expectedChildNameDescInBlock1, it.operation.elementInfo.name) }.assertTextEquals("Invalid") contactListBlock.item2BlockFactory.name.withTimeout(100).withResultHandler { Assert.assertEquals(expectedChildNameDescInBlock2, it.operation.elementInfo.name) }.assertTextEquals("Invalid") } } } @Test fun properSearchOfElementsTest(){ ComposeUiBlockScreen { softAssertion { contactBlock1.statusDeepSearchText.assertTextContains(CONTACTS[0].status) contactListBlock.item1BlockWithDesc.name.assertTextContains(CONTACTS[0].name) contactListBlock.item1BlockWithDesc.status.assertTextContains(CONTACTS[0].status) contactListBlock.item2BlockFactory.name.assertTextContains(CONTACTS[1].name) contactListBlock.item2BlockFactory.status.assertTextContains(CONTACTS[1].status) } } } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/elements/DataPickerTest.kt ================================================ package com.atiurin.sampleapp.tests.compose.elements import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.SemanticsMatcher import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.performCustomAccessibilityActionWithLabel import com.atiurin.sampleapp.activity.ComposeRouterActivity import com.atiurin.sampleapp.compose.DatePickerTestData import com.atiurin.sampleapp.compose.DatePickerTestTags import com.atiurin.sampleapp.compose.DatePickerTestTags.SetDatePickerTimeCustomActionLabel import com.atiurin.sampleapp.compose.screen.NavigationTestTags import com.atiurin.sampleapp.framework.utils.TimeUtils import com.atiurin.sampleapp.tests.BaseTest import com.atiurin.sampleapp.utils.convertMillisToDate import com.atiurin.ultron.core.compose.createSimpleUltronComposeRule import com.atiurin.ultron.core.compose.nodeinteraction.UltronComposeSemanticsNodeInteraction import com.atiurin.ultron.core.compose.operation.ComposeOperationType import com.atiurin.ultron.core.compose.operation.UltronComposeOperationParams import com.atiurin.ultron.extensions.assertTextContains import com.atiurin.ultron.extensions.click import com.atiurin.ultron.testlifecycle.setupteardown.SetUpRule import org.junit.Test import java.util.concurrent.TimeUnit class DataPickerTest : BaseTest() { private val composeRule = createSimpleUltronComposeRule() private val navigateRule = SetUpRule().add { hasTestTag(NavigationTestTags.DatePicker).click() } init { ruleSequence.add(composeRule, navigateRule) } /** * [convertMillisToDate] is defined in app to show the date, see [com.atiurin.sampleapp.compose.DatePickerKt.DatePickerDocked] * [TimeUtils.getTimestampStartOfDay] is used cause DatePicker return a start of the selected date timestamp */ @Test fun selectDateTest(){ hasTestTag(DatePickerTestTags.DockedIconButton).click() val time = TimeUtils.getTimestampStartOfDay() + TimeUnit.DAYS.toMillis(120) hasTestTag(DatePickerTestTags.DataPicker).setDatePickerTime(time) hasTestTag(DatePickerTestTags.SelectedDateValue).assertTextContains(convertMillisToDate(time)) } } // Make this action native for Ultron @OptIn(ExperimentalTestApi::class) fun UltronComposeSemanticsNodeInteraction.setDatePickerTime(timeMs: Long) = perform( UltronComposeOperationParams( operationName = "SetDatePickerTime '${TimeUtils.formatTimestamp(timeMs)}' for '${elementInfo.name}'", operationDescription = "Compose SetDatePickerTime '${TimeUtils.formatTimestamp(timeMs)}' for '${elementInfo.name}' during $timeoutMs ms", operationType = ComposeOperationType.CUSTOM ) ) { DatePickerTestData.time = timeMs semanticsNodeInteraction.performCustomAccessibilityActionWithLabel(SetDatePickerTimeCustomActionLabel) } fun SemanticsMatcher.setDatePickerTime(timeMs: Long) = UltronComposeSemanticsNodeInteraction(this).setDatePickerTime(timeMs) ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso/CustomClicksTest.kt ================================================ package com.atiurin.sampleapp.tests.espresso import androidx.test.core.app.ActivityScenario import androidx.test.espresso.matcher.ViewMatchers.withId import com.atiurin.sampleapp.R import com.atiurin.sampleapp.activity.CustomClicksActivity import com.atiurin.sampleapp.tests.BaseTest import com.atiurin.ultron.extensions.* import com.atiurin.ultron.testlifecycle.setupteardown.SetUpRule import org.junit.Test class CustomClicksTest : BaseTest() { private val startActivity = SetUpRule().add { ActivityScenario.launch(CustomClicksActivity::class.java) } init { ruleSequence.addLast(startActivity) } @Test fun clickTopLeft() { withId(R.id.imageView).clickTopLeft(offsetX = 30, offsetY = 30) withId(R.id.rB_top_left).isChecked() } @Test fun clickTopCenter() { withId(R.id.imageView).clickTopCenter(offsetY = 30) withId(R.id.rB_top_center).isChecked() } @Test fun clickTopRight() { withId(R.id.imageView).clickTopRight(offsetX = -30, offsetY = 30) withId(R.id.rB_top_right).isChecked() } @Test fun clickCenterRight() { withId(R.id.imageView).clickCenterRight(offsetX = -30) withId(R.id.rB_center_right).isChecked() } @Test fun clickBottomRight() { withId(R.id.imageView).clickBottomRight(offsetX = -30, offsetY = -30) withId(R.id.rB_bottom_right).isChecked() } @Test fun clickBottomCenter() { withId(R.id.imageView).clickBottomCenter(offsetY = -30) withId(R.id.rB_bottom_center).isChecked() } @Test fun clickBottomLeft() { withId(R.id.imageView).clickBottomLeft(offsetX = 30, offsetY = -30) withId(R.id.rB_bottom_left).isChecked() } @Test fun clickCenterLeft() { withId(R.id.imageView).clickCenterLeft(offsetX = 30) withId(R.id.rB_center_left).isChecked() } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso/CustomMatchersTest.kt ================================================ package com.atiurin.sampleapp.tests.espresso import androidx.test.espresso.matcher.ViewMatchers.withId import com.atiurin.sampleapp.R import com.atiurin.sampleapp.activity.MainActivity import com.atiurin.sampleapp.data.repositories.CONTACTS import com.atiurin.sampleapp.data.repositories.ContactRepositoty import com.atiurin.sampleapp.pages.ChatPage import com.atiurin.sampleapp.tests.BaseTest import com.atiurin.ultron.custom.espresso.matcher.first import com.atiurin.ultron.custom.espresso.matcher.hierarchyNumber import com.atiurin.ultron.extensions.click import com.atiurin.ultron.extensions.hasText import com.atiurin.ultron.testlifecycle.activity.UltronActivityRule import org.junit.Test class CustomMatchersTest : BaseTest() { private val activityTestRule = UltronActivityRule(MainActivity::class.java) init { ruleSequence.addLast(activityTestRule) } @Test fun actionOnFirstMatchedView(){ withId(R.id.tv_name).first().click() ChatPage.assertToolbarTitle(ContactRepositoty.getFirst().name) } @Test fun assertionOnFirstMatchedView(){ withId(R.id.tv_name).first().hasText(ContactRepositoty.getFirst().name) } @Test fun actionOnHierarchyNumberedItem(){ withId(R.id.tv_name).hierarchyNumber(1).click() ChatPage.assertToolbarTitle(CONTACTS[1].name) } @Test fun assertionOnHierarchyNumberedItem(){ withId(R.id.tv_name).hierarchyNumber(1).hasText(CONTACTS[1].name) } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso/DemoEspressoTest.kt ================================================ package com.atiurin.sampleapp.tests.espresso import com.atiurin.sampleapp.activity.MainActivity import com.atiurin.sampleapp.pages.ChatPage import com.atiurin.sampleapp.pages.FriendsListPage import com.atiurin.sampleapp.tests.BaseTest import com.atiurin.ultron.core.espresso.UltronEspresso import com.atiurin.ultron.extensions.doesNotExist import com.atiurin.ultron.extensions.isDisplayed import com.atiurin.ultron.testlifecycle.activity.UltronActivityRule import org.junit.Test class DemoEspressoTest : BaseTest() { private val activityTestRule = UltronActivityRule(MainActivity::class.java) init { ruleSequence.addLast(activityTestRule) } @Test fun friendsItemCheck() { FriendsListPage { assertName("Janice") assertStatus("Janice", "Oh. My. God") } } @Test fun sendMessage() { FriendsListPage.openChat("Chandler Bing") ChatPage .clearHistory() .sendMessage("test message") } @Test fun checkMessagesPositionsInChat() { val firstMessage = "first message" val secondMessage = "second message" FriendsListPage.openChat("Janice") ChatPage { clearHistory() sendMessage(firstMessage) sendMessage(secondMessage) assertMessageTextAtPosition(0, firstMessage) } } @Test fun pressBackTest(){ FriendsListPage.openChat("Chandler Bing") ChatPage.assertPageDisplayed() UltronEspresso.pressBack() FriendsListPage.assertPageDisplayed() } @Test fun openContextualActionModeOverflowMenuTest(){ FriendsListPage.openChat("Chandler Bing") ChatPage.clearHistoryBtn.doesNotExist() UltronEspresso.openContextualActionModeOverflowMenu() ChatPage.clearHistoryBtn.isDisplayed() } @Test fun openActionBarOverflowOrOptionsMenuTest(){ FriendsListPage.openChat("Chandler Bing") ChatPage.clearHistoryBtn.doesNotExist() UltronEspresso.openActionBarOverflowOrOptionsMenu() ChatPage.clearHistoryBtn.isDisplayed() } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso/RecyclerPerfTest.kt ================================================ package com.atiurin.sampleapp.tests.espresso import androidx.recyclerview.widget.RecyclerView import androidx.test.espresso.Espresso.onView import androidx.test.espresso.contrib.RecyclerViewActions import androidx.test.espresso.matcher.ViewMatchers.hasDescendant import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import com.atiurin.sampleapp.MyApplication import com.atiurin.sampleapp.R import com.atiurin.sampleapp.activity.MainActivity import com.atiurin.sampleapp.data.repositories.ContactRepositoty import com.atiurin.sampleapp.framework.Log import com.atiurin.sampleapp.pages.FriendsListPage import com.atiurin.sampleapp.tests.BaseTest import com.atiurin.ultron.extensions.isDisplayed import com.atiurin.ultron.testlifecycle.activity.UltronActivityRule import com.atiurin.ultron.testlifecycle.setupteardown.SetUpRule import org.junit.Test class RecyclerPerfTest : BaseTest() { private val activityTestRule = UltronActivityRule(MainActivity::class.java) private val timeoutRule = SetUpRule().add { MyApplication.CONTACTS_LOADING_TIMEOUT_MS = 0L } init { ruleSequence.addLast(timeoutRule, activityTestRule) } @Test fun test2() { Log.time("Scroll+Click") { (0..100).forEach { _ -> onView(withId(R.id.recycler_friends)) .perform( RecyclerViewActions .scrollTo( hasDescendant(withText(ContactRepositoty.getLast().name)), ) ) withText(ContactRepositoty.getLast().name).isDisplayed() onView(withId(R.id.recycler_friends)) .perform( RecyclerViewActions .scrollTo( hasDescendant(withText(ContactRepositoty.getFirst().name)), ) ) withText(ContactRepositoty.getFirst().name).isDisplayed() } } } @Test fun recyclerViewV1PerfTest() { Log.time("FriendsPageClick") { (0..100).forEach { _ -> FriendsListPage.getListItem(ContactRepositoty.getLast().name).name.isDisplayed() FriendsListPage.getListItem(ContactRepositoty.getFirst().name).name.isDisplayed() } } } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso/RecyclerViewTest.kt ================================================ package com.atiurin.sampleapp.tests.espresso import androidx.test.espresso.matcher.ViewMatchers.Visibility import androidx.test.espresso.matcher.ViewMatchers.hasDescendant import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import com.atiurin.sampleapp.MyApplication import com.atiurin.sampleapp.R import com.atiurin.sampleapp.activity.MainActivity import com.atiurin.sampleapp.data.repositories.CONTACTS import com.atiurin.sampleapp.data.repositories.ContactRepositoty import com.atiurin.sampleapp.framework.utils.AssertUtils import com.atiurin.sampleapp.pages.ChatPage import com.atiurin.sampleapp.pages.FriendsListPage import com.atiurin.sampleapp.tests.BaseTest import com.atiurin.ultron.core.espresso.recyclerview.withRecyclerView import com.atiurin.ultron.extensions.withAssertion import com.atiurin.ultron.extensions.withName import com.atiurin.ultron.extensions.withTimeout import com.atiurin.ultron.testlifecycle.activity.UltronActivityRule import com.atiurin.ultron.testlifecycle.setupteardown.SetUp import com.atiurin.ultron.testlifecycle.setupteardown.SetUpRule import com.atiurin.ultron.testlifecycle.setupteardown.TearDown import com.atiurin.ultron.testlifecycle.setupteardown.TearDownRule import org.hamcrest.Matchers.allOf import org.hamcrest.Matchers.containsString import org.junit.Assert import org.junit.Test class RecyclerViewTest : BaseTest() { companion object { const val CUSTOM_TIMEOUT = "CUSTOM_TIMEOUT" val notExistItemMatcher = hasDescendant(withText("zxcbmzxmbc")) } private val setUpRule = SetUpRule().add(CUSTOM_TIMEOUT) { MyApplication.CONTACTS_LOADING_TIMEOUT_MS = 6_000L } private val tearDownRule = TearDownRule().add(CUSTOM_TIMEOUT) { MyApplication.CONTACTS_LOADING_TIMEOUT_MS = 500L } init { ruleSequence.add(setUpRule, tearDownRule).addLast(UltronActivityRule(MainActivity::class.java)) } val page = FriendsListPage @Test fun childTest() { withRecyclerView(R.id.recycler_friends).withName("Friends list").item(0).getChild(withId(R.id.tv_status).withName("Status")).hasText(ContactRepositoty.getFirst().status) } @Test fun testDisplayedItemPositions() { for (index in 0..3) { page.recycler.item(index).assertMatches(hasDescendant(withText(CONTACTS[index].name))) .assertMatches(hasDescendant(withText(CONTACTS[index].status))).isDisplayed() } } @Test fun getRecyclerViewTest() { val view = page.recycler.getRecyclerViewList() Assert.assertNotNull(view) Assert.assertEquals(Visibility.VISIBLE.value, view.visibility) } @Test fun scrollToItemTest() { val contact = CONTACTS[CONTACTS.size - 1] val item = page.getListItem(contact.name) item.isDisplayed() item.name.hasText(contact.name) item.status.hasText(contact.status) } @Test fun wrongChild() { val contact = CONTACTS[CONTACTS.size - 1] val wrongContact = CONTACTS[CONTACTS.size - 2] val item = page.getListItem(contact.name) item.isDisplayed() AssertUtils.assertException { item.name.withTimeout(100).hasText(wrongContact.name) } } @Test fun recyclerViewItemClassTest() { val contact = CONTACTS[1] with(page.getListItem(contact.name)) { this.isDisplayed().isClickable() this.name.isDisplayed().isEnabled().hasText(contact.name) this.status.isDisplayed().isEnabled().hasText(contact.status) } } @Test fun scrollToLastItem() { withRecyclerView(R.id.recycler_friends).item(CONTACTS.size - 1).isDisplayed() } @Test fun scrollToLastWithMatcher() { withRecyclerView(R.id.recycler_friends).item(hasDescendant(withText("Friend14"))).isDisplayed() } @Test fun getNotExistedRecyclerItemWithPosition() { AssertUtils.assertException { page.recycler.item(100).withTimeout(100).isDisplayed() } } @Test fun assertListSize() { page.recycler.assertSize(CONTACTS.size) } @Test fun recyclerView_notExist() { AssertUtils.assertException { withRecyclerView(withText("Not existed recycler")).withTimeout(100).isDisplayed() } } @Test fun item_notExist() { AssertUtils.assertException { page.recycler.item(withText("Not existed item"), false).withTimeout(100).isDisplayed() } } @Test fun item_notExist_executionTime() { val timeout = 5_000L AssertUtils.assertExecTimeMoreThen(timeout){ AssertUtils.assertException { runCatching { page.recycler.withTimeout(timeout).item(withText("Not existed item")).isDisplayed() } } } } @Test @SetUp(CUSTOM_TIMEOUT) @TearDown(CUSTOM_TIMEOUT) fun defaultTimeoutOnItemWaiting() { AssertUtils.assertException { page.recycler.item(10).isDisplayed() } } @Test @SetUp(CUSTOM_TIMEOUT) @TearDown(CUSTOM_TIMEOUT) fun customTimeoutOnItemWaiting() { withRecyclerView(R.id.recycler_friends, 8000).item(10).isDisplayed() } @Test @SetUp(CUSTOM_TIMEOUT) @TearDown(CUSTOM_TIMEOUT) fun item_autoScroll_False_item_NotLoaded() { AssertUtils.assertException { page.recycler.item(10, false).isDisplayed() } } @Test @SetUp(CUSTOM_TIMEOUT) @TearDown(CUSTOM_TIMEOUT) fun item_autoScroll_False_scroll_Force() { withRecyclerView(R.id.recycler_friends, 8_000).item(10, false).scrollToItem().isDisplayed() } @Test fun itemMatcher_autoScroll_false_itemNotDisplayed() { AssertUtils.assertException { page.recycler.item( hasDescendant(withText("Friend14")), false ).isDisplayed() } } @Test fun itemMatcher_autoScroll_false_scroll_force() { page.recycler.item(hasDescendant(withText("Friend14")), false).scrollToItem().isDisplayed() } @Test @SetUp(CUSTOM_TIMEOUT) @TearDown(CUSTOM_TIMEOUT) fun itemMatcher_autoScroll_false() { AssertUtils.assertException { page.recycler.item( hasDescendant(withText("Friend14")), false ).scrollToItem().isDisplayed() } } @Test @SetUp(CUSTOM_TIMEOUT) @TearDown(CUSTOM_TIMEOUT) fun itemMatcher_autoScroll_true_custom_timeout() { withRecyclerView(R.id.recycler_friends, 10_000).item(hasDescendant(withText("Friend14"))).isDisplayed() } @Test fun getViewHolder() { val position = CONTACTS.size - 1 Assert.assertEquals( position, page.recycler.item(hasDescendant(withText(CONTACTS[position].name))) .getViewHolder() ?.layoutPosition ) } @Test fun transferFromGenericToSubclass() { val position = 5 page.recycler.getItem(position) .status.hasText(CONTACTS[position].status).isDisplayed() } @Test fun getViewHolderList() { page.recycler.waitItemsLoaded() Assert.assertTrue(page.recycler.getViewHolderList(hasDescendant(withId(R.id.tv_name))).isNotEmpty()) } @Test fun waitLoaded_ofAlreadyLoadedList() { page.recycler.waitItemsLoaded() page.recycler.waitItemsLoaded() } @Test fun waitLoaded_allItemsLoaded() { val count = page.recycler.waitItemsLoaded().getSize() Assert.assertEquals(CONTACTS.size, count) } @Test fun getLastItem() { page.recycler.lastItem().isDisplayed().click() } @Test fun getLastItemWithCustomType() { page.recycler.getLastItem().name.hasText(ContactRepositoty.getLast().name) } @Test fun perfScroll() { page.recycler.apply { for (i in 0..10) { lastItem().isDisplayed() firstItem().isDisplayed() } } } @Test fun getViewHolderAtPosition_outOfVisibleList() { Assert.assertNull(page.recycler.waitItemsLoaded().getViewHolderAtPosition(15)) } @Test fun getViewHolderAtPosition_inVisibleList() { Assert.assertNotNull(page.recycler.waitItemsLoaded().getViewHolderAtPosition(2)) } @Test fun getItemsAdapterPositionList() { page.recycler.waitItemsLoaded() val matcher = hasDescendant(allOf(withId(R.id.tv_name), withText(containsString("Friend")))) Assert.assertEquals(0, page.recycler.getViewHolderList(matcher).size) Assert.assertTrue(page.recycler.getItemsAdapterPositionList(matcher).isNotEmpty()) } @Test fun firstItemMatched_existItem() { val pattern = "Friend" val expectedContacts = CONTACTS.filter { it.name.contains(pattern) } val matcher = hasDescendant(allOf(withId(R.id.tv_name), withText(containsString(pattern)))) page.recycler.firstItemMatched(matcher).isDisplayed().click() ChatPage.assertToolbarTitle(expectedContacts.first().name) } @Test fun itemMatched_existItem() { val pattern = "Friend" val expectedContacts = CONTACTS.filter { it.name.contains(pattern) } val matcher = hasDescendant(allOf(withId(R.id.tv_name), withText(containsString(pattern)))) page.recycler.itemMatched(matcher, 1).isDisplayed().click() ChatPage.assertToolbarTitle(expectedContacts[1].name) } @Test fun itemMatched_notExistItem() { val matcher = hasDescendant(allOf(withId(R.id.tv_name), withText(containsString("Friend")))) AssertUtils.assertException { page.recycler.withTimeout(1000).itemMatched(matcher, 99) } } @Test fun lastItemMatched_existItem() { val pattern = "Friend" val expectedContacts = CONTACTS.filter { it.name.contains(pattern) } val matcher = hasDescendant(allOf(withId(R.id.tv_name), withText(containsString(pattern)))) page.recycler.itemMatched(matcher, expectedContacts.lastIndex).isDisplayed().click() ChatPage.assertToolbarTitle(expectedContacts.last().name) } @Test fun getFirstItemMatched_existItem() { val pattern = "Friend" val expectedContact = CONTACTS.filter { it.name.contains(pattern) }.first() val matcher = hasDescendant(allOf(withId(R.id.tv_name), withText(containsString(pattern)))) page.recycler.getFirstItemMatched(matcher).apply { name.isDisplayed().hasText(expectedContact.name) status.hasText(expectedContact.status) click() } ChatPage.assertToolbarTitle(expectedContact.name) } @Test fun getItemMatched_existItem() { val pattern = "Friend" val expectedContact = CONTACTS.filter { it.name.contains(pattern) }[1] val matcher = hasDescendant(allOf(withId(R.id.tv_name), withText(containsString(pattern)))) page.recycler.getItemMatched(matcher, 1).apply { name.isDisplayed().hasText(expectedContact.name) status.hasText(expectedContact.status) click() } ChatPage.assertToolbarTitle(expectedContact.name) } @Test fun getItemMatched_notExistItem() { val matcher = hasDescendant(allOf(withId(R.id.tv_name), withText(containsString("Friend")))) AssertUtils.assertException { page.recycler.withTimeout(1000).getItemMatched(matcher, 99) } } @Test fun getLastItemMatched_existItem() { val pattern = "Friend" val expectedContacts = CONTACTS.filter { it.name.contains(pattern) } val matcher = hasDescendant(allOf(withId(R.id.tv_name), withText(containsString(pattern)))) page.recycler.getItemMatched(matcher, expectedContacts.lastIndex).apply { name.isDisplayed().hasText(expectedContacts.last().name) status.hasText(expectedContacts.last().status) click() } ChatPage.assertToolbarTitle(expectedContacts.last().name) } @Test fun assertItemNotExist_notExistItem() { page.recycler.assertItemNotExist(notExistItemMatcher, 2000) } @Test fun assertItemNotExist_existItem() { val matcher = hasDescendant(allOf(withId(R.id.tv_name), withText(containsString(CONTACTS.first().name)))) AssertUtils.assertException { page.recycler.assertItemNotExist(matcher, 2000) } } @Test fun assertItemNotExistImmediately_notExistItem() { page.recycler.assertItemNotExistImmediately(notExistItemMatcher, 2000) } @Test fun assertItemNotExistImmediately_existItem() { page.recycler.waitItemsLoaded() val matcher = hasDescendant(allOf(withId(R.id.tv_name), withText(containsString(CONTACTS.first().name)))) AssertUtils.assertException { page.recycler.assertItemNotExistImmediately(matcher, 2000) } } @Test fun assertItemOutOfLimitNotFound() { val rv = withRecyclerView(R.id.recycler_friends, itemSearchLimit = 2) AssertUtils.assertException { rv.withTimeout(2000L) .item(hasDescendant(allOf(withId(R.id.tv_name), withText(containsString(CONTACTS[10].name))))) .click() } } @Test fun assertItemInLimitFound() { val rv = withRecyclerView(R.id.recycler_friends, itemSearchLimit = 10) rv.withTimeout(2000L) .item(hasDescendant(allOf(withId(R.id.tv_name), withText(containsString(CONTACTS[2].name))))) .isDisplayed() } @Test fun createHandlerFromUiTest() { page.recycler.getItemAdapterPositionAtIndex(hasDescendant(allOf(withId(R.id.tv_name), withText(containsString(CONTACTS.last().name)))), 0) } //item+offset @Test fun item_scrollOffsetInItemCountRange_MatcherItem() { val target = 5 val offset = 10 val targetContact = CONTACTS[target] val offsetContact = CONTACTS[target + offset] page.recycler.item(page.getItemMatcher(targetContact), scrollOffset = offset) page.recycler.item(page.getItemMatcher(offsetContact), autoScroll = false).isDisplayed().click() } @Test fun item_scrollOffsetInItemCountRangeBothAreVisible_MatcherItem() { val target = 8 val offset = 2 val targetContact = CONTACTS[target] val offsetContact = CONTACTS[target + offset] page.recycler.item(page.getItemMatcher(targetContact), scrollOffset = offset).isDisplayed() page.recycler.item(page.getItemMatcher(offsetContact), autoScroll = false).isDisplayed() } @Test fun item_scrollOffsetOutOfItemCountRange_MatcherItem() { val target = 5 val offset = CONTACTS.size val targetContact = CONTACTS[target] val offsetContact = CONTACTS.last() page.recycler.item(page.getItemMatcher(targetContact), scrollOffset = offset) page.recycler.item(page.getItemMatcher(offsetContact), autoScroll = false).isDisplayed().click() } @Test fun item_scrollOffsetLessThenZero_MatcherItem() { val target = 8 val offset = -18 val targetContact = CONTACTS[target] val offsetContact = CONTACTS.first() page.recycler.item(page.getItemMatcher(targetContact), scrollOffset = offset) page.recycler.item(page.getItemMatcher(offsetContact), autoScroll = false).isDisplayed().click() } @Test fun item_scrollOffsetInItemCountRange_positionItem() { val target = 2 val offset = 12 val offsetContact = CONTACTS[target + offset] page.recycler.item(target, scrollOffset = offset) page.recycler.item(target + offset, autoScroll = false).isDisplayed() page.recycler.getItem(target + offset, autoScroll = false).name.hasText(offsetContact.name).isDisplayed() } @Test fun item_scrollOffsetInItemCountRangeBothAreVisible_positionItem() { val target = 8 val offset = 2 val targetContact = CONTACTS[target] val offsetContact = CONTACTS[target + offset] page.recycler.item(target, scrollOffset = offset).isDisplayed() page.recycler.item(target + offset, autoScroll = false).isDisplayed() page.recycler.getItem(target, autoScroll = false).name.hasText(targetContact.name).isDisplayed() page.recycler.getItem(target + offset, autoScroll = false).name.hasText(offsetContact.name).isDisplayed() } @Test fun item_scrollOffsetOutOfItemCountRange_positionItem() { val target = 5 val offset = CONTACTS.size val offsetContact = CONTACTS.last() page.recycler.item(target, scrollOffset = offset) page.recycler.lastItem(autoScroll = false).isDisplayed() page.recycler.getLastItem(autoScroll = false).name.hasText(offsetContact.name).isDisplayed() } @Test fun item_scrollOffsetLessThenZero_positionItem() { val target = 10 val offset = -18 val offsetContact = CONTACTS.first() page.recycler.item(target, scrollOffset = offset) page.recycler.getFirstItem(autoScroll = false).name.hasText(offsetContact.name).isDisplayed() } //itemMatched+offset @Test fun itemMatched_scrollOffsetInItemCountRange_MatcherItem() { val target = 5 val offset = 10 val targetContact = CONTACTS[target] val offsetContact = CONTACTS[target + offset] page.recycler.itemMatched(page.getItemMatcher(targetContact), 0, scrollOffset = offset) page.recycler.item(page.getItemMatcher(offsetContact), autoScroll = false).isDisplayed().click() } @Test fun itemMatched_scrollOffsetInItemCountRangeBothAreVisible_MatcherItem() { val target = 8 val offset = 2 val targetContact = CONTACTS[target] val offsetContact = CONTACTS[target + offset] page.recycler.itemMatched(page.getItemMatcher(targetContact), 0, scrollOffset = offset).isDisplayed() page.recycler.item(page.getItemMatcher(offsetContact), autoScroll = false).isDisplayed() } @Test fun itemMatched_scrollOffsetOutOfItemCountRange_MatcherItem() { val target = 5 val offset = CONTACTS.size val targetContact = CONTACTS[target] val offsetContact = CONTACTS.last() page.recycler.itemMatched(page.getItemMatcher(targetContact), 0, scrollOffset = offset) page.recycler.item(page.getItemMatcher(offsetContact), autoScroll = false).isDisplayed().click() } //firstItemMatched+offset @Test fun firstItemMatched_scrollOffsetInItemCountRange_MatcherItem() { val target = 5 val offset = 10 val targetContact = CONTACTS[target] val offsetContact = CONTACTS[target + offset] page.recycler.firstItemMatched(page.getItemMatcher(targetContact), scrollOffset = offset) page.recycler.item(page.getItemMatcher(offsetContact), autoScroll = false).isDisplayed().click() } //lastItemMatched+offset @Test fun lastItemMatched_scrollOffsetInItemCountRange_MatcherItem() { val target = 5 val offset = 10 val targetContact = CONTACTS[target] val offsetContact = CONTACTS[target + offset] page.recycler.lastItemMatched(page.getItemMatcher(targetContact), scrollOffset = offset) page.recycler.item(page.getItemMatcher(offsetContact), autoScroll = false).isDisplayed().click() } //getItem+offset @Test fun getItem_scrollOffsetInItemCountRange_MatcherItem() { val target = 5 val offset = 10 val targetContact = CONTACTS[target] val offsetContact = CONTACTS[target + offset] page.recycler.getItem(page.getItemMatcher(targetContact), scrollOffset = offset) page.recycler.getItem(page.getItemMatcher(offsetContact), autoScroll = false).name.hasText(offsetContact.name).isDisplayed() } @Test fun getItem_scrollOffsetInItemCountRangeBothAreVisible_MatcherItem() { val target = 8 val offset = 2 val targetContact = CONTACTS[target] val offsetContact = CONTACTS[target + offset] page.recycler.getItem(page.getItemMatcher(targetContact), scrollOffset = offset) page.recycler.getItem(page.getItemMatcher(offsetContact), autoScroll = false).name.hasText(offsetContact.name).isDisplayed() } @Test fun getItem_scrollOffsetOutOfItemCountRange_MatcherItem() { val target = 5 val offset = CONTACTS.size val targetContact = CONTACTS[target] val offsetContact = CONTACTS.last() page.recycler.getItem(page.getItemMatcher(targetContact), scrollOffset = offset) page.recycler.getItem(page.getItemMatcher(offsetContact), autoScroll = false).name.hasText(offsetContact.name).isDisplayed() } @Test fun getItem_scrollOffsetLessThenZero_MatcherItem() { val target = 8 val offset = -18 val targetContact = CONTACTS[target] val offsetContact = CONTACTS.first() page.recycler.getItem(page.getItemMatcher(targetContact), scrollOffset = offset) page.recycler.getItem(page.getItemMatcher(offsetContact), autoScroll = false).name.hasText(offsetContact.name).isDisplayed() } @Test fun getItem_scrollOffsetInItemCountRange_positionItem() { val target = 2 val offset = 12 val offsetContact = CONTACTS[target + offset] page.recycler.getItem(target, scrollOffset = offset) page.recycler.getItem(target + offset, autoScroll = false).name.hasText(offsetContact.name).isDisplayed() } @Test fun getItem_scrollOffsetInItemCountRangeBothAreVisible_positionItem() { val target = 8 val offset = 2 val targetContact = CONTACTS[target] val offsetContact = CONTACTS[target + offset] page.recycler.getItem(target, scrollOffset = offset).name.hasText(targetContact.name).isDisplayed() page.recycler.getItem(target + offset, autoScroll = false).name.hasText(offsetContact.name).isDisplayed() } @Test fun getItem_scrollOffsetOutOfItemCountRange_positionItem() { val target = 5 val offset = CONTACTS.size val offsetContact = CONTACTS.last() page.recycler.getItem(target, scrollOffset = offset) page.recycler.getLastItem(autoScroll = false).name.hasText(offsetContact.name).isDisplayed() } @Test fun getItem_scrollOffsetLessThenZero_positionItem() { val target = 10 val offset = -18 val offsetContact = CONTACTS.first() page.recycler.getItem(target, scrollOffset = offset) page.recycler.getFirstItem(autoScroll = false).name.hasText(offsetContact.name).isDisplayed() } //getItemMatched+offset @Test fun getItemMatched_scrollOffsetInItemCountRange_MatcherItem() { val target = 5 val offset = 10 val targetContact = CONTACTS[target] val offsetContact = CONTACTS[target + offset] page.recycler.getItemMatched(page.getItemMatcher(targetContact), 0, scrollOffset = offset) page.recycler.getItem(page.getItemMatcher(offsetContact), autoScroll = false).name.hasText(offsetContact.name).isDisplayed() } @Test fun getItemMatched_scrollOffsetInItemCountRangeBothAreVisible_MatcherItem() { val target = 8 val offset = 2 val targetContact = CONTACTS[target] val offsetContact = CONTACTS[target + offset] page.recycler.getItemMatched(page.getItemMatcher(targetContact), 0, scrollOffset = offset) page.recycler.getItem(page.getItemMatcher(offsetContact), autoScroll = false).name.hasText(offsetContact.name).isDisplayed() } @Test fun getItemMatched_scrollOffsetOutOfItemCountRange_MatcherItem() { val target = 5 val offset = CONTACTS.size val targetContact = CONTACTS[target] val offsetContact = CONTACTS.last() page.recycler.getItemMatched(page.getItemMatcher(targetContact), 0, scrollOffset = offset) page.recycler.getItem(page.getItemMatcher(offsetContact), autoScroll = false).name.hasText(offsetContact.name).isDisplayed() } //getFirstItemMatched+offset @Test fun getFirstItemMatched_scrollOffsetInItemCountRange_MatcherItem() { val target = 5 val offset = 10 val targetContact = CONTACTS[target] val offsetContact = CONTACTS[target + offset] page.recycler.getFirstItemMatched(page.getItemMatcher(targetContact), scrollOffset = offset) page.recycler.getItem(page.getItemMatcher(offsetContact), autoScroll = false).name.hasText(offsetContact.name).isDisplayed() } //getLastItemMatched+offset @Test fun getLastItemMatched_scrollOffsetInItemCountRange_MatcherItem() { val target = 5 val offset = 10 val targetContact = CONTACTS[target] val offsetContact = CONTACTS[target + offset] page.recycler.getLastItemMatched(page.getItemMatcher(targetContact), scrollOffset = offset) page.recycler.getItem(page.getItemMatcher(offsetContact), autoScroll = false).name.hasText(offsetContact.name).isDisplayed() } @Test fun validItemCustomAssertion() { val contact = CONTACTS.first() page.recycler.firstItem().withAssertion("Toolbar title = ${contact.name}") { ChatPage.assertToolbarTitle(contact.name) }.click() } @Test fun invalidItemCustomAssertion() { AssertUtils.assertException { val invalidExpectedName = "InvalidTitle" page.recycler.firstItem().withTimeout(3000).withAssertion("Toolbar title = $invalidExpectedName") { ChatPage.assertToolbarTitle(invalidExpectedName) }.click() } } @Test fun swipeUntil() { withId(R.id.recycler_friends).withAssertion(isListened = true) { withText(CONTACTS.last().name).withTimeout(200).isDisplayed() }.swipeUp() } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso/UltronActivityRuleTest.kt ================================================ package com.atiurin.sampleapp.tests.espresso import android.content.Intent import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.platform.app.InstrumentationRegistry import com.atiurin.sampleapp.activity.BusyActivity import com.atiurin.sampleapp.activity.MainActivity import com.atiurin.sampleapp.tests.BaseTest import com.atiurin.ultron.core.compose.createSimpleUltronComposeRule import com.atiurin.ultron.core.compose.createUltronComposeRule import com.atiurin.ultron.testlifecycle.activity.UltronActivityRule import org.junit.Test import org.junit.rules.Timeout import java.util.concurrent.TimeUnit class UltronActivityRuleTest: BaseTest() { //private val activityTestRule = ActivityScenarioRule(MainActivity::class.java) //private val activityTestRule = UltronActivityRule(MainActivity::class.java) //private val activityTestRule = createUltronComposeRule() private val activityTestRule = createSimpleUltronComposeRule() private val timeoutRule: Timeout = Timeout .builder() .withTimeout(100, TimeUnit.SECONDS) .withLookingForStuckThread(true) .build() init { ruleSequence.add(timeoutRule, activityTestRule) } @Test fun appNotIdle() { val intent = Intent( InstrumentationRegistry.getInstrumentation().targetContext, BusyActivity::class.java ) intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) InstrumentationRegistry.getInstrumentation().targetContext.startActivity(intent) assert(true) } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso/UltronEspressoConfigTest.kt ================================================ package com.atiurin.sampleapp.tests.espresso import androidx.test.espresso.matcher.ViewMatchers import com.atiurin.sampleapp.framework.DummyMetaObject import com.atiurin.sampleapp.framework.Log import com.atiurin.sampleapp.framework.utils.AssertUtils import com.atiurin.sampleapp.pages.UiElementsPage import com.atiurin.sampleapp.tests.UiElementsTest import com.atiurin.ultron.core.common.resultanalyzer.UltronDefaultOperationResultAnalyzer import com.atiurin.ultron.core.config.UltronCommonConfig import com.atiurin.ultron.core.config.UltronConfig import com.atiurin.ultron.core.espresso.EspressoOperationResult import com.atiurin.ultron.core.espresso.UltronEspressoOperation import com.atiurin.ultron.exceptions.UltronException import com.atiurin.ultron.extensions.click import com.atiurin.ultron.extensions.hasText import com.atiurin.ultron.extensions.isDisplayed import com.atiurin.ultron.extensions.isSuccess import com.atiurin.ultron.extensions.withAssertion import com.atiurin.ultron.extensions.withName import com.atiurin.ultron.extensions.withResultHandler import com.atiurin.ultron.extensions.withTimeout import com.atiurin.ultron.testlifecycle.setupteardown.SetUp import com.atiurin.ultron.testlifecycle.setupteardown.SetUpRule import com.atiurin.ultron.testlifecycle.setupteardown.TearDown import com.atiurin.ultron.testlifecycle.setupteardown.TearDownRule import org.junit.Assert import org.junit.Test import kotlin.system.measureTimeMillis class UltronEspressoConfigTest : UiElementsTest() { val page = UiElementsPage companion object { const val SET_CUSTOM_RESULT_ANALYZER = "SET_CUSTOM_RESULT_ANALYZER" const val SET_DEFAULT_CONFIG = "SET_DEFAULT_CONFIG" const val SET_CUSTOM_ASSERTIONS_TIMEOUT = "SET_ASSERTIONS_TIMEOUT" const val SET_CUSTOM_ACTIONS_TIMEOUT = "SET_ACTIONS_TIMEOUT" const val SET_DEFAULT_TIMEOUT = "DROP_DEFAULT_TIMEOUT" const val MODIFIED_OPERATIONS_TIMEOUT = 7_000L } val setUpRule = SetUpRule() .add(SET_CUSTOM_RESULT_ANALYZER) { UltronConfig.Espresso.setResultAnalyzer { Log.debug("SET_CUSTOM_RESULT_ANALYZER ${it.success}") if (it.success) throw UltronException("Special reversed analyzer exception on ${it.description}") it.success } }.add(SET_CUSTOM_ASSERTIONS_TIMEOUT) { UltronConfig.Espresso.ASSERTION_TIMEOUT = MODIFIED_OPERATIONS_TIMEOUT } .add(SET_CUSTOM_ACTIONS_TIMEOUT) { UltronConfig.Espresso.ACTION_TIMEOUT = MODIFIED_OPERATIONS_TIMEOUT } private val tearDownRule = TearDownRule() .add(SET_DEFAULT_CONFIG) { UltronConfig.Espresso.resultAnalyzer = UltronDefaultOperationResultAnalyzer() } .add(SET_DEFAULT_TIMEOUT) { UltronConfig.Espresso.ACTION_TIMEOUT = UltronCommonConfig.Defaults.OPERATION_TIMEOUT_MS UltronConfig.Espresso.ASSERTION_TIMEOUT = UltronCommonConfig.Defaults.OPERATION_TIMEOUT_MS } init { ruleSequence.add(setUpRule, tearDownRule) } @Test @SetUp(SET_CUSTOM_RESULT_ANALYZER) @TearDown(SET_DEFAULT_CONFIG) fun resultAnalyzer_reversed_should_throw_on_success_action() { AssertUtils.assertException { page.button.click() } } @Test @SetUp(SET_CUSTOM_RESULT_ANALYZER) @TearDown(SET_DEFAULT_CONFIG) fun resultAnalyzer_reversed_should_NOT_throw_on_failed_action() { page.notExistElement.withTimeout(100).click() } @Test @SetUp(SET_CUSTOM_RESULT_ANALYZER) @TearDown(SET_DEFAULT_CONFIG) fun resultAnalyzer_reversed_should_throw_on_success_assertion() { AssertUtils.assertException { page.button.isDisplayed() } } @Test @SetUp(SET_CUSTOM_RESULT_ANALYZER) @TearDown(SET_DEFAULT_CONFIG) fun resultAnalyzer_reversed_should_NOT_throw_on_failed_assertion() { page.notExistElement.withTimeout(100).isDisplayed() } //timeouts @Test fun withTimeout_action_default() { val default = UltronCommonConfig.Defaults.OPERATION_TIMEOUT_MS AssertUtils.assertExecTimeBetween(default, default + 5_000) { page.notExistElement.click() } } @Test fun withTimeou_actiont_customValue() { AssertUtils.assertExecTimeBetween(2_000, 4_500) { page.notExistElement.withTimeout(2000).click() } } @Test @SetUp(SET_CUSTOM_ACTIONS_TIMEOUT) @TearDown( SET_DEFAULT_TIMEOUT ) fun withTimeout_action_modifiedDefaultValue() { AssertUtils.assertExecTimeBetween( MODIFIED_OPERATIONS_TIMEOUT, MODIFIED_OPERATIONS_TIMEOUT + 2_000L ) { page.notExistElement.click() } } @Test fun withTimeout_assertion_default() { val default = UltronCommonConfig.Defaults.OPERATION_TIMEOUT_MS AssertUtils.assertExecTimeBetween( default, default + 2_000 ) { page.notExistElement.isDisplayed() } } @Test fun withTimeout_assertion_customValue() { AssertUtils.assertExecTimeBetween(2_000, 4_500) { page.notExistElement.withTimeout(2000).isDisplayed() } } @Test @SetUp(SET_CUSTOM_ASSERTIONS_TIMEOUT) @TearDown( SET_DEFAULT_TIMEOUT ) fun withTimeout_assertion_modifiedDefaultValue() { AssertUtils.assertExecTimeBetween( MODIFIED_OPERATIONS_TIMEOUT, MODIFIED_OPERATIONS_TIMEOUT + 2_000L ) { page.notExistElement.isDisplayed() } } //resultHandler @Test fun withResultHandler_action_default_true() { var result: EspressoOperationResult? = null page.button.withResultHandler { result = it }.click() Assert.assertNotNull(result) Assert.assertTrue(result!!.success) // Assert.assertTrue(result!!.exceptions.isEmpty()) Assert.assertFalse(result!!.operation.name.isNullOrEmpty()) Assert.assertFalse(result!!.operation.description.isNullOrEmpty()) Assert.assertEquals(UltronConfig.Espresso.ACTION_TIMEOUT, result!!.operation.timeoutMs) } @Test fun withResultHandler_action_default_false() { var result: EspressoOperationResult? = null page.notExistElement.withTimeout(100).withResultHandler { result = it }.click() Assert.assertNotNull(result) Assert.assertFalse(result!!.success) Assert.assertTrue(result!!.exceptions.isNotEmpty()) Assert.assertFalse(result!!.operation.name.isNullOrEmpty()) Assert.assertFalse(result!!.operation.description.isNullOrEmpty()) Assert.assertEquals(100, result!!.operation.timeoutMs) } @Test fun customAssertionTest() { val text = "some text" val execTime = measureTimeMillis { page.editTextContentDesc.withAssertion("demo name") { page.editTextContentDesc.hasText(text) }.replaceText(text) } Assert.assertTrue(execTime < UltronConfig.Espresso.ACTION_TIMEOUT) } @Test fun withAssertion_failedAssertion() { AssertUtils.assertException { page.editTextContentDesc.withTimeout(1000).withAssertion { ViewMatchers.withText("asd23213 12312").withTimeout(500).isDisplayed() }.typeText("1") } } @Test fun withAssertion_failedAssertion_timeout() { val operationTime = 1000L val execTime = measureTimeMillis { page.editTextContentDesc.isSuccess { withTimeout(operationTime).withAssertion { ViewMatchers.withText("asd23213 12312").withTimeout(100).isDisplayed() }.typeText("1") } } Assert.assertTrue(execTime > operationTime) } @Test fun withName_inOperationProps_ultronInteraction() { val name = "ElementName" page.notExistElement.withTimeout(100).withName(name).withResultHandler { result -> Assert.assertEquals(name, result.operation.elementInfo.name) }.isDisplayed() } @Test fun withName_inOperationProps_matcherExt() { val name = "ElementName" page.notExistElement.withName(name).withTimeout(100).withResultHandler { result -> Assert.assertEquals(name, result.operation.elementInfo.name) }.isDisplayed() } @Test fun withName_inExceptionMessage() { val name = "ElementNameToBeInException" runCatching { page.notExistElement.withTimeout(100).withName(name).isDisplayed() }.onFailure { exception -> Assert.assertTrue(exception.message!!.contains(name)) } } @Test fun withMeta() { val meta = DummyMetaObject("ElementMetaInfo") page.notExistElement.withTimeout(100).withMetaInfo(meta).withResultHandler { result -> Assert.assertEquals(meta, result.operation.elementInfo.meta) }.isDisplayed() } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso/UltronEspressoUiBlockTest.kt ================================================ package com.atiurin.sampleapp.tests.espresso import android.view.View import androidx.test.espresso.matcher.ViewMatchers.withId import com.atiurin.sampleapp.R import com.atiurin.sampleapp.activity.UiBlockActivity import com.atiurin.sampleapp.data.repositories.CONTACTS import com.atiurin.sampleapp.framework.utils.AssertUtils import com.atiurin.sampleapp.pages.uiblock.EspressoUiBlockScreen import com.atiurin.sampleapp.tests.BaseTest import com.atiurin.ultron.core.common.assertion.softAssertion import com.atiurin.ultron.core.espresso.UltronEspressoUiBlock import com.atiurin.ultron.extensions.withName import com.atiurin.ultron.extensions.withTimeout import com.atiurin.ultron.testlifecycle.activity.UltronActivityRule import org.hamcrest.Matcher import org.junit.Assert import org.junit.Rule import org.junit.Test class UltronEspressoUiBlockTest : BaseTest() { @get:Rule val activityRule = UltronActivityRule(UiBlockActivity::class.java) @Test fun notUniqueUiElement_WithDeepSearch() { EspressoUiBlockScreen { contactItem1.name.isDisplayed() contactItem1.deepSearchChild.withTimeout(100).isDisplayed() } } @Test fun notUniqueUiElement_WithoutDeepSearch() { EspressoUiBlockScreen { AssertUtils.assertException { blockWithoutDeepSearch.deepSearchFalse.withTimeout(100).isDisplayed() } } } @Test fun uiBlockInBlock() { EspressoUiBlockScreen { contactsListBlock.item1.name.isDisplayed().hasText(CONTACTS[0].name) contactsListBlock.item1.status.isDisplayed().hasText(CONTACTS[0].status) contactsListBlock.item2.name.isDisplayed().hasText(CONTACTS[1].name) contactsListBlock.item2.status.isDisplayed().hasText(CONTACTS[1].status) } } @Test fun childElementDescription() { val descriptionPrefix = "Item with parent" val blockDesc = "Parent_Name" val expectedChildName = "$descriptionPrefix $blockDesc" class BlockDesc(blockMatcher: Matcher, blockDescription: String) : UltronEspressoUiBlock(blockMatcher, blockDescription) { val name = child(withId(R.id.name)).withName("$descriptionPrefix $blockDescription") } BlockDesc(withId(R.id.contact_item_1), blockDesc).name.isDisplayed().withResultHandler { Assert.assertEquals(expectedChildName, it.operation.elementInfo.name) }.withTimeout(100).hasText("Invalid text") } @Test fun childBlockDescriptionTest() { val listBlockDesc = "ListBlock" val expectedItem1Description = "1 $descriptionPrefix $listBlockDesc" val expectedItem2Description = "2 $descriptionPrefix $listBlockDesc" val expectedChildNameDescInBlock1 = "$сhildNameDesc $expectedItem1Description" val expectedChildNameDescInBlock2 = "$сhildNameDesc $expectedItem2Description" val listBlock = ListUiBlock(withId(R.id.contact_items), listBlockDesc) softAssertion { listBlock.item1.uiBlock.withTimeout(100).withResultHandler { Assert.assertEquals(expectedItem1Description, it.operation.elementInfo.name) }.hasText("Invalid") listBlock.item1.name.withTimeout(100).withResultHandler { Assert.assertEquals(expectedChildNameDescInBlock1, it.operation.elementInfo.name) }.hasText("Invalid") listBlock.item2.name.withTimeout(100).withResultHandler { Assert.assertEquals(expectedChildNameDescInBlock2, it.operation.elementInfo.name) }.hasText("Invalid") } } } class BlockDesc(blockMatcher: Matcher, blockDescription: String) : UltronEspressoUiBlock(blockMatcher, blockDescription) { val name = child(withId(R.id.name)).withName("$сhildNameDesc $blockDescription") } class ListUiBlock(blockMatcher: Matcher, blockDescription: String) : UltronEspressoUiBlock(blockMatcher, blockDescription) { val item1 = child(BlockDesc(withId(R.id.contact_item_1), "1 $descriptionPrefix $blockDescription")) val item2 = child( childMatcher = withId(R.id.contact_item_2), uiBlockFactory = { updatedMatcher -> BlockDesc(updatedMatcher, blockDescription = "2 $descriptionPrefix $blockDescription") } ) } const val descriptionPrefix = "Item with parent" const val сhildNameDesc = "NamE" ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso/ViewInteractionActionsTest.kt ================================================ package com.atiurin.sampleapp.tests.espresso import android.os.SystemClock import android.view.KeyEvent import androidx.test.espresso.action.EspressoKey import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.matcher.ViewMatchers.withText import com.atiurin.sampleapp.R import com.atiurin.sampleapp.framework.ultronext.appendText import com.atiurin.sampleapp.framework.utils.AssertUtils import com.atiurin.sampleapp.framework.utils.TestDataUtils.getResourceString import com.atiurin.sampleapp.pages.UiElementsPage import com.atiurin.sampleapp.tests.UiElementsTest import com.atiurin.ultron.core.common.assertion.softAssertion import com.atiurin.ultron.core.common.assertion.verifySoftAssertions import com.atiurin.ultron.core.config.UltronCommonConfig import com.atiurin.ultron.core.config.UltronConfig import com.atiurin.ultron.core.espresso.UltronEspresso import com.atiurin.ultron.custom.espresso.action.getContentDescription import com.atiurin.ultron.custom.espresso.action.getDrawable import com.atiurin.ultron.custom.espresso.action.getText import com.atiurin.ultron.custom.espresso.assertion.hasAnyDrawable import com.atiurin.ultron.custom.espresso.assertion.hasDrawable import com.atiurin.ultron.extensions.clearText import com.atiurin.ultron.extensions.click import com.atiurin.ultron.extensions.closeSoftKeyboard import com.atiurin.ultron.extensions.doubleClick import com.atiurin.ultron.extensions.isDisplayed import com.atiurin.ultron.extensions.isSameAs import com.atiurin.ultron.extensions.isSuccess import com.atiurin.ultron.extensions.longClick import com.atiurin.ultron.extensions.perform import com.atiurin.ultron.extensions.replaceText import com.atiurin.ultron.extensions.textContains import com.atiurin.ultron.extensions.withTimeout import com.atiurin.ultron.utils.getTargetString import org.junit.Assert import org.junit.Test class ViewInteractionActionsTest : UiElementsTest() { val page = UiElementsPage @Test fun isSuccess_notExistedElement_return_false() { val startTime = SystemClock.elapsedRealtime() val result = page.notExistElement.isSuccess { isDisplayed() } val endTime = SystemClock.elapsedRealtime() Assert.assertTrue(endTime - startTime >= UltronConfig.Espresso.ASSERTION_TIMEOUT) Assert.assertFalse(result) } @Test fun isSuccess_existedElement_return_true() { Assert.assertTrue(page.button.isSuccess { isDisplayed() }) } @Test fun click_onClickable() { page.button.click() page.eventStatus.textContains(getTargetString(R.string.button_event_click)) } @Test fun click_notExisted() { AssertUtils.assertException { page.notExistElement.withTimeout(100).click() } } @Test fun longClick_onLongClickable() { page.button.longClick() page.eventStatus.textContains(getTargetString(R.string.button_event_long_click)) } @Test fun longClick_notExisted() { AssertUtils.assertException { page.notExistElement.withTimeout(100).longClick() } } @Test fun doubleClick_onClickable() { page.button.doubleClick() page.button.withTimeout(1000).isDisplayed() var success = false with(page.eventStatus) { textContains(getResourceString(R.string.button_event_click)) success = isSuccess { withTimeout(3000).textContains("1") } || isSuccess { withTimeout(2000).textContains("2") } } Assert.assertTrue(success) } @Test fun doubleClick_notExisted() { AssertUtils.assertException { page.notExistElement.withTimeout(100).doubleClick() } } @Test fun typeText_onEditable() { val text1 = "begin" val text2 = "simple text" page.editTextContentDesc .replaceText(text1) .typeText(text2) .hasText("$text1$text2") } @Test fun typeText_onNotEditable() { AssertUtils.assertException { page.eventStatus.withTimeout(100).typeText("simple text") } } @Test fun typeText_notExisted() { AssertUtils.assertException { page.notExistElement.withTimeout(100).typeText("asd") } } @Test fun replaceText_onEditable() { val text = "simple text" page.editTextContentDesc.replaceText(text).hasText(text) } @Test fun replaceText_notExisted() { AssertUtils.assertException { page.notExistElement.withTimeout(100).replaceText("asd") } } @Test fun clearText_onEditable() { page.editTextContentDesc.clearText().hasText("") } @Test fun clearText_notExisted() { AssertUtils.assertException { page.notExistElement.withTimeout(100).clearText() } } @Test fun pressKey_onEditable() { val text = "simple text" val expectedText = text.substring(0, text.length - 1) page.editTextContentDesc .replaceText(text) .click() .pressKey(KeyEvent.KEYCODE_DEL) .hasText(expectedText) } @Test fun pressKey_notExisted() { AssertUtils.assertException { page.notExistElement.withTimeout(100).pressKey(KeyEvent.KEYCODE_DEL) } } @Test fun pressEspressoKey_onEditable() { val text = "simple text" val expectedText = text.substring(0, text.length - 1) page.editTextContentDesc .replaceText(text) .click() .pressKey(EspressoKey.Builder().withKeyCode(KeyEvent.KEYCODE_DEL).build()) .hasText(expectedText) } @Test fun pressEspressoKey_notExisted() { AssertUtils.assertException { page.notExistElement.withTimeout(100) .pressKey(EspressoKey.Builder().withKeyCode(KeyEvent.KEYCODE_DEL).build()) } } @Test fun closeSoftKeyboard_whenItOpened() { page.editTextContentDesc.click() SystemClock.sleep(500) page.editTextContentDesc.closeSoftKeyboard() } @Test fun preformCustomClick_onClickable() { page.button.perform(click()) page.eventStatus.textContains(getResourceString(R.string.button_event_click)) } @Test fun performCustom_notExisted() { AssertUtils.assertException { page.notExistElement.withTimeout(100).perform(click()) } } @Test fun closeSoftKeyboardTest() { page.editTextContentDesc.click() UltronEspresso.closeSoftKeyboard() page.imageView.isDisplayed() } @Test fun customVisibilityAction() { val text = "appended" page.editTextContentDesc.appendText(text) .hasText(getTargetString(R.string.button_default_content_desc) + text) page.button.appendText(text) } @Test fun notExist_customAction() { AssertUtils.assertException { withText("not existed").withTimeout(19).appendText("asd") } } @Test fun getTextActionTest_textExist() { val text = page.appCompatTextView.getText() Assert.assertEquals(getTargetString(R.string.app_compat_text), text) } @Test fun getTextActionTest_noTextInView() { AssertUtils.assertException { page.imageView.withTimeout(100).getText() } } @Test fun getDrawable_drawableExist() { Assert.assertNotNull(page.imageView.getDrawable()) } @Test fun hasDrawableTest() { page.imageView.hasDrawable(R.drawable.ic_account) } @Test fun hasDrawable_wrongResourceId() { AssertUtils.assertException { page.imageView.withTimeout(1000).hasDrawable(R.drawable.chandler) } } @Test fun drawableCompare() { val actDr = page.imageView.getDrawable() val actDr2 = page.imageView2.getDrawable() Assert.assertTrue(actDr!!.isSameAs(actDr2!!)) } @Test fun hasAnyDrawable_noDrawable() { AssertUtils.assertException { page.emptyNotClickableImageView.withTimeout(1000).hasAnyDrawable() } } @Test fun hasAnyDrawable_imageHasDrawable() { page.imageView.hasAnyDrawable() } @Test fun getContentDesc_descIsNull() { Assert.assertEquals(null, page.imageView.getContentDescription()) } @Test fun getContentDesc_descNotNull() { Assert.assertEquals( getTargetString(R.string.button_default_content_desc), page.button.getContentDescription() ) } @Test fun verifySoftAssertionsTest() { UltronCommonConfig.testContext.softAnalyzer.clear() softAssertion(false) { withText("NotExistText").withTimeout(100).click() withText("NotExistTestTag").withTimeout(100).click() } runCatching { verifySoftAssertions() }.onFailure { exception -> val message = exception.message ?: throw RuntimeException("Empty exception message: $exception") Assert.assertTrue(message.contains("NotExistText")) Assert.assertTrue(message.contains("NotExistTestTag")) } } @Test fun softAssertionTest() { UltronCommonConfig.testContext.softAnalyzer.clear() AssertUtils.assertException { softAssertion { withText("NotExistText").withTimeout(100).click() } } } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso/ViewInteractionAssertionsTest.kt ================================================ package com.atiurin.sampleapp.tests.espresso import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.isEnabled import androidx.test.espresso.matcher.ViewMatchers.withText import com.atiurin.sampleapp.R import com.atiurin.sampleapp.framework.ultronext.assertChecked import com.atiurin.sampleapp.framework.utils.AssertUtils import com.atiurin.sampleapp.framework.utils.TestDataUtils.getResourceString import com.atiurin.sampleapp.pages.UiElementsPage import com.atiurin.sampleapp.tests.UiElementsTest import com.atiurin.ultron.core.common.assertion.softAssertion import com.atiurin.ultron.core.common.assertion.verifySoftAssertions import com.atiurin.ultron.core.config.UltronCommonConfig import com.atiurin.ultron.custom.espresso.assertion.doesNotExistInAnyVisibleRoot import com.atiurin.ultron.custom.espresso.assertion.hasCurrentHintTextColor import com.atiurin.ultron.custom.espresso.assertion.hasCurrentTextColor import com.atiurin.ultron.custom.espresso.assertion.hasHighlightColor import com.atiurin.ultron.custom.espresso.assertion.hasShadowColor import com.atiurin.ultron.custom.espresso.matcher.hasAnyDrawable import com.atiurin.ultron.custom.espresso.matcher.withDrawable import com.atiurin.ultron.extensions.assertMatches import com.atiurin.ultron.extensions.click import com.atiurin.ultron.extensions.doesNotExist import com.atiurin.ultron.extensions.exists import com.atiurin.ultron.extensions.hasContentDescription import com.atiurin.ultron.extensions.hasFocus import com.atiurin.ultron.extensions.hasText import com.atiurin.ultron.extensions.isChecked import com.atiurin.ultron.extensions.isClickable import com.atiurin.ultron.extensions.isDisplayed import com.atiurin.ultron.extensions.isEnabled import com.atiurin.ultron.extensions.isFocusable import com.atiurin.ultron.extensions.isJavascriptEnabled import com.atiurin.ultron.extensions.isNotChecked import com.atiurin.ultron.extensions.isNotClickable import com.atiurin.ultron.extensions.isNotDisplayed import com.atiurin.ultron.extensions.isNotEnabled import com.atiurin.ultron.extensions.isNotFocusable import com.atiurin.ultron.extensions.isNotSelected import com.atiurin.ultron.extensions.isSelected import com.atiurin.ultron.extensions.isSuccess import com.atiurin.ultron.extensions.textContains import com.atiurin.ultron.extensions.withTimeout import org.hamcrest.Matchers.allOf import org.hamcrest.Matchers.containsString import org.junit.Assert import org.junit.Test class ViewInteractionAssertionsTest : UiElementsTest() { val page = UiElementsPage //displayed @Test fun isDisplayed_ofDisplayedObject() { page.button.isDisplayed() } @Test fun isDisplayed_ofNotDisplayedObject() { page.radioInvisibleButton.click() AssertUtils.assertException { page.button.withTimeout(100).isDisplayed() } } @Test fun isNotDisplayed_ofDisplayedObject() { page.radioVisibleButton.click() AssertUtils.assertException { page.radioVisibleButton.withTimeout(100).isNotDisplayed() } } @Test fun isNotDisplayed_ofNotDisplayedObject() { page.radioInvisibleButton.click() page.button.isNotDisplayed() } //doesNotExist @Test fun doesNotExist_notExisted() { page.notExistElement.doesNotExist() } @Test fun doesNotExist_existed() { AssertUtils.assertException { page.button.withTimeout(100).doesNotExist() } } //doesNotExistAnyRoot @Test fun doesNotExistAnyRoot_notExisted() { page.notExistElement.doesNotExistInAnyVisibleRoot() } @Test fun doesNotExistAnyRoot_existed() { AssertUtils.assertException { page.button.withTimeout(100).doesNotExistInAnyVisibleRoot() } } //exists @Test fun exists_ExistedHiddenView() { page.hiddenButton.exists() } @Test fun exists_NotExisted() { AssertUtils.assertException { page.notExistElement.withTimeout(100).exists() } } //checked @Test fun isChecked_ofChecked() { page.checkBoxEnabled.isChecked() } @Test fun isChecked_ofNotChecked() { AssertUtils.assertException { page.checkBoxSelected.withTimeout(100).isChecked() } } @Test fun isNotChecked_ofChecked() { AssertUtils.assertException { page.checkBoxClickable.withTimeout(100).isNotChecked() } } @Test fun isNotChecked_ofNotChecked() { page.checkBoxSelected.isNotChecked() } // selected @Test fun isSelected_ofSelected() { page.checkBoxSelected.click() page.button.isSelected() } @Test fun isSelected_ofNotSelected() { AssertUtils.assertException { page.button.withTimeout(100).isSelected() } } @Test fun isNotSelected_ofSelected() { page.checkBoxSelected.click() AssertUtils.assertException { page.button.withTimeout(100).isNotSelected() } } @Test fun isNotSelected_ofNotSelected() { page.button.isNotSelected() } // enabled @Test fun isEnabled_ofEnabled() { page.button.isEnabled() } @Test fun isEnabled_ofNotEnabled() { page.checkBoxEnabled.click() AssertUtils.assertException { page.button.withTimeout(100).isEnabled() } } @Test fun isNotEnabled_ofEnabled() { AssertUtils.assertException { page.button.withTimeout(100).isNotEnabled() } } @Test fun isNotEnabled_ofNotEnabled() { page.checkBoxEnabled.click() page.button.isNotEnabled() } //clickable @Test fun isClickable_ofClickable() { page.button.isClickable() } @Test fun isClickable_ofNotClickable() { page.checkBoxClickable.click() AssertUtils.assertException { page.button.withTimeout(100).isClickable() } } @Test fun isNotClickable_ofClickable() { AssertUtils.assertException { page.button.withTimeout(100).isNotClickable() } } @Test fun isNotClickable_ofNotClickable() { page.checkBoxClickable.click() page.button.isNotClickable() } //focusable @Test fun isFocusable_ofFocusable() { page.button.isFocusable() } @Test fun isFocusable_ofNotFocusable() { page.checkBoxFocusable.click() AssertUtils.assertException { page.button.withTimeout(100).isFocusable() } } @Test fun isNotFocusable_ofFocusable() { AssertUtils.assertException { page.button.withTimeout(100).isNotFocusable() } } @Test fun isNotFocusable_ofNotFocusable() { page.checkBoxFocusable.click() page.button.isNotFocusable() } //hasFocus @Test fun hasFocus_ofFocused() { page.editTextContentDesc.click() page.editTextContentDesc.hasFocus() } @Test fun hasFocus_ofNotFocused() { AssertUtils.assertException { page.editTextContentDesc.withTimeout(100).hasFocus() } } //hasText @Test fun hasText_CorrectText_withResourceId() { page.editTextContentDesc.hasText(R.string.button_default_content_desc) } @Test fun hasText_InvalidSubstringText() { val text = getResourceString(R.string.button_default_content_desc) AssertUtils.assertException { page.editTextContentDesc.withTimeout(100).hasText(text.substring(3)) } } @Test fun hasText_InvalidText_withResourceId() { AssertUtils.assertException { page.editTextContentDesc.withTimeout(100).hasText( R.string.action_clear_history ) } } @Test fun hasText_CorrectText_withString() { val text = getResourceString(R.string.button_default_content_desc) page.editTextContentDesc.hasText(text) } @Test fun hasText_InvalidText_withString() { val text = getResourceString(R.string.button_default_content_desc) AssertUtils.assertException { page.editTextContentDesc.withTimeout(100).hasText( "$text to be invalid" ) } } @Test fun hasText_CorrectText_withStringMatcher() { val text = getResourceString(R.string.button_default_content_desc) page.editTextContentDesc.hasText(containsString(text.substring(2))) } @Test fun hasText_InvalidText_withStringMatcher() { val text = getResourceString(R.string.button_default_content_desc) AssertUtils.assertException { page.editTextContentDesc.withTimeout(100).hasText( containsString("$text to be invalid") ) } } //containsText @Test fun containsText_CorrectText_withResourceId() { val text = getResourceString(R.string.button_default_content_desc) page.editTextContentDesc.textContains(text.substring(3)) } @Test fun containsText_InvalidSubstringText() { val text = getResourceString(R.string.button_default_content_desc) AssertUtils.assertException { page.editTextContentDesc.withTimeout(100).textContains( "${text.substring(3)} to be invalid" ) } } //hasContentDescription @Test fun hasContentDescription_CorrectText_withResourceId() { page.button.hasContentDescription(R.string.button_default_content_desc) } @Test fun hasContentDescription_InvalidSubstringText() { val text = getResourceString(R.string.button_default_content_desc) AssertUtils.assertException { page.button.withTimeout(100).hasContentDescription(text.substring(3)) } } @Test fun hasContentDescription_InvalidText_withResourceId() { AssertUtils.assertException { page.button.withTimeout(100).hasContentDescription( R.string.action_clear_history ) } } @Test fun hasContentDescription_CorrectText_withString() { val text = getResourceString(R.string.button_default_content_desc) page.button.hasContentDescription(text) } @Test fun hasContentDescription_InvalidText_withString() { val text = getResourceString(R.string.button_default_content_desc) AssertUtils.assertException { page.button.withTimeout(100).hasContentDescription( "$text to be invalid" ) } } //contentDescriptionContains @Test fun contentDescriptionContains_CorrectText_withString() { val text = getResourceString(R.string.button_default_content_desc) page.button.withTimeout(100).contentDescriptionContains(text.substring(2)) } @Test fun contentDescriptionContains_InvalidText_withString() { val text = getResourceString(R.string.button_default_content_desc) AssertUtils.assertException { page.button.withTimeout(100).contentDescriptionContains( "${text.substring(2)} to be invalid" ) } } //assertMatches @Test fun assertMatches_ofMatched() { page.button.assertMatches(allOf(isDisplayed(), isEnabled(), withText(R.string.button_text))) } @Test fun assertMatches_ofNotMatched() { page.checkBoxEnabled.click() AssertUtils.assertException { page.button.withTimeout(100).assertMatches( allOf( isDisplayed(), isEnabled(), withText(R.string.button_text) ), ) } } //javascriptEnabled @Test fun jsEnabled_ofEnabled() { page.webView.isJavascriptEnabled() } @Test fun jsEnabled_ofNotEnabled() { page.checkBoxJsEnabled.click() AssertUtils.assertException { page.webView.withTimeout(100).isJavascriptEnabled() } } //isSuccess @Test fun isSuccess_FalseTest() { val success = page.radioVisibleButton.isSuccess { withTimeout(100).isNotDisplayed() } Assert.assertFalse(success) } @Test fun isSuccess_TrueTest() { val success = page.radioVisibleButton.isSuccess { withTimeout(100).isDisplayed() } Assert.assertTrue(success) } @Test fun isSuccess_NotExist_FalseTest() { val success = page.notExistElement.isSuccess { withTimeout(100).isDisplayed() } Assert.assertFalse(success) } // withAppCompatTextView @Test fun appCompatTextView_assertText() { page.appCompatTextView.hasText(getResourceString(R.string.app_compat_text)) } // hasDrawable @Test fun hasDrawable_viewHasDrawable() { page.imageView.assertMatches(hasAnyDrawable()) } @Test fun hasDrawable_viewHasNoDrawable() { AssertUtils.assertException { page.emptyNotClickableImageView.withTimeout(100).assertMatches(hasAnyDrawable()) } } @Test fun withDrawable_correctDrawable() { page.imageView.assertMatches(withDrawable(R.drawable.ic_account)) } @Test fun withDrawable_invalidDrawable() { AssertUtils.assertException { page.imageView.withTimeout(100).assertMatches( withDrawable(R.drawable.ic_attach_file) ) } } @Test fun hasCurrentTextColor() { page.eventStatus.hasCurrentTextColor(R.color.colorPrimary) } @Test fun hasCurrentTextColor_invalidColor() { AssertUtils.assertException { page.eventStatus.withTimeout(100).hasCurrentTextColor(R.color.invalid) } } @Test fun hasCurrentHintTextColor() { page.eventStatus.hasCurrentHintTextColor(R.color.colorHint) } @Test fun hasCurrentHintTextColor_invalidColor() { AssertUtils.assertException { page.eventStatus.withTimeout(100).hasCurrentHintTextColor(R.color.invalid) } } @Test fun hasShadowColor() { page.eventStatus.hasShadowColor(R.color.colorShadow) } @Test fun hasShadowColor_invalidColor() { AssertUtils.assertException { page.eventStatus.withTimeout(100).hasShadowColor(R.color.invalid) } } @Test fun hasHighlightColor() { page.eventStatus.hasHighlightColor(R.color.colorHighlight) } @Test fun hasHighlightColor_invalidColor() { AssertUtils.assertException { page.eventStatus.withTimeout(100).hasHighlightColor(R.color.invalid) } } @Test fun textViewColors() { page.eventStatus .hasCurrentTextColor(R.color.colorPrimary) .hasCurrentHintTextColor(R.color.colorHint) .hasShadowColor(R.color.colorShadow) .hasHighlightColor(R.color.colorHighlight) } @Test fun appCompatTextViewTextColor() { page.appCompatTextView.hasCurrentTextColor(R.color.colorPrimary) } @Test fun appCompatTextViewTextColor_invalidColor() { AssertUtils.assertException { page.appCompatTextView.withTimeout(100).hasCurrentTextColor(R.color.invalid) } } @Test fun customAssertTest() { page.checkBoxEnabled.assertChecked(true) } @Test fun customAssertTest_invalidValue() { AssertUtils.assertException { page.checkBoxEnabled.withTimeout(100).assertChecked(false) } } @Test fun notExist_customAssertion() { AssertUtils.assertException { withText("not exist").withTimeout(100).assertChecked(true) } } @Test fun verifySoftAssertionsTest() { UltronCommonConfig.testContext.softAnalyzer.clear() softAssertion(false) { withText("NotExistText").withTimeout(100).isDisplayed() withText("NotExistTestTag").withTimeout(100).isDisplayed() } runCatching { verifySoftAssertions() }.onFailure { exception -> val message = exception.message ?: throw RuntimeException("Empty exception message: $exception") Assert.assertTrue(message.contains("NotExistText")) Assert.assertTrue(message.contains("NotExistTestTag")) } } @Test fun softAssertionTest() { UltronCommonConfig.testContext.softAnalyzer.clear() AssertUtils.assertException { softAssertion { withText("NotExistText").withTimeout(100).isDisplayed() } } } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso/ViewTest.kt ================================================ package com.atiurin.sampleapp.tests.espresso import android.content.Intent import android.widget.Button import android.widget.RadioButton import androidx.test.core.app.ActivityScenario import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.platform.app.InstrumentationRegistry import com.atiurin.sampleapp.R import com.atiurin.sampleapp.activity.CustomClicksActivity import com.atiurin.sampleapp.async.task.CompatAsyncTask.Companion.ASYNC import com.atiurin.sampleapp.async.task.CompatAsyncTask.Companion.COMPAT_ASYNC_TASK_TIME_EXECUTION import com.atiurin.sampleapp.framework.ultronext.getViewSimple import com.atiurin.sampleapp.tests.BaseTest import com.atiurin.ultron.custom.espresso.action.getView import com.atiurin.ultron.custom.espresso.base.getViewForcibly import com.atiurin.ultron.extensions.isChecked import com.atiurin.ultron.extensions.perform import com.atiurin.ultron.extensions.performOnView import com.atiurin.ultron.extensions.performOnViewForcibly import com.atiurin.ultron.testlifecycle.setupteardown.SetUp import com.atiurin.ultron.testlifecycle.setupteardown.SetUpRule import com.atiurin.ultron.testlifecycle.setupteardown.TearDown import com.atiurin.ultron.testlifecycle.setupteardown.TearDownRule import org.junit.Assert import org.junit.Test class ViewTest : BaseTest() { private lateinit var customClicksActivity: CustomClicksActivity private val startActivity = SetUpRule() .add(START_ACTIVITY) { ActivityScenario.launch(CustomClicksActivity::class.java).onActivity { activity -> customClicksActivity = activity } } .add(START_ACTIVITY_WITH_ASYNC_TASK) { val intent = Intent(InstrumentationRegistry.getInstrumentation().targetContext, CustomClicksActivity::class.java) intent.putExtra(ASYNC, true) ActivityScenario.launch(intent).onActivity { activity -> customClicksActivity = activity } } private val tearDownRule = TearDownRule().add(STOP_ASYNC_TASK) { customClicksActivity.stopCompatAsyncTask() } init { ruleSequence.add(startActivity).add(tearDownRule) } @Test @SetUp(START_ACTIVITY) fun actionGetView() { val view = withId(R.id.button).getView() Assert.assertNotNull(view) Assert.assertTrue((view as Button).text.isNotBlank()) } @Test @SetUp(START_ACTIVITY) fun actionGetViewSimple() { val view = withId(R.id.button).getViewSimple() Assert.assertNotNull(view) Assert.assertTrue((view as Button).text.isNotBlank()) } @Test @SetUp(START_ACTIVITY_WITH_ASYNC_TASK) @TearDown(STOP_ASYNC_TASK) fun actionGetViewForcibly() { val startTime = System.currentTimeMillis() val view = withId(R.id.rB_top_right).getViewForcibly() Assert.assertNotNull(view) Assert.assertTrue(System.currentTimeMillis() < COMPAT_ASYNC_TASK_TIME_EXECUTION + startTime) } @Test @SetUp(START_ACTIVITY) fun matcherActionPerformOnView_Deprecated() { withId(R.id.rB_top_left).apply { performOnView { performClick() Assert.assertTrue((this as RadioButton).isChecked) } isChecked() } } @Test @SetUp(START_ACTIVITY) fun matcherActionPerform() { withId(R.id.rB_top_left).apply { perform { _, view -> view.performClick() Assert.assertTrue((view as RadioButton).isChecked) } isChecked() } } @Test @SetUp(START_ACTIVITY_WITH_ASYNC_TASK) @TearDown(STOP_ASYNC_TASK) fun matcherActionPerformOnViewForcibly() { val startTime = System.currentTimeMillis() withId(R.id.rB_top_left).apply { performOnViewForcibly { performClick() Assert.assertTrue((this as RadioButton).isChecked) } } Assert.assertTrue(System.currentTimeMillis() < COMPAT_ASYNC_TASK_TIME_EXECUTION + startTime) } companion object { const val STOP_ASYNC_TASK = "STOP_ASYNC_TASK" const val START_ACTIVITY = "START_ACTIVITY" const val START_ACTIVITY_WITH_ASYNC_TASK = "START_ACTIVITY_WITH_ASYNC_TASK" } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso/WithSuitableRootTest.kt ================================================ package com.atiurin.sampleapp.tests.espresso import com.atiurin.sampleapp.activity.MainActivity import com.atiurin.sampleapp.data.repositories.ContactRepositoty import com.atiurin.sampleapp.pages.ChatPage import com.atiurin.sampleapp.pages.FriendsListPage import com.atiurin.sampleapp.tests.BaseTest import com.atiurin.ultron.testlifecycle.activity.UltronActivityRule import org.junit.Test class WithSuitableRootTest : BaseTest() { private val activityTestRule = UltronActivityRule(MainActivity::class.java) init { ruleSequence.addLast(activityTestRule) } @Test fun withSuitableRootForViewMatcher() { FriendsListPage .getListItem(0).click() ChatPage .assertToolbarTitleWithSuitableRoot(ContactRepositoty.getFirst().name) } @Test fun withSuitableRootForRecyclerMatcher() { FriendsListPage .assertPageDisplayedWithSuitableRoot() } @Test fun withSuitableRootForRecyclerItemMatcher() { FriendsListPage .assertPageDisplayedWithSuitableRoot() .getListItem(0).withSuitableRoot().isDisplayed().click() ChatPage .assertToolbarTitle(ContactRepositoty.getFirst().name) } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso_web/BaseWebViewTest.kt ================================================ package com.atiurin.sampleapp.tests.espresso_web import androidx.test.core.app.ActivityScenario import com.atiurin.sampleapp.activity.WebViewActivity import com.atiurin.sampleapp.pages.WebViewPage import com.atiurin.sampleapp.tests.BaseTest import com.atiurin.ultron.testlifecycle.setupteardown.SetUpRule abstract class BaseWebViewTest : BaseTest() { val page = WebViewPage() private val startActivity = SetUpRule().add { ActivityScenario.launch(WebViewActivity::class.java) // UltronWebDocument.forceJavascriptEnabled() } init { ruleSequence.addLast(startActivity) } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso_web/EspressoWebUiElementsTest.kt ================================================ package com.atiurin.sampleapp.tests.espresso_web import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.web.assertion.WebViewAssertions.webContent import androidx.test.espresso.web.assertion.WebViewAssertions.webMatches import androidx.test.espresso.web.matcher.DomMatchers.elementById import androidx.test.espresso.web.matcher.DomMatchers.withTextContent import androidx.test.espresso.web.sugar.Web.onWebView import androidx.test.espresso.web.webdriver.DriverAtoms.* import androidx.test.espresso.web.webdriver.Locator import com.atiurin.sampleapp.R import com.atiurin.sampleapp.framework.utils.AssertUtils import com.atiurin.ultron.core.config.UltronConfig import com.atiurin.ultron.core.espressoweb.webelement.UltronWebDocument import com.atiurin.ultron.core.espressoweb.webelement.UltronWebDocument.Companion.evalJS import com.atiurin.ultron.core.espressoweb.webelement.UltronWebElement.Companion.className import com.atiurin.ultron.core.espressoweb.webelement.UltronWebElement.Companion.id import com.atiurin.ultron.core.espressoweb.webelement.UltronWebElement.Companion.linkText import com.atiurin.ultron.core.espressoweb.webelement.UltronWebElement.Companion.xpath import com.atiurin.ultron.core.espressoweb.webelement.UltronWebElements import com.atiurin.ultron.core.espressoweb.webelement.UltronWebElements.Companion.classNames import org.hamcrest.CoreMatchers.containsString import org.hamcrest.Matchers.`is` import org.junit.Assert import org.junit.Test class EspressoWebUiElementsTest : BaseWebViewTest() { @Test fun simpleWebViewTest() { val newTitle = "New title" onWebView() .forceJavascriptEnabled() .withElement(findElement(Locator.ID, "text_input")) .perform(webKeys(newTitle)) .withElement(findElement(Locator.ID, "button1")) .perform(webClick()) .withElement(findElement(Locator.ID, "title")) .check(webMatches(getText(), containsString("New title"))) //findMultipleElements // onWebView().perform(findMultipleElements(Locator.CLASS_NAME, "button")) // .get() // .forEach { // onWebView().withElement(it).perform(webClick()) // } } @Test fun multipleElementsWebViewTest() { UltronWebElements(Locator.CLASS_NAME, "button").getElements().forEach { it.webClick() } UltronWebElements(Locator.CLASS_NAME, "link").getElements() .filter { it.isSuccess { withTimeout(100).hasText("Apple") } } .forEach { it.webClick() } classNames("link").getElements() .filter { it.isSuccess { withTimeout(100).hasText("Apple") } } .forEach { it.webClick() } page.title.containsText("apple") } @Test fun pageMultipleElements() { page.buttons.getElements().forEach { it.webClick() } } @Test fun extVar2WebViewTest() { val newTitle = "New title" id("text_input").webKeys(newTitle).webKeys("and more").replaceText(newTitle) id("button1").webClick() id("title").hasText(newTitle) className("css_title").containsText(newTitle) linkText("Apple").containsText("Apple") } @Test fun jsEvaluationTest() { val jsTitle = "JS_TITLE" val jsTitleNew = "JS_TITLE_NEW" evalJS("document.getElementById(\"title\").innerHTML = '$jsTitle';") className("css_title", withId(R.id.webview)).containsText(jsTitle) // onWebView().script("document.getElementById(\"title\").innerHTML = '$jsTitleNew';") // WebElement(Locator.CLASS_NAME,"css_title", withId(R.id.webview)).containsText(jsTitleNew) } @Test fun webViewFinderTest() { val jsTitleNew = "JS_TITLE_NEW" UltronConfig.Espresso.webViewMatcher = withId(R.id.webview) evalJS("document.getElementById(\"title\").innerHTML = '$jsTitleNew';") className("css_title").containsText(jsTitleNew) } @Test fun pageVar3WebViewTest() { val newTitle = "New title" page.textInput.webKeys(newTitle) page.buttonUpdTitle.webClick() page.title.containsText(newTitle) page.titleWithCss.containsText(newTitle) AssertUtils.assertException { page.appleLink.withTimeout(100).containsText("Apple12312") } } @Test fun getText2Test() { val newTitle = "New title" page.textInput.webKeys(newTitle) page.buttonUpdTitle.webClick() page.title.hasText(newTitle) val titleText = page.title.getText() Assert.assertEquals(newTitle, titleText) } @Test fun checkButtonTextTest() { xpath(".//*[@id='button3']").apply { exists() hasAttribute("value", "Set title active") } } @Test fun webInteractionLyambda() { Assert.assertTrue(id("button2").getText().isBlank()) Assert.assertEquals("Apple", id("apple_link").getText()) } @Test fun elementNotPresentDefaultTimeout() { AssertUtils.assertExecTimeBetween(5_000, 7_000) { id("asdasdasd").getText() } } @Test fun elementNotPresentCustomTimeout() { AssertUtils.assertExecTimeBetween(1_000, 3_000) { id("asdasdasd").withTimeout(2000).getText() } } @Test fun customWebViewAssertionTest() { UltronWebDocument.assertThat(webContent(elementById("apple_link", withTextContent("Apple")))) id("apple_link").hasAttribute("href", `is`("fake_link.html")) } @Test fun customWebAssertionTest() { id("apple_link").assertThat(webMatches(getText(), `is`("Apple"))) } @Test fun contextualElement() { id("student") .withContextual(className("person_name")) .hasText("Plato") } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso_web/UltronWebDocumentTest.kt ================================================ package com.atiurin.sampleapp.tests.espresso_web import androidx.test.espresso.web.assertion.WebViewAssertions.webContent import androidx.test.espresso.web.matcher.DomMatchers.elementById import androidx.test.espresso.web.matcher.DomMatchers.withTextContent import com.atiurin.sampleapp.framework.utils.AssertUtils import com.atiurin.ultron.core.espressoweb.webelement.UltronWebDocument import com.atiurin.ultron.core.espressoweb.webelement.UltronWebDocument.Companion.evalJS import org.junit.Assert import org.junit.Test class UltronWebDocumentTest : BaseWebViewTest() { @Test fun forceJS_Test() { UltronWebDocument.forceJavascriptEnabled() } @Test fun evalJS_Test() { val title = "SOME NEW TITLE" evalJS("document.getElementById(\"title\").innerHTML = '$title';") page.title.hasText(title) } @Test fun assertThat_validValueTest() { UltronWebDocument.assertThat( webContent( elementById( "apple_link", withTextContent("Apple") ) ) ) } @Test fun assertThat_invalidValueTest() { AssertUtils.assertException { UltronWebDocument.assertThat( webContent( elementById( "apple_link", withTextContent("Apple1123123") ) ), timeoutMs = 100 ) } } @Test fun setActiveElement(){ page.buttonSetTitleActive.webClick() val elementRef = UltronWebDocument.selectActiveElement() Assert.assertNotNull(elementRef) } @Test fun selectFrame_validElement(){ val frame = UltronWebDocument.selectFrameByIdOrName("iframe") Assert.assertNotNull(frame) } @Test fun selectFrame_invalidNameOrId(){ AssertUtils.assertException { UltronWebDocument.selectFrameByIdOrName("asdhgasdlkjasdasd", timeoutMs = 100) } } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso_web/UltronWebElementTest.kt ================================================ package com.atiurin.sampleapp.tests.espresso_web import androidx.test.espresso.web.assertion.WebViewAssertions.webMatches import androidx.test.espresso.web.webdriver.DriverAtoms.getText import com.atiurin.sampleapp.framework.DummyMetaObject import com.atiurin.sampleapp.framework.ultronext.appendText import com.atiurin.sampleapp.framework.utils.AssertUtils import com.atiurin.sampleapp.pages.WebViewPage import com.atiurin.ultron.core.common.assertion.softAssertion import com.atiurin.ultron.core.common.assertion.verifySoftAssertions import com.atiurin.ultron.core.config.UltronCommonConfig import com.atiurin.ultron.core.espressoweb.webelement.UltronWebElement.Companion.className import com.atiurin.ultron.core.espressoweb.webelement.UltronWebElement.Companion.id import com.atiurin.ultron.core.espressoweb.webelement.UltronWebElement.Companion.xpath import org.hamcrest.Matchers.`is` import org.junit.Assert import org.junit.Test class UltronWebElementTest : BaseWebViewTest() { @Test fun webClick_onExistElement() { page.buttonSetTitle2.webClick() page.title.hasText(WebViewPage.BUTTON2_TITLE) } @Test fun webClick_onNotExistedElement() { AssertUtils.assertException { xpath("notExistId").withTimeout(100).withName("Custome name").webClick() } } @Test fun webClick_isSuccessTrueValue() { val success = page.buttonSetTitle2.isSuccess { webClick() } Assert.assertTrue(success) } @Test fun webClick_isSuccessFalseValue() { val success = id("notExistId").isSuccess { withTimeout(100).webClick() } Assert.assertFalse(success) } @Test fun exists_onExistElement() { page.buttonUpdTitle.exists() } @Test fun exists_onNotExistedElement() { AssertUtils.assertException { id("notExistId").withTimeout(100).exists() } } @Test fun exists_isSuccessTrueValue() { val success = page.buttonSetTitle2.isSuccess { exists() } Assert.assertTrue(success) } @Test fun exists_isSuccessFalseValue() { val success = id("notExistId").isSuccess { withTimeout(100).exists() } Assert.assertFalse(success) } @Test fun clearElement_onExistedEditableElement() { page.textInput.webKeys("initial text").clearElement().hasText("") } @Test fun clearElement_onExistedNotEditableElement() { AssertUtils.assertException { page.buttonUpdTitle.withTimeout(100).clearElement() } } @Test fun clearElement_onNotExistedElement() { AssertUtils.assertException { id("notExistId").withTimeout(100).clearElement() } } @Test fun getText_onExistedTextContainerElement() { val text = "some text 2" page.textInput.replaceText(text) page.buttonUpdTitle.webClick() val receivedText = page.title.getText() Assert.assertEquals(text, receivedText) } @Test fun getText_onNotExistedElement() { AssertUtils.assertException { id("notExistId").withTimeout(100).getText() } } @Test fun webKeys_onExistedTextContainerElement() { val text = "some text 3" page.textInput.clearElement().webKeys(text) page.buttonUpdTitle.webClick() page.title.hasText(text) } @Test fun webKeys_onNotExistedElement() { AssertUtils.assertException { id("notExistId").withTimeout(100).webKeys("asd") } } @Test fun replaceText_and_hasText_onExistedElement() { val text = "some text 3" page.textInput.replaceText(text) page.buttonUpdTitle.webClick() page.title.hasText(text) } @Test fun replaceText_onNotExistedElement() { AssertUtils.assertException { id("notExistId").withTimeout(100).replaceText("asd") } } @Test fun hasText_onNotExistedElement() { AssertUtils.assertException { id("notExistId").withTimeout(100).hasText("asd") } } @Test fun containsText_onExistedElement() { val text = "some text 3" page.textInput.replaceText(text + "additional") page.buttonUpdTitle.webClick() page.title.containsText(text) } @Test fun containsText_onNotExistedElement() { AssertUtils.assertException { id("notExistId").withTimeout(100).containsText("asd") } } @Test fun hasAttribute_withMatcher_onExistedElement() { page.appleLink.hasAttribute("href", `is`(WebViewPage.APPLE_LINK_HREF)) } @Test fun hasAttribute_withString_onExistedElement() { page.appleLink.hasAttribute("href", WebViewPage.APPLE_LINK_HREF) } @Test fun hasAttribute_invalidMatcher_onExistedElement() { AssertUtils.assertException { page.appleLink.withTimeout(100).hasAttribute("href", `is`("SomeInvalidValue")) } } @Test fun hasAttribute_onNotExistedElement() { AssertUtils.assertException { id("notExistId").withTimeout(100).hasAttribute("asd", `is`("asdasd")) } } @Test fun assertThat_onExistedElement() { page.appleLink.assertThat(webMatches(getText(), `is`("Apple"))) } @Test fun assertThat_onNotExistedElement() { AssertUtils.assertException { id("notExistId").withTimeout(100).assertThat( webMatches( getText(), `is`("Apple") ) ) } } @Test fun withContextual_hasText() { id("teacher").containsText("Teachers").withContextual(className("person_name")).hasText("Socrates") } @Test fun withContextual_containsText() { id("student").withContextual(className("person_name")).hasText("Plato") } @Test fun scrollToWebElement() { id("list_element_12").webScrollIntoView().hasText("list_element_12").webClick() } @Test fun webScrollIntoViewBoolean() { val result = id("list_element_12").webScrollIntoViewBoolean() Assert.assertTrue(result) } @Test fun customActionTest() { val initText = "start" val finishText = "finish" page.textInput .replaceText(initText) .appendText(finishText) .exists() page.buttonUpdTitle.webClick() page.title.hasText(initText+finishText) } @Test fun validAssertion(){ val text = "start" page.textInput.replaceText(text) page.buttonUpdTitle.withAssertion { page.title.hasText(text) }.webClick() } @Test fun invalidAssertion(){ val text = "start" AssertUtils.assertException { page.textInput.replaceText(text) page.buttonUpdTitle.withTimeout(3000).withAssertion { page.title.withTimeout(500).hasText(text + "adas") }.webClick() } } @Test fun withName_inOperationProps_ultronInteraction() { val name = "ElementName" page.notExistedElement.withTimeout(100).withName(name).withResultHandler { result -> Assert.assertEquals(name, result.operation.elementInfo.name) }.exists() } @Test fun withName_inOperationProps_matcherExt() { val name = "ElementName" page.notExistedElement.withName(name).withTimeout(100).withResultHandler { result -> Assert.assertEquals(name, result.operation.elementInfo.name) }.exists() } @Test fun withName_inExceptionMessage() { val name = "ElementNameToBeInException" runCatching { page.notExistedElement.withTimeout(100).withName(name).exists() }.onFailure { exception -> Assert.assertTrue(exception.message!!.contains(name)) } } @Test fun withMeta() { val meta = DummyMetaObject("ElementMetaInfo") page.notExistedElement.withTimeout(100).withMetaInfo(meta).withResultHandler { result -> Assert.assertEquals(meta, result.operation.elementInfo.meta) }.exists() } @Test fun verifySoftAssertionsTest() { UltronCommonConfig.testContext.softAnalyzer.clear() softAssertion(false) { id("NotExistText").withTimeout(100).webClick() id("NotExistTestTag").withTimeout(100).webClick() } runCatching { verifySoftAssertions() }.onFailure { exception -> val message = exception.message ?: throw RuntimeException("Empty exception message: $exception") Assert.assertTrue(message.contains("NotExistText")) Assert.assertTrue(message.contains("NotExistTestTag")) } } @Test fun softAssertionTest() { UltronCommonConfig.testContext.softAnalyzer.clear() AssertUtils.assertException { softAssertion { page.notExistedElement.withTimeout(100).webClick() } } } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso_web/UltronWebElementsTest.kt ================================================ package com.atiurin.sampleapp.tests.espresso_web import com.atiurin.sampleapp.framework.Log import com.atiurin.ultron.core.espressoweb.webelement.UltronWebElements.Companion.classNames import org.junit.Assert import org.junit.Test class UltronWebElementsTest : BaseWebViewTest() { @Test fun getSizeTest() { val buttonsAmount = classNames("button").getSize() Assert.assertTrue(buttonsAmount == 3) } @Test fun getSize_notExistedElement() { Log.debug(">>>" + classNames("not_existed_classname").getSize()) // AssertUtils.assertException { } } @Test fun getListElementTest(){ classNames("link").getElements() .find { ultronWebElement -> ultronWebElement.isSuccess { withTimeout(100).hasText("Apple") } }?.webClick() page.title.containsText("apple") } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso_web/UltronWebUiBlockTest.kt ================================================ package com.atiurin.sampleapp.tests.espresso_web import com.atiurin.sampleapp.pages.uiblock.WebElementUiBlockScreen import org.junit.Test class UltronWebUiBlockTest : BaseWebViewTest() { @Test fun webUiBlock(){ WebElementUiBlockScreen { teacherBlock.name.exists().hasText("Socrates") teacherBlock.uiBlock.exists() studentWithoutDesc.name.exists().hasText("Plato") } } @Test fun uiBlockFactoryTest(){ WebElementUiBlockScreen { persons.student.name.hasText("Plato") } } @Test fun childUiBlockCreation(){ WebElementUiBlockScreen { persons.teacher.name.hasText("Socrates") persons.studentWithoutDesc.name.hasText("Plato") } } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/testlifecycle/ExceptionsProcessingTest.kt ================================================ package com.atiurin.sampleapp.tests.testlifecycle import com.atiurin.sampleapp.framework.Log import com.atiurin.ultron.testlifecycle.rulesequence.RuleSequence import com.atiurin.ultron.testlifecycle.setupteardown.SetUp import com.atiurin.ultron.testlifecycle.setupteardown.SetUpRule import com.atiurin.ultron.testlifecycle.setupteardown.TearDown import com.atiurin.ultron.testlifecycle.setupteardown.TearDownRule import org.junit.* import org.junit.rules.ExpectedException import java.lang.RuntimeException import java.util.concurrent.atomic.AtomicInteger class ExceptionsProcessingTest { val counter = AtomicInteger(0) val conditionsOrderMap = mutableMapOf() companion object { const val setUp1Key = "setUp1" const val setUp2Key = "setUp2" const val testKey = "testKey" const val tearDown1Key = "tearDown1" const val tearDown2Key = "tearDown2" const val NO_EXCEPTION_FLOW = "NO_EXCEPTION_FLOW" const val SET_UP_EXCEPTION_FLOW = "SET_UP_EXCEPTION_FLOW" const val TEST_EXCEPTION_FLOW = "TEST_EXCEPTION_FLOW" const val TEAR_DOWN_EXCEPTION_FLOW = "TEAR_DOWN_EXCEPTION_FLOW" const val SET_UP_WITH_EXCEPTION = "SET_UP_WITH_EXCEPTION" const val TEAR_DOWN_WITH_EXCEPTION = "TEAR_DOWN_WITH_EXCEPTION" } val setUp1 = SetUpRule("setUpRule1").add(name = "setUp1") { conditionsOrderMap[counter.incrementAndGet()] = setUp1Key }.add(key = SET_UP_WITH_EXCEPTION) { throw SetUpException(SET_UP_WITH_EXCEPTION) } val tearDown1 = TearDownRule("tearDown1").add(name = tearDown1Key) { conditionsOrderMap[counter.incrementAndGet()] = tearDown1Key }.add(key = TEAR_DOWN_WITH_EXCEPTION) { throw TearDownException(TEAR_DOWN_WITH_EXCEPTION) } val setUp2 = SetUpRule("setUp2").add(name = setUp2Key) { conditionsOrderMap[counter.incrementAndGet()] = setUp2Key } val tearDown2 = TearDownRule("tearDown2").add(name = tearDown2Key) { conditionsOrderMap[counter.incrementAndGet()] = tearDown2Key } val controlTearDown = TearDownRule().add(key = NO_EXCEPTION_FLOW) { var index = 1 Log.info(conditionsOrderMap.toString()) Assert.assertEquals(setUp1Key, conditionsOrderMap[index++]) Assert.assertEquals(setUp2Key, conditionsOrderMap[index++]) Assert.assertEquals(testKey, conditionsOrderMap[index++]) Assert.assertEquals(tearDown1Key, conditionsOrderMap[index++]) Assert.assertEquals(tearDown2Key, conditionsOrderMap[index]) }.add(key = SET_UP_EXCEPTION_FLOW) { var index = 1 Log.info(conditionsOrderMap.toString()) Assert.assertEquals(setUp1Key, conditionsOrderMap[index++]) Assert.assertEquals(tearDown1Key, conditionsOrderMap[index++]) Assert.assertEquals(tearDown2Key, conditionsOrderMap[index]) }.add(key = NO_EXCEPTION_FLOW) { var index = 1 Log.info(conditionsOrderMap.toString()) Assert.assertEquals(setUp1Key, conditionsOrderMap[index++]) Assert.assertEquals(setUp2Key, conditionsOrderMap[index++]) Assert.assertEquals(testKey, conditionsOrderMap[index++]) Assert.assertEquals(tearDown1Key, conditionsOrderMap[index++]) Assert.assertEquals(tearDown2Key, conditionsOrderMap[index]) }.add(key = TEST_EXCEPTION_FLOW) { var index = 1 Log.info(conditionsOrderMap.toString()) Assert.assertEquals(setUp1Key, conditionsOrderMap[index++]) Assert.assertEquals(setUp2Key, conditionsOrderMap[index++]) Assert.assertEquals(tearDown1Key, conditionsOrderMap[index++]) Assert.assertEquals(tearDown2Key, conditionsOrderMap[index]) }.add { counter.set(0) conditionsOrderMap.clear() } val expectedException = ExpectedException.none() @get:Rule val ruleSequence = RuleSequence(setUp1, tearDown1).add(tearDown2, setUp2).addLast(controlTearDown) @Test @TearDown(NO_EXCEPTION_FLOW) fun noExceptionFlow() { conditionsOrderMap[counter.incrementAndGet()] = testKey } @Ignore("There is a bug in expected part of JUnit. If an expected exception occurs out of the @Test scope the test fails") @SetUp(SET_UP_WITH_EXCEPTION) @TearDown(SET_UP_EXCEPTION_FLOW) @Test(expected = SetUpException::class) fun setUpExceptionFlow() { conditionsOrderMap[counter.incrementAndGet()] = testKey } @Suppress("UNREACHABLE_CODE") @Test(expected = TestException::class) @TearDown(TEST_EXCEPTION_FLOW) fun testExceptionFlow() { throw TestException(TEST_EXCEPTION_FLOW) conditionsOrderMap[counter.incrementAndGet()] = testKey } @Ignore("There is a bug in expected part of JUnit. If an expected exception occurs out of the @Test scope(e.g. @Before or @After) the test fails") @TearDown(TEAR_DOWN_WITH_EXCEPTION) @SetUp(TEAR_DOWN_EXCEPTION_FLOW) @Test(expected = TearDownException::class) fun tearDownExceptionFlow() { conditionsOrderMap[counter.incrementAndGet()] = testKey } private class SetUpException(override val message: String) : RuntimeException() private class TestException(override val message: String) : RuntimeException() private class TearDownException(override val message: String) : RuntimeException() } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/testlifecycle/ParametrizedTest.kt ================================================ package com.atiurin.sampleapp.tests.testlifecycle import com.atiurin.sampleapp.activity.ComposeElementsActivity import com.atiurin.ultron.core.compose.createSimpleUltronComposeRule import com.atiurin.ultron.log.UltronLog import com.atiurin.ultron.testlifecycle.rulesequence.RuleSequence import com.atiurin.ultron.testlifecycle.setupteardown.SetUp import com.atiurin.ultron.testlifecycle.setupteardown.SetUpRule import com.atiurin.ultron.testlifecycle.setupteardown.TearDown import com.atiurin.ultron.testlifecycle.setupteardown.TearDownRule import org.junit.Assert import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized @RunWith(Parameterized::class) class ParametrizedTest(private val testValue1: String, private val testValue2: String) { private var commonSetUpDone = false private var singleTestSetUpDone = false private var commonTearDownDone = false private var singleTestTearDownDone = false private val activityRule = createSimpleUltronComposeRule() private val beforeEachRule = SetUpRule("Precondition before each test") .add { commonSetUpDone = true } .add(key = "singleTestSetUpDone") { singleTestSetUpDone = true } private val afterEachRule = TearDownRule("Post condition after each test") .add { commonTearDownDone = true } .add(key = "singleTestTearDownDone") { singleTestTearDownDone = true } private val controlTearDown = TearDownRule() .add { Assert.assertTrue(commonSetUpDone) Assert.assertTrue(singleTestSetUpDone) Assert.assertTrue(commonTearDownDone) Assert.assertTrue(singleTestTearDownDone) } @get:Rule val ruleSequence = RuleSequence(activityRule, beforeEachRule, afterEachRule, controlTearDown) companion object { @JvmStatic @Parameterized.Parameters(name = "[{0}-{1}]") fun testData() = listOf( arrayOf("param1_1", "param1_2"), arrayOf("param2_1", "param2_2"), ) } @Test @SetUp("singleTestSetUpDone") @TearDown("singleTestTearDownDone") fun myAwesomeTest() { Assert.assertTrue(singleTestSetUpDone) Assert.assertTrue(commonSetUpDone) UltronLog.debug("testValue1 = $testValue1") UltronLog.debug("testValue2 = $testValue2") } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/testlifecycle/SetUpTearDownRuleTest.kt ================================================ package com.atiurin.sampleapp.tests.testlifecycle import com.atiurin.sampleapp.framework.Log import com.atiurin.ultron.allure.step.step import com.atiurin.ultron.core.config.UltronConfig import com.atiurin.ultron.testlifecycle.rulesequence.RuleSequence import com.atiurin.ultron.testlifecycle.setupteardown.* import org.junit.Assert import org.junit.Rule import org.junit.Test import java.util.concurrent.atomic.AtomicInteger import kotlin.reflect.KClass class SetUpTearDownRuleTest { val counter = AtomicInteger(0) val conditionsOrderMap = mutableMapOf() init { UltronConfig.Conditions.conditionExecutorWrapper = CustomConditionExecutorWrapper() UltronConfig.Conditions.conditionsExecutor = CustomConditionsExecutor() } class CustomConditionExecutorWrapper : ConditionExecutorWrapper { override fun execute(condition: Condition) { step(condition.name) { condition.actions() } } } class CustomConditionsExecutor : DefaultConditionsExecutor(){ var lastRuleName: String = "" override val conditionExecutor: ConditionExecutorWrapper = CustomConditionExecutorWrapper() override fun before(name: String, ruleClass: KClass<*>) { this.lastRuleName = name super.before(name, ruleClass) } override fun execute(conditions: List, keys: List, description: String) { step(lastRuleName){ super.execute(conditions, keys, description) } } } companion object { const val firstSetUpKey = "firstSetUp" const val firstTearDownKey = "firstTearDown" const val setUp1Key = "setUp1" const val setUp2Key = "setUp2" const val tearDown1Key = "tearDown1" const val tearDown2Key = "tearDown2" const val lastSetUpKey = "lastSetUp" const val lastTearDownKey = "lastTearDown" } val firstSetUp = SetUpRule("firstSetUp").add { conditionsOrderMap.put(counter.incrementAndGet(), firstSetUpKey) } val firstTearDown = TearDownRule("firstTearDown").add { conditionsOrderMap.put(counter.incrementAndGet(), firstTearDownKey) } val setUp1 = SetUpRule("setUpRule1") .add(name = "setUp1") { conditionsOrderMap.put(counter.incrementAndGet(), setUp1Key) } .add(name = "setUp1_2") { } val tearDown1 = TearDownRule("tearDown1").add { conditionsOrderMap.put(counter.incrementAndGet(), tearDown1Key) } val setUp2 = SetUpRule("setUp2") .add { conditionsOrderMap.put(counter.incrementAndGet(), setUp2Key) } val tearDown2 = TearDownRule("tearDown2").add { conditionsOrderMap.put(counter.incrementAndGet(), tearDown2Key) } val lastSetUp = SetUpRule() .add { conditionsOrderMap.put(counter.incrementAndGet(), lastSetUpKey) // throw Exception("asd") } val lastTearDown = TearDownRule() .add { conditionsOrderMap.put(counter.incrementAndGet(), lastTearDownKey) } val controlTearDown = TearDownRule() .add { Log.info(conditionsOrderMap.toString()) Assert.assertEquals(firstSetUpKey, conditionsOrderMap[1]) Assert.assertEquals(setUp1Key, conditionsOrderMap[2]) Assert.assertEquals(setUp2Key, conditionsOrderMap[3]) Assert.assertEquals(lastSetUpKey, conditionsOrderMap[4]) Assert.assertEquals(firstTearDownKey, conditionsOrderMap[5]) Assert.assertEquals(tearDown1Key, conditionsOrderMap[6]) Assert.assertEquals(tearDown2Key, conditionsOrderMap[7]) Assert.assertEquals(lastTearDownKey, conditionsOrderMap[8]) } @get:Rule val ruleSequence = RuleSequence(setUp1, tearDown1) .add(tearDown2, setUp2) .addFirst(firstSetUp, firstTearDown) .addLast(lastTearDown, lastSetUp, controlTearDown) @Test fun mockTestConditions() { Log.info(">>>> test") // throw Exception("asd") } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/testlifecycle/UltronTestFlowTest.kt ================================================ package com.atiurin.sampleapp.tests.testlifecycle import com.atiurin.sampleapp.tests.BaseTest import com.atiurin.ultron.annotations.ExperimentalUltronApi import com.atiurin.ultron.log.UltronLog import com.atiurin.ultron.testlifecycle.setupteardown.SetUpRule import com.atiurin.ultron.testlifecycle.setupteardown.TearDownRule import org.junit.Assert import org.junit.Test class UltronTestFlowTest : BaseTest() { companion object { var order = 0 var isBeforeFirstTestCounter = 0 var commonBeforeOrder = -1 var commonAfterOrder = -1 var afterOrder = -1 } val ruleSetUp = SetUpRule().add { UltronLog.info("SetUpRule") } val tearDownRule = TearDownRule().add { UltronLog.info("TearDownRule") } init { ruleSequence.add(ruleSetUp, tearDownRule) } @OptIn(ExperimentalUltronApi::class) override val beforeFirstTest = { isBeforeFirstTestCounter++ UltronLog.info("beforeFirstTest") } override val beforeTest = { commonBeforeOrder = order order++ UltronLog.info("Before test common") } override val afterTest = { commonAfterOrder = order order++ assert(afterOrder < commonAfterOrder, lazyMessage = { "CommonAfter block should run after 'after' test block" }) UltronLog.info("After test common") } @Test fun someTest1() = test { var beforeOrder = -1 var goOrder = -1 order++ before { assert(isBeforeFirstTestCounter == 1, lazyMessage = { "beforeFirstTest block should run before commonBefore block" }) beforeOrder = order order++ UltronLog.info("Before TestMethod 1") }.go { goOrder = order order++ UltronLog.info("Run TestMethod 1") }.after { afterOrder = order order++ assert(commonBeforeOrder < beforeOrder, lazyMessage = { "beforeOrder block should run after commonBefore block" }) assert(beforeOrder < goOrder, lazyMessage = { "Before block should run before 'go'" }) assert(goOrder < afterOrder, lazyMessage = { "After block should run after 'go'" }) } } @Test fun someTest2() = test(suppressCommonBefore = true) { before { UltronLog.info("Before TestMethod 2") } after { UltronLog.info("After TestMethod 2") } go { assert(isBeforeFirstTestCounter == 1, lazyMessage = { "beforeFirstTest block should run only once" }) UltronLog.info("Run TestMethod 2") } } @Test fun simpleTest() = test { assert(isBeforeFirstTestCounter == 1, lazyMessage = { "beforeAllTests block should run only once" }) UltronLog.info("UltronTest simpleTest") } @Test fun afterBlockExecutedOnFailedTest() { var isAfterExecuted = false runCatching { test { go{ throw RuntimeException("test exception") } after { isAfterExecuted = true } } } Assert.assertTrue(isAfterExecuted) } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/testlifecycle/UltronTestFlowTest2.kt ================================================ package com.atiurin.sampleapp.tests.testlifecycle import com.atiurin.sampleapp.tests.BaseTest import com.atiurin.ultron.annotations.ExperimentalUltronApi import com.atiurin.ultron.log.UltronLog import org.junit.Test class UltronTestFlowTest2 : BaseTest() { var order = 0 var beforeAllTestCounter = 0 @OptIn(ExperimentalUltronApi::class) override val beforeFirstTest = { beforeAllTestCounter = order order++ UltronLog.info("Before Class") } @Test fun someTest1() = test { var beforeOrder = -1 var afterOrder = -1 var goOrder = -1 order++ before { beforeOrder = order order++ UltronLog.info("Before TestMethod 1") }.go { goOrder = order order++ UltronLog.info("Run TestMethod 1") }.after { afterOrder = order assert(beforeAllTestCounter == 0, lazyMessage = { "beforeAllTests block should run before all test" }) assert(beforeOrder > beforeAllTestCounter, lazyMessage = { "Before block should run after 'Before All'" }) assert(beforeOrder < goOrder, lazyMessage = { "Before block should run before 'go'" }) assert(goOrder < afterOrder, lazyMessage = { "After block should run after 'go'" }) } } @Test fun someTest2() = test(suppressCommonBefore = true) { before { UltronLog.info("Before TestMethod 2") }.after { UltronLog.info("After TestMethod 2") }.go { assert(beforeAllTestCounter == 0, lazyMessage = { "beforeAllTests block should run only once" }) UltronLog.info("Run TestMethod 2") } } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/testlifecycle/UltronTestPlan.kt ================================================ package com.atiurin.sampleapp.tests.testlifecycle //@RunWith(Suite::class) //@Suite.SuiteClasses( // UltronTestFlowTest::class, // UltronTestFlowTest2::class, //) //class UltronTestPlan ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/testlifecycle/UltronTestRuleSequenceMergeTest.kt ================================================ package com.atiurin.sampleapp.tests.testlifecycle import com.atiurin.sampleapp.tests.BaseTest import com.atiurin.ultron.annotations.ExperimentalUltronApi import com.atiurin.ultron.log.UltronLog import com.atiurin.ultron.testlifecycle.setupteardown.SetUpRule import com.atiurin.ultron.testlifecycle.setupteardown.TearDownRule import org.junit.Test class UltronTestRuleSequenceMergeTest : BaseTest() { private val ruleSetUp = SetUpRule().add { UltronLog.info("SetUpRule") } private val tearDownRule = TearDownRule().add { UltronLog.info("TearDownRule") } init { ruleSequence.add(ruleSetUp, tearDownRule) } @OptIn(ExperimentalUltronApi::class) override val beforeFirstTest = { UltronLog.info("beforeFirstTest") } override val beforeTest = { UltronLog.info("Before test common") } override val afterTest = { UltronLog.info("After test common") } @Test fun someTest1() = test { before { UltronLog.info("Before TestMethod 1") }.go { UltronLog.info("Run TestMethod 1") }.after { UltronLog.info("After TestMethod 1") } } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/uiautomator/UiAutomatorCustomAssertionTest.kt ================================================ package com.atiurin.sampleapp.tests.uiautomator import com.atiurin.sampleapp.R import com.atiurin.sampleapp.framework.utils.AssertUtils import com.atiurin.sampleapp.framework.utils.TestDataUtils import com.atiurin.sampleapp.pages.UiObject2ElementsPage import com.atiurin.sampleapp.pages.UiObjectElementsPage import com.atiurin.sampleapp.tests.UiElementsTest import org.junit.Test class UiAutomatorCustomAssertionTest : UiElementsTest() { val uiObjectPage = UiObjectElementsPage() val uiObject2Page = UiObject2ElementsPage() @Test fun uiObjectValidAssertionTest() { uiObjectPage.button.withAssertion { uiObjectPage.eventStatus.textContains(TestDataUtils.getResourceString(R.string.button_event_click)) }.click() } @Test fun uiObjectInvalidAssertionTest() { AssertUtils.assertException { uiObjectPage.button.withTimeout(1000).withAssertion { uiObjectPage.eventStatus.withTimeout(400).textContains("some invalid text") }.click() } } @Test fun withAssertionAllureResult(){ uiObjectPage.editTextContentDesc.withAssertion("some test", isListened = true) { uiObjectPage.editTextContentDesc.hasText("123") }.replaceText("123") } @Test fun uiObject2ValidAssertionTest() { uiObject2Page.button.withAssertion { uiObject2Page.eventStatus.textContains(TestDataUtils.getResourceString(R.string.button_event_click)) }.click() } @Test fun uiObject2InvalidAssertionTest() { AssertUtils.assertException { uiObject2Page.button.withTimeout(1000).withAssertion { uiObject2Page.eventStatus.withTimeout(400).textContains("some invalid text") }.click() } } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/uiautomator/UltronUiAutomatorPerfTest.kt ================================================ package com.atiurin.sampleapp.tests.uiautomator import android.os.SystemClock import com.atiurin.sampleapp.framework.Log import com.atiurin.sampleapp.pages.UiObject2ElementsPage import com.atiurin.sampleapp.tests.UiElementsTest import org.junit.Test class UltronUiAutomatorPerfTest: UiElementsTest() { val page = UiObject2ElementsPage() @Test fun perfTest(){ val startTime = SystemClock.elapsedRealtime() for (i in 1..200){ page.button.click() page.eventStatus.textContains(i.toString()) } Log.debug("Duration ${SystemClock.elapsedRealtime() - startTime} ms") } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/uiautomator/UltronUiObject2ActionsTest.kt ================================================ package com.atiurin.sampleapp.tests.uiautomator import android.view.ViewConfiguration import android.widget.LinearLayout import androidx.compose.ui.test.hasText import androidx.test.filters.FlakyTest import androidx.test.platform.app.InstrumentationRegistry import com.atiurin.sampleapp.R import com.atiurin.sampleapp.activity.UiElementsActivity import com.atiurin.sampleapp.framework.Log import com.atiurin.sampleapp.framework.ultronext.appendText import com.atiurin.sampleapp.framework.utils.AssertUtils import com.atiurin.sampleapp.framework.utils.TestDataUtils import com.atiurin.sampleapp.pages.UiObject2ElementsPage import com.atiurin.sampleapp.tests.UiElementsTest import com.atiurin.ultron.core.uiautomator.uiobject2.UltronUiObject2.Companion.bySelector import com.atiurin.ultron.extensions.withTimeout import com.atiurin.ultron.log.UltronLog import com.atiurin.ultron.utils.getTargetString import org.junit.Assert import org.junit.Test class UltronUiObject2ActionsTest : UiElementsTest() { val page = UiObject2ElementsPage() //getParent @Test fun getParent_parentExist() { Assert.assertEquals( LinearLayout::class.qualifiedName, page.button.getParent()?.getClassName() ) } //getChildren @Test fun getChildren_returnsAllChildren() { val children = page.radioGroup.getChildren() Assert.assertEquals(3, children.size) var foundElements = 0 children.forEach { val resName = it.getResourceName()!! Log.debug(">>> $resName") if (resName.endsWith("radio_visible")) { foundElements++ it.click() page.button.isDisplayed() } else if (resName.endsWith("radio_invisible")) { foundElements++ it.click() page.button.isNotDisplayed() } else if (resName.endsWith("radio_gone")) foundElements++ } Assert.assertEquals(3, foundElements) } @Test fun getChildren_noChildExist() { val children = page.button.getChildren() Assert.assertTrue(children.isEmpty()) } @Test fun getChildrenWithResultHandler() { page.button.getChildren() } //getChildCount @Test fun getChildCount_childExist() { Assert.assertEquals(3, page.radioGroup.getChildCount()) } @Test fun getChildCount_noChildExist() { Assert.assertEquals(0, page.button.getChildCount()) } //findObject @Test fun findObject_existedChildObject() { val child = page.radioGroup.findObject(bySelector(R.id.radio_invisible)) Assert.assertNotNull(child) child!!.click() page.button.isNotDisplayed() } @Test fun findObject_notExistedChildObject() { val child = page.radioGroup.findObject(bySelector(R.id.button1)) Assert.assertNull(child) } @Test fun findObject_notExistedParentObject() { AssertUtils.assertException { page.notExistedObject.withTimeout(100).findObject(bySelector(R.id.button1)) } } //findObjects @Test fun findObjects_existedChildObject() { val children = page.radioGroup.findObjects(bySelector(R.id.radio_invisible)) Assert.assertEquals(1, children.size) children.forEach { it.click() page.button.isNotDisplayed() } } @Test fun findObjects_notExistedChildObject() { val children = page.radioGroup.findObjects(bySelector(R.id.button1)) Assert.assertTrue(children.isEmpty()) } //getText @Test fun getText_objectHasText() { Assert.assertEquals(getTargetString(R.string.button_text), page.button.getText()) } @Test fun getText_objectHasNoText() { Assert.assertEquals(null, page.swipableImageView.getText()) } @Test fun getText_notExistedObject() { AssertUtils.assertException { page.notExistedObject.withTimeout(100).getText() } } //getClassName @Test fun getClassName_existObject() { Assert.assertEquals("android.widget.Button", page.button.getClassName()) } @Test fun getClassName_notExistObject() { AssertUtils.assertException { page.notExistedObject.withTimeout(100).getClassName() } } //getApplicationPackage @Test fun getApplicationPackage_existedObject() { val expected = InstrumentationRegistry.getInstrumentation().targetContext.applicationInfo.packageName Assert.assertEquals(expected, page.button.getApplicationPackage()) } @Test fun getApplicationPackage_notExistedObject() { AssertUtils.assertException { page.notExistedObject.withTimeout(100).getApplicationPackage() } } //getVisibleBounds @Test fun getVisibleBounds_existedObject() { Assert.assertNotNull(page.button.getVisibleBounds()) } @Test fun getVisibleBounds_notExistedObject() { AssertUtils.assertException { page.notExistedObject.withTimeout(100).getVisibleBounds() } } //getVisibleCenter @Test fun getVisibleCenter_existedObject() { val bounds = page.button.getVisibleBounds()!! val point = page.button.getVisibleCenter()!! Assert.assertEquals(bounds.exactCenterX().toInt(), point.x) Assert.assertEquals(bounds.exactCenterY().toInt(), point.y) } @Test fun getVisibleCenter_notExistedObject() { AssertUtils.assertException { page.notExistedObject.withTimeout(100).getVisibleCenter() } } //getResourceName @Test fun getResourceName_existedResourceName() { val resName = page.button.getResourceName() val pkgName = InstrumentationRegistry.getInstrumentation().targetContext.applicationInfo.packageName Assert.assertEquals("$pkgName:id/button1", resName) } @Test fun getResourceName_notExistedResourceName() { Assert.assertNull(page.button.getParent()?.getResourceName()) } @Test fun getResourceName_notExistedObject() { AssertUtils.assertException { page.notExistedObject.withTimeout(100).getResourceName() } } //getContentDescription @Test fun getContentDescription_existedContentDescription() { val expectedContDesc = getTargetString(R.string.button_default_content_desc) Assert.assertEquals(expectedContDesc, page.button.getContentDescription()) } @Test fun getContentDescription_notExistedResourceName() { Assert.assertNull(page.button.getParent()?.getContentDescription()) } @Test fun getContentDescription_notExistedObject() { AssertUtils.assertException { page.notExistedObject.withTimeout(100).getContentDescription() } } //click @Test fun click_onClickable() { page.button.click() page.eventStatus.textContains(TestDataUtils.getResourceString(R.string.button_event_click)) } @Test fun click_withDuration_onClickable() { page.button.click(duration = 50) page.eventStatus.textContains(TestDataUtils.getResourceString(R.string.button_event_click)) } @Test fun click_onNotExistedObject() { AssertUtils.assertException { page.notExistedObject.withTimeout(100).click() } } //longClick @Test fun longClick_onLongClickable() { val duration = ViewConfiguration.getLongPressTimeout().toLong() page.button.click(duration * 2) page.eventStatus.textContains(getTargetString(R.string.button_event_long_click)) } @Test fun longClick_onNotExistedObject() { AssertUtils.assertException { page.notExistedObject.withTimeout(100).longClick() } } //clear @Test fun clear_editableObject() { page.editTextContentDesc.clear().textIsNullOrEmpty() } @Test fun clear_UneditableObject() { val btnText = getTargetString(R.string.button_text) page.button.hasText(btnText).clear().hasText(btnText) } @Test fun clear_notExistedObject() { AssertUtils.assertException { page.notExistedObject.withTimeout(100).clear() } } //addText @Test fun addText_toEditableObject() { val startText = "start " val textToAdd = "added new Text" page.editTextContentDesc .replaceText(startText) .addText(textToAdd) .hasText(startText + textToAdd) } @Test fun addText_toUneditableObject() { val btnText = getTargetString(R.string.button_text) page.button.addText("some new text").hasText(btnText) } @Test fun addText_toNotExistedObject() { AssertUtils.assertException { page.notExistedObject.withTimeout(100).addText("asdasd") } } @Test fun legacySetText_toEditable() { val text = "text to replace " page.editTextContentDesc.legacySetText(text).withAssertion { hasText(text).withTimeout(1000) } } @Test fun legacySetText_toUneditableObject() { AssertUtils.assertException { page.button.withTimeout(100).legacySetText("some new text") } } @Test fun legacySetText_toNotExistedObject() { AssertUtils.assertException { page.notExistedObject.withTimeout(100).legacySetText("some new text") } } //replaceText @Test fun replaceText_toEditable() { val text = "replaceText to new" page.editTextContentDesc .replaceText(text) .hasText(text) } @Test fun replaceText_toUneditableObject() { val btnText = getTargetString(R.string.button_text) page.button.replaceText("some new text").hasText(btnText) } @Test fun replaceText_toNotExistedObject() { AssertUtils.assertException { page.notExistedObject.withTimeout(100).replaceText("some new text") } } //perform @Test fun perform_existedObject() { page.button.perform({ this.click() }, "Click on button") page.eventStatus.textContains(TestDataUtils.getResourceString(R.string.button_event_click)) } @Test fun perform_notExistedObject() { AssertUtils.assertException { page.notExistedObject.withTimeout(100).perform({ this.click() }, "Click on button") } } //swipe @FlakyTest @Test fun swipeUpTest() { page.editTextContentDesc.replaceText("some text") page.swipableImageView.withAssertion { page.eventStatus.withTimeout(300).textContains(UiElementsActivity.Event.SWIPE_UP.name) }.swipeUp(speed = 2000) } @FlakyTest @Test fun swipeDownTest() { page.editTextContentDesc.replaceText("some text") page.swipableImageView.withAssertion { page.eventStatus.withTimeout(300).textContains(UiElementsActivity.Event.SWIPE_DOWN.name) }.swipeDown(speed = 2000) } @Test @FlakyTest fun swipeRightTest() { page.editTextContentDesc.replaceText("some text") UltronLog.info("In assertion, text = '${page.eventStatus.getText()}'") page.swipableImageView.withAssertion { UltronLog.info("In assertion, text = '${page.eventStatus.getText()}'") page.eventStatus.withTimeout(500).textContains(UiElementsActivity.Event.SWIPE_RIGHT.name) }.swipeRight(speed = 2000) } @Test @FlakyTest fun swipeLeftTest() { page.editTextContentDesc.replaceText("some text") page.swipableImageView.withAssertion { page.eventStatus.withTimeout(300).textContains(UiElementsActivity.Event.SWIPE_LEFT.name) }.swipeLeft(speed = 2000) } @Test fun customAppendText_toEditableObject() { val startText = "start " val textToAdd = "added new Text" page.editTextContentDesc .replaceText(startText) .appendText(textToAdd) .hasText(startText + textToAdd) } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/uiautomator/UltronUiObject2AssertionsTest.kt ================================================ package com.atiurin.sampleapp.tests.uiautomator import com.atiurin.sampleapp.R import com.atiurin.sampleapp.framework.Log import com.atiurin.sampleapp.framework.utils.AssertUtils import com.atiurin.sampleapp.pages.UiObject2ElementsPage import com.atiurin.sampleapp.tests.UiElementsTest import com.atiurin.ultron.utils.getTargetString import org.hamcrest.Matchers.containsString import org.hamcrest.Matchers.equalToIgnoringCase import org.junit.Assert import org.junit.Test class UltronUiObject2AssertionsTest: UiElementsTest() { val page = UiObject2ElementsPage() //hasText @Test fun hasText_CorrectText_withResourceId() { page.editTextContentDesc.hasText(getTargetString(R.string.button_default_content_desc)) } @Test fun hasText_InvalidText_withResourceId() { AssertUtils.assertException { page.editTextContentDesc.withTimeout(100).withTimeout(100).hasText("invalid text") } } @Test fun hasText_notExisted(){ AssertUtils.assertException { page.notExistedObject.withTimeout(100).withTimeout(100).hasText("asd") } } //hasText matcher @Test fun hasText_matcher_CorrectText() { page.editTextContentDesc.hasText(containsString("content description")) } @Test fun hasText_matcher_InvalidText() { AssertUtils.assertException { page.editTextContentDesc.withTimeout(100).hasText(equalToIgnoringCase("invalid text") ) } } @Test fun hasText_matcher_notExisted(){ AssertUtils.assertException { page.notExistedObject.withTimeout(100).hasText(containsString("asd") ) } } //textContains @Test fun textContains_validText(){ val substring = getTargetString(R.string.button_text).substring(0, 5) page.button.textContains(substring) } @Test fun textContains_invalidText(){ AssertUtils.assertException { page.button.withTimeout(100).textContains("invalid substring" ) } } @Test fun textContains_notExisted(){ AssertUtils.assertException { page.notExistedObject.withTimeout(100).textContains("invalid substring" ) } } //textIsNullOrEmpty @Test fun textIsNullOrEmpty_nullText(){ page.editTextContentDesc.clear().withTimeout(100).textIsNullOrEmpty() } @Test fun textIsNullOrEmpty_notEmptyText(){ AssertUtils.assertException { page.editTextContentDesc.withTimeout(100).textIsNullOrEmpty() } } @Test fun textIsNullOrEmpty_notExisted(){ AssertUtils.assertException { page.notExistedObject.withTimeout(100).textIsNullOrEmpty() } } //textIsNotNullOrEmpty @Test fun textIsNotNullOrEmpty_notEmptyText(){ page.editTextContentDesc.textIsNotNullOrEmpty() } @Test fun textIsNotNullOrEmpty_nullText(){ AssertUtils.assertException { page.editTextContentDesc.clear().withTimeout(100).textIsNotNullOrEmpty() } } //contentDescription //hasContentDescription @Test fun hasContentDescription_CorrectText_withResourceId() { page.button.hasContentDescription(getTargetString(R.string.button_default_content_desc)) } @Test fun hasContentDescription_InvalidText_withResourceId() { AssertUtils.assertException { page.button.withTimeout(100).hasContentDescription("invalid text" ) } } @Test fun hasContentDescription_notExisted(){ AssertUtils.assertException { page.notExistedObject.withTimeout(100).hasContentDescription("asd" ) } } //contentDescription matcher @Test fun hasContentDescription_matcher_CorrectText() { page.button.hasContentDescription(containsString(getTargetString(R.string.button_default_content_desc).substring(0, 5))) } @Test fun hasContentDescription_matcher_InvalidText() { AssertUtils.assertException { page.button.withTimeout(100).hasContentDescription(equalToIgnoringCase("invalid text") ) } } @Test fun hasContentDescription_matcher_notExisted(){ AssertUtils.assertException { page.notExistedObject.withTimeout(100).hasContentDescription(containsString("asd") ) } } //contentDescriptionContains @Test fun contentDescriptionContains_validText(){ val substring = getTargetString(R.string.button_default_content_desc).substring(0, 5) page.button.contentDescriptionContains(substring) } @Test fun contentDescriptionContains_invalidText(){ AssertUtils.assertException { page.button.withTimeout(100).contentDescriptionContains("invalid substring" ) } } @Test fun contentDescriptionContains_notExisted(){ AssertUtils.assertException { page.notExistedObject.withTimeout(100).contentDescriptionContains("invalid substring" ) } } //contentDescriptionIsNullOrEmpty @Test fun contentDescriptionIsNullOrEmpty_nullText(){ page.editTextContentDesc.clear() page.button.contentDescriptionIsNullOrEmpty() } @Test fun contentDescriptionIsNullOrEmpty_notEmptyText(){ AssertUtils.assertException { page.button.withTimeout(100).contentDescriptionIsNullOrEmpty() } } @Test fun contentDescriptionIsNullOrEmpty_notExisted(){ AssertUtils.assertException { page.notExistedObject.withTimeout(100).contentDescriptionIsNullOrEmpty() } } //contentDescriptionIsNotNullOrEmpty @Test fun contentDescriptionIsNotNullOrEmpty_notEmptyText(){ page.button.contentDescriptionIsNotNullOrEmpty() } @Test fun contentDescriptionIsNotNullOrEmpty_nullText(){ page.editTextContentDesc.clear() AssertUtils.assertException { page.button.withTimeout(100).contentDescriptionIsNotNullOrEmpty() } } //isDisplayed @Test fun isDisplayed_ofDisplayedObject() { page.button.isDisplayed() } @Test fun isDisplayed_ofInvisibleObject() { page.radioInvisibleButton.click() AssertUtils.assertException { page.button.withTimeout(100).isDisplayed() } } @Test fun isDisplayed_BooleanTrue_ofInvisibleObject() { val result = page.radioVisibleButton.isSuccess { withTimeout(100).isDisplayed() } Assert.assertTrue(result) } @Test fun isDisplayed_BooleanFalse_ofInvisibleObject() { page.radioInvisibleButton.click() val result = page.button.isSuccess { withTimeout(100).isDisplayed() } Assert.assertFalse(result) } //isNotDisplayed @Test fun isNotDisplayed_ofDisplayedObject() { AssertUtils.assertException { page.button.isDisplayed().withTimeout(100).isNotDisplayed() } } @Test fun isNotDisplayed_ofInvisibleObject() { page.radioInvisibleButton.click() page.button.isNotDisplayed() } //isCheckable @Test fun isCheckable_ofCheckable(){ page.checkBoxClickable.isCheckable() } @Test fun isCheckable_ofNotCheckable(){ AssertUtils.assertException { page.emptyImageView.withTimeout(100).isCheckable() } } //isNotCheckable @Test fun isNotCheckable_ofNotCheckable(){ page.emptyImageView.isNotCheckable() } @Test fun isNotCheckable_ofCheckable(){ AssertUtils.assertException { page.checkBoxClickable.withTimeout(100).isNotCheckable() } } //isChecked @Test fun isChecked_ofChecked(){ page.checkBoxClickable.isChecked() } @Test fun isChecked_ofNotChecked(){ AssertUtils.assertException { page.checkBoxSelected.withTimeout(100).isChecked() } } //isNotChecked @Test fun isNotChecked_ofNotChecked(){ page.checkBoxSelected.isNotChecked() } @Test fun isNotChecked_ofChecked(){ AssertUtils.assertException { page.checkBoxClickable.withTimeout(100).isNotChecked() } } //isClickable @Test fun isClickable_ofClickable(){ page.button.isClickable() } @Test fun isClickable_ofNotClickable(){ page.checkBoxClickable.click() AssertUtils.assertException { page.button.withTimeout(100).isClickable() } } //isNotClickable @Test fun isNotClickable_ofNotClickable(){ page.checkBoxClickable.click() page.button.isNotClickable() } @Test fun isNotClickable_ofClickable(){ AssertUtils.assertException { page.button.withTimeout(100).isNotClickable() } } //isEnabled @Test fun isEnabled_ofEnable(){ page.button.isEnabled() } @Test fun isEnabled_ofNotEnable(){ page.checkBoxEnabled.click() AssertUtils.assertException { page.button.withTimeout(100).isEnabled() } } //isNotEnabled @Test fun isNotEnabled_ofNotEnable(){ page.checkBoxEnabled.click() page.button.isNotEnabled() } @Test fun isNotEnabled_ofEnable(){ AssertUtils.assertException { page.button.withTimeout(100).isNotEnabled() } } //isFocusable @Test fun isFocusable_ofFocusable(){ page.button.isFocusable() } @Test fun isFocusable_ofNotFocusable(){ page.checkBoxFocusable.click() AssertUtils.assertException { page.button.withTimeout(100).isFocusable() } } //isNotFocusable @Test fun isNotFocusable_ofNotFocusable(){ page.checkBoxFocusable.click() page.button.isNotFocusable() } @Test fun isNotFocusable_ofFocusable(){ AssertUtils.assertException { page.button.withTimeout(100).isNotFocusable() } } //isFocused @Test fun isFocused_ofFocused(){ page.editTextContentDesc.click().isFocused() } @Test fun isFocused_ofNotFocused(){ AssertUtils.assertException { page.editTextContentDesc.withTimeout(100).isFocused() } } //isNotFocused @Test fun isNotFocused_ofNotFocused(){ page.editTextContentDesc.isNotFocused() } @Test fun isNotFocused_ofFocused(){ AssertUtils.assertException { page.editTextContentDesc.click().withTimeout(100).isNotFocused() } } //isLongClickable @Test fun isLongClickable_ofLongClickable(){ page.button.isLongClickable() } @Test fun isLongClickable_ofNotLongClickable(){ AssertUtils.assertException { page.emptyImageView.withTimeout(100).isLongClickable() } } //isNotLongClickable @Test fun isNotLongClickable_ofNotLongClickable(){ page.emptyImageView.isNotLongClickable() } @Test fun isNotLongClickable_ofLongClickable(){ AssertUtils.assertException { page.button.withTimeout(100).isNotLongClickable() } } //isSelected @Test fun isSelected_ofSelected(){ page.checkBoxSelected.click() page.button.isSelected() } @Test fun isSelected_ofNotSelected(){ AssertUtils.assertException { page.button.withTimeout(100).isSelected() } } //isNotSelected @Test fun isNotSelected_ofNotSelected(){ page.button.isNotSelected() } @Test fun isNotSelected_ofSelected(){ page.checkBoxSelected.click() AssertUtils.assertException { page.button.withTimeout(100).isNotSelected() } } //isNotScrollable @Test fun isNotScrollable_ofNotScrollable(){ page.button.isNotScrollable() } @Test fun isScrollable_ofNotScrollable(){ AssertUtils.assertException { page.button.withTimeout(100).isScrollable() } } //assertThat @Test fun assertThat_validAssertion_existedObject(){ page.button.assertThat({ this.isClickable }, "Object is clickable") } @Test fun assertThat_invalidAssertion_existedObject(){ AssertUtils.assertException { page.button.withTimeout(100).assertThat({ !this.isClickable }, "Object isn't clickable") } } @Test fun assertThat_notExistedObject(){ AssertUtils.assertException { page.notExistedObject.withTimeout(100).assertThat({ !this.isClickable }, "Should fail") } } @Test fun resultHandlerTest(){ page.editTextContentDesc.withResultHandler { Assert.assertFalse(it.success) Assert.assertTrue(it.description.isNotEmpty()) }.withTimeout(100).hasText("invalid text") AssertUtils.assertException { page.editTextContentDesc.withTimeout(100).isNotDisplayed() } } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/uiautomator/UltronUiObject2ScrollTest.kt ================================================ package com.atiurin.sampleapp.tests.uiautomator import android.os.Build import com.atiurin.sampleapp.activity.MainActivity import com.atiurin.sampleapp.pages.UiObject2FriendsListPage import com.atiurin.sampleapp.tests.BaseTest import com.atiurin.ultron.core.config.UltronConfig import com.atiurin.ultron.testlifecycle.activity.UltronActivityRule import org.junit.BeforeClass import org.junit.Test class UltronUiObject2ScrollTest : BaseTest() { init { ruleSequence.addLast(UltronActivityRule(MainActivity::class.java)) } companion object { @BeforeClass @JvmStatic fun speedUpAutomator() { UltronConfig.UiAutomator.speedUp() } } val page = UiObject2FriendsListPage @Test fun scrollToBottom() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { for (i in 0..10) { if (page.bottomElement.isSuccess { withTimeout(100).isDisplayed() }) break page.list.scrollDown(percent = 0.5f) } page.bottomElement.isDisplayed() } } @Test fun scrollToTop() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { for (i in 0..10) { if (page.bottomElement.isSuccess { withTimeout(100).isDisplayed() }) break page.list.scrollDown(percent = 0.5f) } page.bottomElement.isDisplayed() for (i in 0..10) { if (page.topElement.isSuccess { withTimeout(100).isDisplayed() }) break page.list.scrollUp(percent = 0.5f) } } } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/uiautomator/UltronUiObject2UiBlockTest.kt ================================================ package com.atiurin.sampleapp.tests.uiautomator import com.atiurin.sampleapp.activity.UiBlockActivity import com.atiurin.sampleapp.data.repositories.CONTACTS import com.atiurin.sampleapp.framework.utils.AssertUtils import com.atiurin.sampleapp.pages.uiblock.UiObject2UiBlockScreen import com.atiurin.sampleapp.tests.BaseTest import com.atiurin.ultron.testlifecycle.activity.UltronActivityRule import org.junit.Rule import org.junit.Test class UltronUiObject2UiBlockTest: BaseTest() { @get:Rule val activityRule = UltronActivityRule(UiBlockActivity::class.java) @Test fun notUniqueUiElement_WithDeepSearch(){ UiObject2UiBlockScreen { block1.uiBlock.isDisplayed() block2.name.isDisplayed().hasText(CONTACTS[1].name) block1.deepSearchChild.withTimeout(100).isDisplayed() } } @Test fun notUniqueUiElement_WithoutDeepSearch(){ UiObject2UiBlockScreen { AssertUtils.assertException { block2.notExisted.isDisplayed() } } } @Test fun uiBlockInBlock(){ UiObject2UiBlockScreen { blocks.uiBlock.isDisplayed() blocks.item1.uiBlock.isDisplayed() blocks.item1.name.isDisplayed().hasText(CONTACTS[0].name) blocks.item1.status.isDisplayed().hasText(CONTACTS[0].status) AssertUtils.assertException { blocks.item2.notExisted.isDisplayed() } } } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/uiautomator/UltronUiObjectActionsTest.kt ================================================ package com.atiurin.sampleapp.tests.uiautomator import androidx.test.platform.app.InstrumentationRegistry import com.atiurin.sampleapp.R import com.atiurin.sampleapp.activity.UiElementsActivity import com.atiurin.sampleapp.framework.utils.AssertUtils import com.atiurin.sampleapp.framework.utils.TestDataUtils import com.atiurin.sampleapp.pages.UiObjectElementsPage import com.atiurin.sampleapp.tests.UiElementsTest import com.atiurin.ultron.core.uiautomator.uiobject.UltronUiObject.Companion.uiSelector import com.atiurin.ultron.utils.getTargetString import org.junit.Assert import org.junit.Test class UltronUiObjectActionsTest: UiElementsTest() { val page = UiObjectElementsPage() //getChild @Test fun getChild_existedChild(){ val child = page.radioGroup.getChild(uiSelector(R.id.radio_gone)) Assert.assertNotNull(child) child.click() } @Test fun getChild_notExistedChild(){ val child = page.radioGroup.getChild(uiSelector(R.id.button1)) Assert.assertNotNull(child) AssertUtils.assertException { child.withTimeout(100).click() } } //getChildCount @Test fun getChildCount_childExist(){ Assert.assertEquals(3, page.radioGroup.getChildCount()) } @Test fun getChildCount_noChildExist(){ Assert.assertEquals(0, page.button.getChildCount()) } //getFromParent @Test fun findObject_existedChildObject(){ val child = page.radioGroup.getFromParent(uiSelector(R.id.radio_invisible)) Assert.assertNotNull(child) child.click() page.button.notExists() } @Test fun getFromParent_notExistedChildObject(){ val child = page.radioGoneButton.getFromParent(uiSelector(R.id.button1)) Assert.assertNotNull(child) AssertUtils.assertException { child.withTimeout(100).click() } } //getText @Test fun getText_objectHasText(){ Assert.assertEquals(getTargetString(R.string.button_text), page.button.getText()) } @Test fun getText_objectHasNoText(){ Assert.assertTrue( page.swipableImageView.getText()!!.isEmpty()) } @Test fun getText_notExistedObject(){ AssertUtils.assertException { page.notExistedObject.withTimeout(100).getText() } } //getClassName @Test fun getClassName_existObject(){ Assert.assertEquals("android.widget.Button", page.button.getClassName()) } @Test fun getClassName_notExistObject(){ AssertUtils.assertException { page.notExistedObject.withTimeout(100).getClassName() } } //getApplicationPackage @Test fun getApplicationPackage_existedObject(){ val expected = InstrumentationRegistry.getInstrumentation().targetContext.applicationInfo.packageName Assert.assertEquals(expected, page.button.getPackageName()) } @Test fun getApplicationPackage_notExistedObject(){ AssertUtils.assertException { page.notExistedObject.withTimeout(100).getPackageName() } } //getVisibleBounds @Test fun getVisibleBounds_existedObject(){ Assert.assertNotNull(page.button.getVisibleBounds()) } @Test fun getVisibleBounds_notExistedObject(){ AssertUtils.assertException { page.notExistedObject.withTimeout(100).getVisibleBounds() } } //getContentDescription @Test fun getContentDescription_existedContentDescription(){ val expectedContDesc = getTargetString(R.string.button_default_content_desc) Assert.assertEquals(expectedContDesc, page.button.getContentDescription()) } @Test fun getContentDescription_notExistedResourceName(){ Assert.assertTrue(page.checkBoxClickable.getContentDescription()!!.isEmpty()) } @Test fun getContentDescription_notExistedObject(){ AssertUtils.assertException { page.notExistedObject.withTimeout(100).getContentDescription() } } //click @Test fun click_onClickable() { page.button.exists().click() page.eventStatus.textContains(TestDataUtils.getResourceString(R.string.button_event_click)) } @Test fun click_withDuration_onClickable() { page.button.exists().click() page.eventStatus.textContains(TestDataUtils.getResourceString(R.string.button_event_click)) } @Test fun click_onNotExistedObject() { AssertUtils.assertException { page.notExistedObject.withTimeout(100).click() } } //longClick @Test fun longClick_onLongClickable() { page.button.exists().withAssertion { page.eventStatus.textContains(getTargetString(R.string.button_event_long_click)) }.longClick() } @Test fun longClick_onNotExistedObject() { AssertUtils.assertException { page.notExistedObject.withTimeout(100).longClick() } } @Test fun legacySetText_toEditable(){ val text = "text to replace " page.editTextContentDesc .clearTextField() .legacyAddText(text) .hasText(text) } @Test fun legacySetText_toNotExistedObject(){ AssertUtils.assertException { page.notExistedObject.withTimeout(100).legacyAddText("some new text") } } //replaceText @Test fun replaceText_toEditable(){ val text = "replaceText to new" page.editTextContentDesc .replaceText(text) .hasText(text) } @Test fun replaceText_toUneditableObject(){ AssertUtils.assertException { page.button.withTimeout(100).replaceText("some new text") } } @Test fun replaceText_toNotExistedObject(){ AssertUtils.assertException { page.notExistedObject.withTimeout(100).replaceText("some new text") } } //perform @Test fun perform_existedObject(){ page.button.perform({ this.click() }, "Click on button") page.eventStatus.textContains(TestDataUtils.getResourceString(R.string.button_event_click)) } @Test fun perform_notExistedObject(){ AssertUtils.assertException { page.notExistedObject.withTimeout(100).perform({this.click()}, "Click on button") } } //swipe @Test fun swipeUpTest(){ page.swipableImageView.withAssertion { page.eventStatus.withTimeout(300).textContains(UiElementsActivity.Event.SWIPE_UP.name) }.swipeUp(40) } @Test fun swipeDownTest(){ page.eventStatus.hasText(getTargetString(R.string.button_text)) page.swipableImageView.withAssertion { page.eventStatus.withTimeout(300).textContains(UiElementsActivity.Event.SWIPE_DOWN.name) }.swipeDown(40) } @Test fun swipeRightTest(){ page.swipableImageView.withAssertion { page.eventStatus.withTimeout(300).textContains(UiElementsActivity.Event.SWIPE_RIGHT.name) }.swipeRight(40) } @Test fun swipeLeftTest(){ page.swipableImageView.withAssertion { page.eventStatus.withTimeout(300).textContains(UiElementsActivity.Event.SWIPE_LEFT.name) }.swipeLeft(40) } } ================================================ FILE: sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/uiautomator/UltronUiObjectAssertionsTest.kt ================================================ package com.atiurin.sampleapp.tests.uiautomator import com.atiurin.sampleapp.R import com.atiurin.sampleapp.framework.utils.AssertUtils import com.atiurin.sampleapp.pages.UiObjectElementsPage import com.atiurin.sampleapp.tests.UiElementsTest import com.atiurin.ultron.utils.getTargetString import org.hamcrest.Matchers.* import org.junit.Assert import org.junit.Test class UltronUiObjectAssertionsTest: UiElementsTest() { val page = UiObjectElementsPage() //hasText @Test fun hasText_CorrectText_withResourceId() { page.editTextContentDesc.hasText(getTargetString(R.string.button_default_content_desc)) } @Test fun hasText_InvalidText_withResourceId() { AssertUtils.assertException { page.editTextContentDesc.withTimeout(100).withTimeout(100).hasText("invalid text") } } @Test fun hasText_notExisted(){ AssertUtils.assertException { page.notExistedObject.withTimeout(100).withTimeout(100).hasText("asd") } } //hasText matcher @Test fun hasText_matcher_CorrectText() { page.editTextContentDesc.hasText(containsString("content description")) } @Test fun hasText_matcher_InvalidText() { AssertUtils.assertException { page.editTextContentDesc.withTimeout(100).hasText(equalToIgnoringCase("invalid text") ) } } @Test fun hasText_matcher_notExisted(){ AssertUtils.assertException { page.notExistedObject.withTimeout(100).hasText(containsString("asd") ) } } //textContains @Test fun textContains_validText(){ val substring = getTargetString(R.string.button_text).substring(0, 5) page.button.textContains(substring) } @Test fun textContains_invalidText(){ AssertUtils.assertException { page.button.withTimeout(100).textContains("invalid substring" ) } } @Test fun textContains_notExisted(){ AssertUtils.assertException { page.notExistedObject.withTimeout(100).textContains("invalid substring" ) } } //textIsNullOrEmpty @Test fun textIsNullOrEmpty_nullText(){ page.editTextContentDesc.clearTextField().withTimeout(100).textIsNullOrEmpty() } @Test fun textIsNullOrEmpty_notEmptyText(){ AssertUtils.assertException { page.editTextContentDesc.withTimeout(100).hasText("") } } @Test fun textIsNullOrEmpty_notExisted(){ AssertUtils.assertException { page.notExistedObject.withTimeout(100).hasText("") } } //textIsNotNullOrEmpty @Test fun textIsNotNullOrEmpty_notEmptyText(){ page.editTextContentDesc.textIsNotNullOrEmpty() } @Test fun textIsNotNullOrEmpty_nullText(){ AssertUtils.assertException { page.editTextContentDesc.clearTextField().withTimeout(100).textIsNotNullOrEmpty() } } //contentDescription //hasContentDescription @Test fun hasContentDescription_CorrectText_withResourceId() { page.button.hasContentDescription(getTargetString(R.string.button_default_content_desc)) } @Test fun hasContentDescription_InvalidText_withResourceId() { AssertUtils.assertException { page.button.withTimeout(100).hasContentDescription("invalid text" ) } } @Test fun hasContentDescription_notExisted(){ AssertUtils.assertException { page.notExistedObject.withTimeout(100).hasContentDescription("asd" ) } } //contentDescription matcher @Test fun hasContentDescription_matcher_CorrectText() { page.button.hasContentDescription(containsString(getTargetString(R.string.button_default_content_desc).substring(0, 5))) } @Test fun hasContentDescription_matcher_InvalidText() { AssertUtils.assertException { page.button.withTimeout(100).hasContentDescription(equalToIgnoringCase("invalid text") ) } } @Test fun hasContentDescription_matcher_notExisted(){ AssertUtils.assertException { page.notExistedObject.withTimeout(100).hasContentDescription(containsString("asd") ) } } //contentDescriptionContains @Test fun contentDescriptionContains_validText(){ val substring = getTargetString(R.string.button_default_content_desc).substring(0, 5) page.button.contentDescriptionContains(substring) } @Test fun contentDescriptionContains_invalidText(){ AssertUtils.assertException { page.button.withTimeout(100).contentDescriptionContains("invalid substring" ) } } @Test fun contentDescriptionContains_notExisted(){ AssertUtils.assertException { page.notExistedObject.withTimeout(100).contentDescriptionContains("invalid substring" ) } } //contentDescriptionIsNullOrEmpty @Test fun contentDescriptionIsNullOrEmpty_nullText(){ page.editTextContentDesc.clearTextField() page.button.contentDescriptionIsNullOrEmpty() } @Test fun contentDescriptionIsNullOrEmpty_notEmptyText(){ AssertUtils.assertException { page.button.withTimeout(100).contentDescriptionIsNullOrEmpty() } } @Test fun contentDescriptionIsNullOrEmpty_notExisted(){ AssertUtils.assertException { page.notExistedObject.withTimeout(100).contentDescriptionIsNullOrEmpty() } } //contentDescriptionIsNotNullOrEmpty @Test fun contentDescriptionIsNotNullOrEmpty_notEmptyText(){ page.button.contentDescriptionIsNotNullOrEmpty() } @Test fun contentDescriptionIsNotNullOrEmpty_nullText(){ page.editTextContentDesc.clearTextField() page.button.contentDescriptionIsNullOrEmpty() } //exists @Test fun isDisplayed_ofDisplayedObject() { page.button.exists() } @Test fun isDisplayed_ofInvisibleObject() { page.radioInvisibleButton.click() AssertUtils.assertException { page.button.withTimeout(100).exists() } } @Test fun isDisplayed_BooleanTrue_ofInvisibleObject() { val result = page.radioInvisibleButton.isSuccess { withTimeout(100).exists() } Assert.assertTrue(result) } @Test fun isDisplayed_BooleanFalse_ofInvisibleObject() { page.radioInvisibleButton.click() val result = page.button.isSuccess { withTimeout(100).exists() } Assert.assertFalse(result) } //notExists @Test fun isNotDisplayed_ofDisplayedObject() { AssertUtils.assertException { page.button.exists().withTimeout(100).notExists() } } @Test fun isNotDisplayed_ofInvisibleObject() { page.radioInvisibleButton.click() page.button.notExists() } //isCheckable @Test fun isCheckable_ofCheckable(){ page.checkBoxClickable.isCheckable() } @Test fun isCheckable_ofNotCheckable(){ AssertUtils.assertException { page.emptyImageView.withTimeout(100).isCheckable() } } //isNotCheckable @Test fun isNotCheckable_ofNotCheckable(){ page.emptyImageView.isNotCheckable() } @Test fun isNotCheckable_ofCheckable(){ AssertUtils.assertException { page.checkBoxClickable.withTimeout(100).isNotCheckable() } } //isChecked @Test fun isChecked_ofChecked(){ page.checkBoxClickable.isChecked() } @Test fun isChecked_ofNotChecked(){ AssertUtils.assertException { page.checkBoxSelected.withTimeout(100).isChecked() } } //isNotChecked @Test fun isNotChecked_ofNotChecked(){ page.checkBoxSelected.isNotChecked() } @Test fun isNotChecked_ofChecked(){ AssertUtils.assertException { page.checkBoxClickable.withTimeout(100).isNotChecked() } } //isClickable @Test fun isClickable_ofClickable(){ page.button.isClickable() } @Test fun isClickable_ofNotClickable(){ page.checkBoxClickable.click() AssertUtils.assertException { page.button.withTimeout(100).isClickable() } } //isNotClickable @Test fun isNotClickable_ofNotClickable(){ page.checkBoxClickable.click() page.button.isNotClickable() } @Test fun isNotClickable_ofClickable(){ AssertUtils.assertException { page.button.withTimeout(100).isNotClickable() } } //isEnabled @Test fun isEnabled_ofEnable(){ page.button.isEnabled() } @Test fun isEnabled_ofNotEnable(){ page.checkBoxEnabled.click() AssertUtils.assertException { page.button.withTimeout(100).isEnabled() } } //isNotEnabled @Test fun isNotEnabled_ofNotEnable(){ page.checkBoxEnabled.click() page.button.isNotEnabled() } @Test fun isNotEnabled_ofEnable(){ AssertUtils.assertException { page.button.withTimeout(100).isNotEnabled() } } //isFocusable @Test fun isFocusable_ofFocusable(){ page.button.isFocusable() } @Test fun isFocusable_ofNotFocusable(){ page.checkBoxFocusable.click() AssertUtils.assertException { page.button.withTimeout(100).isFocusable() } } //isNotFocusable @Test fun isNotFocusable_ofNotFocusable(){ page.checkBoxFocusable.withAssertion { page.button.isNotFocusable() }.click() } @Test fun isNotFocusable_ofFocusable(){ AssertUtils.assertException { page.button.withTimeout(100).isNotFocusable() } } //isFocused @Test fun isFocused_ofFocused(){ page.editTextContentDesc.click().isFocused() } @Test fun isFocused_ofNotFocused(){ AssertUtils.assertException { page.editTextContentDesc.withTimeout(100).isFocused() } } //isNotFocused @Test fun isNotFocused_ofNotFocused(){ page.editTextContentDesc.isNotFocused() } @Test fun isNotFocused_ofFocused(){ AssertUtils.assertException { page.editTextContentDesc.click().withTimeout(100).isNotFocused() } } //isLongClickable @Test fun isLongClickable_ofLongClickable(){ page.button.isLongClickable() } @Test fun isLongClickable_ofNotLongClickable(){ AssertUtils.assertException { page.emptyImageView.withTimeout(100).isLongClickable() } } //isNotLongClickable @Test fun isNotLongClickable_ofNotLongClickable(){ page.emptyImageView.isNotLongClickable() } @Test fun isNotLongClickable_ofLongClickable(){ AssertUtils.assertException { page.button.withTimeout(100).isNotLongClickable() } } //isSelected @Test fun isSelected_ofSelected(){ page.checkBoxSelected.click() page.button.isSelected() } @Test fun isSelected_ofNotSelected(){ AssertUtils.assertException { page.button.withTimeout(100).isSelected() } } //isNotSelected @Test fun isNotSelected_ofNotSelected(){ page.button.isNotSelected() } @Test fun isNotSelected_ofSelected(){ page.checkBoxSelected.click() AssertUtils.assertException { page.button.withTimeout(100).isNotSelected() } } //isNotScrollable @Test fun isNotScrollable_ofNotScrollable(){ page.button.isNotScrollable() } @Test fun isScrollable_ofNotScrollable(){ AssertUtils.assertException { page.button.withTimeout(100).isScrollable() } } //assertThat @Test fun assertThat_validAssertion_existedObject(){ page.button.assertThat({ this.isClickable }, "Object is clickable") } @Test fun assertThat_invalidAssertion_existedObject(){ AssertUtils.assertException { page.button.withTimeout(100).assertThat({ !this.isClickable }, "Object isn't clickable") } } @Test fun assertThat_notExistedObject(){ AssertUtils.assertException { page.notExistedObject.withTimeout(100).assertThat({ !this.isClickable }, "Should fail") } } @Test fun resultHandlerTest(){ page.editTextContentDesc.withResultHandler { Assert.assertFalse(it.success) Assert.assertTrue(it.description.isNotEmpty()) }.withTimeout(100).hasText("invalid text") AssertUtils.assertException { page.editTextContentDesc.withTimeout(100).notExists() } } } ================================================ FILE: sample-app/src/debug/AndroidManifest.xml ================================================ ================================================ FILE: sample-app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: sample-app/src/main/assets/webview.html ================================================ Android Web View

WebView title

  1. Apple
  2. Banana

The button below activates a JavaScript when it is clicked.

Teachers

Socrates

Students

Plato

list_element_1

list_element_2

list_element_3

list_element_4

list_element_5

list_element_6

list_element_7

list_element_8

list_element_9

list_element_10

list_element_11

list_element_12

================================================ FILE: sample-app/src/main/assets/webview_small.html ================================================ Android Web View

WebView title

================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/MyApplication.kt ================================================ package com.atiurin.sampleapp import android.app.Application import android.content.Context object MyApplication : Application() { var context: Context? = null override fun onCreate() { super.onCreate() context = applicationContext } var CONTACTS_LOADING_TIMEOUT_MS = 2000L } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/activity/BusyActivity.kt ================================================ package com.atiurin.sampleapp.activity import android.app.Activity import android.os.Bundle import android.os.Handler import android.os.Looper class BusyActivity : Activity(){ private val handler = Handler(Looper.getMainLooper()) private val busyRunnable = object : Runnable { override fun run() { // Post a delayed runnable to keep the main thread busy indefinitely handler.postDelayed(this, 0) } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Start the busy loop handler.post(busyRunnable) } override fun onDestroy() { super.onDestroy() handler.removeCallbacks(busyRunnable) } } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/activity/ChatActivity.kt ================================================ package com.atiurin.sampleapp.activity import android.os.Build import android.os.Bundle import android.util.Log import android.view.Menu import android.view.MenuItem import android.view.View import android.widget.EditText import android.widget.ImageView import android.widget.TextView import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.atiurin.sampleapp.R import com.atiurin.sampleapp.adapters.MessageAdapter import com.atiurin.sampleapp.data.entities.Contact import com.atiurin.sampleapp.data.entities.Message import com.atiurin.sampleapp.data.repositories.CURRENT_USER import com.atiurin.sampleapp.data.repositories.ContactRepositoty import com.atiurin.sampleapp.data.repositories.MessageRepository import com.atiurin.sampleapp.view.CircleImageView const val INTENT_CONTACT_ID_EXTRA_NAME = "contactId" class ChatActivity : AppCompatActivity(){ private lateinit var recyclerView: RecyclerView private lateinit var viewAdapter: MessageAdapter private lateinit var viewManager: RecyclerView.LayoutManager private lateinit var contact: Contact private val onItemClickListener: View.OnClickListener? = null override fun onCreate(savedInstanceState: Bundle?) { // enableEdgeToEdge() super.onCreate(savedInstanceState) setContentView(R.layout.activity_chat) val context = this //TOOLBAR val toolbar: Toolbar = findViewById(R.id.toolbar) toolbar.title = "" setSupportActionBar(toolbar) supportActionBar!!.setDisplayHomeAsUpEnabled(true) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { window.statusBarColor = getColor(R.color.colorPrimaryDark) } val mIntent = intent val title = findViewById(R.id.toolbar_title) val contactId = mIntent.getIntExtra(INTENT_CONTACT_ID_EXTRA_NAME, -1) if (contactId < 0){ Log.d("EspressoGuide", "Something goes wrong!") } contact = ContactRepositoty.getContact(contactId) title.text = contact.name val avatar = findViewById(R.id.toolbar_avatar) avatar.setImageDrawable(getDrawable(contact.avatar)) //message input area val messageInput = findViewById(R.id.message_input_text) val sendBtn = findViewById(R.id.send_button) val attachBtn = findViewById(R.id.attach_button) //recycler view and adapter viewManager = LinearLayoutManager(this) viewAdapter = MessageAdapter( ArrayList(), object : MessageAdapter.OnItemClickListener { override fun onItemClick(message: Message) { Log.w("EspressoGuid", "Clicked message ${message.text}") } }) recyclerView = findViewById(R.id.messages_list).apply { setHasFixedSize(true) layoutManager = viewManager adapter = viewAdapter } sendBtn.setOnClickListener( object: View.OnClickListener{ override fun onClick(v: View?) { if (messageInput.text.isEmpty()){ Toast.makeText(context, "Type message text", Toast.LENGTH_LONG).show() }else{ val mes = Message(CURRENT_USER.id, contactId, messageInput.text.toString()) val curMessages = MessageRepository.messages curMessages.add(mes) updateAdapter(curMessages) messageInput.setText("") recyclerView.smoothScrollToPosition(viewAdapter.itemCount - 1) } } }) } override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.main, menu) return true } override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.action_clear -> { updateAdapter(ArrayList()) true } else -> super.onOptionsItemSelected(item) } } override fun onSupportNavigateUp(): Boolean { onBackPressed() return true } override fun onResume() { super.onResume() viewAdapter.updateData(MessageRepository.getChatMessages(contact.id)) viewAdapter.notifyDataSetChanged() } private fun updateAdapter(list: ArrayList){ MessageRepository.messages = list viewAdapter.updateData(list) viewAdapter.notifyDataSetChanged() } } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/activity/ComposeElementsActivity.kt ================================================ package com.atiurin.sampleapp.activity import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.selection.toggleable import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Button import androidx.compose.material.ButtonDefaults import androidx.compose.material.Checkbox import androidx.compose.material.CheckboxDefaults import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Icon import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Favorite import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.stateDescription import androidx.compose.ui.semantics.testTag import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.ExperimentalUnitApi import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.atiurin.sampleapp.activity.ComposeElementsActivity.Constants.clickListenerButton import com.atiurin.sampleapp.activity.ComposeElementsActivity.Constants.contactBlock1Tag import com.atiurin.sampleapp.activity.ComposeElementsActivity.Constants.contactBlock2Tag import com.atiurin.sampleapp.activity.ComposeElementsActivity.Constants.contactNameTag import com.atiurin.sampleapp.activity.ComposeElementsActivity.Constants.contactStatusTag import com.atiurin.sampleapp.activity.ComposeElementsActivity.Constants.disabledButton import com.atiurin.sampleapp.activity.ComposeElementsActivity.Constants.likesCounterButton import com.atiurin.sampleapp.activity.ComposeElementsActivity.Constants.likesCounterContentDesc import com.atiurin.sampleapp.activity.ComposeElementsActivity.Constants.likesCounterTextContainer import com.atiurin.sampleapp.activity.ComposeElementsActivity.Constants.likesCounterTextContainerContentDesc import com.atiurin.sampleapp.compose.CustomButton import com.atiurin.sampleapp.compose.LinearProgressBar import com.atiurin.sampleapp.compose.RadioGroup import com.atiurin.sampleapp.compose.RegionsClickListener import com.atiurin.sampleapp.compose.SimpleOutlinedText import com.atiurin.sampleapp.compose.SwipeableNode import com.atiurin.sampleapp.data.repositories.CONTACTS class ComposeElementsActivity : ComponentActivity() { companion object Constants { const val statusText = "statusText" const val likesCounterButton = "LikeS_CounteR" const val likesCounterTextContainer = "text_container" const val clickListenerButton = "click_listener" const val simpleCheckbox = "simpleCheckbox" const val editableText = "editableText" const val swipeableNode = "swipableNode" const val progressBar = "ProgressBar" const val disabledButton = "disabledButton" const val radioButtonMaleTestTag = "radioButtonMaleTestTag" const val radioButtonFemaleTestTag = "radioButtonFemaleTestTag" const val likesCounterContentDesc = "LikeS_CounteR_ContentDesc" const val likesCounterTextContainerContentDesc = "text_container_content_desc" const val contactsListTag = "contactsListTag" const val contactBlock1Tag = "contactBlock1" const val contactBlock2Tag = "contactBlock2" const val contactNameTag = "firstNameTag" const val contactStatusTag = "lastNameTag" } @ExperimentalMaterialApi @ExperimentalUnitApi @ExperimentalFoundationApi override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState) setContent { Column( modifier = Modifier .fillMaxSize() .padding(start = 16.dp), verticalArrangement = Arrangement.Top, horizontalAlignment = Alignment.Start ) { val status = remember { mutableStateOf("nothing") } Text(text = status.value, modifier = Modifier.semantics { testTag = statusText }) ButtonWithCount() CheckBox(title = "Simple checkbox", testTagValue = simpleCheckbox, status) ClickListener(status) RegionsClickListener(status) SimpleOutlinedText(myTestTag = editableText) SwipeableNode(status) DisabledButton() LinearProgressBar(statusState = status) RadioGroup() ContactsList(modifier = Modifier.testTag(contactsListTag)) } } } } enum class ActionsStatus { LongClicked, DoubleClicked, Clicked, SwipeDown, SwipeUp, SwipeRight, SwipeLeft } @Composable fun CheckBox(title: String, testTagValue: String, statusState: MutableState) { val state = remember { mutableStateOf(false) } Row(verticalAlignment = Alignment.CenterVertically) { Checkbox( checked = state.value, onCheckedChange = { state.value = it }, enabled = true, colors = CheckboxDefaults.colors(Color.Green), modifier = Modifier .semantics { testTag = testTagValue stateDescription = "default" } .toggleable(state.value, true, onValueChange = { newValue -> statusState.value = "toggleable state $newValue" }) ) Text(text = title, modifier = Modifier.padding(16.dp)) } } @OptIn(ExperimentalFoundationApi::class) @Composable fun ButtonWithCount() { var count = remember { mutableStateOf(0) } Button(onClick = { count.value++ }, modifier = Modifier .semantics { testTag = likesCounterButton contentDescription = likesCounterContentDesc }) { Icon( imageVector = Icons.Filled.Favorite, contentDescription = null ) Spacer(Modifier.size(ButtonDefaults.IconSpacing)) Text("Like count = ${count.value}", modifier = Modifier.semantics { testTag = likesCounterTextContainer contentDescription = likesCounterTextContainerContentDesc } ) } } @Preview @Composable fun ContactInfoBlockPreview() { ContactInfoBlock(name= "Tonny Kark", status = "Wipes off Thanos" ) } @Composable fun ClickListener(statusState: MutableState) { CustomButton( onClickAction = { statusState.value = ActionsStatus.Clicked.name }, onLongClick = { statusState.value = ActionsStatus.LongClicked.name }, onDoubleClick = { statusState.value = ActionsStatus.DoubleClicked.name }, modifier = Modifier .semantics { testTag = clickListenerButton contentDescription = clickListenerButton } ) { Text(text = "Click listener button") } } @Composable fun DisabledButton() { Button( modifier = Modifier.semantics { testTag = disabledButton }, onClick = {}, content = { Text(text = "Disabled button") }, enabled = false ) } @Composable fun ContactsList(modifier: Modifier){ Column(modifier = modifier) { ContactInfoBlock(Modifier.testTag(contactBlock1Tag), CONTACTS[0].name, CONTACTS[0].status) ContactInfoBlock(Modifier.testTag(contactBlock2Tag), CONTACTS[1].name, CONTACTS[1].status) } } @Composable fun ContactInfoBlock( modifier: Modifier = Modifier, name: String, status: String ) { Card( modifier = modifier .fillMaxWidth() .padding(16.dp), shape = RoundedCornerShape(12.dp), elevation = CardDefaults.cardElevation(8.dp), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface) ) { Row( modifier = Modifier .padding(16.dp) .fillMaxWidth() .testTag("Inner row"), verticalAlignment = Alignment.CenterVertically ) { Box( modifier = Modifier .size(48.dp) .background( color = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f), shape = RoundedCornerShape(24.dp) ) ) { Text( text = name.firstOrNull()?.toString()?.uppercase() ?: "", modifier = Modifier.align(Alignment.Center), style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.Bold), color = MaterialTheme.colorScheme.primary ) } Spacer(modifier = Modifier.width(16.dp)) Column { Text( modifier = Modifier.testTag(contactNameTag), text = name, style = MaterialTheme.typography.bodyLarge.copy( fontSize = 18.sp, fontWeight = FontWeight.Bold ), color = MaterialTheme.colorScheme.onSurface ) Text( modifier = Modifier.testTag(contactStatusTag), text = status, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) } } } } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/activity/ComposeListActivity.kt ================================================ package com.atiurin.sampleapp.activity import android.os.Bundle import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.ExperimentalUnitApi import androidx.lifecycle.Observer import com.atiurin.sampleapp.async.GetContacts import com.atiurin.sampleapp.async.UseCase import com.atiurin.sampleapp.compose.ContactsList import com.atiurin.sampleapp.compose.LoadingAnimation import com.atiurin.sampleapp.compose.getContactItemTestTagById import com.atiurin.sampleapp.compose.listItemPosition import com.atiurin.sampleapp.data.entities.Contact import com.atiurin.sampleapp.data.repositories.ContactRepositoty import com.atiurin.sampleapp.data.viewmodel.ContactsViewModel import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.async class ComposeListActivity : ComponentActivity() { val model: ContactsViewModel by viewModels() @ExperimentalMaterialApi @ExperimentalUnitApi @ExperimentalFoundationApi override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState) setContent { Column( modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { LoadingAnimation() } } val contactsObserver = Observer> { setContent { Column { ContactsList( contacts = ContactRepositoty.all(), context = this@ComposeListActivity, testTagProvider = { contact, _ -> getContactItemTestTagById(contact) }, modifierProvider = { position -> Modifier.listItemPosition(position) } ) } } } model.contacts.observe(this, contactsObserver) GlobalScope.async { GetContacts(0)( UseCase.None, onSuccess = { model.contacts.value = it }, onFailure = { Toast.makeText(this@ComposeListActivity, "Failed to load contacts", Toast.LENGTH_LONG).show() } ) } } } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/activity/ComposeListWithPositionTestTagActivity.kt ================================================ package com.atiurin.sampleapp.activity import android.os.Bundle import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.ExperimentalUnitApi import androidx.lifecycle.Observer import com.atiurin.sampleapp.async.GetContacts import com.atiurin.sampleapp.async.UseCase import com.atiurin.sampleapp.compose.ContactsList import com.atiurin.sampleapp.compose.LoadingAnimation import com.atiurin.sampleapp.compose.getContactItemTestTagById import com.atiurin.sampleapp.compose.getContactItemTestTagByPosition import com.atiurin.sampleapp.compose.listItemPosition import com.atiurin.sampleapp.data.entities.Contact import com.atiurin.sampleapp.data.repositories.ContactRepositoty import com.atiurin.sampleapp.data.viewmodel.ContactsViewModel import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.async class ComposeListWithPositionTestTagActivity: ComponentActivity() { val model: ContactsViewModel by viewModels() @ExperimentalMaterialApi @ExperimentalUnitApi @ExperimentalFoundationApi override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState) setContent { Column( modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { LoadingAnimation() } } val contactsObserver = Observer> { setContent { Column { ContactsList( contacts = ContactRepositoty.all(), context = this@ComposeListWithPositionTestTagActivity, addStickyHeader = false, testTagProvider = { _, position -> getContactItemTestTagByPosition(position) }, modifierProvider = { _ -> Modifier } ) } } } model.contacts.observe(this, contactsObserver) GlobalScope.async { GetContacts(0)( UseCase.None, onSuccess = { model.contacts.value = it }, onFailure = { Toast.makeText(this@ComposeListWithPositionTestTagActivity, "Failed to load contacts", Toast.LENGTH_LONG).show() } ) } } } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/activity/ComposeRouterActivity.kt ================================================ package com.atiurin.sampleapp.activity import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.ui.unit.ExperimentalUnitApi import com.atiurin.sampleapp.compose.app.App class ComposeRouterActivity : ComponentActivity() { @ExperimentalMaterialApi @ExperimentalUnitApi @ExperimentalFoundationApi override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState) setContent { App() } } } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/activity/ComposeSecondActivity.kt ================================================ package com.atiurin.sampleapp.activity import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.material.Text import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag import androidx.compose.ui.unit.ExperimentalUnitApi import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.TextUnitType import androidx.compose.ui.unit.dp import com.atiurin.sampleapp.data.repositories.ContactRepositoty class ComposeSecondActivity : ComponentActivity() { @ExperimentalUnitApi override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState) val contactId = intent.getIntExtra(INTENT_CONTACT_ID, -1) val contact = ContactRepositoty.getContact(contactId) setContent { Column() { Text(contact.name, Modifier.semantics { testTag = "name" }, fontSize = TextUnit(16f, TextUnitType.Sp) ) Spacer(modifier = Modifier.height(8.dp)) Text(text = contact.status, Modifier.semantics { testTag = "status" }) } } } companion object { const val INTENT_CONTACT_ID = "contactId" } } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/activity/CustomClicksActivity.kt ================================================ package com.atiurin.sampleapp.activity import android.os.Bundle import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import com.atiurin.sampleapp.R import com.atiurin.sampleapp.async.task.CompatAsyncTask import com.atiurin.sampleapp.async.task.CompatAsyncTask.Companion.ASYNC class CustomClicksActivity : AppCompatActivity() { private val compatAsyncTask = CompatAsyncTask() override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState) setContentView(R.layout.activity_custom_clicks) if(shouldBeAsyncTaskStart()) { startCompatAsyncTask() } } fun shouldBeAsyncTaskStart(): Boolean = intent.getBooleanExtra(ASYNC, false) fun startCompatAsyncTask() { compatAsyncTask.start() } fun stopCompatAsyncTask() { compatAsyncTask.stop() } } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/activity/LoginActivity.kt ================================================ package com.atiurin.sampleapp.activity import android.content.Intent import android.os.Bundle import android.view.Gravity import android.widget.Button import android.widget.EditText import android.widget.Toast import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import com.atiurin.sampleapp.R import com.atiurin.sampleapp.managers.AccountManager class LoginActivity : AppCompatActivity(){ lateinit var etUserName : EditText lateinit var etPassword : EditText lateinit var loginBtn : Button override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState) setContentView(R.layout.activity_login) val bar = supportActionBar bar!!.title = "Login or Sign Up" etUserName = findViewById(R.id.et_username) etPassword = findViewById(R.id.et_password) loginBtn = findViewById(R.id.login_button) loginBtn.setOnClickListener{ val accountManager = AccountManager(applicationContext) val userName = etUserName.text.toString() val password = etPassword.text.toString() if (userName.isEmpty()){ with(etUserName){ setHint("Enter user name") setHintTextColor(resources.getColor(android.R.color.holo_red_dark)) } } if (password.isEmpty()){ with(etPassword){ setHint("Enter password") setHintTextColor(resources.getColor(android.R.color.holo_red_dark)) } } val result = accountManager.login(userName, password) if (result){ var intent = Intent(this, MainActivity::class.java) startActivity(intent) }else{ var toast = Toast.makeText(applicationContext, "Wrong login or password", Toast.LENGTH_LONG) toast.setGravity(Gravity.CENTER_VERTICAL, 0, 0) toast.show() } } } } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/activity/MainActivity.kt ================================================ package com.atiurin.sampleapp.activity import android.content.Intent import android.os.Bundle import android.view.MenuItem import android.view.View import android.widget.Toast import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.ActionBarDrawerToggle import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar import androidx.core.view.GravityCompat import androidx.drawerlayout.widget.DrawerLayout import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.atiurin.sampleapp.MyApplication import com.atiurin.sampleapp.R import com.atiurin.sampleapp.adapters.ContactAdapter import com.atiurin.sampleapp.async.ContactsPresenter import com.atiurin.sampleapp.async.ContactsProvider import com.atiurin.sampleapp.data.Tags import com.atiurin.sampleapp.data.entities.Contact import com.atiurin.sampleapp.idlingresources.IdlingHelper import com.atiurin.sampleapp.idlingresources.resources.ContactsIdlingResource import com.atiurin.sampleapp.managers.AccountManager import com.atiurin.sampleapp.view.CircleImageView import com.google.android.material.navigation.NavigationView import com.google.android.material.snackbar.Snackbar class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener, ContactsProvider { private lateinit var recyclerView: RecyclerView private lateinit var viewAdapter: ContactAdapter private lateinit var viewManager: RecyclerView.LayoutManager private lateinit var accountManager: AccountManager private val onItemClickListener: View.OnClickListener? = null private val contactsPresenter = ContactsPresenter(this) override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState) accountManager = AccountManager(applicationContext) if (!accountManager.isLogedIn()) { val intent = Intent(applicationContext, LoginActivity::class.java) startActivity(intent) } setContentView(R.layout.activity_main) MyApplication.context = applicationContext val toolbar: Toolbar = findViewById(R.id.toolbar) toolbar.setTitle(R.string.title_friends_list) setSupportActionBar(toolbar) val drawerLayout: DrawerLayout = findViewById(R.id.drawer_layout) val navView: NavigationView = findViewById(R.id.nav_view) val navigationAvatar = navView.getHeaderView(0).findViewById(R.id.navigation_user_avatar) navigationAvatar.setOnClickListener { startActivity(Intent(applicationContext, ProfileActivity::class.java)) } val toggle = ActionBarDrawerToggle( this, drawerLayout, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close ) drawerLayout.addDrawerListener(toggle) toggle.syncState() navView.setNavigationItemSelectedListener(this) viewManager = LinearLayoutManager(this) viewAdapter = ContactAdapter(this, ArrayList(), object : ContactAdapter.OnItemClickListener { override fun onItemClick(contact: Contact) { val intent = Intent(applicationContext, ChatActivity::class.java) intent.putExtra(INTENT_CONTACT_ID_EXTRA_NAME, contact.id) startActivity(intent) } }) recyclerView = findViewById(R.id.recycler_friends).apply { setHasFixedSize(true) layoutManager = viewManager adapter = viewAdapter } recyclerView.tag = Tags.CONTACTS_LIST contactsPresenter.getAllContacts() } override fun onBackPressed() { val drawerLayout: DrawerLayout = findViewById(com.atiurin.sampleapp.R.id.drawer_layout) if (drawerLayout.isDrawerOpen(GravityCompat.START)) { drawerLayout.closeDrawer(GravityCompat.START) } else { super.onBackPressed() } } override fun onContactsLoaded(contacts: ArrayList) { viewAdapter.updateData(contacts) viewAdapter.notifyDataSetChanged() IdlingHelper.ifAllowed { ContactsIdlingResource.getInstanceFromApp()?.setIdleState(true) } } override fun onFailedToLoadContacts(message: String?) { Toast.makeText(this, message, Toast.LENGTH_LONG).show() } override fun onNavigationItemSelected(item: MenuItem): Boolean { // Handle navigation view item clicks here. when (item.itemId) { R.id.nav_settings -> { Snackbar.make(recyclerView, "Settings not implemented", Snackbar.LENGTH_LONG) .setAction("Action", null).show() } R.id.nav_saved_messages -> { Snackbar.make(recyclerView, "Saved messages not implemented", Snackbar.LENGTH_LONG) .setAction("Action", null).show() } R.id.nav_profile -> { Toast.makeText(this, "Profile not implemented", Toast.LENGTH_LONG).show() } R.id.nav_logout -> { accountManager.logout() val intent = Intent(applicationContext, LoginActivity::class.java) startActivity(intent) } R.id.ui_elements -> { val intent = Intent(applicationContext, UiElementsActivity::class.java) startActivity(intent) } R.id.web_view_nav_item -> { val intent = Intent(applicationContext, WebViewActivity::class.java) startActivity(intent) } R.id.compose_elements -> { startActivity(Intent(applicationContext, ComposeElementsActivity::class.java)) } R.id.compose_router -> { startActivity(Intent(applicationContext, ComposeRouterActivity::class.java)) } R.id.compose_list -> { startActivity(Intent(applicationContext, ComposeListActivity::class.java)) } R.id.custom_clicks_nav_item -> { val intent = Intent(applicationContext, CustomClicksActivity::class.java) startActivity(intent) } } val drawerLayout: DrawerLayout = findViewById(R.id.drawer_layout) drawerLayout.closeDrawer(GravityCompat.START) return true } } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/activity/ProfileActivity.kt ================================================ package com.atiurin.sampleapp.activity import android.os.Bundle import android.widget.EditText import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import com.atiurin.sampleapp.R import com.atiurin.sampleapp.data.repositories.CURRENT_USER import com.atiurin.sampleapp.view.CircleImageView class ProfileActivity : AppCompatActivity(){ override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState) setContentView(R.layout.activity_profile) val avatar = findViewById(R.id.avatar) avatar.setImageDrawable(getDrawable(CURRENT_USER.avatar)) val name = findViewById(R.id.et_username) name.hint = CURRENT_USER.name } } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/activity/SplashActivity.kt ================================================ package com.atiurin.sampleapp.activity import android.content.Intent import android.os.Bundle import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import com.atiurin.sampleapp.managers.AccountManager class SplashActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState) val accountManager = AccountManager(applicationContext) if (accountManager.isLogedIn()){ val intent = Intent(applicationContext, MainActivity::class.java) startActivity(intent) }else{ val intent = Intent(applicationContext, LoginActivity::class.java) startActivity(intent) } finish() } } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/activity/UiBlockActivity.kt ================================================ package com.atiurin.sampleapp.activity import android.os.Bundle import android.widget.LinearLayout import android.widget.TextView import androidx.appcompat.app.AppCompatActivity import com.atiurin.sampleapp.R import com.atiurin.sampleapp.data.repositories.CONTACTS class UiBlockActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_uiblock) val contactItem1: LinearLayout = this.findViewById(R.id.contact_item_1) val contactItem2: LinearLayout = this.findViewById(R.id.contact_item_2) contactItem1.findViewById(R.id.name).text = CONTACTS[0].name contactItem1.findViewById(R.id.status).text = CONTACTS[0].status contactItem2.findViewById(R.id.name).text = CONTACTS[1].name contactItem2.findViewById(R.id.status).text = CONTACTS[1].status } override fun onSupportNavigateUp(): Boolean { onBackPressed() return true } } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/activity/UiElementsActivity.kt ================================================ package com.atiurin.sampleapp.activity import android.annotation.SuppressLint import android.os.Build import android.os.Bundle import android.text.Editable import android.text.TextWatcher import android.util.Log import android.view.View import android.view.View.* import android.webkit.WebView import android.widget.* import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.Observer import com.atiurin.sampleapp.R import com.atiurin.sampleapp.async.AsyncDataLoading import com.atiurin.sampleapp.async.UseCase import com.atiurin.sampleapp.data.viewmodel.DataViewModel import com.atiurin.sampleapp.view.listeners.OnSwipeTouchListener import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.async class UiElementsActivity : AppCompatActivity() { var lastEventDescription: TextView? = null var clickedInRow = 0 var lastEvent = Event.NO_EVENT val model: DataViewModel by viewModels() @SuppressLint("ClickableViewAccessibility") override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState) setContentView(R.layout.activity_uielements) val simpleButton: Button = findViewById(R.id.button1) simpleButton.visibility = GONE lastEventDescription = findViewById(R.id.last_event_status) val enableCheckBox: CheckBox = findViewById(R.id.checkbox_enable) val clickableCheckBox: CheckBox = findViewById(R.id.checkbox_clickable) val selectedCheckBox: CheckBox = findViewById(R.id.checkbox_selected) val focusableCheckBox: CheckBox = findViewById(R.id.checkbox_focusable) val radioGroupVisibility: RadioGroup = findViewById(R.id.radio_group_visibility) val etContentDescription: EditText = findViewById(R.id.et_contentDesc) val webView: WebView = findViewById(R.id.webview) val jsCheckBox: CheckBox = findViewById(R.id.checkbox_js_enabled) val imageView : ImageView = findViewById(R.id.swipe_image_view) webView.settings.javaScriptEnabled = true val customHtml = applicationContext.assets.open("webview_small.html").reader().readText() webView.loadData(customHtml, "text/html", "UTF-8") simpleButton.setOnClickListener { setLastEvent(Event.CLICK, getString(R.string.button_event_click)) } simpleButton.setOnLongClickListener { view -> setLastEvent(Event.LONG_CLICK, getString(R.string.button_event_long_click)) return@setOnLongClickListener true } enableCheckBox.setOnClickListener { view -> val checked = (view as CheckBox).isChecked simpleButton.isEnabled = checked setLastEvent(Event.ENABLED, checked.toString()) } clickableCheckBox.setOnClickListener { view -> val checked = (view as CheckBox).isChecked simpleButton.isClickable = checked setLastEvent(Event.CLICKABLE, checked.toString()) } selectedCheckBox.setOnClickListener { view -> val checked = (view as CheckBox).isChecked simpleButton.isSelected = checked setLastEvent(Event.SELECTED, checked.toString()) } jsCheckBox.setOnClickListener { view -> val checked = (view as CheckBox).isChecked webView.settings.javaScriptEnabled = checked setLastEvent(Event.JS_ENABLED, checked.toString()) } radioGroupVisibility.setOnCheckedChangeListener { group, checkedId -> when (checkedId) { R.id.radio_visible -> simpleButton.visibility = View.VISIBLE R.id.radio_invisible -> simpleButton.visibility = View.INVISIBLE R.id.radio_gone -> simpleButton.visibility = View.GONE } setLastEvent(Event.DISPLAYED, simpleButton.visibility.toString()) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { focusableCheckBox.visibility = VISIBLE focusableCheckBox.setOnClickListener { view -> val checked = (view as CheckBox).isChecked if (checked) { simpleButton.focusable = FOCUSABLE } else { simpleButton.focusable = NOT_FOCUSABLE } setLastEvent(Event.FOCUSABLE, checked.toString()) } } val addTextChangedListener = etContentDescription.addTextChangedListener(object : TextWatcher { override fun afterTextChanged(text: Editable?) { simpleButton.contentDescription = text setLastEvent(Event.CONTENT_DESC, text.toString()) } override fun beforeTextChanged( s: CharSequence?, start: Int, count: Int, after: Int ) { } override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} }) val context = this imageView.setOnTouchListener(object : OnSwipeTouchListener(context) { override fun onSwipeUp() { setLastEvent(Event.SWIPE_UP) Log.d("Ultron", "onSwipeTop") } override fun onSwipeRight() { setLastEvent(Event.SWIPE_RIGHT) Log.d("Ultron", "onSwipeRight") } override fun onSwipeLeft() { setLastEvent(Event.SWIPE_LEFT) Log.d("Ultron", "onSwipeLeft") } override fun onSwipeDown() { setLastEvent(Event.SWIPE_DOWN) Log.d("Ultron", "onSwipeBottom") } }) val observer = Observer { simpleButton.visibility = VISIBLE setLastEvent(Event.DATA_LOADED) } model.data.observe(this, observer) GlobalScope.async { AsyncDataLoading(1600)( UseCase.None, onSuccess = { model.data.value = it }, onFailure = { Toast.makeText(this@UiElementsActivity, "Failed to load data", Toast.LENGTH_LONG).show() } ) } } override fun onSupportNavigateUp(): Boolean { onBackPressed() return true } override fun onResume() { super.onResume() } @SuppressLint("SetTextI18n") fun setLastEvent(event: Event, desc: String? = null) { var status = desc lastEvent = event if (lastEvent == Event.CLICK) { clickedInRow++ status += " $clickedInRow" } else clickedInRow = 0 lastEventDescription?.text = "${event.name}${if (desc != null) ": $status" else ""}" } enum class Event { NO_EVENT, CLICK, LONG_CLICK, CLICKABLE, ENABLED, SELECTED, FOCUSABLE, DISPLAYED, JS_ENABLED, CONTENT_DESC, SWIPE_LEFT, SWIPE_RIGHT, SWIPE_UP, SWIPE_DOWN, DATA_LOADED } } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/activity/WebViewActivity.kt ================================================ package com.atiurin.sampleapp.activity import android.os.Build import android.os.Bundle import android.text.Editable import android.text.TextWatcher import android.view.View import android.view.View.* import android.webkit.WebView import android.widget.* import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import com.atiurin.sampleapp.R class WebViewActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState) setContentView(R.layout.activity_webview) val webView: WebView = findViewById(R.id.webview) webView.settings.javaScriptEnabled = true val customHtml = applicationContext.assets.open("webview.html").reader().readText() webView.loadData(customHtml, "text/html", "UTF-8") } override fun onSupportNavigateUp(): Boolean { onBackPressed() return true } override fun onResume() { super.onResume() } } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/adapters/ContactAdapter.kt ================================================ package com.atiurin.sampleapp.adapters import android.content.Context import android.view.GestureDetector import android.view.LayoutInflater import android.view.MotionEvent import android.view.ViewGroup import android.widget.LinearLayout import android.widget.TextView import androidx.core.view.GestureDetectorCompat import androidx.recyclerview.widget.RecyclerView import com.atiurin.sampleapp.R import com.atiurin.sampleapp.data.entities.Contact import com.atiurin.sampleapp.view.CircleImageView class ContactAdapter( val context: Context, private var mDataset: ArrayList, val listener: OnItemClickListener ) : RecyclerView.Adapter(), GestureDetector.OnGestureListener { interface OnItemClickListener { fun onItemClick(item: Contact) } class MyViewHolder(val view: LinearLayout) : RecyclerView.ViewHolder(view) open fun updateData(data: ArrayList) { mDataset = data } override fun onCreateViewHolder( parent: ViewGroup, viewType: Int ): MyViewHolder { val view = LayoutInflater.from(parent.context) .inflate(R.layout.list_item, parent, false) as LinearLayout return MyViewHolder(view) } override fun onBindViewHolder(holder: MyViewHolder, position: Int) { holder.view.setOnClickListener { listener.onItemClick(mDataset.get(position)) } val tvTitle = holder.view.findViewById(R.id.tv_name) as TextView val avatar = holder.view.findViewById(R.id.avatar) as CircleImageView val status = holder.view.findViewById(R.id.tv_status) as TextView tvTitle.text = mDataset[position].name status.text = mDataset[position].status avatar.setImageDrawable( holder.view.context.getResources().getDrawable(mDataset[position].avatar) ) GestureDetectorCompat(context, this) } override fun getItemCount() = mDataset.size // GestureDetector.OnGestureListener override fun onDown(p0: MotionEvent): Boolean = true override fun onShowPress(p0: MotionEvent) = Unit override fun onSingleTapUp(p0: MotionEvent): Boolean = true override fun onScroll(p0: MotionEvent?, p1: MotionEvent, p2: Float, p3: Float): Boolean = true override fun onLongPress(p0: MotionEvent) = Unit override fun onFling(p0: MotionEvent?, p1: MotionEvent, p2: Float, p3: Float): Boolean = true } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/adapters/MessageAdapter.kt ================================================ package com.atiurin.sampleapp.adapters import android.view.Gravity import android.view.LayoutInflater import android.view.ViewGroup import android.widget.LinearLayout import android.widget.TextView import androidx.cardview.widget.CardView import androidx.core.view.get import androidx.recyclerview.widget.RecyclerView import com.atiurin.sampleapp.R import com.atiurin.sampleapp.data.entities.Message import com.atiurin.sampleapp.data.repositories.CURRENT_USER class MessageAdapter(private var messages: ArrayList, val listener: OnItemClickListener) : RecyclerView.Adapter() { interface OnItemClickListener { fun onItemClick(item: Message) } class MessageViewHolder(val view: LinearLayout) : RecyclerView.ViewHolder(view) open fun updateData(data: ArrayList) { messages = data } override fun onCreateViewHolder( parent: ViewGroup, viewType: Int ): MessageViewHolder { val view = LayoutInflater.from(parent.context) .inflate(R.layout.message_item, parent, false) as LinearLayout return MessageViewHolder(view) } override fun onBindViewHolder(holder: MessageViewHolder, position: Int) { holder.view.setOnClickListener { listener.onItemClick(messages.get(position)) } val messageText = holder.view.findViewById(R.id.message_text) as TextView val authorName = holder.view.findViewById(R.id.author) as TextView val message = messages[position] messageText.text = message.text if (message.authorId == CURRENT_USER.id){ val view = holder.view.get(0) val cardView = view.findViewById(R.id.card_view) cardView.setCardBackgroundColor(view.context.resources.getColor(R.color.colorLight)) val layoutParams = view.layoutParams if (layoutParams is LinearLayout.LayoutParams){ layoutParams.gravity = Gravity.RIGHT } view.layoutParams = layoutParams } } override fun getItemCount() = messages.size } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/async/AsyncDataLoading.kt ================================================ package com.atiurin.sampleapp.async import com.atiurin.sampleapp.MyApplication.CONTACTS_LOADING_TIMEOUT_MS import kotlinx.coroutines.delay class AsyncDataLoading(val delayMs: Long = CONTACTS_LOADING_TIMEOUT_MS) : UseCase() { override suspend fun run(params: None): Either { return try { delay(delayMs) Success( "Loaded") } catch (e: Exception) { Failure(e) } } } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/async/ContactsPresenter.kt ================================================ package com.atiurin.sampleapp.async import com.atiurin.sampleapp.data.entities.Contact import com.atiurin.sampleapp.idlingresources.IdlingHelper import com.atiurin.sampleapp.idlingresources.resources.ContactsIdlingResource import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlin.coroutines.CoroutineContext class ContactsPresenter ( private val executor: T, private val coroutineContext: CoroutineContext = Dispatchers.Default) { protected lateinit var scope: PresenterCoroutineScope fun getAllContacts() { IdlingHelper.ifAllowed { ContactsIdlingResource.getInstanceFromApp()?.setIdleState(false) } scope = PresenterCoroutineScope(coroutineContext) scope.launch { GetContacts()( UseCase.None, onSuccess = { executor.onContactsLoaded(it) }, onFailure = { executor.onFailedToLoadContacts(it.message) } ) } } } class PresenterCoroutineScope(context: CoroutineContext) : CoroutineScope { override val coroutineContext: CoroutineContext = context + Job() } interface ContactsProvider { fun onContactsLoaded(contacts: ArrayList) fun onFailedToLoadContacts(message: String?) } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/async/Either.kt ================================================ package com.atiurin.sampleapp.async /** * An algebraic data type to provide either a [Failure][F] or a [Success][S] result. */ sealed class Either { /** * Calls [failed] with the [failure][Failure.failure] value if result is a [Failure] * or [succeeded] with the [success][Success.success] value if result is a [Success] */ inline fun fold(failed: (F) -> T, succeeded: (S) -> T): T = when (this) { is Failure -> failed(failure) is Success -> succeeded(success) } } data class Failure(val failure: F) : Either() data class Success(val success: S) : Either() /** * Allows chaining of multiple calls taking as argument the [success][Success.success] value of the previous call and * returning an [Either]. * * 1. Unwrap the result of the first call from the [Either] wrapper. * 2. Check if it is a [Success]. * 3. If yes, call the next function (passed as [ifSucceeded]) with the value of the [success][Success.success] * property as an input parameter (chain the calls). * 4. If no, just pass the [Failure] through as the end result of the whole call chain. * * In case any of the calls in the chain returns a [Failure], none of the subsequent flatmapped functions is called * and the whole chain returns this failure. * * @param ifSucceeded next function which should be called if this is a [Success]. The [success][Success.success] * value will be then passed as the input parameter. */ inline fun Either.flatMap(succeeded: (S1) -> Either): Either = fold({ this as Failure }, succeeded) /** * Map the [Success] value of the [Either] to another value. * * You can for example map an `Success` to an `Success` by * using the following code: * ``` * val fiveString: Either = Success("5") * val fiveInt : Either = fiveString.map { it.toInt() } * ``` */ inline fun Either.map(f: (S1) -> S2): Either = flatMap { Success(f(it)) } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/async/GetContacts.kt ================================================ package com.atiurin.sampleapp.async import com.atiurin.sampleapp.MyApplication.CONTACTS_LOADING_TIMEOUT_MS import com.atiurin.sampleapp.data.entities.Contact import com.atiurin.sampleapp.data.repositories.CONTACTS import kotlinx.coroutines.delay class GetContacts(val delayMs: Long = CONTACTS_LOADING_TIMEOUT_MS) : UseCase, UseCase.None>() { override suspend fun run(params: None): Either> { return try { delay(delayMs) val contacts = CONTACTS Success(contacts) } catch (e: Exception) { Failure(e) } } } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/async/UseCase.kt ================================================ package com.atiurin.sampleapp.async import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch /** * Base class for a `coroutine` use case. */ abstract class UseCase where Type : Any { /** * Runs the actual logic of the use case. */ abstract suspend fun run(params: Params): Either suspend operator fun invoke(params: Params, onSuccess: (Type) -> Unit, onFailure: (Exception) -> Unit) { val result = run(params) coroutineScope { launch(Dispatchers.Main) { result.fold( failed = { onFailure(it) }, succeeded = { onSuccess(it) } ) } } } /** * Placeholder for a use case that doesn't need any input parameters. */ object None } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/async/task/CompatAsyncTask.kt ================================================ package com.atiurin.sampleapp.async.task import android.os.AsyncTask @Suppress("DEPRECATION") class CompatAsyncTask : AsyncTask() { companion object { const val COMPAT_ASYNC_TASK_TIME_EXECUTION = 5000 const val ASYNC = "ASYNC" } @Deprecated("Suppress") override fun doInBackground(vararg params: Void?): Void? { val startTime = System.currentTimeMillis() while (!isCancelled && System.currentTimeMillis() - startTime < COMPAT_ASYNC_TASK_TIME_EXECUTION) { Thread.sleep(1000) } return null } @Deprecated("Suppress") override fun onPostExecute(result: Void?) {} fun start() { executeOnExecutor(THREAD_POOL_EXECUTOR) } fun stop() { cancel(true) } } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/compose/ContacsList.kt ================================================ package com.atiurin.sampleapp.compose import android.content.Context import android.content.Intent import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.Divider import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.semantics.SemanticsPropertyKey import androidx.compose.ui.semantics.SemanticsPropertyReceiver import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag import androidx.compose.ui.unit.ExperimentalUnitApi import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.TextUnitType import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat import com.atiurin.sampleapp.activity.ComposeSecondActivity import com.atiurin.sampleapp.data.entities.Contact const val contactsListHeaderTag = "headerTestTag" const val contactNameTestTag = "nameTestTag" const val contactStatusTestTag = "statusTestTag" const val contactsListContentDesc = "contacts list" const val contactsListTestTag = "contactsListTestTag" @ExperimentalMaterialApi @ExperimentalUnitApi @OptIn(ExperimentalFoundationApi::class) @Composable fun ContactsList( contacts: List, context: Context, addStickyHeader: Boolean = true, testTagProvider: (Contact, Int) -> String, modifierProvider: (Int) -> Modifier, ) { val selectedItem = remember { mutableStateOf("") } Text(text = "Selected item = ${selectedItem.value}") LazyColumn( contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp), verticalArrangement = Arrangement.spacedBy(4.dp), modifier = Modifier.semantics { contentDescription = contactsListContentDesc testTag = contactsListTestTag } ) { if (addStickyHeader) { stickyHeader(key = "header") { Text(text = "Lazy column header", modifier = Modifier.semantics { testTag = contactsListHeaderTag }) } } itemsIndexed(contacts, key = { _, c -> c.name }) { index, contact -> Box(modifier = modifierProvider .invoke(index) .semantics { testTag = testTagProvider.invoke(contact, index) } ) { Column( modifier = Modifier .then(Modifier.clickable { selectedItem.value = contact.name val intent = Intent(context, ComposeSecondActivity::class.java) intent.putExtra(ComposeSecondActivity.INTENT_CONTACT_ID, contact.id) ContextCompat.startActivity(context, intent, null) }) ) { Row { Image( painter = painterResource(contact.avatar), contentDescription = "avatar", contentScale = ContentScale.Crop, // crop the image if it's not a square modifier = Modifier .size(80.dp) .clip(CircleShape) // clip to the circle shape .border(2.dp, Color.Transparent, CircleShape) // add a border (optional) ) Spacer(modifier = Modifier.width(16.dp)) Column { Text(contact.name, Modifier.semantics { testTag = contactNameTestTag }, fontSize = TextUnit(20f, TextUnitType.Sp)) Spacer(modifier = Modifier.height(8.dp)) Text(text = contact.status, Modifier.semantics { testTag = contactStatusTestTag }, fontSize = TextUnit(16f, TextUnitType.Sp)) Spacer(modifier = Modifier.height(8.dp)) } } Spacer(modifier = Modifier.height(8.dp)) Divider(color = Color.Black) } } } } } fun getContactItemTestTagById(contact: Contact) = "contactId=${contact.id}" fun getContactItemTestTagByPosition(position: Int) = "position=$position" // configure position matching for lazy list val ListItemPositionPropertyKey = SemanticsPropertyKey("ListItemPosition") var SemanticsPropertyReceiver.listItemPosition by ListItemPositionPropertyKey fun Modifier.listItemPosition(position: Int): Modifier { return semantics { listItemPosition = position } } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/compose/CustomButton.kt ================================================ package com.atiurin.sampleapp.compose import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Shape import androidx.compose.ui.semantics.Role @Composable @OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class) fun CustomButton( onClickAction: () -> Unit, modifier: Modifier = Modifier, onLongClick: (() -> Unit)? = null, onDoubleClick: (() -> Unit)? = null, enabled: Boolean = true, elevation: ButtonElevation? = ButtonDefaults.elevation(), shape: Shape = MaterialTheme.shapes.small, border: BorderStroke? = null, colors: ButtonColors = ButtonDefaults.buttonColors(), contentPadding: PaddingValues = ButtonDefaults.ContentPadding, content: @Composable RowScope.() -> Unit ) { val contentColor by colors.contentColor(enabled) Surface( shape = shape, color = colors.backgroundColor(enabled).value, contentColor = contentColor.copy(alpha = 1f), border = border, modifier = modifier .combinedClickable( onClick = onClickAction, onDoubleClick = onDoubleClick, onLongClick = onLongClick, enabled = enabled, role = Role.Button ) ) { CompositionLocalProvider(LocalContentAlpha provides contentColor.alpha) { ProvideTextStyle( value = MaterialTheme.typography.button ) { Row( Modifier .defaultMinSize( minWidth = ButtonDefaults.MinWidth, minHeight = ButtonDefaults.MinHeight ) .padding(contentPadding), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, content = content ) } } } } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/compose/DatePicker.kt ================================================ package com.atiurin.sampleapp.compose import android.os.SystemClock import androidx.compose.foundation.background import androidx.compose.foundation.gestures.awaitEachGesture import androidx.compose.foundation.gestures.awaitFirstDown import androidx.compose.foundation.gestures.waitForUpOrCancellation import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.DateRange import androidx.compose.material3.DatePicker import androidx.compose.material3.DatePickerDialog import androidx.compose.material3.DatePickerState import androidx.compose.material3.DisplayMode import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.rememberDatePickerState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.shadow import androidx.compose.ui.input.pointer.PointerEventPass import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.testTag import androidx.compose.ui.semantics.CustomAccessibilityAction import androidx.compose.ui.semantics.customActions import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Popup import com.atiurin.sampleapp.BuildConfig import com.atiurin.sampleapp.compose.DatePickerTestTags.SelectedDateValue import com.atiurin.sampleapp.compose.DatePickerTestTags.SetDatePickerTimeCustomActionLabel import com.atiurin.sampleapp.utils.convertMillisToDate object DatePickerTestTags { const val DockedIconButton = "DockerIconButton" const val SelectedDateValue = "SelectedDateValue" const val DataPicker = "DataPicker" const val SetDatePickerTimeCustomActionLabel = "SetDatePickerTimeCustomAction" } object DatePickerTestData { var time: Long = SystemClock.currentThreadTimeMillis() } @OptIn(ExperimentalMaterial3Api::class) fun Modifier.setDatePickerTimeCustomAction(state: DatePickerState): Modifier { return this.semantics { customActions = listOf( CustomAccessibilityAction(SetDatePickerTimeCustomActionLabel) { state.selectedDateMillis = DatePickerTestData.time true } ) } } @Composable @OptIn(ExperimentalMaterial3Api::class) fun TestableDatePicker( state: DatePickerState, modifier: Modifier = Modifier, showModeToggle: Boolean = true, ) { val testableModifier = if (BuildConfig.DEBUG) { modifier .testTag(DatePickerTestTags.DataPicker) .setDatePickerTimeCustomAction(state) } else modifier DatePicker( state = state, modifier = testableModifier, showModeToggle = showModeToggle ) } @OptIn(ExperimentalMaterial3Api::class) @Composable fun DatePickerDocked() { var showDatePicker by remember { mutableStateOf(false) } val datePickerState = rememberDatePickerState() val selectedDate = datePickerState.selectedDateMillis?.let { convertMillisToDate(it) } ?: "" Box( modifier = Modifier.fillMaxWidth() ) { OutlinedTextField( value = selectedDate, onValueChange = { }, label = { Text("No value selected") }, readOnly = true, trailingIcon = { IconButton( modifier = Modifier.testTag(DatePickerTestTags.DockedIconButton), onClick = { showDatePicker = !showDatePicker } ) { Icon( imageVector = Icons.Default.DateRange, contentDescription = "Select date" ) } }, modifier = Modifier.testTag(SelectedDateValue) .fillMaxWidth() .height(64.dp) ) if (showDatePicker) { Popup( onDismissRequest = { showDatePicker = false }, alignment = Alignment.TopStart ) { Box( modifier = Modifier .fillMaxWidth() .offset(y = 64.dp) .shadow(elevation = 4.dp) .background(MaterialTheme.colorScheme.surface) .padding(16.dp) ) { TestableDatePicker( modifier = Modifier, state = datePickerState, showModeToggle = false, ) } } } } } @Composable fun DatePickerFieldToModal(modifier: Modifier = Modifier) { var selectedDate by remember { mutableStateOf(null) } var showModal by remember { mutableStateOf(false) } OutlinedTextField( value = selectedDate?.let { convertMillisToDate(it) } ?: "", onValueChange = { }, label = { Text("DOB") }, placeholder = { Text("MM/DD/YYYY") }, trailingIcon = { Icon(Icons.Default.DateRange, contentDescription = "Select date") }, modifier = modifier .fillMaxWidth() .pointerInput(selectedDate) { awaitEachGesture { // Modifier.clickable doesn't work for text fields, so we use Modifier.pointerInput // in the Initial pass to observe events before the text field consumes them // in the Main pass. awaitFirstDown(pass = PointerEventPass.Initial) val upEvent = waitForUpOrCancellation(pass = PointerEventPass.Initial) if (upEvent != null) { showModal = true } } } ) if (showModal) { DatePickerModal( onDateSelected = { selectedDate = it }, onDismiss = { showModal = false } ) } } @OptIn(ExperimentalMaterial3Api::class) // [START android_compose_components_datepicker_modal] @Composable fun DatePickerModal( onDateSelected: (Long?) -> Unit, onDismiss: () -> Unit ) { val datePickerState = rememberDatePickerState() DatePickerDialog( onDismissRequest = onDismiss, confirmButton = { TextButton(onClick = { onDateSelected(datePickerState.selectedDateMillis) onDismiss() }) { Text("OK") } }, dismissButton = { TextButton(onClick = onDismiss) { Text("Cancel") } } ) { TestableDatePicker(state = datePickerState) } } // [END android_compose_components_datepicker_modal] @OptIn(ExperimentalMaterial3Api::class) // [START android_compose_components_datepicker_inputmodal] @Composable fun DatePickerModalInput( onDateSelected: (Long?) -> Unit, onDismiss: () -> Unit ) { val datePickerState = rememberDatePickerState(initialDisplayMode = DisplayMode.Input) DatePickerDialog( onDismissRequest = onDismiss, confirmButton = { TextButton(onClick = { onDateSelected(datePickerState.selectedDateMillis) onDismiss() }) { Text("OK") } }, dismissButton = { TextButton(onClick = onDismiss) { Text("Cancel") } } ) { TestableDatePicker(state = datePickerState) } } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/compose/LinearProgressBar.kt ================================================ package com.atiurin.sampleapp.compose import androidx.compose.foundation.progressSemantics import androidx.compose.material.LinearProgressIndicator import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.* import com.atiurin.sampleapp.activity.ComposeElementsActivity @Composable fun LinearProgressBar(statusState: MutableState){ val progressState = remember { mutableStateOf(0f) } LinearProgressIndicator(progress = progressState.value, modifier = Modifier .semantics { testTag = ComposeElementsActivity.progressBar setProgress { value -> progressState.value = value statusState.value = "set progress $value" true } progressBarRangeInfo = ProgressBarRangeInfo(progressState.value, 0f..progressState.value, 100) } .getProgress(progressState.value) .progressSemantics() ) } val GetProgress = SemanticsPropertyKey("ProgressValue") var SemanticsPropertyReceiver.getProgress by GetProgress fun Modifier.getProgress(progress: Float): Modifier { return semantics { getProgress = progress } } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/compose/LoadingAnimation.kt ================================================ package com.atiurin.sampleapp.compose import androidx.compose.animation.core.* import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import kotlinx.coroutines.delay @Composable fun LoadingAnimation( modifier: Modifier = Modifier, circleSize: Dp = 25.dp, circleColor: Color = MaterialTheme.colors.primary, spaceBetween: Dp = 10.dp, travelDistance: Dp = 20.dp ) { val circles = listOf( remember { Animatable(initialValue = 0f) }, remember { Animatable(initialValue = 0f) }, remember { Animatable(initialValue = 0f) } ) circles.forEachIndexed { index, animatable -> LaunchedEffect(key1 = animatable) { delay(index * 100L) animatable.animateTo( targetValue = 1f, animationSpec = infiniteRepeatable( animation = keyframes { durationMillis = 1200 0.0f at 0 with LinearOutSlowInEasing 1.0f at 300 with LinearOutSlowInEasing 0.0f at 600 with LinearOutSlowInEasing 0.0f at 1200 with LinearOutSlowInEasing }, repeatMode = RepeatMode.Restart ) ) } } val circleValues = circles.map { it.value } val distance = with(LocalDensity.current) { travelDistance.toPx() } Row( modifier = modifier, horizontalArrangement = Arrangement.spacedBy(spaceBetween) ) { circleValues.forEach { value -> Box( modifier = Modifier .size(circleSize) .graphicsLayer { translationY = -value * distance } .background( color = circleColor, shape = CircleShape ) ) } } } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/compose/RadioGroup.kt ================================================ package com.atiurin.sampleapp.compose import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.selection.selectableGroup import androidx.compose.material.RadioButton import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag import androidx.compose.ui.unit.dp import com.atiurin.sampleapp.activity.ComposeElementsActivity @Composable fun RadioGroup(){ var selected by remember { mutableStateOf("Male") } Row (verticalAlignment = Alignment.CenterVertically){ val male = "Male" val female = "Female" RadioButton(selected = selected == male, onClick = { selected = male }, modifier = Modifier .selectableGroup() .semantics { testTag = ComposeElementsActivity.radioButtonMaleTestTag }) Text( text = male, modifier = Modifier .clickable(onClick = { selected = male }) .padding(start = 4.dp) ) Spacer(modifier = Modifier.size(4.dp)) RadioButton(selected = selected == female, onClick = { selected = female }, modifier = Modifier .selectableGroup() .semantics { testTag = ComposeElementsActivity.radioButtonFemaleTestTag }) Text( text = female, modifier = Modifier .clickable(onClick = { selected = female }) .padding(start = 4.dp) ) } } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/compose/RegionsClickListener.kt ================================================ package com.atiurin.sampleapp.compose import android.annotation.SuppressLint import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.padding import androidx.compose.material.Button import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.atiurin.sampleapp.compose.RegionsClickListenerTestTags.regionsNode object RegionsClickListenerTestTags { const val regionsNode = "regionsClickListener" const val regionsClickedText = "regionsClickedText" } @Composable fun RegionsClickListener(clickState: MutableState) { Column(modifier = Modifier.semantics { testTag = regionsNode }) { Row (Modifier.defaultMinSize(minHeight = 1.dp).padding(vertical = 0.dp)){//top Button(modifier = Modifier.padding(0.dp),onClick = { clickState.value = RegionName.TopLeft.name }) { Text(text = "TL") } Button(modifier = Modifier.padding(0.dp),onClick = { clickState.value = RegionName.TopCenter.name }) { Text(text = "TC") } Button(modifier = Modifier.padding(0.dp),onClick = { clickState.value = RegionName.TopRight.name }) { Text(text = "TR") } } Row (Modifier.defaultMinSize(minHeight = 1.dp).padding(vertical = 0.dp)){//Center Button(onClick = { clickState.value = RegionName.CenterLeft.name }) { Text(text = "CL") } Button(onClick = { clickState.value = RegionName.Center.name }) { Text(text = "CC") } Button(onClick = { clickState.value = RegionName.CenterRight.name }) { Text(text = "CR") } } Row (Modifier.padding(vertical = 0.dp)){//Bottom Button(modifier = Modifier.padding(0.dp), onClick = { clickState.value = RegionName.BottomLeft.name }) { Text(text = "BL") } Button(onClick = { clickState.value = RegionName.BottomCenter.name }) { Text(text = "BC") } Button(onClick = { clickState.value = RegionName.BottomRight.name }) { Text(text = "BR") } } } } @SuppressLint("UnrememberedMutableState") @Preview @Composable fun RegionsClickListenerPreview(){ RegionsClickListener(mutableStateOf("")) } enum class RegionName { TopLeft, TopCenter, TopRight, CenterLeft, Center, CenterRight, BottomLeft, BottomCenter, BottomRight } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/compose/SimpleOutlinedText.kt ================================================ package com.atiurin.sampleapp.compose import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.OutlinedTextField import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag @Composable fun SimpleOutlinedText(defaultValue: String = "", myTestTag: String = "outlinedText") { var text by remember { mutableStateOf(defaultValue) } SelectionContainer { OutlinedTextField( value = text, onValueChange = { text = it }, label = { Text("Label") }, modifier = Modifier.semantics { testTag = myTestTag }, ) } } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/compose/SwipeableNode.kt ================================================ package com.atiurin.sampleapp.compose import androidx.compose.foundation.background import androidx.compose.foundation.gestures.detectDragGestures import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.consumeAllChanges import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag import androidx.compose.ui.unit.dp import com.atiurin.sampleapp.activity.ActionsStatus import com.atiurin.sampleapp.activity.ComposeElementsActivity @Composable fun SwipeableNode(statusState: MutableState) { Box( modifier = Modifier .pointerInput(Unit) { detectDragGestures { change, dragAmount -> change.consumeAllChanges() val (x, y) = dragAmount when { x > 0 -> { statusState.value = ActionsStatus.SwipeRight.name } x < 0 -> { statusState.value = ActionsStatus.SwipeLeft.name } } when { y > 0 -> { statusState.value = ActionsStatus.SwipeDown.name } y < 0 -> { statusState.value = ActionsStatus.SwipeUp.name } } } } .semantics { testTag = ComposeElementsActivity.swipeableNode } .width(100.dp) .height(100.dp) .background(Color.Blue) ) } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/compose/app/App.kt ================================================ package com.atiurin.sampleapp.compose.app import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material.Scaffold import androidx.compose.material3.Snackbar import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import com.atiurin.sampleapp.compose.screen.DatePickerScreen import com.atiurin.sampleapp.compose.screen.NavigationScreen @Composable fun App( navController: NavHostController = rememberNavController(), ) { // Get current back stack entry val backStackEntry by navController.currentBackStackEntryAsState() val snackbarHostState = remember { SnackbarHostState() } val currentScreen = AppScreen.valueOf( backStackEntry?.destination?.route ?: AppScreen.DataPicker.name ) Scaffold( topBar = { AppBar( currentScreen = currentScreen, canNavigateBack = navController.previousBackStackEntry != null, navigateUp = { navController.navigateUp() }, onRefresh = {} ) }, snackbarHost = { SnackbarHost(snackbarHostState) { snackbarData -> Snackbar( snackbarData = snackbarData, modifier = Modifier.padding(16.dp) ) } } ) { innerPadding -> NavHost( navController = navController, startDestination = AppScreen.Navigation.name, modifier = Modifier .fillMaxSize() .padding(innerPadding) ) { composable(route = AppScreen.Navigation.name) { NavigationScreen { screen -> navController.navigate(screen.name) } } composable(route = AppScreen.DataPicker.name) { DatePickerScreen() } } } } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/compose/app/AppBar.kt ================================================ package com.atiurin.sampleapp.compose.app import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Refresh import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @OptIn(ExperimentalMaterial3Api::class) @Composable fun AppBar( currentScreen: AppScreen, canNavigateBack: Boolean, navigateUp: () -> Unit, modifier: Modifier = Modifier, onRefresh: (() -> Unit)? = null ) { TopAppBar( title = { Text(currentScreen.title) }, colors = TopAppBarDefaults.mediumTopAppBarColors( containerColor = Color(0xffffde02) ), modifier = modifier, navigationIcon = { if (canNavigateBack) { IconButton(onClick = navigateUp) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "BackButton" ) } } }, actions = { if (onRefresh != null) { IconButton(onClick = onRefresh) { Icon(Icons.Default.Refresh, contentDescription = "Refresh List") } } } ) } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/compose/app/AppScreen.kt ================================================ package com.atiurin.sampleapp.compose.app enum class AppScreen(val title: String) { Navigation("Navigation"), DataPicker("Date Picker") } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/compose/screen/DatePickerScreen.kt ================================================ package com.atiurin.sampleapp.compose.screen import androidx.compose.foundation.layout.Column import androidx.compose.runtime.Composable import com.atiurin.sampleapp.compose.DatePickerDocked @Composable fun DatePickerScreen() { Column { DatePickerDocked() // val modalDate = remember { mutableStateOf("No modal date selected") } // val showModal = remember { mutableStateOf(false) } // Text(modalDate.value) // if (showModal.value){ // DatePickerModal({ date -> // date?.let { modalDate.value = convertMillisToDate(date) } // showModal.value = false // }) { // showModal.value = false // } // } } } @Composable fun ShowModalButton(){ } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/compose/screen/NavigationScreen.kt ================================================ package com.atiurin.sampleapp.compose.screen import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material.Button import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag import androidx.compose.ui.unit.dp import com.atiurin.sampleapp.compose.app.AppScreen object NavigationTestTags { const val DatePicker = "DatePicker" } @Composable fun NavigationScreen( onNavButtonClicked: (AppScreen) -> Unit ){ Column( modifier = Modifier .fillMaxSize() .padding(start = 16.dp), verticalArrangement = Arrangement.Top, horizontalAlignment = Alignment.Start ) { NavButton("DatePicker", NavigationTestTags.DatePicker){ onNavButtonClicked(AppScreen.DataPicker) } } } @Composable fun NavButton(name: String, testId: String, onClick: () -> Unit){ Button(modifier = Modifier.semantics { testTag = testId }, onClick = onClick, content = { Text(text = name) }, ) } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/data/Tags.kt ================================================ package com.atiurin.sampleapp.data enum class Tags{ CONTACTS_LIST, MESSAGES_LIST } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/data/entities/Contact.kt ================================================ package com.atiurin.sampleapp.data.entities data class Contact( val id: Int,val name: String, val status: String, val avatar: Int) ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/data/entities/Message.kt ================================================ package com.atiurin.sampleapp.data.entities data class Message(val authorId: Int, val receiverId: Int, val text: String) ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/data/entities/User.kt ================================================ package com.atiurin.sampleapp.data.entities data class User( val id: Int,val name: String, val avatar: Int, val login: String, val password: String) ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/data/loaders/MessageLoader.kt ================================================ package com.atiurin.sampleapp.data.loaders import com.atiurin.sampleapp.data.entities.Message import com.atiurin.sampleapp.data.repositories.MESSAGES open class MessageLoader{ open fun load() : ArrayList{ return MESSAGES } } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/data/repositories/ContactRepositoty.kt ================================================ package com.atiurin.sampleapp.data.repositories import com.atiurin.sampleapp.data.entities.Contact object ContactRepositoty { fun getContact(id: Int) : Contact{ return contacts.find { it.id == id }!! } fun getFirst(): Contact { return contacts.first() } fun getLast() : Contact{ return contacts.last() } fun all() = contacts.toList() private val contacts = CONTACTS } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/data/repositories/MessageRepository.kt ================================================ package com.atiurin.sampleapp.data.repositories import com.atiurin.sampleapp.data.entities.Message import com.atiurin.sampleapp.data.loaders.MessageLoader object MessageRepository { var messages : ArrayList init { messages = loadMessages(MessageLoader()) } fun loadMessages(loader: MessageLoader) : ArrayList{ messages = loader.load() return messages } fun searchMessage(author: Int, recipient: Int, text: String) : Message?{ return messages.find { it.authorId == author && it.receiverId == recipient && it.text == text } } fun getChatMessages(contactId: Int): ArrayList{ return ArrayList(messages.filter {message -> (message.authorId == contactId && message.receiverId == CURRENT_USER.id) || (message.authorId == CURRENT_USER.id && message.receiverId == contactId) }) } fun clearMessages(){ messages.clear() } fun addMessage(message: Message){ messages.add(message) } fun getMessagesCount() : Int{ return messages.size } } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/data/repositories/Storage.kt ================================================ package com.atiurin.sampleapp.data.repositories import com.atiurin.sampleapp.R import com.atiurin.sampleapp.data.entities.Contact import com.atiurin.sampleapp.data.entities.Message import com.atiurin.sampleapp.data.entities.User val CURRENT_USER = User(1, "Joey Tribbiani", Avatars.JOEY.drawable, "joey", "1234") val CONTACTS = arrayListOf( Contact(2, "Chandler Bing", "Joey doesn't share food!", Avatars.CHANDLER.drawable), Contact(3, "Ross Geller", "UNAGI", Avatars.ROSS.drawable), Contact(4, "Rachel Green", "I got off the plane!", Avatars.RACHEL.drawable), Contact(5, "Phoebe Buffay", "Smelly cat, smelly cat..", Avatars.PHOEBE.drawable), Contact(6, "Monica Geller", "I need to clean up..", Avatars.MONICA.drawable), Contact(7, "Gunther", "They were on break :(", Avatars.GUNTHER.drawable), Contact(8, "Janice", "Oh. My. God", Avatars.JANICE.drawable), Contact(9, "Bob", "I wanna drink", Avatars.DEFAULT.drawable), Contact(10, "Marty McFly", "Back to the ...", Avatars.DEFAULT.drawable), Contact(12, "Emmet Brown", "Time fluid capacitor", Avatars.DEFAULT.drawable), Contact(13, "Friend1", "Time fluid capacitor", Avatars.DEFAULT.drawable), Contact(14, "Friend2", "Time fluid capacitor", Avatars.DEFAULT.drawable), Contact(15, "Friend3", "Time fluid capacitor", Avatars.DEFAULT.drawable), Contact(16, "Friend4", "Time fluid capacitor", Avatars.DEFAULT.drawable), Contact(17, "Friend5", "Time fluid capacitor", Avatars.DEFAULT.drawable), Contact(18, "Friend6", "Time fluid capacitor", Avatars.DEFAULT.drawable), Contact(19, "Friend7", "Time fluid capacitor", Avatars.DEFAULT.drawable), Contact(20, "Friend8", "Time fluid capacitor", Avatars.DEFAULT.drawable), Contact(21, "Friend9", "Time fluid capacitor", Avatars.DEFAULT.drawable), Contact(22, "Friend10", "Time fluid capacitor", Avatars.DEFAULT.drawable), Contact(23, "Friend11", "Time fluid capacitor", Avatars.DEFAULT.drawable), Contact(24, "Friend12", "Time fluid capacitor", Avatars.DEFAULT.drawable), Contact(25, "Friend13", "Time fluid capacitor", Avatars.DEFAULT.drawable), Contact(26, "Friend14", "Time fluid capacitor", Avatars.DEFAULT.drawable), Contact(27, "Friend15", "Time fluid capacitor", Avatars.DEFAULT.drawable), Contact(28, "Friend16", "Time fluid capacitor", Avatars.DEFAULT.drawable), Contact(29, "Friend17", "Time fluid capacitor", Avatars.DEFAULT.drawable), Contact(30, "Friend18", "Time fluid capacitor", Avatars.DEFAULT.drawable), Contact(31, "Friend19", "Time fluid capacitor", Avatars.DEFAULT.drawable), Contact(32, "Friend20", "Time fluid capacitor", Avatars.DEFAULT.drawable) ) enum class Avatars(val drawable: Int) { CHANDLER(R.drawable.chandler), ROSS(R.drawable.ross), MONICA(R.drawable.monica), RACHEL(R.drawable.rachel), PHOEBE(R.drawable.phoebe), GUNTHER(R.drawable.gunther), JOEY(R.drawable.joey), JANICE(R.drawable.janice), DEFAULT(R.drawable.default_avatar) } val MESSAGES = arrayListOf( Message(1, 2, "What's up Chandler"), Message(2, 1, "Hi Joey"), Message(1, 2, "Let's drink coffee"), Message(2, 1, "Ok"), Message(1, 3, "Do u wanna coffee?"), Message(3, 1, "yep, let's go") ) ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/data/viewmodel/ContactsViewModel.kt ================================================ package com.atiurin.sampleapp.data.viewmodel import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import com.atiurin.sampleapp.data.entities.Contact class ContactsViewModel : ViewModel(){ val contacts: MutableLiveData> by lazy { MutableLiveData() } } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/data/viewmodel/DataViewModel.kt ================================================ package com.atiurin.sampleapp.data.viewmodel import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import com.atiurin.sampleapp.data.entities.Contact class DataViewModel : ViewModel(){ val data: MutableLiveData by lazy { MutableLiveData() } } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/idlingresources/AbstractIdlingResource.kt ================================================ package com.atiurin.sampleapp.idlingresources import androidx.annotation.Nullable import androidx.test.espresso.IdlingResource import java.util.concurrent.atomic.AtomicBoolean import androidx.test.espresso.IdlingResource.ResourceCallback abstract class AbstractIdlingResource : IdlingResource { @Nullable @Volatile private var mCallback: ResourceCallback? = null private val mIsIdleNow = AtomicBoolean(true) override fun getName(): String { return this.javaClass.name } override fun isIdleNow(): Boolean { return mIsIdleNow.get() } override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) { mCallback = callback } fun setIdleState(isIdleNow: Boolean) { mIsIdleNow.set(isIdleNow) if (isIdleNow && mCallback != null) { mCallback?.onTransitionToIdle() } } } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/idlingresources/Holder.kt ================================================ package com.atiurin.sampleapp.idlingresources import androidx.annotation.VisibleForTesting open class Holder(private val constructor: () -> T) { @Volatile private var instance: T? = null @VisibleForTesting fun getInstanceFromTest(): T? { return when { instance != null -> instance else -> synchronized(this) { instance = constructor() instance } } } fun getInstanceFromApp(): T? { return instance } } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/idlingresources/IdlingHelper.kt ================================================ package com.atiurin.sampleapp.idlingresources const val RELEASE_BUILD = false; object IdlingHelper{ @JvmStatic fun ifAllowed(resourceAction:() -> Unit){ if (!RELEASE_BUILD){ resourceAction() } } } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/idlingresources/resources/ChatIdlingResource.kt ================================================ package com.atiurin.sampleapp.idlingresources.resources import com.atiurin.sampleapp.idlingresources.AbstractIdlingResource import com.atiurin.sampleapp.idlingresources.Holder class ChatIdlingResource : AbstractIdlingResource(){ companion object : Holder(::ChatIdlingResource) } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/idlingresources/resources/ContactsIdlingResource.kt ================================================ package com.atiurin.sampleapp.idlingresources.resources import com.atiurin.sampleapp.idlingresources.AbstractIdlingResource import com.atiurin.sampleapp.idlingresources.Holder class ContactsIdlingResource : AbstractIdlingResource(){ companion object : Holder(::ContactsIdlingResource) } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/managers/AccountManager.kt ================================================ package com.atiurin.sampleapp.managers import android.content.Context class AccountManager(val context: Context){ companion object { private const val expectedUserName = "joey" private const val expectedPassword = "1234" private const val USER_KEY = "username" private const val PASSWORD_KEY = "password" } fun login(user: String, password: String) : Boolean{ var success = false // there should be some network request to app server if ((user == Companion.expectedUserName) &&(password == expectedPassword)){ success = true with(PrefsManager(context)){ savePref(USER_KEY, user) savePref(PASSWORD_KEY, password) } } return success } fun isLogedIn() : Boolean{ var userName = "" var password = "" with(PrefsManager(context)){ userName = getPref(USER_KEY) password = getPref(PASSWORD_KEY) } if (userName.isEmpty() || password.isEmpty()) return false return true } fun logout(){ with(PrefsManager(context)){ remove(USER_KEY) remove(PASSWORD_KEY) } } } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/managers/PrefsManager.kt ================================================ package com.atiurin.sampleapp.managers import android.content.Context.MODE_PRIVATE import android.content.Context class PrefsManager(val context: Context){ val PREFS_NAME = "MyPrefsFileName" fun savePref(key: String, value: String){ val editor = context.getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit() editor.putString(key, value) editor.apply() } fun getPref(key: String) : String{ val prefs = context.getSharedPreferences(PREFS_NAME, MODE_PRIVATE) var value = prefs.getString(key, null) if (value == null) value = "" return value } fun remove(key: String){ val editor = context.getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit() editor.remove(key) editor.commit() } } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/utils/TimeUtils.kt ================================================ package com.atiurin.sampleapp.utils import java.text.SimpleDateFormat import java.util.Date import java.util.Locale fun convertMillisToDate(millis: Long): String { val formatter = SimpleDateFormat("MM/dd/yyyy", Locale.getDefault()) return formatter.format(Date(millis)) } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/view/CircleImageView.java ================================================ package com.atiurin.sampleapp.view; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.BitmapShader; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.ColorFilter; import android.graphics.Matrix; import android.graphics.Outline; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Shader; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.ViewOutlineProvider; import androidx.annotation.ColorInt; import androidx.annotation.ColorRes; import androidx.annotation.DrawableRes; import androidx.annotation.RequiresApi; import androidx.appcompat.widget.AppCompatImageView; import com.atiurin.sampleapp.R; @SuppressWarnings("UnusedDeclaration") public class CircleImageView extends AppCompatImageView { private static final ScaleType SCALE_TYPE = ScaleType.CENTER_CROP; private static final Bitmap.Config BITMAP_CONFIG = Bitmap.Config.ARGB_8888; private static final int COLORDRAWABLE_DIMENSION = 2; private static final int DEFAULT_BORDER_WIDTH = 0; private static final int DEFAULT_BORDER_COLOR = Color.BLACK; private static final int DEFAULT_CIRCLE_BACKGROUND_COLOR = Color.TRANSPARENT; private static final boolean DEFAULT_BORDER_OVERLAY = false; private final RectF mDrawableRect = new RectF(); private final RectF mBorderRect = new RectF(); private final Matrix mShaderMatrix = new Matrix(); private final Paint mBitmapPaint = new Paint(); private final Paint mBorderPaint = new Paint(); private final Paint mCircleBackgroundPaint = new Paint(); private int mBorderColor = DEFAULT_BORDER_COLOR; private int mBorderWidth = DEFAULT_BORDER_WIDTH; private int mCircleBackgroundColor = DEFAULT_CIRCLE_BACKGROUND_COLOR; private Bitmap mBitmap; private BitmapShader mBitmapShader; private int mBitmapWidth; private int mBitmapHeight; private float mDrawableRadius; private float mBorderRadius; private ColorFilter mColorFilter; private boolean mReady; private boolean mSetupPending; private boolean mBorderOverlay; private boolean mDisableCircularTransformation; public CircleImageView(Context context) { super(context); init(); } public CircleImageView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public CircleImageView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleImageView, defStyle, 0); mBorderWidth = a.getDimensionPixelSize(R.styleable.CircleImageView_civ_border_width, DEFAULT_BORDER_WIDTH); mBorderColor = a.getColor(R.styleable.CircleImageView_civ_border_color, DEFAULT_BORDER_COLOR); mBorderOverlay = a.getBoolean(R.styleable.CircleImageView_civ_border_overlay, DEFAULT_BORDER_OVERLAY); mCircleBackgroundColor = a.getColor(R.styleable.CircleImageView_civ_circle_background_color, DEFAULT_CIRCLE_BACKGROUND_COLOR); a.recycle(); init(); } private void init() { super.setScaleType(SCALE_TYPE); mReady = true; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { setOutlineProvider(new OutlineProvider()); } if (mSetupPending) { setup(); mSetupPending = false; } } @Override public ScaleType getScaleType() { return SCALE_TYPE; } @Override public void setScaleType(ScaleType scaleType) { if (scaleType != SCALE_TYPE) { throw new IllegalArgumentException(String.format("ScaleType %s not supported.", scaleType)); } } @Override public void setAdjustViewBounds(boolean adjustViewBounds) { if (adjustViewBounds) { throw new IllegalArgumentException("adjustViewBounds not supported."); } } @Override protected void onDraw(Canvas canvas) { if (mDisableCircularTransformation) { super.onDraw(canvas); return; } if (mBitmap == null) { return; } if (mCircleBackgroundColor != Color.TRANSPARENT) { canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mCircleBackgroundPaint); } canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mBitmapPaint); if (mBorderWidth > 0) { canvas.drawCircle(mBorderRect.centerX(), mBorderRect.centerY(), mBorderRadius, mBorderPaint); } } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); setup(); } @Override public void setPadding(int left, int top, int right, int bottom) { super.setPadding(left, top, right, bottom); setup(); } @Override public void setPaddingRelative(int start, int top, int end, int bottom) { super.setPaddingRelative(start, top, end, bottom); setup(); } public int getBorderColor() { return mBorderColor; } public void setBorderColor(@ColorInt int borderColor) { if (borderColor == mBorderColor) { return; } mBorderColor = borderColor; mBorderPaint.setColor(mBorderColor); invalidate(); } public int getCircleBackgroundColor() { return mCircleBackgroundColor; } public void setCircleBackgroundColor(@ColorInt int circleBackgroundColor) { if (circleBackgroundColor == mCircleBackgroundColor) { return; } mCircleBackgroundColor = circleBackgroundColor; mCircleBackgroundPaint.setColor(circleBackgroundColor); invalidate(); } public void setCircleBackgroundColorResource(@ColorRes int circleBackgroundRes) { setCircleBackgroundColor(getContext().getResources().getColor(circleBackgroundRes)); } public int getBorderWidth() { return mBorderWidth; } public void setBorderWidth(int borderWidth) { if (borderWidth == mBorderWidth) { return; } mBorderWidth = borderWidth; setup(); } public boolean isBorderOverlay() { return mBorderOverlay; } public void setBorderOverlay(boolean borderOverlay) { if (borderOverlay == mBorderOverlay) { return; } mBorderOverlay = borderOverlay; setup(); } public boolean isDisableCircularTransformation() { return mDisableCircularTransformation; } public void setDisableCircularTransformation(boolean disableCircularTransformation) { if (mDisableCircularTransformation == disableCircularTransformation) { return; } mDisableCircularTransformation = disableCircularTransformation; initializeBitmap(); } @Override public void setImageBitmap(Bitmap bm) { super.setImageBitmap(bm); initializeBitmap(); } @Override public void setImageDrawable(Drawable drawable) { super.setImageDrawable(drawable); initializeBitmap(); } @Override public void setImageResource(@DrawableRes int resId) { super.setImageResource(resId); initializeBitmap(); } @Override public void setImageURI(Uri uri) { super.setImageURI(uri); initializeBitmap(); } @Override public void setColorFilter(ColorFilter cf) { if (cf == mColorFilter) { return; } mColorFilter = cf; applyColorFilter(); invalidate(); } @Override public ColorFilter getColorFilter() { return mColorFilter; } private void applyColorFilter() { mBitmapPaint.setColorFilter(mColorFilter); } private Bitmap getBitmapFromDrawable(Drawable drawable) { if (drawable == null) { return null; } if (drawable instanceof BitmapDrawable) { return ((BitmapDrawable) drawable).getBitmap(); } try { Bitmap bitmap; if (drawable instanceof ColorDrawable) { bitmap = Bitmap.createBitmap(COLORDRAWABLE_DIMENSION, COLORDRAWABLE_DIMENSION, BITMAP_CONFIG); } else { bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), BITMAP_CONFIG); } Canvas canvas = new Canvas(bitmap); drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); drawable.draw(canvas); return bitmap; } catch (Exception e) { e.printStackTrace(); return null; } } private void initializeBitmap() { if (mDisableCircularTransformation) { mBitmap = null; } else { mBitmap = getBitmapFromDrawable(getDrawable()); } setup(); } private void setup() { if (!mReady) { mSetupPending = true; return; } if (getWidth() == 0 && getHeight() == 0) { return; } if (mBitmap == null) { invalidate(); return; } mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); mBitmapPaint.setAntiAlias(true); mBitmapPaint.setShader(mBitmapShader); mBorderPaint.setStyle(Paint.Style.STROKE); mBorderPaint.setAntiAlias(true); mBorderPaint.setColor(mBorderColor); mBorderPaint.setStrokeWidth(mBorderWidth); mCircleBackgroundPaint.setStyle(Paint.Style.FILL); mCircleBackgroundPaint.setAntiAlias(true); mCircleBackgroundPaint.setColor(mCircleBackgroundColor); mBitmapHeight = mBitmap.getHeight(); mBitmapWidth = mBitmap.getWidth(); mBorderRect.set(calculateBounds()); mBorderRadius = Math.min((mBorderRect.height() - mBorderWidth) / 2.0f, (mBorderRect.width() - mBorderWidth) / 2.0f); mDrawableRect.set(mBorderRect); if (!mBorderOverlay && mBorderWidth > 0) { mDrawableRect.inset(mBorderWidth - 1.0f, mBorderWidth - 1.0f); } mDrawableRadius = Math.min(mDrawableRect.height() / 2.0f, mDrawableRect.width() / 2.0f); applyColorFilter(); updateShaderMatrix(); invalidate(); } private RectF calculateBounds() { int availableWidth = getWidth() - getPaddingLeft() - getPaddingRight(); int availableHeight = getHeight() - getPaddingTop() - getPaddingBottom(); int sideLength = Math.min(availableWidth, availableHeight); float left = getPaddingLeft() + (availableWidth - sideLength) / 2f; float top = getPaddingTop() + (availableHeight - sideLength) / 2f; return new RectF(left, top, left + sideLength, top + sideLength); } private void updateShaderMatrix() { float scale; float dx = 0; float dy = 0; mShaderMatrix.set(null); if (mBitmapWidth * mDrawableRect.height() > mDrawableRect.width() * mBitmapHeight) { scale = mDrawableRect.height() / (float) mBitmapHeight; dx = (mDrawableRect.width() - mBitmapWidth * scale) * 0.5f; } else { scale = mDrawableRect.width() / (float) mBitmapWidth; dy = (mDrawableRect.height() - mBitmapHeight * scale) * 0.5f; } mShaderMatrix.setScale(scale, scale); mShaderMatrix.postTranslate((int) (dx + 0.5f) + mDrawableRect.left, (int) (dy + 0.5f) + mDrawableRect.top); mBitmapShader.setLocalMatrix(mShaderMatrix); } @SuppressLint("ClickableViewAccessibility") @Override public boolean onTouchEvent(MotionEvent event) { return inTouchableArea(event.getX(), event.getY()) && super.onTouchEvent(event); } private boolean inTouchableArea(float x, float y) { return Math.pow(x - mBorderRect.centerX(), 2) + Math.pow(y - mBorderRect.centerY(), 2) <= Math.pow(mBorderRadius, 2); } @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) private class OutlineProvider extends ViewOutlineProvider { @Override public void getOutline(View view, Outline outline) { Rect bounds = new Rect(); mBorderRect.roundOut(bounds); outline.setRoundRect(bounds, bounds.width() / 2.0f); } } } ================================================ FILE: sample-app/src/main/java/com/atiurin/sampleapp/view/listeners/OnSwipeTouchListener.kt ================================================ package com.atiurin.sampleapp.view.listeners import android.content.Context import android.view.GestureDetector import android.view.GestureDetector.SimpleOnGestureListener import android.view.MotionEvent import android.view.View import android.view.View.OnTouchListener open class OnSwipeTouchListener(ctx: Context?) : OnTouchListener { private val gestureDetector: GestureDetector override fun onTouch(v: View?, event: MotionEvent): Boolean { return gestureDetector.onTouchEvent(event) } private inner class GestureListener : SimpleOnGestureListener() { override fun onDown(e: MotionEvent): Boolean { return true } override fun onFling(e1: MotionEvent?, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean { var result = false try { val diffY = e2.y - e1!!.y val diffX = e2.x - e1.x if (Math.abs(diffX) > Math.abs(diffY)) { if (Math.abs(diffX) > Companion.SWIPE_THRESHOLD && Math.abs(velocityX) > Companion.SWIPE_VELOCITY_THRESHOLD) { if (diffX > 0) { onSwipeRight() } else { onSwipeLeft() } result = true } } else if (Math.abs(diffY) > Companion.SWIPE_THRESHOLD && Math.abs(velocityY) > Companion.SWIPE_VELOCITY_THRESHOLD) { if (diffY > 0) { onSwipeDown() } else { onSwipeUp() } result = true } } catch (exception: Exception) { exception.printStackTrace() } return result } } open fun onSwipeRight() {} open fun onSwipeLeft() {} open fun onSwipeUp() {} open fun onSwipeDown() {} init { gestureDetector = GestureDetector(ctx, GestureListener()) } companion object { private const val SWIPE_THRESHOLD = 100 private const val SWIPE_VELOCITY_THRESHOLD = 100 } } ================================================ FILE: sample-app/src/main/res/drawable/background_splash.xml ================================================ ================================================ FILE: sample-app/src/main/res/drawable/circle.xml ================================================ ================================================ FILE: sample-app/src/main/res/drawable/ic_launcher_background.xml ================================================ ================================================ FILE: sample-app/src/main/res/drawable/ic_menu_camera.xml ================================================ ================================================ FILE: sample-app/src/main/res/drawable/ic_menu_gallery.xml ================================================ ================================================ FILE: sample-app/src/main/res/drawable/ic_menu_manage.xml ================================================ ================================================ FILE: sample-app/src/main/res/drawable/ic_menu_send.xml ================================================ ================================================ FILE: sample-app/src/main/res/drawable/ic_menu_share.xml ================================================ ================================================ FILE: sample-app/src/main/res/drawable/ic_menu_slideshow.xml ================================================ ================================================ FILE: sample-app/src/main/res/drawable/img.xml ================================================ ================================================ FILE: sample-app/src/main/res/drawable/side_nav_bar.xml ================================================ ================================================ FILE: sample-app/src/main/res/drawable-anydpi/ic_account.xml ================================================ ================================================ FILE: sample-app/src/main/res/drawable-anydpi/ic_attach_file.xml ================================================ ================================================ FILE: sample-app/src/main/res/drawable-anydpi/ic_exit.xml ================================================ ================================================ FILE: sample-app/src/main/res/drawable-anydpi/ic_messages.xml ================================================ ================================================ FILE: sample-app/src/main/res/drawable-anydpi/ic_send.xml ================================================ ================================================ FILE: sample-app/src/main/res/drawable-v24/ic_launcher_foreground.xml ================================================ ================================================ FILE: sample-app/src/main/res/layout/activity_chat.xml ================================================ ================================================ FILE: sample-app/src/main/res/layout/activity_custom_clicks.xml ================================================