Full Code of open-tool/ultron for AI

master f2b51e76a92a cached
559 files
1.3 MB
337.5k tokens
47 symbols
1 requests
Download .txt
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() 
}
```
![allure](https://github.com/open-tool/ultron/assets/12834123/c05c813a-ece6-45e6-a04f-e1c92b82ffb1)

![allure compose](https://github.com/open-tool/ultron/assets/12834123/1f751f3d-fc58-4874-a850-acd9181bfb70)

## Add Ultron to your project

Gradle
```groovy
repositories {
    mavenCentral()
}

dependencies {
    androidTestImplementation 'com.atiurin:ultron-android:<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.

![Terms](https://user-images.githubusercontent.com/12834123/107883156-4008d000-6efe-11eb-9764-8c57e767e5e2.png)

## UltronRecyclerView

Create an instance of `UltronRecyclerView` using the method `withRecyclerView(..)` method:

```kotlin
withRecyclerView(R.id.recycler_friends).assertSize(CONTACTS.size)
```

### Parameters for `withRecyclerView` method

The withRecyclerView method allows creating an instance of UltronRecyclerView with customizable parameters:

- `recyclerViewMatcher: Matcher`/ `resourceId: Int`, - A Matcher / @IntegerRes that identifies the target RecyclerView in the layout.
- `loadTimeout: Long` - The maximum time (in milliseconds) to wait for RecyclerView items to load. The default value is defined by `UltronConfig.Espresso.RECYCLER_VIEW_LOAD_TIMEOUT`.
- `itemSearchLimit: Int` - The maximum number of items to search through when locating an item in the RecyclerView. The default value is defined by `UltronConfig.Espresso.RECYCLER_VIEW_ITEM_SEARCH_LIMIT`.
- `operationsTimeoutMs: Long` - The maximum time (in milliseconds) to wait for operations on RecyclerView to complete. The default value is defined by `UltronConfig.Espresso.RECYCLER_VIEW_OPERATIONS_TIMEOUT`.
- `implementation: UltronRecyclerViewImpl`- Specifies the implementation of UltronRecyclerView to use. The default value is `UltronConfig.Espresso.RECYCLER_VIEW_IMPLEMENTATION`, which is set in the configuration.


### UltronRecyclerViewImpl

`UltronRecyclerViewImpl` has two available modes:

- `STANDARD`: This is the default implementation. It is already 4 times faster than the previous version. When multiple identical child elements are found within an UltronRecyclerViewItem, the first matching element is selected without throwing an AmbiguousViewMatcherException.

- `PERFORMANCE`: Optimized for higher performance. However, if multiple child elements matching the same criteria are found, an exception (AmbiguousViewMatcherException) will be thrown.

The choice of implementation affects not only performance but also how child elements of `UltronRecyclerViewItem` are handled.
For now, the actual difference in performance between these modes is minimal, but it could be highly valuable if your RecyclerView item contains many child elements.

### _Best practice_ - save `UltronRecyclerView` as page class properties   

```kotlin
object FriendsListPage : Page<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.

![allure](https://github.com/open-tool/ultron/assets/12834123/c05c813a-ece6-45e6-a04f-e1c92b82ffb1)

***
## Ultron `step`
Ultron wraps Allure `step` method into it's own one. 

It's recommended to use Ultron method cause it will provide more info to report in future releases.

### Best practice

Wraps all steps with Ultron `step` method e.g.

```kotlin
object ChatPage: Page<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 

![conditions](https://user-images.githubusercontent.com/12834123/232789449-1b6a0bc8-5c68-4dd3-836c-8d39696ce8dd.png)

## How to add custom artifacts to Allure report?

### Write artifact to report

The framework has special methods to write your artifacts into report.

`createCacheFile` - creates temp file to write the content ([see InstrumentationUtil.kt](https://github.com/open-tool/ultron/blob/master/ultron/src/main/java/com/atiurin/ultron/utils/InstrumentationUtil.kt))\

`AttachUtil.attachFile(...)` - to attach file to report [see AttachUtil](https://github.com/open-tool/ultron/blob/master/ultron-allure/src/main/java/com/atiurin/ultron/allure/attachment/AttachUtil.kt)

You method can looks like

```kotlin
fun addMyArtifactToAllure(){
    val tempFile = createCacheFile()
    val result = writeContentToFile(tempFile)
    val fileName = AttachUtil.attachFile(
        name = "file_name.xml",
        file = tempFile,
        mimeType = "text/xml"
    )
}
```
`writeContentToFile(tempFile)` - you should implement it.

### Manage artifact creation

You can attach artifact using 2 types of Ultron listeners:

- [UltronLifecycleListener](https://github.com/open-tool/ultron/blob/master/ultron/src/main/java/com/atiurin/ultron/listeners/UltronLifecycleListener.kt) - once Ultron operation finished with any result. Sample - [ScreenshotAttachListener.kt](https://github.com/open-tool/ultron/blob/master/ultron-allure/src/main/java/com/atiurin/ultron/allure/listeners/ScreenshotAttachListener.kt)

- [UltronRunListener](https://github.com/open-tool/ultron/blob/master/ultron/src/main/java/com/atiurin/ultron/runner/UltronRunListener.kt) which is inherited from [RunListener](https://github.com/open-tool/ultron/blob/master/ultron/src/main/java/com/atiurin/ultron/runner/RunListener.kt). This type can be used to add artifact in different test lifecycle state. Sample - [WindowHierarchyAttachRunListener.kt](https://github.com/open-tool/ultron/blob/master/ultron-allure/src/main/java/com/atiurin/ultron/allure/runner/WindowHierarchyAttachRunListener.kt)

Refer to the [Listeners doc page](../common/listeners.md) for details.

================================================
FILE: docs/docs/common/boolean.md
================================================
---
sidebar_position: 5
---

# Boolean result

While using the **Ultron** framework you always can get the result of any operation as boolean value. 

```kotlin
object SomePage : Page<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?

![resultHandler](https://user-images.githubusercontent.com/12834123/113351564-bc872f00-9343-11eb-925a-432dbc191b32.png)

**_A little explanation in case you would like to be more familiar with **Ultron** framework_**

There is an entity which we call `ResultHandler`. By default all Ultron operations has the same `ResultHandler`. 
It catches the result of operation and asks `OperationResultAnalyzer` to analyze the result. 
In case `operationResult.success` is `false` the result analyzer throws catched exception.

## How to use?
There are 2 ways of using custom ResultHandler:
- Specify it for page property and it will be applied for all operations with this element
```kotlin
object SomePage : Page<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.

![UI Block](/img/uiblock.png)

We can describe this block and use it on different screens. 

_Supported: Compose (CMP & Android), Espresso, Espresso Web, UiAutomator (UiObject2)_

## Compose
***
Create a class that inherits from `UltronComposeUiBlock`.

```kotlin

class ContactCard(blockMatcher: SemanticsMatcher, blockDescription: String) 
    : UltronComposeUiBlock(blockMatcher, blockDescription) {
    val name = child(hasTestTag(contactNameTag)).withName("Name in '$blockDescription'")
    val status = child(hasTestTag(contactStatusTag))
}
```

`UltronComposeUiBlock` accepts two parameters:

- `blockMatcher` – describes how to locate this block in the Compose element tree. This is a required parameter and must always be provided.
- `blockDescription` – a description of the block that clearly identifies the UI container. This parameter is optional, with a default value of `blockDescription = ""`.

**Note**: To describe child elements of a UI block, you need to use the `child()` method.

As shown in the example above, we added a custom name to the `name` field. If an error occurs, this name will appear in the description of the element we tried to interact with. We recommend including the value of `blockDescription` in the element description. This provides better context about the specific element being checked (or any other operation performed).

The next step is to integrate the block into a screen.

```kotlin
object SomeComposeScreen : Screen<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.

![lazyColumn](https://user-images.githubusercontent.com/12834123/188237127-32e501ca-ae8b-4cd4-8114-e3e17843dc55.PNG)

***
## UltronComposeList

Create an instance of `UltronComposeList` by calling a method `composeList(..)`

```kotlin
composeList(hasTestTag(contactsListTestTag)).assertNotEmpty()
```
### _Best practice_ - define `UltronComposeList` object as page class property

```kotlin
object ContactsListPage : Page<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

![Docusaurus themed image](/img/ultron_banner_light.png#gh-light-mode-only)![Docusaurus themed image](/img/ultron_banner_dark.png#gh-dark-mode-only)

Ultron is the simplest framework to develop UI tests for **Android** & **Compose Multiplatform**. 

It's constructed upon the Espresso, UI Automator and Compose UI testing frameworks. Ultron introduces a range of remarkable new features. Furthermore, Ultron puts you in complete control of your tests! 

You don't need to learn any new classes or special syntax. All magic actions and assertions are provided from crunch. Ultron can be easially customised and extended. 

## What are the benefits of using the framework?

- Page/Screen Object pattern support
- Exceptional simplification for [**Compose UI tests**](compose/index.md)
- Out-of-the-box generation of [**Allure report**](./common/allure.md) (Now, for Android UI tests only)
- A straightforward and expressive syntax
- Ensured **Stability** for all actions and assertions
- Complete control over every action and assertion
- Incredible interaction with lists: [**RecyclerView**](./android/recyclerview.md) and [**Compose LazyList**](compose/lazylist.md).
- An **Architectural** approach to developing UI tests (search "Best practice")
- An incredible mechanism for setups and teardowns (You can even set up preconditions for a single test within a test class, without affecting the others)
- [The ability to effortlessly extend the framework with your own operations](common/extension.md)
- Accelerated UI Automator operations
- Ability to monitor each stage of operation execution with [Listeners](common/listeners.md)
- [Custom operation assertions](common/customassertion.md)
***

### A few words about syntax

The standard syntax provided by Google is intricate and not intuitive. This is especially evident when dealing with **LazyList** and **RecyclerView** interactions.

Let's explore some examples:

#### 1. Simple compose operation (refer to the doc [here](./compose/index.md))

_Compose framework_

```kotlin
composeTestRule.onNode(hasTestTag("Continue")).performClick()
composeTestRule.onNodeWithText("Welcome").assertIsDisplayed()
```
_Ultron_

```kotlin
hasTestTag("Continue").click()
hasText("Welcome").assertIsDisplayed()
```

#### 2. Compose list operation (refer to the [doc](./compose/lazylist.md))

_Compose framework_

```kotlin
val itemMatcher = hasText(contact.name)
composeRule
    .onNodeWithTag(contactsListTestTag)
    .performScrollToNode(itemMatcher)
    .onChildren()
    .filterToOne(itemMatcher)
    .assertTextContains(contact.name)
```

_Ultron_

```kotlin
composeList(hasTestTag(contactsListTestTag))
    .item(hasText(contact.name))
    .assertTextContains(contact.name)
```
#### 3. Simple Espresso assertion and action.

_Espresso_

```kotlin
onView(withId(R.id.send_button)).check(isDisplayed()).perform(click())
```
_Ultron_

```kotlin
withId(R.id.send_button).isDisplayed().click()
```
This presents a cleaner approach. Ultron's operation names mirror Espresso's, while also providing additional operations. 

Refer to the [doc](./android/espress.md) for further details.

#### 4. Action on RecyclerView list item

_Espresso_

```kotlin
onView(withId(R.id.recycler_friends))
    .perform(
        RecyclerViewActions
            .actionOnItem<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
Download .txt
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
Download .txt
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![Docusaurus themed image](/img/ultron_banner_light.png#gh-light-mode-only)"
  },
  {
    "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.

Copied to clipboard!