Showing preview only (1,563K chars total). Download the full file or copy to clipboard to get everything.
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
================================================
<p align="center">
<img src="https://user-images.githubusercontent.com/12834123/252489846-db6cb0f8-6b28-4ae4-bceb-8b5907f1d59f.png#gh-light-mode-only" width=600>
<img src="https://user-images.githubusercontent.com/12834123/252498170-61e5a440-c2b5-42ea-8bfb-91ee12248422.png#gh-dark-mode-only" width=600>
</p>
<div align="center">
[![Documentation][documentation-badge]][documentation]
[![Releases][releases-badge]][releases]
[![Telegram][telegram-badge]][telegram]
</div>
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<RecyclerView.ViewHolder>(
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<View>` objects.
```kotlin
object ChatPage : Page<ChatPage>() {
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<ChatPage>() {
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()
}
```


## Add Ultron to your project
Gradle
```groovy
repositories {
mavenCentral()
}
dependencies {
androidTestImplementation 'com.atiurin:ultron-android:<latest_version>'
androidTestImplementation 'com.atiurin:ultron-allure:<latest_version>'
androidTestImplementation 'com.atiurin:ultron-compose:<latest_version>'
}
```
Please, read [gradle dependencies management](https://open-tool.github.io/ultron/docs/intro/dependencies) doc.
<!--
Link References
-->
[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
================================================
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@android:style/Theme.Material.Light.NoActionBar">
<activity
android:exported="true"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|mnc|colorMode|density|fontScale|fontWeightAdjustment|keyboard|layoutDirection|locale|mcc|navigation|smallestScreenSize|touchscreen|uiMode"
android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
================================================
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
================================================
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>
================================================
FILE: composeApp/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>
================================================
FILE: composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
================================================
FILE: composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
================================================
FILE: composeApp/src/androidMain/res/values/strings.xml
================================================
<resources>
<string name="app_name">sample-kmp</string>
</resources>
================================================
FILE: composeApp/src/commonMain/composeResources/drawable/compose-multiplatform.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="600dp"
android:height="600dp"
android:viewportWidth="600"
android:viewportHeight="600">
<path
android:pathData="M301.21,418.53C300.97,418.54 300.73,418.56 300.49,418.56C297.09,418.59 293.74,417.72 290.79,416.05L222.6,377.54C220.63,376.43 219,374.82 217.85,372.88C216.7,370.94 216.09,368.73 216.07,366.47L216.07,288.16C216.06,287.32 216.09,286.49 216.17,285.67C216.38,283.54 216.91,281.5 217.71,279.6L199.29,268.27L177.74,256.19C175.72,260.43 174.73,265.23 174.78,270.22L174.79,387.05C174.85,393.89 178.57,400.2 184.53,403.56L286.26,461.02C290.67,463.51 295.66,464.8 300.73,464.76C300.91,464.76 301.09,464.74 301.27,464.74C301.24,449.84 301.22,439.23 301.22,439.23L301.21,418.53Z"
android:fillColor="#041619"
android:fillType="nonZero"/>
<path
android:pathData="M409.45,242.91L312.64,188.23C303.64,183.15 292.58,183.26 283.68,188.51L187.92,245C183.31,247.73 179.93,251.62 177.75,256.17L177.74,256.19L199.29,268.27L217.71,279.6C217.83,279.32 217.92,279.02 218.05,278.74C218.24,278.36 218.43,277.98 218.64,277.62C219.06,276.88 219.52,276.18 220.04,275.51C221.37,273.8 223.01,272.35 224.87,271.25L289.06,233.39C290.42,232.59 291.87,231.96 293.39,231.51C295.53,230.87 297.77,230.6 300,230.72C302.98,230.88 305.88,231.73 308.47,233.2L373.37,269.85C375.54,271.08 377.49,272.68 379.13,274.57C379.68,275.19 380.18,275.85 380.65,276.53C380.86,276.84 381.05,277.15 381.24,277.47L397.79,266.39L420.34,252.93L420.31,252.88C417.55,248.8 413.77,245.35 409.45,242.91Z"
android:fillColor="#37BF6E"
android:fillType="nonZero"/>
<path
android:pathData="M381.24,277.47C381.51,277.92 381.77,278.38 382.01,278.84C382.21,279.24 382.39,279.65 382.57,280.06C382.91,280.88 383.19,281.73 383.41,282.59C383.74,283.88 383.92,285.21 383.93,286.57L383.93,361.1C383.96,363.95 383.35,366.77 382.16,369.36C381.93,369.86 381.69,370.35 381.42,370.83C379.75,373.79 377.32,376.27 374.39,378L310.2,415.87C307.47,417.48 304.38,418.39 301.21,418.53L301.22,439.23C301.22,439.23 301.24,449.84 301.27,464.74C306.1,464.61 310.91,463.3 315.21,460.75L410.98,404.25C419.88,399 425.31,389.37 425.22,379.03L425.22,267.85C425.17,262.48 423.34,257.34 420.34,252.93L397.79,266.39L381.24,277.47Z"
android:fillColor="#3870B2"
android:fillType="nonZero"/>
<path
android:pathData="M177.75,256.17C179.93,251.62 183.31,247.73 187.92,245L283.68,188.51C292.58,183.26 303.64,183.15 312.64,188.23L409.45,242.91C413.77,245.35 417.55,248.8 420.31,252.88L420.34,252.93L498.59,206.19C494.03,199.46 487.79,193.78 480.67,189.75L320.86,99.49C306.01,91.1 287.75,91.27 273.07,99.95L114.99,193.2C107.39,197.69 101.81,204.11 98.21,211.63L177.74,256.19L177.75,256.17ZM301.27,464.74C301.09,464.74 300.91,464.76 300.73,464.76C295.66,464.8 290.67,463.51 286.26,461.02L184.53,403.56C178.57,400.2 174.85,393.89 174.79,387.05L174.78,270.22C174.73,265.23 175.72,260.43 177.74,256.19L98.21,211.63C94.86,218.63 93.23,226.58 93.31,234.82L93.31,427.67C93.42,438.97 99.54,449.37 109.4,454.92L277.31,549.77C284.6,553.88 292.84,556.01 301.2,555.94L301.2,555.8C301.39,543.78 301.33,495.26 301.27,464.74Z"
android:strokeWidth="10"
android:fillColor="#00000000"
android:strokeColor="#083042"
android:fillType="nonZero"/>
<path
android:pathData="M498.59,206.19L420.34,252.93C423.34,257.34 425.17,262.48 425.22,267.85L425.22,379.03C425.31,389.37 419.88,399 410.98,404.25L315.21,460.75C310.91,463.3 306.1,464.61 301.27,464.74C301.33,495.26 301.39,543.78 301.2,555.8L301.2,555.94C309.48,555.87 317.74,553.68 325.11,549.32L483.18,456.06C497.87,447.39 506.85,431.49 506.69,414.43L506.69,230.91C506.6,222.02 503.57,213.5 498.59,206.19Z"
android:strokeWidth="10"
android:fillColor="#00000000"
android:strokeColor="#083042"
android:fillType="nonZero"/>
<path
android:pathData="M301.2,555.94C292.84,556.01 284.6,553.88 277.31,549.76L109.4,454.92C99.54,449.37 93.42,438.97 93.31,427.67L93.31,234.82C93.23,226.58 94.86,218.63 98.21,211.63C101.81,204.11 107.39,197.69 114.99,193.2L273.07,99.95C287.75,91.27 306.01,91.1 320.86,99.49L480.67,189.75C487.79,193.78 494.03,199.46 498.59,206.19C503.57,213.5 506.6,222.02 506.69,230.91L506.69,414.43C506.85,431.49 497.87,447.39 483.18,456.06L325.11,549.32C317.74,553.68 309.48,555.87 301.2,555.94Z"
android:strokeWidth="10"
android:fillColor="#00000000"
android:strokeColor="#083042"
android:fillType="nonZero"/>
</vector>
================================================
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<Contact>()) }
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<Contact> {
// 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<ListScreen.ListItem>().apply {
name.assertIsDisplayed().assertTextContains(contact.name)
status.assertIsDisplayed().assertTextContains(contact.status)
}
}
}
}
object ListScreen : Screen<ListScreen>() {
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<SamplePage>() {
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<SamplePage>() {
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
================================================
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>sample-kmp</title>
<link type="text/css" rel="stylesheet" href="styles.css">
<script type="application/javascript" src="composeApp.js"></script>
</head>
<body>
</body>
</html>
================================================
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=<Your GitHub username> 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)
<T> 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<String>)
textContains(text: String)
hasContentDescription(text: String)
hasContentDescription(resourceId: Int)
hasContentDescription(charSequenceMatcher: Matcher<CharSequence>)
contentDescriptionContains(text: String)
assertMatches(condition: Matcher<View>) // 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<SomePage>() {
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<SomePage>() {
//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<SomePage>() {
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<SomePage>() {
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<View>` 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<DialogPage>() {
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 <T> UltronEspressoInteraction<T>.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<View>
fun Matcher<View>.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<View>, 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.

## 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<FriendsListPage>() {
// 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<View>) // Assert RecyclerView matches custom condition
assertItemNotExist(matcher: Matcher<View>, timeoutMs: Long) // watch java doc to understand how it works
assertItemNotExistImmediately(matcher: Matcher<View>, timeoutMs: Long)
isDisplayed()
isNotDisplayed()
doesNotExist()
isEnabled()
isNotEnabled()
hasContentDescription(contentDescription: String)
hasContentDescription(resourceId: Int)
hasContentDescription(charSequenceMatcher: Matcher<CharSequence>)
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<View>, 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<View>, index: Int): UltronRecyclerViewItem
firstItemMatched(matcher: Matcher<View>, ..): UltronRecyclerViewItem
lastItemMatched(matcher: Matcher<View>, ..): UltronRecyclerViewItem
// ----- item providers for UltronRecyclerViewItem subclasses -----
// following methods return a generic type T which is a subclass of UltronRecyclerViewItem
getItem(matcher: Matcher<View>, 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<View>, index: Int, ..): T
getFirstItemMatched(matcher: Matcher<View>, ..): T
getLastItemMatched(matcher: Matcher<View>, ..): 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<FriendRecyclerItem>(position = 10, autoScroll = true).status.hasText("UNAGI")
recycler.getItem<FriendRecyclerItem>(matcher = hasDescendant(withText("Janice"))).status.textContains("Oh. My")
recycler.getFirstItem<FriendRecyclerItem>().avatar.click() //take first RecyclerView item
recycler.getLastItem<FriendRecyclerItem>().isCompletelyDisplayed()
// if it's impossible to specify unique matcher for target item
val matcher = hasDescendant(withText(containsString("Friend")))
recycler.getItemMatched<FriendRecyclerItem>(matcher, index = 2).name.click() //return 3rd matched item, because index starts from zero
recycler.getFirstItemMatched<FriendRecyclerItem>(matcher).name.hasText("Friend1")
recycler.getLastItemMatched<FriendRecyclerItem>(matcher).avatar.isDisplayed()
```
### _Best practice_ - add a method to Page class that returns `FriendRecyclerItem`
```kotlin
object FriendsListPage : Page<FriendsListPage>() {
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<FriendsListPage>() {
...
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<View>)
hasContentDescription(contentDescription: String)
hasContentDescription(resourceId: Int)
hasContentDescription(charSequenceMatcher: Matcher<CharSequence>)
contentDescriptionContains(text: String)
//general
getViewHolder(): RecyclerView.ViewHolder?
getChild(childMatcher: Matcher<View>): Matcher<View> //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<UltronUiObject2> // 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<UltronUiObject2> // 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<String>)
hasText(text: String)
textContains(textSubstring: String)
textIsNullOrEmpty()
textIsNotNullOrEmpty()
hasContentDescription(contentDescMatcher: Matcher<String>)
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<SomePage>() {
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<SomePage>() {
//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<SomePage>() {
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<SomePage>() {
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<String>) // 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<UltronWebElement>
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<WebViewPage>() {
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<WebViewPage>() {
//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 `<app_directory>/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.

***
## 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<ChatPage>(){
...
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

## 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<SomePage>{
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 <T> UltronEspressoInteraction<T>.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 <T> UltronEspressoInteraction<T>.getText(): String = execute { _, view ->
(view as TextView).text.toString()
}
```
- `assertMatches`: This evaluates the assertion and returns an updated `UltronEspressoInteraction` object.
```kotlin
fun <T> UltronEspressoInteraction<T>.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<View>`, `ViewInteraction`, `DataInteraction`:
```kotlin
//support action for all Matcher<View>
fun Matcher<View>.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 <T> UltronEspressoInteraction<T>.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 <T> UltronEspressoInteraction<T>.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<SomePage>() {
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<SomePage>() {
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<Operation>) = Unit
/**
* called when action or assertion has been executed successfully
*/
override fun afterSuccess(operationResult: OperationResult<Operation>) = Unit
/**
* called in any case of action or assertion result
*/
override fun after(operationResult: OperationResult<Operation>) = 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<Operation>) {
UltronLog.info("Successfully executed ${operationResult.operation.name}")
}
override fun afterFailure(operationResult: OperationResult<Operation>) {
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<SomePage>{
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?

**_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<SomePage>() {
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<SomePage>() {
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.

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<SomeComposeScreen>(){
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<SomeComposeScreen>(){
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<SomeComposeScreen>(){
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<View>, 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<SomeEspressoScreen>(){
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<View>, 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<View>, 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<SomeEspressoScreen>(){
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<SomeWebScreen>(){
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<SomeUiAutomatorScreen>(){
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<A>`
```kotlin
class ActivityComposeTest {
@get:Rule
val composeTestRule = createAndroidComposeRule<YourActivity>()
@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<A>` or `createSimpleUltronComposeRule<A>`
```kotlin
@get:Rule
val composeTestRule = createUltronComposeRule<YourActivity>()
```
`createSimpleUltronComposeRule<A>` 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<UltronComposeOperation>) -> Unit) // provide a scope to modify operation result processing
fun <T> 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<AccessibilityAction<() -> Boolean>>)
fun perform(params: UltronComposeOperationParams? = null, block: (SemanticsNodeInteraction) -> Unit)
fun <T> execute(params: UltronComposeOperationParams? = null, block: (SemanticsNodeInteraction) -> T): T
fun getNode(): SemanticsNode
fun <T> getNodeConfigProperty(key: SemanticsPropertyKey<T>): 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<SomePage>() {
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<SomePage>() {
//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<SomeScreen>() { ... }
```
## 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<String>){
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<Float>("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.

***
## 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<ContactsListPage >() {
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 <T> 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<ComposeFriendListItem>()
lazyList.getVisibleItem<ComposeFriendListItem>(index)
lazyList.getItem<ComposeFriendListItem>(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<ComposeListPage>() {
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<ComposeListPage>() {
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<ComposeListItem>()
lazyList.getVisibleItem<ComposeListItem>(index = 3)
lazyList.getLastVisibleItem<ComposeListItem>()
```
### 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<ComposeListItem>(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<Int>("ListItemPosition")
var SemanticsPropertyReceiver.listItemPosition by ListItemPositionPropertyKey
// specify it for item and store item index in this property
@Composable
fun ContactsListWithPosition(contacts: List<Contact>
) {
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<ComposeListItem>()
lazyList.getItem<ComposeListItem>(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<Contact>
) {
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<ComposeListItem>(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

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<RecyclerView.ViewHolder>(
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<View>` objects.
```kotlin
object ChatPage : Page<ChatPage>() {
private val messagesList = withId(R.i
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
SYMBOL INDEX (47 symbols across 5 files)
FILE: composeApp/karma.config.d/wasm/config.js
function KarmaWebpackOutputFramework (line 29) | function KarmaWebpackOutputFramework(config) {
FILE: docs/src/components/HomepageFeatures/index.tsx
type FeatureItem (line 5) | type FeatureItem = {
function Feature (line 54) | function Feature({ title, pict, description }: FeatureItem) {
function HomepageFeatures (line 71) | function HomepageFeatures(): JSX.Element {
FILE: docs/src/pages/index.tsx
function HomepageHeader (line 10) | function HomepageHeader() {
function Home (line 31) | function Home(): JSX.Element {
FILE: sample-app/src/main/java/com/atiurin/sampleapp/view/CircleImageView.java
class CircleImageView (line 34) | @SuppressWarnings("UnusedDeclaration")
method CircleImageView (line 74) | public CircleImageView(Context context) {
method CircleImageView (line 80) | public CircleImageView(Context context, AttributeSet attrs) {
method CircleImageView (line 84) | public CircleImageView(Context context, AttributeSet attrs, int defSty...
method init (line 99) | private void init() {
method getScaleType (line 113) | @Override
method setScaleType (line 118) | @Override
method setAdjustViewBounds (line 125) | @Override
method onDraw (line 132) | @Override
method onSizeChanged (line 152) | @Override
method setPadding (line 158) | @Override
method setPaddingRelative (line 164) | @Override
method getBorderColor (line 170) | public int getBorderColor() {
method setBorderColor (line 174) | public void setBorderColor(@ColorInt int borderColor) {
method getCircleBackgroundColor (line 184) | public int getCircleBackgroundColor() {
method setCircleBackgroundColor (line 188) | public void setCircleBackgroundColor(@ColorInt int circleBackgroundCol...
method setCircleBackgroundColorResource (line 198) | public void setCircleBackgroundColorResource(@ColorRes int circleBackg...
method getBorderWidth (line 202) | public int getBorderWidth() {
method setBorderWidth (line 206) | public void setBorderWidth(int borderWidth) {
method isBorderOverlay (line 215) | public boolean isBorderOverlay() {
method setBorderOverlay (line 219) | public void setBorderOverlay(boolean borderOverlay) {
method isDisableCircularTransformation (line 228) | public boolean isDisableCircularTransformation() {
method setDisableCircularTransformation (line 232) | public void setDisableCircularTransformation(boolean disableCircularTr...
method setImageBitmap (line 241) | @Override
method setImageDrawable (line 247) | @Override
method setImageResource (line 253) | @Override
method setImageURI (line 259) | @Override
method setColorFilter (line 265) | @Override
method getColorFilter (line 276) | @Override
method applyColorFilter (line 281) | private void applyColorFilter() {
method getBitmapFromDrawable (line 285) | private Bitmap getBitmapFromDrawable(Drawable drawable) {
method initializeBitmap (line 313) | private void initializeBitmap() {
method setup (line 322) | private void setup() {
method calculateBounds (line 368) | private RectF calculateBounds() {
method updateShaderMatrix (line 380) | private void updateShaderMatrix() {
method onTouchEvent (line 401) | @SuppressLint("ClickableViewAccessibility")
method inTouchableArea (line 407) | private boolean inTouchableArea(float x, float y) {
class OutlineProvider (line 411) | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
method getOutline (line 414) | @Override
FILE: ultron-android/src/test/java/com/atiurin/ultron/ExampleUnitTest.java
class ExampleUnitTest (line 12) | public class ExampleUnitTest {
method addition_isCorrect (line 13) | @Test
Condensed preview — 559 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,516K chars).
[
{
"path": ".github/workflows/ci-pipeline.yml",
"chars": 504,
"preview": "name: MultiplatformCI\n\non:\n push:\n branches: [ master ]\n pull_request:\n branches: [ master ]\n\njobs:\n compileKot"
},
{
"path": ".github/workflows/docs.yml",
"chars": 544,
"preview": "name: Build and deploy docs\n\non:\n push:\n branches:\n - master\n\njobs:\n github-pages:\n runs-on: ubuntu-22.04\n "
},
{
"path": ".github/workflows/maven_central_publish.yml",
"chars": 1498,
"preview": "name: Publish\n\npermissions:\n contents: read\n\non:\n push:\n branches:\n - 'release/*'\n\njobs:\n publish:\n name: "
},
{
"path": ".gitignore",
"chars": 234,
"preview": "*.iml\n.gradle\n/local.properties\n/.idea\n/.idea/caches\n/.idea/libraries\n/.idea/modules.xml\n/.idea/workspace.xml\n/.idea/nav"
},
{
"path": "LICENSE",
"chars": 11357,
"preview": "\n Apache License\n Version 2.0, January 2004\n "
},
{
"path": "README.md",
"chars": 9810,
"preview": "<p align=\"center\">\n<img src=\"https://user-images.githubusercontent.com/12834123/252489846-db6cb0f8-6b28-4ae4-bceb-8b5907"
},
{
"path": "build.gradle.kts",
"chars": 1188,
"preview": "import org.jetbrains.compose.internal.utils.getLocalProperty\n\nbuildscript {\n extra.apply {\n set(\"RELEASE_REPOS"
},
{
"path": "buildSrc/build.gradle.kts",
"chars": 147,
"preview": "import org.gradle.kotlin.dsl.`kotlin-dsl`\n\nplugins {\n `kotlin-dsl`\n}\n\nrepositories {\n google()\n mavenCentral()\n"
},
{
"path": "buildSrc/src/main/kotlin/Versions.kt",
"chars": 5043,
"preview": "object Versions {\n const val kotlin = \"2.1.21\"\n const val androidToolsBuildGradle = \"8.3.1\"\n const val androidM"
},
{
"path": "composeApp/build.gradle.kts",
"chars": 4517,
"preview": "import org.jetbrains.compose.ExperimentalComposeLibrary\nimport org.jetbrains.compose.desktop.application.dsl.TargetForma"
},
{
"path": "composeApp/karma.config.d/wasm/config.js",
"chars": 2278,
"preview": "// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file\n// This file provides karma.config.d"
},
{
"path": "composeApp/src/androidMain/AndroidManifest.xml",
"chars": 984,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n <appli"
},
{
"path": "composeApp/src/androidMain/kotlin/Platform.android.kt",
"chars": 184,
"preview": "import android.os.Build\n\nclass AndroidPlatform : Platform {\n override val name: String = \"Android ${Build.VERSION.SDK"
},
{
"path": "composeApp/src/androidMain/kotlin/com/atiurin/samplekmp/MainActivity.kt",
"chars": 578,
"preview": "package com.atiurin.samplekmp\n\nimport App\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport and"
},
{
"path": "composeApp/src/androidMain/res/drawable/ic_launcher_background.xml",
"chars": 5605,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:wi"
},
{
"path": "composeApp/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml",
"chars": 1702,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:aapt=\"http://schemas.android.com/aapt\"\n "
},
{
"path": "composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml",
"chars": 272,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n <b"
},
{
"path": "composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml",
"chars": 272,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n <b"
},
{
"path": "composeApp/src/androidMain/res/values/strings.xml",
"chars": 72,
"preview": "<resources>\n <string name=\"app_name\">sample-kmp</string>\n</resources>"
},
{
"path": "composeApp/src/commonMain/composeResources/drawable/compose-multiplatform.xml",
"chars": 4513,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"600dp\"\n android:height=\"600dp\"\n"
},
{
"path": "composeApp/src/commonMain/kotlin/App.kt",
"chars": 1757,
"preview": "import androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.foundation.Image\nimport androidx.compose.fo"
},
{
"path": "composeApp/src/commonMain/kotlin/Greeting.kt",
"chars": 134,
"preview": "class Greeting {\n private val platform = getPlatform()\n\n fun greet(): String {\n return \"Hello, ${platform.n"
},
{
"path": "composeApp/src/commonMain/kotlin/Platform.kt",
"chars": 79,
"preview": "interface Platform {\n val name: String\n}\n\nexpect fun getPlatform(): Platform"
},
{
"path": "composeApp/src/commonMain/kotlin/repositories/ContactRepository.kt",
"chars": 350,
"preview": "package repositories\n\nobject ContactRepository {\n fun getContact(id: Int) : Contact {\n return contacts.find { "
},
{
"path": "composeApp/src/commonMain/kotlin/repositories/Storage.kt",
"chars": 2846,
"preview": "package repositories\n\n\ndata class Contact( val id: Int,val name: String, val status: String, val avatar: Int)\ndata class"
},
{
"path": "composeApp/src/commonMain/kotlin/ui/screens/ContactsListScreen.kt",
"chars": 3092,
"preview": "package ui.screens\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\n"
},
{
"path": "composeApp/src/commonTest/kotlin/BaseInteractionTest.kt",
"chars": 2218,
"preview": "\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.material.Button\nimport androidx.compose.materi"
},
{
"path": "composeApp/src/commonTest/kotlin/ExampleTest.kt",
"chars": 1363,
"preview": "\nimport androidx.compose.material.Button\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.getValue\n"
},
{
"path": "composeApp/src/commonTest/kotlin/ListTest.kt",
"chars": 1714,
"preview": "import androidx.compose.ui.test.ExperimentalTestApi\nimport androidx.compose.ui.test.hasTestTag\nimport com.atiurin.ultron"
},
{
"path": "composeApp/src/commonTest/kotlin/UltronTestFlowTest.kt",
"chars": 3309,
"preview": "import com.atiurin.ultron.annotations.ExperimentalUltronApi\nimport com.atiurin.ultron.core.test.UltronTest\nimport com.at"
},
{
"path": "composeApp/src/commonTest/kotlin/UltronTestFlowTest2.kt",
"chars": 1669,
"preview": "import com.atiurin.ultron.annotations.ExperimentalUltronApi\nimport com.atiurin.ultron.core.test.UltronTest\nimport com.at"
},
{
"path": "composeApp/src/desktopMain/kotlin/Platform.jvm.kt",
"chars": 160,
"preview": "class JVMPlatform: Platform {\n override val name: String = \"Java ${System.getProperty(\"java.version\")}\"\n}\n\nactual fun"
},
{
"path": "composeApp/src/desktopMain/kotlin/main.kt",
"chars": 230,
"preview": "import androidx.compose.ui.window.Window\nimport androidx.compose.ui.window.application\n\nfun main() = application {\n W"
},
{
"path": "composeApp/src/desktopTest/kotlin/DesktopSampleTest.kt",
"chars": 1628,
"preview": "\nimport androidx.compose.material.Button\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.getValue\n"
},
{
"path": "composeApp/src/iosMain/kotlin/MainViewController.kt",
"chars": 119,
"preview": "import androidx.compose.ui.window.ComposeUIViewController\n\nfun MainViewController() = ComposeUIViewController { App() }"
},
{
"path": "composeApp/src/iosMain/kotlin/Platform.ios.kt",
"chars": 228,
"preview": "import platform.UIKit.UIDevice\n\nclass IOSPlatform: Platform {\n override val name: String = UIDevice.currentDevice.sys"
},
{
"path": "composeApp/src/iosTest/kotlin/IOSSampleTest.kt",
"chars": 1463,
"preview": "import androidx.compose.material.Button\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.getValue\ni"
},
{
"path": "composeApp/src/jsMain/kotlin/Platform.js.kt",
"chars": 70,
"preview": "actual fun getPlatform(): Platform {\n TODO(\"Not yet implemented\")\n}"
},
{
"path": "composeApp/src/jsTest/kotlin/JsSampleTest.kt",
"chars": 1610,
"preview": "import androidx.compose.material.Button\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.getValue\ni"
},
{
"path": "composeApp/src/wasmJsMain/kotlin/Platform.wasmJs.kt",
"chars": 140,
"preview": "class WasmPlatform: Platform {\n override val name: String = \"Web with Kotlin/Wasm\"\n}\n\nactual fun getPlatform(): Platf"
},
{
"path": "composeApp/src/wasmJsMain/kotlin/main.kt",
"chars": 248,
"preview": "import androidx.compose.ui.ExperimentalComposeUiApi\nimport androidx.compose.ui.window.ComposeViewport\nimport kotlinx.bro"
},
{
"path": "composeApp/src/wasmJsMain/resources/index.html",
"chars": 336,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width"
},
{
"path": "composeApp/src/wasmJsMain/resources/styles.css",
"chars": 102,
"preview": "html, body {\n width: 100%;\n height: 100%;\n margin: 0;\n padding: 0;\n overflow: hidden;\n}"
},
{
"path": "docs/.gitignore",
"chars": 233,
"preview": "# Dependencies\n/node_modules\n\n# Production\n/build\n\n# Generated files\n.docusaurus\n.cache-loader\n\n# Misc\n.DS_Store\n.env.lo"
},
{
"path": "docs/README.md",
"chars": 768,
"preview": "# Website\n\nThis website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator.\n\n### Ins"
},
{
"path": "docs/babel.config.js",
"chars": 89,
"preview": "module.exports = {\n presets: [require.resolve('@docusaurus/core/lib/babel/preset')],\n};\n"
},
{
"path": "docs/docs/android/_category_.json",
"chars": 64,
"preview": "{\n \"label\": \"Android\",\n \"position\": 3,\n \"collapsed\": false\n}\n"
},
{
"path": "docs/docs/android/espress.md",
"chars": 7870,
"preview": "---\nsidebar_position: 1\n---\n\n# Espresso\n\n## How to use?\n\nSimple espresso operation looks like this\n\n```kotlin\nonView(wit"
},
{
"path": "docs/docs/android/recyclerview.md",
"chars": 9998,
"preview": "---\nsidebar_position: 3\n---\n\n# RecyclerView\n\n## Terms\nBefore we go forward we need to define some terms:\n- RecyclerView "
},
{
"path": "docs/docs/android/rootview.md",
"chars": 2179,
"preview": "---\nsidebar_position: 5\n---\n\n# withSuitableRoot\n\nMethod allows to avoiding nontrivial element lookup exceptions\n\nIn some"
},
{
"path": "docs/docs/android/testconditions.md",
"chars": 6103,
"preview": "---\nsidebar_position: 6\n---\n\n# Test Conditions Management\n\nIt is a feature that includes 3 parts\n\n- RuleSequence\n- SetUp"
},
{
"path": "docs/docs/android/uiautomator.md",
"chars": 8399,
"preview": "---\nsidebar_position: 4\n---\n\n# UI Automator \n\n**Ultron** makes UI Automator actions and assertions much more stable and "
},
{
"path": "docs/docs/android/webview.md",
"chars": 5602,
"preview": "---\nsidebar_position: 2\n---\n\n# WebView\n\nThere are 3 different objects to interact with.\n\n* `UltronWebDocument` - wraps o"
},
{
"path": "docs/docs/common/_category_.json",
"chars": 63,
"preview": "{\n \"label\": \"Common\",\n \"position\": 4,\n \"collapsed\": false\n}\n"
},
{
"path": "docs/docs/common/allure.md",
"chars": 6020,
"preview": "---\nsidebar_position: 1\n---\n\n# Allure\n\nUltron can generate artifacts for Allure report only for Android UI tests. \n\nJust"
},
{
"path": "docs/docs/common/boolean.md",
"chars": 928,
"preview": "---\nsidebar_position: 5\n---\n\n# Boolean result\n\nWhile using the **Ultron** framework you always can get the result of any"
},
{
"path": "docs/docs/common/customassertion.md",
"chars": 1878,
"preview": "---\nsidebar_position: 6\n---\n\n# Custom assertions\n\nOur applications are not perfect. It's often happens, that some action"
},
{
"path": "docs/docs/common/extension.md",
"chars": 7796,
"preview": "---\nsidebar_position: 3\n---\n\n# Ultron Extension\n\nUltron leverages the power of [Kotlin extension functions](https://kotl"
},
{
"path": "docs/docs/common/listeners.md",
"chars": 5762,
"preview": "---\nsidebar_position: 4\n---\n\n# Listeners\n\nThe framework has 2 types of listeners: UltronLifecycleListener & UltronRunLis"
},
{
"path": "docs/docs/common/resulthandler.md",
"chars": 2014,
"preview": "---\nsidebar_position: 7\n---\n\n# Result handler\n\n**Ultron** allows you to process the result of any operation in your own "
},
{
"path": "docs/docs/common/uiblock.md",
"chars": 10489,
"preview": "---\nsidebar_position: 2\n---\n\n# UI Block\n\nUI blocks are a powerful tool for describing and interacting with user interfac"
},
{
"path": "docs/docs/common/ultrontest.md",
"chars": 6350,
"preview": "---\nsidebar_position: 2\n---\n\n# UltronTest\n\n`UltronTest` is a powerful base class provided by the Ultron framework that e"
},
{
"path": "docs/docs/compose/_category_.json",
"chars": 64,
"preview": "{\n \"label\": \"Compose\",\n \"position\": 2,\n \"collapsed\": false\n}\n"
},
{
"path": "docs/docs/compose/android.md",
"chars": 2300,
"preview": "---\nsidebar_position: 2\n---\n\n# Android\n\nNote: it's possible to use Multiplatform approach using methods `runComposeUiTes"
},
{
"path": "docs/docs/compose/api.md",
"chars": 8117,
"preview": "---\nsidebar_position: 4\n---\n\n# Ultron Compose API\n\nThe framework provides an extended API for Compose UI testing. Basica"
},
{
"path": "docs/docs/compose/index.md",
"chars": 405,
"preview": "# Compose\n\nThere are two types of UI tests you can write with Compose.\n\n1. Kotlin Multiplatform UI test ([Kotlin documen"
},
{
"path": "docs/docs/compose/lazylist.md",
"chars": 11357,
"preview": "---\nsidebar_position: 5\n---\n\n# LazyList\n\n## Ultron LazyColumn/LazyRow\n\nIt's pretty much familiar with `UltronRecyclerVie"
},
{
"path": "docs/docs/compose/multiplatform.md",
"chars": 3339,
"preview": "---\nsidebar_position: 1\n---\n\n# Multiplatform\n\n> Multiplatform support is in Alpha state.\n\nCompose Multiplatform provides"
},
{
"path": "docs/docs/index.md",
"chars": 7967,
"preview": "---\nsidebar_position: 1\n---\n\n# Introduction\n\n"
},
{
"path": "docs/docs/intro/_category_.json",
"chars": 72,
"preview": "{\n \"label\": \"Getting started\",\n \"position\": 1,\n \"collapsed\": false\n}\n"
},
{
"path": "docs/docs/intro/configuration.md",
"chars": 4480,
"preview": "---\nsidebar_position: 4\n---\n\n# Configuration\n\nEach library of the framework has it's own config onject. \n\n- `UltronCompo"
},
{
"path": "docs/docs/intro/connect.md",
"chars": 1676,
"preview": "---\nsidebar_position: 2\n---\n\n# Connect to project\n\nThe framework has three libraries that could be added as dependencies"
},
{
"path": "docs/docs/intro/dependencies.md",
"chars": 1395,
"preview": "---\nsidebar_position: 3\n---\n\n# Dependencies Management\n\nUltron provides all the required dependencies in a transitive ma"
},
{
"path": "docs/docusaurus.config.ts",
"chars": 2932,
"preview": "import {themes as prismThemes} from 'prism-react-renderer';\nimport type {Config} from '@docusaurus/types';\nimport type *"
},
{
"path": "docs/package.json",
"chars": 1203,
"preview": "{\n \"name\": \"my-website\",\n \"version\": \"0.0.0\",\n \"private\": true,\n \"scripts\": {\n \"docusaurus\": \"docusaurus\",\n \"s"
},
{
"path": "docs/sidebars.ts",
"chars": 778,
"preview": "import type {SidebarsConfig} from '@docusaurus/plugin-content-docs';\n\n/**\n * Creating a sidebar enables you to:\n - creat"
},
{
"path": "docs/src/components/HomepageFeatures/index.tsx",
"chars": 1942,
"preview": "import clsx from 'clsx';\nimport Heading from '@theme/Heading';\nimport styles from './styles.module.css';\n\ntype FeatureIt"
},
{
"path": "docs/src/components/HomepageFeatures/styles.module.css",
"chars": 486,
"preview": ".features {\n display: flex;\n align-items: center;\n padding: 2rem 0;\n width: 100%;\n}\n\n.featureSvg {\n height: 150px;\n"
},
{
"path": "docs/src/css/custom.css",
"chars": 3034,
"preview": "/**\n * Any CSS included here will be global. The classic template\n * bundles Infima by default. Infima is a CSS framewor"
},
{
"path": "docs/src/pages/index.module.css",
"chars": 365,
"preview": "/**\n * CSS files with the .module.css suffix will be treated as CSS modules\n * and scoped locally.\n */\n\n.heroBanner {\n "
},
{
"path": "docs/src/pages/index.tsx",
"chars": 1203,
"preview": "import clsx from 'clsx';\nimport Link from '@docusaurus/Link';\nimport useDocusaurusContext from '@docusaurus/useDocusauru"
},
{
"path": "docs/src/pages/markdown-page.md",
"chars": 118,
"preview": "---\ntitle: Markdown page example\n---\n\n# Markdown page example\n\nYou don't need React to write simple standalone pages.\n"
},
{
"path": "docs/static/.nojekyll",
"chars": 0,
"preview": ""
},
{
"path": "docs/tsconfig.json",
"chars": 176,
"preview": "{\n // This file is not used in compilation. It is here just for a nice editor experience.\n \"extends\": \"@docusaurus/tsc"
},
{
"path": "gradle/libs.versions.toml",
"chars": 2690,
"preview": "[versions]\nagp = \"8.9.3\"\natomicfu = \"0.27.0\"\nkotlin = \"2.1.21\"\nandroidx-activityCompose = \"1.10.1\"\nkotlinxCoroutinesCore"
},
{
"path": "gradle/wrapper/gradle-wrapper.properties",
"chars": 231,
"preview": "#Tue May 20 16:16:07 MSK 2025\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://"
},
{
"path": "gradle.properties",
"chars": 580,
"preview": "org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\\=\"-Xmx2048M\"\norg.gradle.caching=true\norg."
},
{
"path": "gradlew",
"chars": 8024,
"preview": "#!/bin/sh\n\n#\n# Copyright 2015-2021 the original authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"Licen"
},
{
"path": "gradlew.bat",
"chars": 2763,
"preview": "@rem\r\n@rem Copyright 2015 the original author or authors.\r\n@rem\r\n@rem Licensed under the Apache License, Version 2.0 (th"
},
{
"path": "iosApp/Configuration/Config.xcconfig",
"chars": 71,
"preview": "TEAM_ID=\nBUNDLE_ID=com.atiurin.samplekmp.sample-kmp\nAPP_NAME=sample-kmp"
},
{
"path": "iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json",
"chars": 122,
"preview": "{\n \"colors\" : [\n {\n \"idiom\" : \"universal\"\n }\n ],\n \"info\" : {\n \"author\" : \"xcode\",\n \"version\" : 1\n }"
},
{
"path": "iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json",
"chars": 217,
"preview": "{\n \"images\" : [\n {\n \"filename\" : \"app-icon-1024.png\",\n \"idiom\" : \"universal\",\n \"platform\" : \"ios\",\n "
},
{
"path": "iosApp/iosApp/Assets.xcassets/Contents.json",
"chars": 62,
"preview": "{\n \"info\" : {\n \"author\" : \"xcode\",\n \"version\" : 1\n }\n}"
},
{
"path": "iosApp/iosApp/ContentView.swift",
"chars": 486,
"preview": "import UIKit\nimport SwiftUI\nimport ComposeApp\n\nstruct ComposeView: UIViewControllerRepresentable {\n func makeUIViewCo"
},
{
"path": "iosApp/iosApp/Info.plist",
"chars": 1577,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json",
"chars": 62,
"preview": "{\n \"info\" : {\n \"author\" : \"xcode\",\n \"version\" : 1\n }\n}"
},
{
"path": "iosApp/iosApp/iOSApp.swift",
"chars": 108,
"preview": "import SwiftUI\n\n@main\nstruct iOSApp: App {\n\tvar body: some Scene {\n\t\tWindowGroup {\n\t\t\tContentView()\n\t\t}\n\t}\n}"
},
{
"path": "iosApp/iosApp.xcodeproj/project.pbxproj",
"chars": 14540,
"preview": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 60;\n\tobjects = {\n\n/* Begin PBXBuildFile section *"
},
{
"path": "prepare-emulator.bat",
"chars": 236,
"preview": "adb shell settings put global development_settings_enabled 1\nadb shell settings put global window_animation_scale 0.0\nad"
},
{
"path": "prepare-emulator.sh",
"chars": 236,
"preview": "adb shell settings put global development_settings_enabled 1\nadb shell settings put global window_animation_scale 0.0\nad"
},
{
"path": "sample-app/.gitignore",
"chars": 7,
"preview": "/build\n"
},
{
"path": "sample-app/build.gradle.kts",
"chars": 2817,
"preview": "plugins {\n id(\"com.android.application\")\n id(\"kotlin-android\")\n alias(libs.plugins.compose.compiler)\n}\n\nandroid"
},
{
"path": "sample-app/proguard-rules.pro",
"chars": 751,
"preview": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguar"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/framework/CustomTestRunner.kt",
"chars": 150,
"preview": "package com.atiurin.sampleapp.framework\n\nimport com.atiurin.ultron.allure.UltronAllureTestRunner\n\nclass CustomTestRunner"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/framework/DummyMetaObject.kt",
"chars": 86,
"preview": "package com.atiurin.sampleapp.framework\n\ndata class DummyMetaObject(val value: String)"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/framework/Log.kt",
"chars": 626,
"preview": "package com.atiurin.sampleapp.framework\n\nimport android.os.SystemClock\nimport android.util.Log\n\nobject Log {\n const v"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/framework/ScreenshotLifecycleListener.kt",
"chars": 433,
"preview": "package com.atiurin.sampleapp.framework\n\nimport com.atiurin.ultron.core.common.Operation\nimport com.atiurin.ultron.core."
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/framework/ultronext/UltronComposeExt.kt",
"chars": 2288,
"preview": "package com.atiurin.sampleapp.framework.ultronext\n\nimport androidx.compose.ui.semantics.SemanticsProperties\nimport andro"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/framework/ultronext/UltronEspressoExt.kt",
"chars": 2493,
"preview": "package com.atiurin.sampleapp.framework.ultronext\n\nimport android.view.View\nimport android.widget.CheckBox\nimport androi"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/framework/ultronext/UltronEspressoWebExt.kt",
"chars": 609,
"preview": "package com.atiurin.sampleapp.framework.ultronext\n\nimport androidx.test.espresso.web.webdriver.DriverAtoms\nimport com.at"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/framework/ultronext/UltronUiAutomatorExt.kt",
"chars": 1324,
"preview": "package com.atiurin.sampleapp.framework.ultronext\n\nimport com.atiurin.ultron.core.common.UltronOperationType\nimport com."
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/framework/utils/AssertUtils.kt",
"chars": 1339,
"preview": "package com.atiurin.sampleapp.framework.utils\n\nimport org.junit.Assert\n\nobject AssertUtils {\n fun assertException(exp"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/framework/utils/EspressoUtil.kt",
"chars": 322,
"preview": "package com.atiurin.sampleapp.framework.utils\n\nimport androidx.test.espresso.Espresso\nimport androidx.test.platform.app."
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/framework/utils/TestDataUtils.kt",
"chars": 295,
"preview": "package com.atiurin.sampleapp.framework.utils\n\nimport androidx.test.platform.app.InstrumentationRegistry\n\nobject TestDat"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/framework/utils/TimeUtils.kt",
"chars": 769,
"preview": "package com.atiurin.sampleapp.framework.utils\n\nimport android.annotation.SuppressLint\nimport java.time.Clock\nimport java"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/ChatPage.kt",
"chars": 2908,
"preview": "package com.atiurin.sampleapp.pages\n\nimport android.view.View\nimport androidx.test.espresso.matcher.ViewMatchers.*\nimpor"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/ComposeElementsPage.kt",
"chars": 1667,
"preview": "package com.atiurin.sampleapp.pages\n\nimport androidx.compose.ui.test.hasTestTag\nimport com.atiurin.sampleapp.activity.Co"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/ComposeListPage.kt",
"chars": 2209,
"preview": "package com.atiurin.sampleapp.pages\n\nimport androidx.compose.ui.test.hasAnyDescendant\nimport androidx.compose.ui.test.ha"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/ComposeSecondPage.kt",
"chars": 245,
"preview": "package com.atiurin.sampleapp.pages\n\nimport androidx.compose.ui.test.hasTestTag\nimport com.atiurin.ultron.page.Page\n\nobj"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/FriendsListPage.kt",
"chars": 2909,
"preview": "package com.atiurin.sampleapp.pages\n\nimport androidx.test.espresso.matcher.ViewMatchers.hasDescendant\nimport androidx.te"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/UiElementsPage.kt",
"chars": 1587,
"preview": "package com.atiurin.sampleapp.pages\n\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.matcher"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/UiObject2ElementsPage.kt",
"chars": 1379,
"preview": "package com.atiurin.sampleapp.pages\n\nimport androidx.test.uiautomator.By\nimport com.atiurin.sampleapp.R\nimport com.atiur"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/UiObject2FriendsListPage.kt",
"chars": 629,
"preview": "package com.atiurin.sampleapp.pages\n\nimport androidx.test.uiautomator.By\nimport com.atiurin.sampleapp.R\nimport com.atiur"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/UiObjectElementsPage.kt",
"chars": 1128,
"preview": "package com.atiurin.sampleapp.pages\n\nimport com.atiurin.sampleapp.R\nimport com.atiurin.ultron.core.uiautomator.uiobject."
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/WebViewPage.kt",
"chars": 929,
"preview": "package com.atiurin.sampleapp.pages\n\nimport com.atiurin.ultron.core.espressoweb.webelement.UltronWebElements.Companion.c"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/uiblock/ComposeUiBlockScreen.kt",
"chars": 2583,
"preview": "package com.atiurin.sampleapp.pages.uiblock\n\nimport androidx.compose.ui.test.SemanticsMatcher\nimport androidx.compose.ui"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/uiblock/EspressoUiBlockScreen.kt",
"chars": 1694,
"preview": "package com.atiurin.sampleapp.pages.uiblock\n\nimport android.view.View\nimport androidx.test.espresso.matcher.ViewMatchers"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/uiblock/UiObject2UiBlockScreen.kt",
"chars": 1510,
"preview": "package com.atiurin.sampleapp.pages.uiblock\n\nimport androidx.test.uiautomator.BySelector\nimport com.atiurin.sampleapp.R\n"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/uiblock/WebElementUiBlockScreen.kt",
"chars": 1475,
"preview": "package com.atiurin.sampleapp.pages.uiblock\n\nimport com.atiurin.ultron.core.espressoweb.webelement.UltronWebElement\nimpo"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/BaseTest.kt",
"chars": 1808,
"preview": "package com.atiurin.sampleapp.tests\n\nimport android.os.Environment\nimport androidx.test.platform.app.InstrumentationRegi"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/UiElementsTest.kt",
"chars": 340,
"preview": "package com.atiurin.sampleapp.tests\n\nimport com.atiurin.sampleapp.activity.UiElementsActivity\nimport com.atiurin.ultron."
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/CheckboxTest.kt",
"chars": 1561,
"preview": "package com.atiurin.sampleapp.tests.compose\n\nimport androidx.compose.material.TriStateCheckbox\nimport androidx.compose.r"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/CollectionInteractionTest.kt",
"chars": 966,
"preview": "package com.atiurin.sampleapp.tests.compose\n\nimport androidx.compose.ui.test.hasTestTag\nimport com.atiurin.sampleapp.act"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/ComposeConfigTest.kt",
"chars": 4560,
"preview": "package com.atiurin.sampleapp.tests.compose\n\nimport com.atiurin.sampleapp.activity.ComposeElementsActivity\nimport com.at"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/ComposeCustomAssertionTest.kt",
"chars": 1169,
"preview": "package com.atiurin.sampleapp.tests.compose\n\nimport com.atiurin.sampleapp.activity.ComposeElementsActivity\nimport com.at"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/ComposeEmptyListTest.kt",
"chars": 1272,
"preview": "package com.atiurin.sampleapp.tests.compose\n\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose."
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/ComposeListTest.kt",
"chars": 10105,
"preview": "package com.atiurin.sampleapp.tests.compose\n\nimport androidx.compose.ui.test.hasAnyDescendant\nimport androidx.compose.ui"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/ComposeListWithPositionTestTagTest.kt",
"chars": 2643,
"preview": "package com.atiurin.sampleapp.tests.compose\n\nimport androidx.compose.ui.test.hasAnyDescendant\nimport androidx.compose.ui"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/ComposeUIElementsTest.kt",
"chars": 26520,
"preview": "package com.atiurin.sampleapp.tests.compose\n\nimport androidx.compose.ui.semantics.ProgressBarRangeInfo\nimport androidx.c"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/DefaultComponentActivityTest.kt",
"chars": 863,
"preview": "package com.atiurin.sampleapp.tests.compose\n\nimport androidx.compose.material.Text\nimport androidx.compose.ui.Modifier\ni"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/RunUltronUiTest.kt",
"chars": 1101,
"preview": "package com.atiurin.sampleapp.tests.compose\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.ma"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/SampleClassTest.kt",
"chars": 1291,
"preview": "package com.atiurin.sampleapp.tests.compose\n\nimport androidx.compose.ui.test.hasTestTag\nimport com.atiurin.sampleapp.act"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/SemNodeInteractionObjectTest.kt",
"chars": 2907,
"preview": "package com.atiurin.sampleapp.tests.compose\n\nimport com.atiurin.sampleapp.activity.ActionsStatus\nimport com.atiurin.samp"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/TreeTest.kt",
"chars": 1080,
"preview": "package com.atiurin.sampleapp.tests.compose\n\nimport androidx.compose.ui.test.onRoot\nimport androidx.compose.ui.test.prin"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/UltronComposeUiBlockTest.kt",
"chars": 4428,
"preview": "package com.atiurin.sampleapp.tests.compose\n\nimport androidx.compose.ui.test.hasTestTag\nimport com.atiurin.sampleapp.act"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/elements/DataPickerTest.kt",
"chars": 2993,
"preview": "package com.atiurin.sampleapp.tests.compose.elements\n\nimport androidx.compose.ui.test.ExperimentalTestApi\nimport android"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso/CustomClicksTest.kt",
"chars": 1923,
"preview": "package com.atiurin.sampleapp.tests.espresso\n\nimport androidx.test.core.app.ActivityScenario\nimport androidx.test.espres"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso/CustomMatchersTest.kt",
"chars": 1491,
"preview": "package com.atiurin.sampleapp.tests.espresso\n\nimport androidx.test.espresso.matcher.ViewMatchers.withId\nimport com.atiur"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso/DemoEspressoTest.kt",
"chars": 2148,
"preview": "package com.atiurin.sampleapp.tests.espresso\n\nimport com.atiurin.sampleapp.activity.MainActivity\nimport com.atiurin.samp"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso/RecyclerPerfTest.kt",
"chars": 2516,
"preview": "package com.atiurin.sampleapp.tests.espresso\n\nimport androidx.recyclerview.widget.RecyclerView\nimport androidx.test.espr"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso/RecyclerViewTest.kt",
"chars": 27417,
"preview": "package com.atiurin.sampleapp.tests.espresso\n\nimport androidx.test.espresso.matcher.ViewMatchers.Visibility\nimport andro"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso/UltronActivityRuleTest.kt",
"chars": 1593,
"preview": "package com.atiurin.sampleapp.tests.espresso\n\nimport android.content.Intent\nimport androidx.test.ext.junit.rules.Activit"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso/UltronEspressoConfigTest.kt",
"chars": 9081,
"preview": "package com.atiurin.sampleapp.tests.espresso\n\nimport androidx.test.espresso.matcher.ViewMatchers\nimport com.atiurin.samp"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso/UltronEspressoUiBlockTest.kt",
"chars": 4436,
"preview": "package com.atiurin.sampleapp.tests.espresso\n\nimport android.view.View\nimport androidx.test.espresso.matcher.ViewMatcher"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso/ViewInteractionActionsTest.kt",
"chars": 9391,
"preview": "package com.atiurin.sampleapp.tests.espresso\n\n\nimport android.os.SystemClock\nimport android.view.KeyEvent\nimport android"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso/ViewInteractionAssertionsTest.kt",
"chars": 16309,
"preview": "package com.atiurin.sampleapp.tests.espresso\n\nimport androidx.test.espresso.matcher.ViewMatchers.isDisplayed\nimport andr"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso/ViewTest.kt",
"chars": 4416,
"preview": "package com.atiurin.sampleapp.tests.espresso\n\nimport android.content.Intent\nimport android.widget.Button\nimport android."
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso/WithSuitableRootTest.kt",
"chars": 1236,
"preview": "package com.atiurin.sampleapp.tests.espresso\n\nimport com.atiurin.sampleapp.activity.MainActivity\nimport com.atiurin.samp"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso_web/BaseWebViewTest.kt",
"chars": 616,
"preview": "package com.atiurin.sampleapp.tests.espresso_web\n\nimport androidx.test.core.app.ActivityScenario\nimport com.atiurin.samp"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso_web/EspressoWebUiElementsTest.kt",
"chars": 5987,
"preview": "package com.atiurin.sampleapp.tests.espresso_web\n\nimport androidx.test.espresso.matcher.ViewMatchers.withId\nimport andro"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso_web/UltronWebDocumentTest.kt",
"chars": 2039,
"preview": "package com.atiurin.sampleapp.tests.espresso_web\n\nimport androidx.test.espresso.web.assertion.WebViewAssertions.webConte"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso_web/UltronWebElementTest.kt",
"chars": 8904,
"preview": "package com.atiurin.sampleapp.tests.espresso_web\n\nimport androidx.test.espresso.web.assertion.WebViewAssertions.webMatch"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso_web/UltronWebElementsTest.kt",
"chars": 912,
"preview": "package com.atiurin.sampleapp.tests.espresso_web\n\nimport com.atiurin.sampleapp.framework.Log\nimport com.atiurin.ultron.c"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso_web/UltronWebUiBlockTest.kt",
"chars": 783,
"preview": "package com.atiurin.sampleapp.tests.espresso_web\n\nimport com.atiurin.sampleapp.pages.uiblock.WebElementUiBlockScreen\nimp"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/testlifecycle/ExceptionsProcessingTest.kt",
"chars": 5352,
"preview": "package com.atiurin.sampleapp.tests.testlifecycle\n\nimport com.atiurin.sampleapp.framework.Log\nimport com.atiurin.ultron."
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/testlifecycle/ParametrizedTest.kt",
"chars": 2473,
"preview": "package com.atiurin.sampleapp.tests.testlifecycle\n\nimport com.atiurin.sampleapp.activity.ComposeElementsActivity\nimport "
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/testlifecycle/SetUpTearDownRuleTest.kt",
"chars": 4109,
"preview": "package com.atiurin.sampleapp.tests.testlifecycle\n\nimport com.atiurin.sampleapp.framework.Log\nimport com.atiurin.ultron."
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/testlifecycle/UltronTestFlowTest.kt",
"chars": 3242,
"preview": "package com.atiurin.sampleapp.tests.testlifecycle\n\nimport com.atiurin.sampleapp.tests.BaseTest\nimport com.atiurin.ultron"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/testlifecycle/UltronTestFlowTest2.kt",
"chars": 1737,
"preview": "package com.atiurin.sampleapp.tests.testlifecycle\n\nimport com.atiurin.sampleapp.tests.BaseTest\nimport com.atiurin.ultron"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/testlifecycle/UltronTestPlan.kt",
"chars": 192,
"preview": "package com.atiurin.sampleapp.tests.testlifecycle\n\n//@RunWith(Suite::class)\n//@Suite.SuiteClasses(\n// UltronTestFlowT"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/testlifecycle/UltronTestRuleSequenceMergeTest.kt",
"chars": 1176,
"preview": "package com.atiurin.sampleapp.tests.testlifecycle\n\nimport com.atiurin.sampleapp.tests.BaseTest\nimport com.atiurin.ultron"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/uiautomator/UiAutomatorCustomAssertionTest.kt",
"chars": 1805,
"preview": "package com.atiurin.sampleapp.tests.uiautomator\n\nimport com.atiurin.sampleapp.R\nimport com.atiurin.sampleapp.framework.u"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/uiautomator/UltronUiAutomatorPerfTest.kt",
"chars": 638,
"preview": "package com.atiurin.sampleapp.tests.uiautomator\n\nimport android.os.SystemClock\nimport com.atiurin.sampleapp.framework.Lo"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/uiautomator/UltronUiObject2ActionsTest.kt",
"chars": 12080,
"preview": "package com.atiurin.sampleapp.tests.uiautomator\n\nimport android.view.ViewConfiguration\nimport android.widget.LinearLayou"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/uiautomator/UltronUiObject2AssertionsTest.kt",
"chars": 12311,
"preview": "package com.atiurin.sampleapp.tests.uiautomator\n\nimport com.atiurin.sampleapp.R\nimport com.atiurin.sampleapp.framework.L"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/uiautomator/UltronUiObject2ScrollTest.kt",
"chars": 1633,
"preview": "package com.atiurin.sampleapp.tests.uiautomator\n\nimport android.os.Build\nimport com.atiurin.sampleapp.activity.MainActiv"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/uiautomator/UltronUiObject2UiBlockTest.kt",
"chars": 1488,
"preview": "package com.atiurin.sampleapp.tests.uiautomator\n\nimport com.atiurin.sampleapp.activity.UiBlockActivity\nimport com.atiuri"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/uiautomator/UltronUiObjectActionsTest.kt",
"chars": 7100,
"preview": "package com.atiurin.sampleapp.tests.uiautomator\n\nimport androidx.test.platform.app.InstrumentationRegistry\nimport com.at"
},
{
"path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/uiautomator/UltronUiObjectAssertionsTest.kt",
"chars": 12153,
"preview": "package com.atiurin.sampleapp.tests.uiautomator\n\nimport com.atiurin.sampleapp.R\nimport com.atiurin.sampleapp.framework.u"
},
{
"path": "sample-app/src/debug/AndroidManifest.xml",
"chars": 266,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n package="
},
{
"path": "sample-app/src/main/AndroidManifest.xml",
"chars": 3443,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n <uses-"
},
{
"path": "sample-app/src/main/assets/webview.html",
"chars": 2766,
"preview": "<!DOCTYPE html>\n<html>\n<head>\n <title>Android Web View</title>\n</head>\n<body>\n <h1 id=\"title\" class=\"css_title\">We"
},
{
"path": "sample-app/src/main/assets/webview_small.html",
"chars": 202,
"preview": "<!DOCTYPE html>\n<html>\n<head>\n <title>Android Web View</title>\n</head>\n<body>\n<h1 id=\"title\" class=\"css_title\">WebVie"
},
{
"path": "sample-app/src/main/java/com/atiurin/sampleapp/MyApplication.kt",
"chars": 310,
"preview": "package com.atiurin.sampleapp\n\nimport android.app.Application\nimport android.content.Context\n\nobject MyApplication : App"
},
{
"path": "sample-app/src/main/java/com/atiurin/sampleapp/activity/BusyActivity.kt",
"chars": 738,
"preview": "package com.atiurin.sampleapp.activity\n\nimport android.app.Activity\nimport android.os.Bundle\nimport android.os.Handler\ni"
},
{
"path": "sample-app/src/main/java/com/atiurin/sampleapp/activity/ChatActivity.kt",
"chars": 4822,
"preview": "package com.atiurin.sampleapp.activity\n\nimport android.os.Build\nimport android.os.Bundle\nimport android.util.Log\nimport "
},
{
"path": "sample-app/src/main/java/com/atiurin/sampleapp/activity/ComposeElementsActivity.kt",
"chars": 10756,
"preview": "package com.atiurin.sampleapp.activity\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport andro"
},
{
"path": "sample-app/src/main/java/com/atiurin/sampleapp/activity/ComposeListActivity.kt",
"chars": 2733,
"preview": "package com.atiurin.sampleapp.activity\n\nimport android.os.Bundle\nimport android.widget.Toast\nimport androidx.activity.Co"
},
{
"path": "sample-app/src/main/java/com/atiurin/sampleapp/activity/ComposeListWithPositionTestTagActivity.kt",
"chars": 2881,
"preview": "package com.atiurin.sampleapp.activity\n\nimport android.os.Bundle\nimport android.widget.Toast\nimport androidx.activity.Co"
},
{
"path": "sample-app/src/main/java/com/atiurin/sampleapp/activity/ComposeRouterActivity.kt",
"chars": 734,
"preview": "package com.atiurin.sampleapp.activity\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport andro"
},
{
"path": "sample-app/src/main/java/com/atiurin/sampleapp/activity/ComposeSecondActivity.kt",
"chars": 1498,
"preview": "package com.atiurin.sampleapp.activity\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport andro"
},
{
"path": "sample-app/src/main/java/com/atiurin/sampleapp/activity/CustomClicksActivity.kt",
"chars": 919,
"preview": "package com.atiurin.sampleapp.activity\n\nimport android.os.Bundle\nimport androidx.activity.enableEdgeToEdge\nimport androi"
},
{
"path": "sample-app/src/main/java/com/atiurin/sampleapp/activity/LoginActivity.kt",
"chars": 2100,
"preview": "package com.atiurin.sampleapp.activity\n\nimport android.content.Intent\nimport android.os.Bundle\nimport android.view.Gravi"
},
{
"path": "sample-app/src/main/java/com/atiurin/sampleapp/activity/MainActivity.kt",
"chars": 6519,
"preview": "package com.atiurin.sampleapp.activity\n\nimport android.content.Intent\nimport android.os.Bundle\nimport android.view.MenuI"
},
{
"path": "sample-app/src/main/java/com/atiurin/sampleapp/activity/ProfileActivity.kt",
"chars": 794,
"preview": "package com.atiurin.sampleapp.activity\n\nimport android.os.Bundle\nimport android.widget.EditText\nimport androidx.activity"
},
{
"path": "sample-app/src/main/java/com/atiurin/sampleapp/activity/SplashActivity.kt",
"chars": 793,
"preview": "package com.atiurin.sampleapp.activity\n\nimport android.content.Intent\nimport android.os.Bundle\nimport androidx.activity."
},
{
"path": "sample-app/src/main/java/com/atiurin/sampleapp/activity/UiBlockActivity.kt",
"chars": 1056,
"preview": "package com.atiurin.sampleapp.activity\n\nimport android.os.Bundle\nimport android.widget.LinearLayout\nimport android.widge"
},
{
"path": "sample-app/src/main/java/com/atiurin/sampleapp/activity/UiElementsActivity.kt",
"chars": 7102,
"preview": "package com.atiurin.sampleapp.activity\n\nimport android.annotation.SuppressLint\nimport android.os.Build\nimport android.os"
},
{
"path": "sample-app/src/main/java/com/atiurin/sampleapp/activity/WebViewActivity.kt",
"chars": 1030,
"preview": "package com.atiurin.sampleapp.activity\n\nimport android.os.Build\nimport android.os.Bundle\nimport android.text.Editable\nim"
},
{
"path": "sample-app/src/main/java/com/atiurin/sampleapp/adapters/ContactAdapter.kt",
"chars": 2431,
"preview": "package com.atiurin.sampleapp.adapters\n\nimport android.content.Context\nimport android.view.GestureDetector\nimport androi"
}
]
// ... and 359 more files (download for full content)
About this extraction
This page contains the full source code of the open-tool/ultron GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 559 files (1.3 MB), approximately 337.5k tokens, and a symbol index with 47 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.