├── docs ├── static │ ├── .nojekyll │ └── img │ │ ├── favicon.ico │ │ ├── kotlin.png │ │ ├── simple.webp │ │ ├── stable.webp │ │ ├── uiblock.png │ │ ├── maintainability.webp │ │ ├── ultron_banner_dark.png │ │ ├── ultron_full_light.png │ │ └── ultron_banner_light.png ├── docs │ ├── android │ │ └── _category_.json │ ├── common │ │ ├── _category_.json │ │ └── boolean.md │ ├── compose │ │ ├── _category_.json │ │ └── index.md │ └── intro │ │ └── _category_.json ├── babel.config.js ├── src │ ├── pages │ │ ├── markdown-page.md │ │ └── index.module.css │ └── components │ │ └── HomepageFeatures │ │ └── styles.module.css ├── tsconfig.json ├── .gitignore ├── sidebars.ts └── README.md ├── sample-app ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── drawable │ │ │ │ ├── janice.png │ │ │ │ ├── monica.png │ │ │ │ ├── phoebe.png │ │ │ │ ├── rachel.png │ │ │ │ ├── gunther.png │ │ │ │ ├── default_avatar.png │ │ │ │ ├── img.xml │ │ │ │ ├── side_nav_bar.xml │ │ │ │ ├── ic_menu_send.xml │ │ │ │ ├── circle.xml │ │ │ │ ├── background_splash.xml │ │ │ │ ├── ic_menu_slideshow.xml │ │ │ │ ├── ic_menu_gallery.xml │ │ │ │ ├── ic_menu_manage.xml │ │ │ │ ├── ic_menu_camera.xml │ │ │ │ └── ic_menu_share.xml │ │ │ ├── drawable-v24 │ │ │ │ ├── joey.png │ │ │ │ ├── ross.png │ │ │ │ └── chandler.png │ │ │ ├── drawable-hdpi │ │ │ │ ├── ic_exit.png │ │ │ │ ├── ic_send.png │ │ │ │ ├── ic_account.png │ │ │ │ ├── ic_messages.png │ │ │ │ └── ic_attach_file.png │ │ │ ├── drawable-mdpi │ │ │ │ ├── ic_exit.png │ │ │ │ ├── ic_send.png │ │ │ │ ├── ic_account.png │ │ │ │ ├── ic_messages.png │ │ │ │ └── ic_attach_file.png │ │ │ ├── drawable-xhdpi │ │ │ │ ├── ic_exit.png │ │ │ │ ├── ic_send.png │ │ │ │ ├── ic_account.png │ │ │ │ ├── ic_messages.png │ │ │ │ └── ic_attach_file.png │ │ │ ├── drawable-xxhdpi │ │ │ │ ├── ic_exit.png │ │ │ │ ├── ic_send.png │ │ │ │ ├── ic_account.png │ │ │ │ ├── ic_messages.png │ │ │ │ └── ic_attach_file.png │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── layout │ │ │ │ ├── my_text_view.xml │ │ │ │ ├── activity_webview.xml │ │ │ │ ├── content_main.xml │ │ │ │ ├── app_bar_main.xml │ │ │ │ ├── activity_main.xml │ │ │ │ └── activity_uiblock.xml │ │ │ ├── values-v21 │ │ │ │ └── styles.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── menu │ │ │ │ └── main.xml │ │ │ ├── values │ │ │ │ ├── dimens.xml │ │ │ │ ├── attrs.xml │ │ │ │ ├── colors.xml │ │ │ │ └── styles.xml │ │ │ └── drawable-anydpi │ │ │ │ ├── ic_send.xml │ │ │ │ ├── ic_messages.xml │ │ │ │ ├── ic_exit.xml │ │ │ │ ├── ic_account.xml │ │ │ │ └── ic_attach_file.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── atiurin │ │ │ │ └── sampleapp │ │ │ │ ├── data │ │ │ │ ├── Tags.kt │ │ │ │ ├── entities │ │ │ │ │ ├── Contact.kt │ │ │ │ │ ├── User.kt │ │ │ │ │ └── Message.kt │ │ │ │ ├── loaders │ │ │ │ │ └── MessageLoader.kt │ │ │ │ ├── viewmodel │ │ │ │ │ ├── DataViewModel.kt │ │ │ │ │ └── ContactsViewModel.kt │ │ │ │ └── repositories │ │ │ │ │ └── ContactRepositoty.kt │ │ │ │ ├── compose │ │ │ │ ├── app │ │ │ │ │ └── AppScreen.kt │ │ │ │ ├── SimpleOutlinedText.kt │ │ │ │ └── screen │ │ │ │ │ └── DatePickerScreen.kt │ │ │ │ ├── idlingresources │ │ │ │ ├── IdlingHelper.kt │ │ │ │ ├── resources │ │ │ │ │ ├── ChatIdlingResource.kt │ │ │ │ │ └── ContactsIdlingResource.kt │ │ │ │ ├── Holder.kt │ │ │ │ └── AbstractIdlingResource.kt │ │ │ │ ├── utils │ │ │ │ └── TimeUtils.kt │ │ │ │ ├── MyApplication.kt │ │ │ │ ├── async │ │ │ │ ├── AsyncDataLoading.kt │ │ │ │ ├── GetContacts.kt │ │ │ │ ├── task │ │ │ │ │ └── CompatAsyncTask.kt │ │ │ │ └── UseCase.kt │ │ │ │ ├── activity │ │ │ │ ├── BusyActivity.kt │ │ │ │ ├── ComposeRouterActivity.kt │ │ │ │ ├── ProfileActivity.kt │ │ │ │ ├── SplashActivity.kt │ │ │ │ ├── CustomClicksActivity.kt │ │ │ │ ├── UiBlockActivity.kt │ │ │ │ └── WebViewActivity.kt │ │ │ │ └── managers │ │ │ │ └── PrefsManager.kt │ │ └── assets │ │ │ └── webview_small.html │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── atiurin │ │ │ └── sampleapp │ │ │ ├── framework │ │ │ ├── DummyMetaObject.kt │ │ │ ├── CustomTestRunner.kt │ │ │ ├── utils │ │ │ │ ├── TestDataUtils.kt │ │ │ │ ├── EspressoUtil.kt │ │ │ │ └── TimeUtils.kt │ │ │ ├── ScreenshotLifecycleListener.kt │ │ │ ├── Log.kt │ │ │ └── ultronext │ │ │ │ └── UltronEspressoWebExt.kt │ │ │ ├── tests │ │ │ ├── testlifecycle │ │ │ │ └── UltronTestPlan.kt │ │ │ ├── UiElementsTest.kt │ │ │ ├── espresso_web │ │ │ │ ├── BaseWebViewTest.kt │ │ │ │ ├── UltronWebUiBlockTest.kt │ │ │ │ └── UltronWebElementsTest.kt │ │ │ ├── uiautomator │ │ │ │ └── UltronUiAutomatorPerfTest.kt │ │ │ └── compose │ │ │ │ ├── DefaultComponentActivityTest.kt │ │ │ │ ├── CollectionInteractionTest.kt │ │ │ │ ├── TreeTest.kt │ │ │ │ └── RunUltronUiTest.kt │ │ │ └── pages │ │ │ ├── ComposeSecondPage.kt │ │ │ ├── UiObject2FriendsListPage.kt │ │ │ ├── WebViewPage.kt │ │ │ └── UiObjectElementsPage.kt │ └── debug │ │ └── AndroidManifest.xml └── proguard-rules.pro ├── ultron-allure ├── .gitignore ├── gradle.properties └── src │ ├── main │ └── java │ │ └── com │ │ └── atiurin │ │ └── ultron │ │ └── allure │ │ ├── config │ │ ├── AllureAttachStrategy.kt │ │ └── AllureConfigParams.kt │ │ ├── runner │ │ ├── UltronAllureRunInformer.kt │ │ ├── UltronLogCleanerRunListener.kt │ │ ├── ScreenshotAttachRunListener.kt │ │ ├── WindowHierarchyAttachRunListener.kt │ │ ├── UltronTestRunListener.kt │ │ └── UltronLogAttachRunListener.kt │ │ ├── step │ │ └── UltronStep.kt │ │ ├── condition │ │ ├── AllureConditionExecutorWrapper.kt │ │ └── AllureConditionsExecutor.kt │ │ ├── attachment │ │ ├── AllureDirectoryUtil.kt │ │ └── AttachUtil.kt │ │ ├── hierarchy │ │ └── AllureHierarchyDumper.kt │ │ └── UltronAllureTestRunner.kt │ └── test │ └── java │ └── com │ └── atiurin │ └── ultron │ └── allure │ └── ExampleUnitTest.kt ├── ultron-common ├── .gitignore ├── src │ ├── commonMain │ │ └── kotlin │ │ │ └── com │ │ │ └── atiurin │ │ │ └── ultron │ │ │ ├── utils │ │ │ ├── ThreadUtil.kt │ │ │ └── TimeUtil.kt │ │ │ ├── log │ │ │ ├── LogLevel.kt │ │ │ ├── UltronFileLogger.kt │ │ │ ├── UltronLogUtil.kt │ │ │ └── ULogger.kt │ │ │ ├── extensions │ │ │ └── AnyCommonExt.kt │ │ │ ├── exceptions │ │ │ ├── UltronException.kt │ │ │ ├── UltronAssertionException.kt │ │ │ ├── UltronUiAutomatorException.kt │ │ │ ├── UltronAssertionBlockException.kt │ │ │ ├── UltronOperationException.kt │ │ │ └── UltronWrapperException.kt │ │ │ ├── core │ │ │ ├── common │ │ │ │ ├── options │ │ │ │ │ ├── ClickOption.kt │ │ │ │ │ ├── TextEqualsOption.kt │ │ │ │ │ ├── TextContainsOption.kt │ │ │ │ │ ├── LongClickOption.kt │ │ │ │ │ ├── DoubleClickOption.kt │ │ │ │ │ ├── ContentDescriptionContainsOption.kt │ │ │ │ │ └── PerformCustomBlockOption.kt │ │ │ │ ├── OperationIterationResult.kt │ │ │ │ ├── UltronOperationType.kt │ │ │ │ ├── assertion │ │ │ │ │ ├── OperationAssertion.kt │ │ │ │ │ ├── DefaultOperationAssertion.kt │ │ │ │ │ ├── NoListenersOperationAssertion.kt │ │ │ │ │ ├── EmptyOperationAssertion.kt │ │ │ │ │ └── SoftAssertion.kt │ │ │ │ ├── ElementInfo.kt │ │ │ │ ├── OperationProcessor.kt │ │ │ │ ├── DefaultOperationIterationResult.kt │ │ │ │ ├── DefaultElementInfo.kt │ │ │ │ ├── OperationResult.kt │ │ │ │ ├── Operation.kt │ │ │ │ └── resultanalyzer │ │ │ │ │ ├── OperationResultAnalyzer.kt │ │ │ │ │ ├── CheckOperationResultAnalyzer.kt │ │ │ │ │ └── SoftAssertionOperationResultAnalyzer.kt │ │ │ └── test │ │ │ │ ├── context │ │ │ │ ├── UltronTestContextProvider.kt │ │ │ │ ├── DefaultUltronTestContextProvider.kt │ │ │ │ ├── UltronTestContext.kt │ │ │ │ └── DefaultUltronTestContext.kt │ │ │ │ └── TestMethod.kt │ │ │ ├── page │ │ │ ├── Page.kt │ │ │ └── Screen.kt │ │ │ ├── listeners │ │ │ ├── AbstractListener.kt │ │ │ ├── UltronLifecycleListener.kt │ │ │ ├── LifecycleListener.kt │ │ │ ├── UltronListenerUtil.kt │ │ │ ├── LogLifecycleListener.kt │ │ │ └── AbstractListenersContainer.kt │ │ │ ├── annotations │ │ │ └── ExperimentalUltronApi.kt │ │ │ └── file │ │ │ └── MimeType.kt │ ├── jsWasmMain │ │ └── kotlin │ │ │ └── com │ │ │ └── atiurin │ │ │ └── ultron │ │ │ └── utils │ │ │ └── ThreadUtil.jsWasm.kt │ ├── androidMain │ │ └── kotlin │ │ │ └── com │ │ │ └── atiurin │ │ │ └── ultron │ │ │ ├── log │ │ │ ├── UltronLog.android.kt │ │ │ └── UltronLogcatLogger.android.kt │ │ │ ├── testlifecycle │ │ │ └── setupteardown │ │ │ │ ├── RuleSequenceTearDown.kt │ │ │ │ ├── Condition.kt │ │ │ │ ├── ConditionExecutorWrapper.kt │ │ │ │ ├── TearDown.kt │ │ │ │ ├── SetUp.kt │ │ │ │ ├── ConditionsExecutor.kt │ │ │ │ ├── DefaultConditionExecutorWrapper.kt │ │ │ │ ├── DefaultConditionsExecutor.kt │ │ │ │ ├── SetUpRule.kt │ │ │ │ └── TearDownRule.kt │ │ │ ├── utils │ │ │ ├── ThreadUtil.android.kt │ │ │ └── ActivityUtil.android.kt.kt │ │ │ ├── extensions │ │ │ ├── DescriptionExt.kt │ │ │ ├── BundleExt.kt │ │ │ ├── FileExt.android.kt │ │ │ └── AnyExt.android.kt │ │ │ ├── runner │ │ │ ├── UltronRunListener.kt │ │ │ └── UltronRunInformer.kt │ │ │ ├── screenshot │ │ │ ├── Screenshoter.kt │ │ │ └── ScreenshotResult.kt │ │ │ ├── hierarchy │ │ │ ├── HierarchyDumper.kt │ │ │ ├── HierarchyDumpResult.kt │ │ │ └── UiDeviceHierarchyDumper.kt │ │ │ └── core │ │ │ └── config │ │ │ └── UltronAndroidCommonConfig.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 └── gradle.properties ├── ultron-android ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ └── values │ │ │ │ └── strings.xml │ │ └── kotlin │ │ │ └── com │ │ │ └── atiurin │ │ │ └── ultron │ │ │ ├── core │ │ │ ├── uiautomator │ │ │ │ ├── UiAutomatorOperation.kt │ │ │ │ ├── UltronUiAutomatorLifecycle.kt │ │ │ │ ├── UiAutomatorOperationResult.kt │ │ │ │ ├── uiobject │ │ │ │ │ └── UiAutomatorUiSelectorOperationExecutor.kt │ │ │ │ ├── uiobject2 │ │ │ │ │ ├── UiAutomatorBySelectorActionExecutor.kt │ │ │ │ │ └── UiAutomatorBySelectorAssertionExecutor.kt │ │ │ │ ├── UiAutomatorActionType.kt │ │ │ │ └── UiAutomatorAssertionType.kt │ │ │ ├── espresso │ │ │ │ ├── recyclerview │ │ │ │ │ ├── UltronRecyclerViewImpl.kt │ │ │ │ │ ├── RecyclerViewItemExecutor.kt │ │ │ │ │ └── RecyclerViewScrollToPositionViewAction.kt │ │ │ │ ├── UltronEspressoOperationLifecycle.kt │ │ │ │ ├── assertion │ │ │ │ │ ├── UltronEspressoAssertionParams.kt │ │ │ │ │ ├── EspressoAssertionExecutor.kt │ │ │ │ │ └── EspressoAssertionType.kt │ │ │ │ ├── EspressoOperationResult.kt │ │ │ │ └── action │ │ │ │ │ ├── EspressoActionType.kt │ │ │ │ │ ├── EspressoActionExecutor.kt │ │ │ │ │ └── UltronEspressoActionParams.kt │ │ │ ├── espressoweb │ │ │ │ ├── UltronWebLifecycle.kt │ │ │ │ └── operation │ │ │ │ │ ├── WebInteractionOperationIterationResult.kt │ │ │ │ │ ├── WebOperationResult.kt │ │ │ │ │ ├── EspressoWebOperationType.kt │ │ │ │ │ └── WebInteractionOperationExecutor.kt │ │ │ └── config │ │ │ │ └── UltronConfigParams.kt │ │ │ ├── utils │ │ │ └── ViewGroupUtils.kt │ │ │ ├── custom │ │ │ └── espresso │ │ │ │ ├── action │ │ │ │ ├── CustomEspressoActionType.kt │ │ │ │ └── AnonymousViewAction.kt │ │ │ │ ├── assertion │ │ │ │ ├── CustomEspressoAssertionType.kt │ │ │ │ └── ExistsEspressoViewAssertion.kt │ │ │ │ ├── base │ │ │ │ ├── UltronRootViewFinder.kt │ │ │ │ └── IterableUtils.kt │ │ │ │ └── matcher │ │ │ │ └── ElementWithAttributeMatcher.kt │ │ │ └── extensions │ │ │ ├── ViewExt.kt │ │ │ └── BitmapExt.kt │ └── test │ │ └── java │ │ └── com │ │ └── atiurin │ │ └── ultron │ │ └── ExampleUnitTest.java └── gradle.properties ├── gradlew ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── iosApp ├── Configuration │ └── Config.xcconfig └── iosApp │ ├── Assets.xcassets │ ├── Contents.json │ ├── AppIcon.appiconset │ │ ├── app-icon-1024.png │ │ └── Contents.json │ └── AccentColor.colorset │ │ └── Contents.json │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ ├── iOSApp.swift │ └── ContentView.swift ├── composeApp └── src │ ├── jsMain │ └── kotlin │ │ └── Platform.js.kt │ ├── androidMain │ ├── res │ │ ├── values │ │ │ └── strings.xml │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ └── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ ├── kotlin │ │ ├── Platform.android.kt │ │ └── com │ │ │ └── atiurin │ │ │ └── samplekmp │ │ │ └── MainActivity.kt │ └── AndroidManifest.xml │ ├── commonMain │ └── kotlin │ │ ├── Platform.kt │ │ ├── Greeting.kt │ │ └── repositories │ │ └── ContactRepository.kt │ ├── wasmJsMain │ ├── resources │ │ ├── styles.css │ │ └── index.html │ └── kotlin │ │ ├── Platform.wasmJs.kt │ │ └── main.kt │ ├── iosMain │ └── kotlin │ │ ├── MainViewController.kt │ │ └── Platform.ios.kt │ └── desktopMain │ └── kotlin │ ├── Platform.jvm.kt │ └── main.kt ├── ultron-compose ├── gradle.properties └── src │ ├── commonMain │ └── kotlin │ │ └── com │ │ └── atiurin │ │ └── ultron │ │ ├── core │ │ └── compose │ │ │ ├── nodeinteraction │ │ │ ├── SwipePosition.kt │ │ │ └── UltronComposeOffsets.kt │ │ │ ├── operation │ │ │ ├── UltronComposeOperationLifecycle.kt │ │ │ ├── UltronComposeOperationParams.kt │ │ │ ├── ComposeOperationResult.kt │ │ │ └── UltronComposeOperation.kt │ │ │ ├── option │ │ │ └── ComposeSwipeOption.kt │ │ │ ├── config │ │ │ └── UltronComposeConfigParams.kt │ │ │ ├── list │ │ │ ├── ComposeItemExecutor.kt │ │ │ ├── IndexComposeItemExecutor.kt │ │ │ └── MatcherComposeItemExecutor.kt │ │ │ ├── ComposeTestEnvironment.kt │ │ │ ├── UltronUiTest.kt │ │ │ └── ComposeTestContainer.kt │ │ └── extensions │ │ ├── AssertionsExt.kt │ │ ├── FiltersExt.kt │ │ ├── SemanticsSelectorExt.kt │ │ ├── SemanticsNodeExt.kt │ │ └── SemanticsNodeInteractionExt.kt │ ├── shared │ └── kotlin │ │ └── com │ │ └── atiurin │ │ └── ultron │ │ ├── core │ │ └── compose │ │ │ ├── config │ │ │ └── UltronComposeConfig.shared.kt │ │ │ └── list │ │ │ └── ItemChildInteractionProvider.shared.kt │ │ └── extensions │ │ └── SemanticsNodeInteractionCommonExt.shared.kt │ ├── androidMain │ └── kotlin │ │ └── com │ │ └── atiurin │ │ └── ultron │ │ ├── core │ │ └── compose │ │ │ ├── config │ │ │ └── UltronComposeConfig.android.kt │ │ │ └── listeners │ │ │ └── ComposDebugListener.kt │ │ └── extensions │ │ ├── ReflectionComposeExt.android.kt │ │ └── SemanticsMatcherExt.android.kt │ ├── test │ └── java │ │ └── com │ │ └── atiurin │ │ └── ultron │ │ └── compose │ │ └── ExampleUnitTest.kt │ └── jvmMain │ └── kotlin │ └── com │ └── atiurin │ └── ultron │ └── core │ └── compose │ └── UltronUiTest.jvm.kt ├── prepare-emulator.bat ├── prepare-emulator.sh ├── .gitignore ├── .github └── workflows │ ├── ci-pipeline.yml │ └── docs.yml ├── settings.gradle.kts └── gradle.properties /docs/static/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sample-app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /ultron-allure/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /ultron-common/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /ultron-android/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/gradlew -------------------------------------------------------------------------------- /docs/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/docs/static/img/favicon.ico -------------------------------------------------------------------------------- /docs/static/img/kotlin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/docs/static/img/kotlin.png -------------------------------------------------------------------------------- /docs/static/img/simple.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/docs/static/img/simple.webp -------------------------------------------------------------------------------- /docs/static/img/stable.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/docs/static/img/stable.webp -------------------------------------------------------------------------------- /docs/static/img/uiblock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/docs/static/img/uiblock.png -------------------------------------------------------------------------------- /docs/docs/android/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Android", 3 | "position": 3, 4 | "collapsed": false 5 | } 6 | -------------------------------------------------------------------------------- /docs/docs/common/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Common", 3 | "position": 4, 4 | "collapsed": false 5 | } 6 | -------------------------------------------------------------------------------- /docs/docs/compose/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Compose", 3 | "position": 2, 4 | "collapsed": false 5 | } 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /iosApp/Configuration/Config.xcconfig: -------------------------------------------------------------------------------- 1 | TEAM_ID= 2 | BUNDLE_ID=com.atiurin.samplekmp.sample-kmp 3 | APP_NAME=sample-kmp -------------------------------------------------------------------------------- /composeApp/src/jsMain/kotlin/Platform.js.kt: -------------------------------------------------------------------------------- 1 | actual fun getPlatform(): Platform { 2 | TODO("Not yet implemented") 3 | } -------------------------------------------------------------------------------- /docs/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /docs/docs/intro/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Getting started", 3 | "position": 1, 4 | "collapsed": false 5 | } 6 | -------------------------------------------------------------------------------- /docs/static/img/maintainability.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/docs/static/img/maintainability.webp -------------------------------------------------------------------------------- /docs/static/img/ultron_banner_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/docs/static/img/ultron_banner_dark.png -------------------------------------------------------------------------------- /docs/static/img/ultron_full_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/docs/static/img/ultron_full_light.png -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | sample-kmp 3 | -------------------------------------------------------------------------------- /docs/static/img/ultron_banner_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/docs/static/img/ultron_banner_light.png -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable/janice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/sample-app/src/main/res/drawable/janice.png -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable/monica.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/sample-app/src/main/res/drawable/monica.png -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable/phoebe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/sample-app/src/main/res/drawable/phoebe.png -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable/rachel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/sample-app/src/main/res/drawable/rachel.png -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/Platform.kt: -------------------------------------------------------------------------------- 1 | interface Platform { 2 | val name: String 3 | } 4 | 5 | expect fun getPlatform(): Platform -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable-v24/joey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/sample-app/src/main/res/drawable-v24/joey.png -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable-v24/ross.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/sample-app/src/main/res/drawable-v24/ross.png -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable/gunther.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/sample-app/src/main/res/drawable/gunther.png -------------------------------------------------------------------------------- /ultron-android/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | EspressoPageObject 3 | 4 | -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/utils/ThreadUtil.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.utils 2 | 3 | expect fun sleep(timeMs: Long) -------------------------------------------------------------------------------- /iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable-hdpi/ic_exit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/sample-app/src/main/res/drawable-hdpi/ic_exit.png -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable-hdpi/ic_send.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/sample-app/src/main/res/drawable-hdpi/ic_send.png -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable-mdpi/ic_exit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/sample-app/src/main/res/drawable-mdpi/ic_exit.png -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable-mdpi/ic_send.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/sample-app/src/main/res/drawable-mdpi/ic_send.png -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable-v24/chandler.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/sample-app/src/main/res/drawable-v24/chandler.png -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable-xhdpi/ic_exit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/sample-app/src/main/res/drawable-xhdpi/ic_exit.png -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable-xhdpi/ic_send.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/sample-app/src/main/res/drawable-xhdpi/ic_send.png -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable-hdpi/ic_account.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/sample-app/src/main/res/drawable-hdpi/ic_account.png -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable-hdpi/ic_messages.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/sample-app/src/main/res/drawable-hdpi/ic_messages.png -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable-mdpi/ic_account.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/sample-app/src/main/res/drawable-mdpi/ic_account.png -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable-mdpi/ic_messages.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/sample-app/src/main/res/drawable-mdpi/ic_messages.png -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable-xhdpi/ic_account.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/sample-app/src/main/res/drawable-xhdpi/ic_account.png -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable-xxhdpi/ic_exit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/sample-app/src/main/res/drawable-xxhdpi/ic_exit.png -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable-xxhdpi/ic_send.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/sample-app/src/main/res/drawable-xxhdpi/ic_send.png -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable/default_avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/sample-app/src/main/res/drawable/default_avatar.png -------------------------------------------------------------------------------- /sample-app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/sample-app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample-app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/sample-app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample-app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/sample-app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample-app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/sample-app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/log/LogLevel.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.log 2 | 3 | enum class LogLevel { 4 | I, D, W, E 5 | } -------------------------------------------------------------------------------- /ultron-common/src/jsWasmMain/kotlin/com/atiurin/ultron/utils/ThreadUtil.jsWasm.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.utils 2 | 3 | actual fun sleep(timeMs: Long) {} -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable-xhdpi/ic_messages.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/sample-app/src/main/res/drawable-xhdpi/ic_messages.png -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable-xxhdpi/ic_account.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/sample-app/src/main/res/drawable-xxhdpi/ic_account.png -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable-xxhdpi/ic_messages.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/sample-app/src/main/res/drawable-xxhdpi/ic_messages.png -------------------------------------------------------------------------------- /sample-app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/sample-app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable-hdpi/ic_attach_file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/sample-app/src/main/res/drawable-hdpi/ic_attach_file.png -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable-mdpi/ic_attach_file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/sample-app/src/main/res/drawable-mdpi/ic_attach_file.png -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable-xhdpi/ic_attach_file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/sample-app/src/main/res/drawable-xhdpi/ic_attach_file.png -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable-xxhdpi/ic_attach_file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/sample-app/src/main/res/drawable-xxhdpi/ic_attach_file.png -------------------------------------------------------------------------------- /sample-app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/sample-app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample-app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/sample-app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample-app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/sample-app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /composeApp/src/wasmJsMain/resources/styles.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | width: 100%; 3 | height: 100%; 4 | margin: 0; 5 | padding: 0; 6 | overflow: hidden; 7 | } -------------------------------------------------------------------------------- /sample-app/src/main/java/com/atiurin/sampleapp/data/Tags.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.data 2 | 3 | enum class Tags{ 4 | CONTACTS_LIST, 5 | MESSAGES_LIST 6 | } -------------------------------------------------------------------------------- /sample-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/sample-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/sample-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /composeApp/src/iosMain/kotlin/MainViewController.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.window.ComposeUIViewController 2 | 3 | fun MainViewController() = ComposeUIViewController { App() } -------------------------------------------------------------------------------- /docs/src/pages/markdown-page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Markdown page example 3 | --- 4 | 5 | # Markdown page example 6 | 7 | You don't need React to write simple standalone pages. 8 | -------------------------------------------------------------------------------- /sample-app/src/androidTest/java/com/atiurin/sampleapp/framework/DummyMetaObject.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.framework 2 | 3 | data class DummyMetaObject(val value: String) -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /iosApp/iosApp/iOSApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @main 4 | struct iOSApp: App { 5 | var body: some Scene { 6 | WindowGroup { 7 | ContentView() 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-tool/ultron/HEAD/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png -------------------------------------------------------------------------------- /ultron-common/src/androidMain/kotlin/com/atiurin/ultron/log/UltronLog.android.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.log 2 | 3 | actual fun getFileLogger(): UltronFileLogger = UltronFileLoggerImpl() -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/extensions/AnyCommonExt.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.extensions 2 | 3 | fun Any?.simpleClassName() = this?.let { it::class.simpleName } 4 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/Greeting.kt: -------------------------------------------------------------------------------- 1 | class Greeting { 2 | private val platform = getPlatform() 3 | 4 | fun greet(): String { 5 | return "Hello, ${platform.name}!" 6 | } 7 | } -------------------------------------------------------------------------------- /ultron-common/src/androidMain/kotlin/com/atiurin/ultron/testlifecycle/setupteardown/RuleSequenceTearDown.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.testlifecycle.setupteardown 2 | 3 | interface RuleSequenceTearDown -------------------------------------------------------------------------------- /composeApp/src/wasmJsMain/kotlin/Platform.wasmJs.kt: -------------------------------------------------------------------------------- 1 | class WasmPlatform: Platform { 2 | override val name: String = "Web with Kotlin/Wasm" 3 | } 4 | 5 | actual fun getPlatform(): Platform = WasmPlatform() -------------------------------------------------------------------------------- /sample-app/src/main/java/com/atiurin/sampleapp/data/entities/Contact.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.data.entities 2 | 3 | data class Contact( val id: Int,val name: String, val status: String, val avatar: Int) -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/exceptions/UltronException.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.exceptions 2 | 3 | class UltronException(override val message: String) : RuntimeException(message) -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/options/ClickOption.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.common.options 2 | 3 | data class ClickOption(val xOffset: Long = 0, val yOffset: Long = 0) 4 | -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/options/TextEqualsOption.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.common.options 2 | 3 | data class TextEqualsOption(val includeEditableText: Boolean = true) -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // This file is not used in compilation. It is here just for a nice editor experience. 3 | "extends": "@docusaurus/tsconfig", 4 | "compilerOptions": { 5 | "baseUrl": "." 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /composeApp/src/desktopMain/kotlin/Platform.jvm.kt: -------------------------------------------------------------------------------- 1 | class JVMPlatform: Platform { 2 | override val name: String = "Java ${System.getProperty("java.version")}" 3 | } 4 | 5 | actual fun getPlatform(): Platform = JVMPlatform() -------------------------------------------------------------------------------- /sample-app/src/main/java/com/atiurin/sampleapp/data/entities/User.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.data.entities 2 | 3 | data class User( val id: Int,val name: String, val avatar: Int, val login: String, val password: String) -------------------------------------------------------------------------------- /ultron-allure/gradle.properties: -------------------------------------------------------------------------------- 1 | GROUP=com.atiurin 2 | POM_ARTIFACT_ID=ultron-allure 3 | 4 | POM_NAME=ultron-allure 5 | POM_PACKAGING=aar 6 | 7 | POM_DESCRIPTION=Ultron support of Allure report 8 | POM_INCEPTION_YEAR=2024 9 | -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/exceptions/UltronAssertionException.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.exceptions 2 | 3 | class UltronAssertionException(override val message: String) : AssertionError(message) -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/exceptions/UltronUiAutomatorException.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.exceptions 2 | 3 | class UltronUiAutomatorException(override val message: String) : AssertionError(message) -------------------------------------------------------------------------------- /ultron-compose/gradle.properties: -------------------------------------------------------------------------------- 1 | GROUP=com.atiurin 2 | POM_ARTIFACT_ID=ultron-compose 3 | 4 | POM_NAME=ultron-compose 5 | POM_PACKAGING=aar 6 | 7 | POM_DESCRIPTION=UI testing framework for Compose 8 | POM_INCEPTION_YEAR=2024 9 | -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } -------------------------------------------------------------------------------- /sample-app/src/main/java/com/atiurin/sampleapp/compose/app/AppScreen.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.compose.app 2 | 3 | enum class AppScreen(val title: String) { 4 | Navigation("Navigation"), 5 | DataPicker("Date Picker") 6 | } -------------------------------------------------------------------------------- /ultron-common/gradle.properties: -------------------------------------------------------------------------------- 1 | GROUP=com.atiurin 2 | POM_ARTIFACT_ID=ultron-common 3 | 4 | POM_NAME=ultron-common 5 | POM_PACKAGING=aar 6 | 7 | POM_DESCRIPTION=Ultron UI testing framework core library 8 | POM_INCEPTION_YEAR=2024 9 | -------------------------------------------------------------------------------- /ultron-common/src/androidMain/kotlin/com/atiurin/ultron/utils/ThreadUtil.android.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.utils 2 | 3 | import android.os.SystemClock 4 | 5 | actual fun sleep(timeMs: Long) { 6 | SystemClock.sleep(timeMs) 7 | } -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/exceptions/UltronAssertionBlockException.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.exceptions 2 | 3 | class UltronAssertionBlockException(override val message: String) : AssertionError(message) -------------------------------------------------------------------------------- /ultron-android/src/main/kotlin/com/atiurin/ultron/core/uiautomator/UiAutomatorOperation.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.uiautomator 2 | 3 | import com.atiurin.ultron.core.common.Operation 4 | 5 | interface UiAutomatorOperation : Operation -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/test/context/UltronTestContextProvider.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.test.context 2 | 3 | interface UltronTestContextProvider { 4 | fun provide(): UltronTestContext 5 | } -------------------------------------------------------------------------------- /sample-app/src/main/java/com/atiurin/sampleapp/data/entities/Message.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.data.entities 2 | 3 | data class Message(val authorId: Int, 4 | val receiverId: Int, 5 | val text: String) -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/OperationIterationResult.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.common 2 | 3 | interface OperationIterationResult { 4 | val success: Boolean 5 | val exception: Throwable? 6 | } -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/UltronOperationType.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.common 2 | 3 | interface UltronOperationType 4 | 5 | enum class CommonOperationType : UltronOperationType { DEFAULT } 6 | -------------------------------------------------------------------------------- /sample-app/src/androidTest/java/com/atiurin/sampleapp/framework/CustomTestRunner.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.framework 2 | 3 | import com.atiurin.ultron.allure.UltronAllureTestRunner 4 | 5 | class CustomTestRunner : UltronAllureTestRunner() {} -------------------------------------------------------------------------------- /ultron-android/gradle.properties: -------------------------------------------------------------------------------- 1 | GROUP=com.atiurin 2 | POM_ARTIFACT_ID=ultron-android 3 | 4 | POM_NAME=ultron-android 5 | POM_PACKAGING=aar 6 | 7 | POM_DESCRIPTION=Ultron support of Espresso and UIAutomator for Android 8 | POM_INCEPTION_YEAR=2024 9 | -------------------------------------------------------------------------------- /ultron-android/src/main/kotlin/com/atiurin/ultron/core/espresso/recyclerview/UltronRecyclerViewImpl.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.espresso.recyclerview 2 | 3 | enum class UltronRecyclerViewImpl { 4 | STANDARD, 5 | PERFORMANCE 6 | } 7 | -------------------------------------------------------------------------------- /ultron-common/src/androidMain/kotlin/com/atiurin/ultron/extensions/DescriptionExt.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.extensions 2 | 3 | import org.junit.runner.Description 4 | 5 | fun Description.fullTestName() = "'${this.className}.${this.methodName}'" -------------------------------------------------------------------------------- /ultron-common/src/androidMain/kotlin/com/atiurin/ultron/runner/UltronRunListener.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.runner 2 | 3 | import com.atiurin.ultron.listeners.AbstractListener 4 | 5 | abstract class UltronRunListener: RunListener, AbstractListener() -------------------------------------------------------------------------------- /ultron-common/src/androidMain/kotlin/com/atiurin/ultron/screenshot/Screenshoter.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.screenshot 2 | 3 | import java.io.File 4 | 5 | interface Screenshoter { 6 | fun takeFullScreenShot(file: File): ScreenshotResult 7 | } -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/log/UltronFileLogger.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.log 2 | 3 | 4 | abstract class UltronFileLogger : ULogger() { 5 | abstract fun getLogFilePath(): String 6 | abstract fun clearFile() 7 | } -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/page/Page.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.page 2 | 3 | abstract class Page{ 4 | inline operator fun invoke(block: T.() -> R): R { 5 | return block.invoke(this as T) 6 | } 7 | } -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/assertion/OperationAssertion.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.common.assertion 2 | 3 | interface OperationAssertion { 4 | val name: String 5 | val block: () -> Unit 6 | } 7 | 8 | -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/page/Screen.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.page 2 | 3 | abstract class Screen { 4 | inline operator fun invoke(block: T.() -> R): R { 5 | return block.invoke(this as T) 6 | } 7 | } -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/Platform.android.kt: -------------------------------------------------------------------------------- 1 | import android.os.Build 2 | 3 | class AndroidPlatform : Platform { 4 | override val name: String = "Android ${Build.VERSION.SDK_INT}" 5 | } 6 | 7 | actual fun getPlatform(): Platform = AndroidPlatform() -------------------------------------------------------------------------------- /ultron-common/src/androidMain/kotlin/com/atiurin/ultron/hierarchy/HierarchyDumper.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.hierarchy 2 | 3 | import java.io.File 4 | 5 | interface HierarchyDumper { 6 | fun dumpFullWindowHierarchy(file: File): HierarchyDumpResult 7 | } -------------------------------------------------------------------------------- /ultron-common/src/androidMain/kotlin/com/atiurin/ultron/testlifecycle/setupteardown/Condition.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.testlifecycle.setupteardown 2 | 3 | data class Condition(val counter: Int, val key: String, val name: String = "", val actions: () -> Unit) -------------------------------------------------------------------------------- /ultron-common/src/androidMain/kotlin/com/atiurin/ultron/testlifecycle/setupteardown/ConditionExecutorWrapper.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.testlifecycle.setupteardown 2 | 3 | interface ConditionExecutorWrapper { 4 | fun execute(condition: Condition) 5 | } -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/ElementInfo.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.common 2 | 3 | interface ElementInfo { 4 | var name: String 5 | var meta: Any? 6 | 7 | fun copy(): ElementInfo 8 | } 9 | 10 | -------------------------------------------------------------------------------- /prepare-emulator.bat: -------------------------------------------------------------------------------- 1 | adb shell settings put global development_settings_enabled 1 2 | adb shell settings put global window_animation_scale 0.0 3 | adb shell settings put global transition_animation_scale 0.0 4 | adb shell settings put global animator_duration_scale 0.0 -------------------------------------------------------------------------------- /prepare-emulator.sh: -------------------------------------------------------------------------------- 1 | adb shell settings put global development_settings_enabled 1 2 | adb shell settings put global window_animation_scale 0.0 3 | adb shell settings put global transition_animation_scale 0.0 4 | adb shell settings put global animator_duration_scale 0.0 -------------------------------------------------------------------------------- /ultron-android/src/main/kotlin/com/atiurin/ultron/core/espressoweb/UltronWebLifecycle.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.espressoweb 2 | 3 | import com.atiurin.ultron.core.common.AbstractOperationLifecycle 4 | 5 | object UltronWebLifecycle : AbstractOperationLifecycle() -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/options/TextContainsOption.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.common.options 2 | 3 | data class TextContainsOption( 4 | val substring: Boolean = false, 5 | val ignoreCase: Boolean = false 6 | ) 7 | -------------------------------------------------------------------------------- /ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/core/compose/nodeinteraction/SwipePosition.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.compose.nodeinteraction 2 | 3 | import androidx.compose.ui.geometry.Offset 4 | 5 | data class SwipePosition(val start: Offset, val end: Offset) -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/assertion/DefaultOperationAssertion.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.common.assertion 2 | 3 | open class DefaultOperationAssertion(override val name: String, override val block: () -> Unit) : OperationAssertion -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/options/LongClickOption.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.common.options 2 | 3 | data class LongClickOption( 4 | val xOffset: Long = 0, 5 | val yOffset: Long = 0, 6 | val durationMs: Long? = null 7 | ) -------------------------------------------------------------------------------- /ultron-android/src/main/kotlin/com/atiurin/ultron/core/uiautomator/UltronUiAutomatorLifecycle.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.uiautomator 2 | 3 | import com.atiurin.ultron.core.common.AbstractOperationLifecycle 4 | 5 | object UltronUiAutomatorLifecycle : AbstractOperationLifecycle() -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/OperationProcessor.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.common 2 | 3 | interface OperationProcessor { 4 | fun > process(executor: OperationExecutor): OpRes 5 | } -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/options/DoubleClickOption.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.common.options 2 | 3 | data class DoubleClickOption( 4 | val xOffset: Long = 0, 5 | val yOffset: Long = 0, 6 | val delayMs: Long? = null 7 | ) 8 | -------------------------------------------------------------------------------- /ultron-compose/src/shared/kotlin/com/atiurin/ultron/core/compose/config/UltronComposeConfig.shared.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.compose.config 2 | 3 | import com.atiurin.ultron.log.ULogger 4 | 5 | actual fun getPlatformLoggers(): List { 6 | return emptyList() 7 | } -------------------------------------------------------------------------------- /sample-app/src/main/assets/webview_small.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Android Web View 5 | 6 | 7 |

WebView title

8 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/iosMain/kotlin/Platform.ios.kt: -------------------------------------------------------------------------------- 1 | import platform.UIKit.UIDevice 2 | 3 | class IOSPlatform: Platform { 4 | override val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion 5 | } 6 | 7 | actual fun getPlatform(): Platform = IOSPlatform() -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue May 20 16:16:07 MSK 2025 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable/img.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ultron-android/src/main/kotlin/com/atiurin/ultron/core/espresso/UltronEspressoOperationLifecycle.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.espresso 2 | 3 | import com.atiurin.ultron.core.common.AbstractOperationLifecycle 4 | 5 | 6 | object UltronEspressoOperationLifecycle : AbstractOperationLifecycle() -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/options/ContentDescriptionContainsOption.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.common.options 2 | 3 | data class ContentDescriptionContainsOption( 4 | val substring: Boolean = false, 5 | val ignoreCase: Boolean = false 6 | ) 7 | -------------------------------------------------------------------------------- /ultron-compose/src/shared/kotlin/com/atiurin/ultron/core/compose/list/ItemChildInteractionProvider.shared.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.compose.list 2 | 3 | actual fun getItemChildInteractionProvider(): ItemChildInteractionProvider { 4 | return object : ItemChildInteractionProvider {} 5 | } -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/DefaultOperationIterationResult.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.common 2 | 3 | data class DefaultOperationIterationResult( 4 | override val success: Boolean, 5 | override val exception: Throwable? 6 | ) : OperationIterationResult -------------------------------------------------------------------------------- /ultron-common/src/jvmMain/kotlin/com/atiurin/ultron/utils/ThreadUtil.jvm.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.utils 2 | 3 | import kotlinx.coroutines.delay 4 | import kotlinx.coroutines.runBlocking 5 | 6 | actual fun sleep(timeMs: Long) { 7 | runBlocking { 8 | delay(timeMs) 9 | } 10 | } -------------------------------------------------------------------------------- /sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/testlifecycle/UltronTestPlan.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.tests.testlifecycle 2 | 3 | //@RunWith(Suite::class) 4 | //@Suite.SuiteClasses( 5 | // UltronTestFlowTest::class, 6 | // UltronTestFlowTest2::class, 7 | //) 8 | //class UltronTestPlan -------------------------------------------------------------------------------- /ultron-common/src/nativeMain/kotlin/com/atiurin/ultron/utils/ThreadUtil.native.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.utils 2 | 3 | import kotlinx.coroutines.delay 4 | import kotlinx.coroutines.runBlocking 5 | 6 | actual fun sleep(timeMs: Long) { 7 | runBlocking { 8 | delay(timeMs) 9 | } 10 | } -------------------------------------------------------------------------------- /sample-app/src/main/res/layout/my_text_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | -------------------------------------------------------------------------------- /ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/core/compose/operation/UltronComposeOperationLifecycle.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.compose.operation 2 | 3 | import com.atiurin.ultron.core.common.AbstractOperationLifecycle 4 | 5 | object UltronComposeOperationLifecycle : AbstractOperationLifecycle() -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | /.idea/caches 6 | /.idea/libraries 7 | /.idea/modules.xml 8 | /.idea/workspace.xml 9 | /.idea/navEditor.xml 10 | /.idea/assetWizardSettings.xml 11 | .DS_Store 12 | build/ 13 | /captures 14 | .externalNativeBuild 15 | /allure-results 16 | /.kotlin -------------------------------------------------------------------------------- /composeApp/src/desktopMain/kotlin/main.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.window.Window 2 | import androidx.compose.ui.window.application 3 | 4 | fun main() = application { 5 | Window( 6 | onCloseRequest = ::exitApplication, 7 | title = "sample-kmp", 8 | ) { 9 | App() 10 | } 11 | } -------------------------------------------------------------------------------- /sample-app/src/main/res/values-v21/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /composeApp/src/wasmJsMain/kotlin/main.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.ExperimentalComposeUiApi 2 | import androidx.compose.ui.window.ComposeViewport 3 | import kotlinx.browser.document 4 | 5 | @OptIn(ExperimentalComposeUiApi::class) 6 | fun main() { 7 | ComposeViewport(document.body!!) { 8 | App() 9 | } 10 | } -------------------------------------------------------------------------------- /sample-app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/exceptions/UltronOperationException.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.exceptions 2 | 3 | 4 | class UltronOperationException : RuntimeException { 5 | constructor(message: String) : super(message) 6 | constructor(message: String, cause: Throwable) : super(message, cause) 7 | } -------------------------------------------------------------------------------- /sample-app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/extensions/AssertionsExt.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.extensions 2 | 3 | import androidx.compose.ui.test.SemanticsNodeInteraction 4 | import androidx.compose.ui.test.assert 5 | 6 | fun SemanticsNodeInteraction.assertIsIndeterminate(): SemanticsNodeInteraction = assert(isIndeterminate()) -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "app-icon-1024.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /sample-app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/options/PerformCustomBlockOption.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.common.options 2 | 3 | import com.atiurin.ultron.core.common.UltronOperationType 4 | 5 | data class PerformCustomBlockOption( 6 | val operationType: UltronOperationType, 7 | val description: String = "" 8 | ) -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/test/context/DefaultUltronTestContextProvider.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.test.context 2 | 3 | open class DefaultUltronTestContextProvider : UltronTestContextProvider { 4 | override fun provide(): UltronTestContext { 5 | return DefaultUltronTestContext() 6 | } 7 | } -------------------------------------------------------------------------------- /ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/core/compose/nodeinteraction/UltronComposeOffsets.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.compose.nodeinteraction 2 | 3 | enum class UltronComposeOffsets { 4 | CENTER, CENTER_LEFT, CENTER_RIGHT, 5 | TOP_CENTER, TOP_LEFT, TOP_RIGHT, 6 | BOTTOM_CENTER, BOTTOM_LEFT, BOTTOM_RIGHT 7 | } 8 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/ComposeSecondPage.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.pages 2 | 3 | import androidx.compose.ui.test.hasTestTag 4 | import com.atiurin.ultron.page.Page 5 | 6 | object ComposeSecondPage : Page() { 7 | val name = hasTestTag("name") 8 | val status = hasTestTag("status") 9 | } -------------------------------------------------------------------------------- /ultron-common/src/androidMain/kotlin/com/atiurin/ultron/hierarchy/HierarchyDumpResult.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.hierarchy 2 | 3 | import com.atiurin.ultron.file.MimeType 4 | import java.io.File 5 | 6 | data class HierarchyDumpResult( 7 | val isSuccess: Boolean, 8 | val file: File, 9 | val mimeType: MimeType = MimeType.XML 10 | ) 11 | -------------------------------------------------------------------------------- /ultron-common/src/androidMain/kotlin/com/atiurin/ultron/screenshot/ScreenshotResult.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.screenshot 2 | 3 | import com.atiurin.ultron.file.MimeType 4 | import java.io.File 5 | 6 | data class ScreenshotResult( 7 | val isSuccess: Boolean, 8 | val file: File, 9 | val mimeType: MimeType = MimeType.JPEG 10 | ) 11 | -------------------------------------------------------------------------------- /ultron-common/src/androidMain/kotlin/com/atiurin/ultron/testlifecycle/setupteardown/TearDown.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.testlifecycle.setupteardown 2 | 3 | @Retention(AnnotationRetention.RUNTIME) 4 | @Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER) 5 | annotation class TearDown(vararg val value: String) -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/listeners/AbstractListener.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.listeners 2 | 3 | abstract class AbstractListener { 4 | var id: String 5 | constructor(id: String){ 6 | this.id = id 7 | } 8 | constructor(){ 9 | this.id = this::class.simpleName.orEmpty() 10 | } 11 | } -------------------------------------------------------------------------------- /ultron-compose/src/androidMain/kotlin/com/atiurin/ultron/core/compose/config/UltronComposeConfig.android.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.compose.config 2 | 3 | import com.atiurin.ultron.log.ULogger 4 | import com.atiurin.ultron.log.UltronLogcatLogger 5 | 6 | actual fun getPlatformLoggers(): List { 7 | return listOf(UltronLogcatLogger()) 8 | } -------------------------------------------------------------------------------- /sample-app/src/main/java/com/atiurin/sampleapp/idlingresources/IdlingHelper.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.idlingresources 2 | const val RELEASE_BUILD = false; 3 | 4 | object IdlingHelper{ 5 | @JvmStatic 6 | fun ifAllowed(resourceAction:() -> Unit){ 7 | if (!RELEASE_BUILD){ 8 | resourceAction() 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /sample-app/src/main/java/com/atiurin/sampleapp/data/loaders/MessageLoader.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.data.loaders 2 | 3 | import com.atiurin.sampleapp.data.entities.Message 4 | import com.atiurin.sampleapp.data.repositories.MESSAGES 5 | 6 | open class MessageLoader{ 7 | open fun load() : ArrayList{ 8 | return MESSAGES 9 | } 10 | } -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/DefaultElementInfo.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.common 2 | 3 | data class DefaultElementInfo(override var name: String = "", override var meta: Any? = null) : ElementInfo { 4 | override fun copy(): DefaultElementInfo { 5 | return DefaultElementInfo(name, meta) 6 | } 7 | } 8 | 9 | -------------------------------------------------------------------------------- /sample-app/src/main/java/com/atiurin/sampleapp/utils/TimeUtils.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.utils 2 | 3 | import java.text.SimpleDateFormat 4 | import java.util.Date 5 | import java.util.Locale 6 | 7 | fun convertMillisToDate(millis: Long): String { 8 | val formatter = SimpleDateFormat("MM/dd/yyyy", Locale.getDefault()) 9 | return formatter.format(Date(millis)) 10 | } -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable/side_nav_bar.xml: -------------------------------------------------------------------------------- 1 | 3 | 9 | -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/assertion/NoListenersOperationAssertion.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.common.assertion 2 | 3 | import com.atiurin.ultron.listeners.setListenersState 4 | 5 | class NoListenersOperationAssertion(override val name: String, override val block: () -> Unit) : 6 | DefaultOperationAssertion(name, block.setListenersState(false)) -------------------------------------------------------------------------------- /ultron-common/src/androidMain/kotlin/com/atiurin/ultron/testlifecycle/setupteardown/SetUp.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.testlifecycle.setupteardown 2 | 3 | import kotlin.annotation.Retention 4 | 5 | @Retention(AnnotationRetention.RUNTIME) 6 | @Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER) 7 | annotation class SetUp(vararg val value: String) -------------------------------------------------------------------------------- /sample-app/src/androidTest/java/com/atiurin/sampleapp/framework/utils/TestDataUtils.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.framework.utils 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | 5 | object TestDataUtils { 6 | fun getResourceString(resourceId: Int): String { 7 | return InstrumentationRegistry.getInstrumentation().targetContext.resources.getString(resourceId) 8 | } 9 | } -------------------------------------------------------------------------------- /sample-app/src/main/res/menu/main.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | 9 | -------------------------------------------------------------------------------- /ultron-allure/src/main/java/com/atiurin/ultron/allure/config/AllureAttachStrategy.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.allure.config 2 | 3 | enum class AllureAttachStrategy { 4 | TEST_FAILURE, 5 | OPERATION_FAILURE, // attach artifact for failed operation 6 | OPERATION_SUCCESS, // attach artifact for each succeeded operation 7 | OPERATION_FINISH, // attach artifact for each operation 8 | NONE 9 | } -------------------------------------------------------------------------------- /sample-app/src/main/java/com/atiurin/sampleapp/data/viewmodel/DataViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.data.viewmodel 2 | 3 | import androidx.lifecycle.MutableLiveData 4 | import androidx.lifecycle.ViewModel 5 | import com.atiurin.sampleapp.data.entities.Contact 6 | 7 | class DataViewModel : ViewModel(){ 8 | val data: MutableLiveData by lazy { 9 | MutableLiveData() 10 | } 11 | } -------------------------------------------------------------------------------- /ultron-android/src/main/kotlin/com/atiurin/ultron/utils/ViewGroupUtils.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.utils 2 | 3 | import android.view.View 4 | import android.view.ViewGroup 5 | 6 | /** Performs the given action on each view in this view group. */ 7 | inline fun ViewGroup.forEach(action: (view: View) -> Unit) { 8 | for (index in 0 until childCount) { 9 | action(getChildAt(index)) 10 | } 11 | } -------------------------------------------------------------------------------- /sample-app/src/main/java/com/atiurin/sampleapp/idlingresources/resources/ChatIdlingResource.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.idlingresources.resources 2 | 3 | import com.atiurin.sampleapp.idlingresources.AbstractIdlingResource 4 | import com.atiurin.sampleapp.idlingresources.Holder 5 | 6 | class ChatIdlingResource : AbstractIdlingResource(){ 7 | companion object : Holder(::ChatIdlingResource) 8 | } -------------------------------------------------------------------------------- /ultron-common/src/androidMain/kotlin/com/atiurin/ultron/extensions/BundleExt.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.extensions 2 | 3 | import android.os.Bundle 4 | 5 | fun Bundle.putArguments(key: String, vararg values: CharSequence) { 6 | val arguments: String = listOfNotNull( 7 | getCharSequence(key), 8 | *values 9 | ).joinToString(separator = ",") 10 | putCharSequence(key, arguments) 11 | } -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable/ic_menu_send.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /sample-app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 8dp 6 | 176dp 7 | 16dp 8 | -------------------------------------------------------------------------------- /ultron-android/src/main/kotlin/com/atiurin/ultron/custom/espresso/action/CustomEspressoActionType.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.custom.espresso.action 2 | 3 | import com.atiurin.ultron.core.common.UltronOperationType 4 | 5 | enum class CustomEspressoActionType : UltronOperationType { 6 | GET_TEXT, GET_CONTENT_DESCRIPTION, GET_DRAWABLE, GET_VIEW, GET_VIEW_FORCIBLY, 7 | PERFORM_ON_VIEW, PERFORM_ON_VIEW_FORCIBLY 8 | } 9 | -------------------------------------------------------------------------------- /composeApp/src/wasmJsMain/resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | sample-kmp 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable-anydpi/ic_send.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /sample-app/src/main/java/com/atiurin/sampleapp/MyApplication.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | 6 | object MyApplication : Application() { 7 | var context: Context? = null 8 | override fun onCreate() { 9 | super.onCreate() 10 | context = applicationContext 11 | } 12 | 13 | var CONTACTS_LOADING_TIMEOUT_MS = 2000L 14 | } -------------------------------------------------------------------------------- /sample-app/src/main/java/com/atiurin/sampleapp/data/viewmodel/ContactsViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.data.viewmodel 2 | 3 | import androidx.lifecycle.MutableLiveData 4 | import androidx.lifecycle.ViewModel 5 | import com.atiurin.sampleapp.data.entities.Contact 6 | 7 | class ContactsViewModel : ViewModel(){ 8 | val contacts: MutableLiveData> by lazy { 9 | MutableLiveData() 10 | } 11 | } -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/assertion/EmptyOperationAssertion.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.common.assertion 2 | 3 | class EmptyOperationAssertion : OperationAssertion { 4 | override val name: String 5 | get() = "" 6 | override val block: () -> Unit 7 | get() = {} 8 | } 9 | 10 | fun OperationAssertion.isEmptyAssertion(): Boolean = this is EmptyOperationAssertion -------------------------------------------------------------------------------- /ultron-compose/src/shared/kotlin/com/atiurin/ultron/extensions/SemanticsNodeInteractionCommonExt.shared.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.extensions 2 | 3 | import androidx.compose.ui.test.SemanticsNodeInteraction 4 | 5 | actual fun SemanticsNodeInteraction.getSelectorDescription(): String = 6 | "[UI element description isn't implemented non Android platforms due to https://issuetracker.google.com/issues/342778294. Vote for this issue!]" -------------------------------------------------------------------------------- /sample-app/src/main/java/com/atiurin/sampleapp/idlingresources/resources/ContactsIdlingResource.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.idlingresources.resources 2 | 3 | import com.atiurin.sampleapp.idlingresources.AbstractIdlingResource 4 | import com.atiurin.sampleapp.idlingresources.Holder 5 | 6 | class ContactsIdlingResource : AbstractIdlingResource(){ 7 | companion object : Holder(::ContactsIdlingResource) 8 | } 9 | -------------------------------------------------------------------------------- /ultron-android/src/main/kotlin/com/atiurin/ultron/custom/espresso/assertion/CustomEspressoAssertionType.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.custom.espresso.assertion 2 | 3 | import com.atiurin.ultron.core.common.UltronOperationType 4 | 5 | enum class CustomEspressoAssertionType : UltronOperationType { 6 | HAS_DRAWABLE, HAS_ANY_DRAWABLE, 7 | HAS_CURRENT_TEXT_COLOR, HAS_CURRENT_HINT_TEXT_COLOR, HAS_HIGHLIGHT_COLOR, HAS_SHADOW_COLOR 8 | } 9 | -------------------------------------------------------------------------------- /sample-app/src/androidTest/java/com/atiurin/sampleapp/framework/utils/EspressoUtil.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.framework.utils 2 | 3 | import androidx.test.espresso.Espresso 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | 6 | object EspressoUtil { 7 | // fun openOptionsMenu() = apply { 8 | // Espresso.openActionBarOverflowOrOptionsMenu(InstrumentationRegistry.getInstrumentation().context) 9 | // } 10 | } -------------------------------------------------------------------------------- /sample-app/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ultron-common/src/androidMain/kotlin/com/atiurin/ultron/testlifecycle/setupteardown/ConditionsExecutor.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.testlifecycle.setupteardown 2 | 3 | import kotlin.reflect.KClass 4 | 5 | interface ConditionsExecutor { 6 | val conditionExecutor: ConditionExecutorWrapper 7 | fun before(name: String, ruleClass: KClass<*>) 8 | fun execute(conditions: List, keys: List, description: String = "") 9 | } -------------------------------------------------------------------------------- /ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/core/compose/operation/UltronComposeOperationParams.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.compose.operation 2 | 3 | import com.atiurin.ultron.core.common.UltronOperationType 4 | 5 | data class UltronComposeOperationParams( 6 | val operationName: String, 7 | val operationDescription: String, 8 | val operationType: UltronOperationType = ComposeOperationType.CUSTOM 9 | ) 10 | -------------------------------------------------------------------------------- /ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/extensions/FiltersExt.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.extensions 2 | 3 | import androidx.compose.ui.semantics.SemanticsProperties 4 | import androidx.compose.ui.state.ToggleableState 5 | import androidx.compose.ui.test.SemanticsMatcher 6 | 7 | fun isIndeterminate(): SemanticsMatcher = SemanticsMatcher.expectValue( 8 | SemanticsProperties.ToggleableState, ToggleableState.Indeterminate 9 | ) -------------------------------------------------------------------------------- /docs/docs/compose/index.md: -------------------------------------------------------------------------------- 1 | # Compose 2 | 3 | There are two types of UI tests you can write with Compose. 4 | 5 | 1. Kotlin Multiplatform UI test ([Kotlin documentation](https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-test.html)) 6 | 2. Platform Specific JUnit-based tests ([Android documentation](https://developer.android.com/develop/ui/compose/testing)) 7 | 8 | Ultron supports both types of UI tests and make it`s development much easier. -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable/circle.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/UiElementsTest.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.tests 2 | 3 | import com.atiurin.sampleapp.activity.UiElementsActivity 4 | import com.atiurin.ultron.testlifecycle.activity.UltronActivityRule 5 | 6 | abstract class UiElementsTest : BaseTest() { 7 | val activityRule = UltronActivityRule(UiElementsActivity::class.java) 8 | 9 | init { 10 | ruleSequence.add(activityRule) 11 | } 12 | } -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable/background_splash.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /ultron-allure/src/main/java/com/atiurin/ultron/allure/runner/UltronAllureRunInformer.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.allure.runner 2 | 3 | import com.atiurin.ultron.runner.* 4 | 5 | class UltronAllureRunInformer : UltronRunInformer() { 6 | init { 7 | addListener(UltronLogRunListener()) 8 | addListener(LogcatAttachRunListener()) 9 | addListener(UltronLogAttachRunListener()) 10 | addListener(UltronLogCleanerRunListener()) 11 | } 12 | } -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/OperationResult.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.common 2 | 3 | /** 4 | * Interface contains references to [Operation] 5 | */ 6 | interface OperationResult { 7 | val operation: T 8 | val success: Boolean 9 | val exceptions: List 10 | var description: String 11 | var operationIterationResult: OperationIterationResult? 12 | val executionTimeMs: Long 13 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/repositories/ContactRepository.kt: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | object ContactRepository { 4 | fun getContact(id: Int) : Contact { 5 | return contacts.find { it.id == id }!! 6 | } 7 | 8 | fun getFirst(): Contact { 9 | return contacts.first() 10 | } 11 | fun getLast() : Contact { 12 | return contacts.last() 13 | } 14 | fun all() = contacts.toList() 15 | 16 | private val contacts = CONTACTS 17 | } -------------------------------------------------------------------------------- /ultron-allure/src/test/java/com/atiurin/ultron/allure/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.allure 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/Operation.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.common 2 | 3 | import com.atiurin.ultron.core.common.assertion.OperationAssertion 4 | 5 | interface Operation { 6 | val name: String 7 | val description: String 8 | val type: UltronOperationType 9 | val timeoutMs: Long 10 | val assertion: OperationAssertion 11 | val elementInfo: ElementInfo 12 | fun execute(): OperationIterationResult 13 | } 14 | -------------------------------------------------------------------------------- /ultron-common/src/androidMain/kotlin/com/atiurin/ultron/testlifecycle/setupteardown/DefaultConditionExecutorWrapper.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.testlifecycle.setupteardown 2 | 3 | import com.atiurin.ultron.log.UltronLog 4 | 5 | class DefaultConditionExecutorWrapper : ConditionExecutorWrapper { 6 | override fun execute(condition: Condition) { 7 | UltronLog.info("Execute condition '${condition.name}' with key '${condition.key}'") 8 | condition.actions() 9 | } 10 | } -------------------------------------------------------------------------------- /ultron-compose/src/test/java/com/atiurin/ultron/compose/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.compose 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /docs/src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | /** 2 | * CSS files with the .module.css suffix will be treated as CSS modules 3 | * and scoped locally. 4 | */ 5 | 6 | .heroBanner { 7 | padding: 4rem 0; 8 | text-align: center; 9 | position: relative; 10 | overflow: hidden; 11 | } 12 | 13 | @media screen and (max-width: 996px) { 14 | .heroBanner { 15 | padding: 2rem; 16 | } 17 | } 18 | 19 | .buttons { 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | } 24 | -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/resultanalyzer/OperationResultAnalyzer.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.common.resultanalyzer 2 | 3 | import com.atiurin.ultron.core.common.Operation 4 | import com.atiurin.ultron.core.common.OperationResult 5 | 6 | interface OperationResultAnalyzer { 7 | /** 8 | * @return success status of operation execution 9 | */ 10 | fun > analyze(operationResult: OpRes): Boolean 11 | } -------------------------------------------------------------------------------- /ultron-android/src/test/java/com/atiurin/ultron/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /sample-app/src/main/res/layout/activity_webview.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/resultanalyzer/CheckOperationResultAnalyzer.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.common.resultanalyzer 2 | 3 | import com.atiurin.ultron.core.common.Operation 4 | import com.atiurin.ultron.core.common.OperationResult 5 | 6 | class CheckOperationResultAnalyzer : OperationResultAnalyzer { 7 | override fun > analyze(operationResult: OpRes): Boolean { 8 | return operationResult.success 9 | } 10 | } -------------------------------------------------------------------------------- /sample-app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #008577 4 | #00574B 5 | #2A4B9F 6 | #58E1CA 7 | #8158E1 8 | #A1E158 9 | #E158BF 10 | #221F21 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /ultron-android/src/main/kotlin/com/atiurin/ultron/core/espresso/assertion/UltronEspressoAssertionParams.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.espresso.assertion 2 | 3 | import com.atiurin.ultron.core.common.UltronOperationType 4 | 5 | data class UltronEspressoAssertionParams( 6 | val operationName: String, 7 | val operationDescription: String, 8 | val operationType: UltronOperationType = EspressoAssertionType.ASSERT_MATCHES, 9 | val descriptionToAppend: String = "Default assert matcher description" 10 | ) 11 | -------------------------------------------------------------------------------- /ultron-android/src/main/kotlin/com/atiurin/ultron/core/espressoweb/operation/WebInteractionOperationIterationResult.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.espressoweb.operation 2 | 3 | import androidx.test.espresso.web.sugar.Web 4 | import com.atiurin.ultron.core.common.OperationIterationResult 5 | 6 | internal data class WebInteractionOperationIterationResult( 7 | override val success: Boolean, 8 | override val exception: Throwable?, 9 | val webInteraction: Web.WebInteraction? 10 | ) : OperationIterationResult -------------------------------------------------------------------------------- /ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/core/compose/option/ComposeSwipeOption.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.compose.option 2 | 3 | import com.atiurin.ultron.core.compose.nodeinteraction.UltronComposeSemanticsNodeInteraction 4 | 5 | data class ComposeSwipeOption( 6 | val startXOffset: Float = 0f, 7 | val endXOffset: Float = 0f, 8 | val startYOffset: Float = 0f, 9 | val endYOffset: Float = 0f, 10 | val durationMs: Long = UltronComposeSemanticsNodeInteraction.DEFAULT_SWIPE_DURATION 11 | ) -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable/ic_menu_slideshow.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/utils/TimeUtil.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.utils 2 | 3 | import kotlin.time.ExperimentalTime 4 | import kotlin.time.Clock 5 | 6 | @OptIn(ExperimentalTime::class) 7 | fun now() = Clock.System.now() 8 | @OptIn(ExperimentalTime::class) 9 | fun nowMs() = now().toEpochMilliseconds() 10 | 11 | @OptIn(ExperimentalTime::class) 12 | fun measureTimeMillis(function: () -> Any): Long { 13 | val start = now() 14 | function() 15 | return now().minus(start).inWholeMilliseconds 16 | } -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable-anydpi/ic_messages.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable/ic_menu_gallery.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/core/compose/config/UltronComposeConfigParams.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.compose.config 2 | 3 | data class UltronComposeConfigParams( 4 | var operationTimeoutMs: Long = UltronComposeConfig.DEFAULT_OPERATION_TIMEOUT, 5 | var operationPollingTimeoutMs: Long = 0, 6 | var lazyColumnOperationTimeoutMs: Long = UltronComposeConfig.DEFAULT_LAZY_COLUMN_OPERATIONS_TIMEOUT, 7 | var lazyColumnItemSearchLimit: Int = -1, 8 | var useUnmergedTree: Boolean = false 9 | ) 10 | -------------------------------------------------------------------------------- /ultron-compose/src/androidMain/kotlin/com/atiurin/ultron/extensions/ReflectionComposeExt.android.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.extensions 2 | 3 | import androidx.compose.ui.test.SemanticsNodeInteraction 4 | import androidx.compose.ui.test.SemanticsNodeInteractionCollection 5 | 6 | internal fun SemanticsNodeInteraction.getUseMergedTree(): Boolean? { 7 | return this.getProperty("useUnmergedTree") 8 | } 9 | internal fun SemanticsNodeInteractionCollection.getUseMergedTree(): Boolean? { 10 | return this.getProperty("useUnmergedTree") 11 | } -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/exceptions/UltronWrapperException.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.exceptions 2 | 3 | class UltronWrapperException : AssertionError { 4 | constructor(message: String) : super(message) 5 | constructor(message: String, cause: Throwable) 6 | : super( 7 | "$message${ 8 | if (cause is UltronWrapperException || cause is UltronOperationException) "" 9 | else "\nOriginal error - ${cause::class.simpleName}: ${cause.message}" 10 | }" 11 | ) 12 | } -------------------------------------------------------------------------------- /ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/core/compose/list/ComposeItemExecutor.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.compose.list 2 | 3 | import androidx.compose.ui.test.SemanticsMatcher 4 | import com.atiurin.ultron.core.compose.nodeinteraction.UltronComposeSemanticsNodeInteraction 5 | 6 | interface ComposeItemExecutor { 7 | fun scrollToItem(offset: Int = 0) 8 | fun getItemInteraction(): UltronComposeSemanticsNodeInteraction 9 | fun getItemChildInteraction(childMatcher: SemanticsMatcher): UltronComposeSemanticsNodeInteraction 10 | } -------------------------------------------------------------------------------- /ultron-android/src/main/kotlin/com/atiurin/ultron/extensions/ViewExt.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.extensions 2 | 3 | import android.view.View 4 | import androidx.test.espresso.util.TreeIterables 5 | import org.hamcrest.Matcher 6 | 7 | internal fun View.findChildView(matcher: Matcher): View? { 8 | var childView: View? = null 9 | for (child in TreeIterables.breadthFirstViewTraversal(this)) { 10 | if (matcher.matches(child)) { 11 | childView = child 12 | break 13 | } 14 | } 15 | return childView 16 | } -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable-anydpi/ic_exit.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable/ic_menu_manage.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /ultron-android/src/main/kotlin/com/atiurin/ultron/custom/espresso/base/UltronRootViewFinder.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.custom.espresso.base 2 | 3 | import androidx.test.espresso.Root 4 | import com.atiurin.ultron.core.config.UltronConfig 5 | import com.atiurin.ultron.utils.isVisible 6 | import com.atiurin.ultron.utils.runOnUiThread 7 | 8 | fun getRootViewsList(): List = runOnUiThread { 9 | UltronConfig.Espresso.activeRootLister.listActiveRoots() 10 | } 11 | 12 | fun getVisibleRootViews(): List = getRootViewsList().filter { it.decorView.isVisible } -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/test/context/UltronTestContext.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.test.context 2 | 3 | import com.atiurin.ultron.core.common.resultanalyzer.OperationResultAnalyzer 4 | import com.atiurin.ultron.core.common.resultanalyzer.SoftAssertionOperationResultAnalyzer 5 | 6 | interface UltronTestContext { 7 | var softAssertion: Boolean 8 | val softAnalyzer: SoftAssertionOperationResultAnalyzer 9 | 10 | fun wrapAnalyzerIfSoftAssertion(analyzer: OperationResultAnalyzer): OperationResultAnalyzer 11 | } 12 | -------------------------------------------------------------------------------- /sample-app/src/androidTest/java/com/atiurin/sampleapp/framework/ScreenshotLifecycleListener.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.framework 2 | 3 | import com.atiurin.ultron.core.common.Operation 4 | import com.atiurin.ultron.core.common.OperationResult 5 | import com.atiurin.ultron.listeners.UltronLifecycleListener 6 | 7 | class ScreenshotLifecycleListener : UltronLifecycleListener(){ 8 | override fun before(operation: Operation) { 9 | } 10 | 11 | override fun after(operationResult: OperationResult) { 12 | operationResult.operation 13 | } 14 | } -------------------------------------------------------------------------------- /ultron-android/src/main/kotlin/com/atiurin/ultron/extensions/BitmapExt.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.extensions 2 | 3 | import android.graphics.Bitmap 4 | import java.nio.ByteBuffer 5 | import java.util.Arrays 6 | 7 | fun Bitmap.isSameAs(expected: Bitmap): Boolean { 8 | val buffer1 = ByteBuffer.allocate(this.height * this.rowBytes); 9 | this.copyPixelsToBuffer(buffer1) 10 | 11 | val buffer2 = ByteBuffer.allocate(expected.height * expected.rowBytes); 12 | expected.copyPixelsToBuffer(buffer2) 13 | return Arrays.equals(buffer1.array(), buffer2.array()) 14 | } -------------------------------------------------------------------------------- /ultron-common/src/androidMain/kotlin/com/atiurin/ultron/extensions/FileExt.android.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.extensions 2 | 3 | import com.atiurin.ultron.log.UltronLog 4 | import java.io.File 5 | import java.io.PrintWriter 6 | 7 | fun File.clearContent() { 8 | PrintWriter(this).apply { 9 | print("") 10 | close() 11 | } 12 | } 13 | 14 | fun File.createDirectoryIfNotExists() { 15 | if (!exists()) { 16 | val result = mkdirs() 17 | if (!result) UltronLog.error("Unable to create directory '${this.absolutePath}'") 18 | } 19 | } -------------------------------------------------------------------------------- /sample-app/src/main/java/com/atiurin/sampleapp/data/repositories/ContactRepositoty.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.data.repositories 2 | 3 | import com.atiurin.sampleapp.data.entities.Contact 4 | object ContactRepositoty { 5 | 6 | fun getContact(id: Int) : Contact{ 7 | return contacts.find { it.id == id }!! 8 | } 9 | 10 | fun getFirst(): Contact { 11 | return contacts.first() 12 | } 13 | fun getLast() : Contact{ 14 | return contacts.last() 15 | } 16 | fun all() = contacts.toList() 17 | 18 | private val contacts = CONTACTS 19 | } -------------------------------------------------------------------------------- /ultron-android/src/main/kotlin/com/atiurin/ultron/custom/espresso/action/AnonymousViewAction.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.custom.espresso.action 2 | 3 | import android.view.View 4 | import androidx.test.espresso.ViewAction 5 | import com.atiurin.ultron.core.espresso.action.UltronEspressoActionParams 6 | import org.hamcrest.Matcher 7 | 8 | abstract class AnonymousViewAction(val params: UltronEspressoActionParams) : ViewAction { 9 | override fun getConstraints(): Matcher = params.viewActionConstraints 10 | override fun getDescription(): String = params.viewActionDescription 11 | } -------------------------------------------------------------------------------- /ultron-android/src/main/kotlin/com/atiurin/ultron/core/config/UltronConfigParams.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.config 2 | 3 | data class UltronConfigParams( 4 | var accelerateUiAutomator: Boolean = true, 5 | var operationTimeoutMs: Long = UltronCommonConfig.operationTimeoutMs, 6 | ){ 7 | @Deprecated("Use global setting UltronCommonConfig.logToFile", ReplaceWith("UltronCommonConfig.logToFile")) 8 | var logToFile: Boolean = UltronCommonConfig.logToFile 9 | set(value) { 10 | field = value 11 | UltronCommonConfig.logToFile = value 12 | } 13 | } -------------------------------------------------------------------------------- /iosApp/iosApp/ContentView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SwiftUI 3 | import ComposeApp 4 | 5 | struct ComposeView: UIViewControllerRepresentable { 6 | func makeUIViewController(context: Context) -> UIViewController { 7 | MainViewControllerKt.MainViewController() 8 | } 9 | 10 | func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} 11 | } 12 | 13 | struct ContentView: View { 14 | var body: some View { 15 | ComposeView() 16 | .ignoresSafeArea(.keyboard) // Compose has own keyboard handler 17 | } 18 | } 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /sample-app/src/main/java/com/atiurin/sampleapp/async/AsyncDataLoading.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.async 2 | 3 | import com.atiurin.sampleapp.MyApplication.CONTACTS_LOADING_TIMEOUT_MS 4 | import kotlinx.coroutines.delay 5 | 6 | class AsyncDataLoading(val delayMs: Long = CONTACTS_LOADING_TIMEOUT_MS) : UseCase() { 7 | 8 | override suspend fun run(params: None): Either { 9 | return try { 10 | delay(delayMs) 11 | Success( "Loaded") 12 | } catch (e: Exception) { 13 | Failure(e) 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /.github/workflows/ci-pipeline.yml: -------------------------------------------------------------------------------- 1 | name: MultiplatformCI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | compileKotlin: 11 | runs-on: macos-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-java@v4 15 | with: 16 | distribution: 'adopt' 17 | java-version: '17' 18 | 19 | - name: Compile framework 20 | run: ./gradlew compileDebugKotlin compileDebugKotlinAndroid compileKotlinDesktop compileKotlinIosArm64 compileKotlinIosSimulatorArm64 compileKotlinJs compileKotlinWasmJs 21 | -------------------------------------------------------------------------------- /ultron-allure/src/main/java/com/atiurin/ultron/allure/step/UltronStep.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.allure.step 2 | 3 | import com.atiurin.ultron.log.UltronLogUtil.logTextBlock 4 | import com.atiurin.ultron.log.UltronLogUtil.stepDelimiter 5 | import io.qameta.allure.kotlin.Allure 6 | 7 | inline fun step (description: String, crossinline block: () -> T): T { 8 | logTextBlock("Begin STEP '$description'", delimiter = stepDelimiter) 9 | val result = Allure.step(description) { 10 | block() 11 | } 12 | logTextBlock("End STEP '$description'", delimiter = stepDelimiter) 13 | return result 14 | } -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable-anydpi/ic_account.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /ultron-allure/src/main/java/com/atiurin/ultron/allure/runner/UltronLogCleanerRunListener.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.allure.runner 2 | 3 | import com.atiurin.ultron.core.config.UltronCommonConfig 4 | import com.atiurin.ultron.log.UltronLog 5 | import com.atiurin.ultron.runner.UltronRunListener 6 | import org.junit.runner.Description 7 | 8 | class UltronLogCleanerRunListener : UltronRunListener() { 9 | override fun testFinished(description: Description) { 10 | if (UltronCommonConfig.logToFile){ 11 | UltronLog.info("Clear log file") 12 | UltronLog.fileLogger.clearFile() 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/listeners/UltronLifecycleListener.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.listeners 2 | 3 | import com.atiurin.ultron.core.common.Operation 4 | import com.atiurin.ultron.core.common.OperationResult 5 | 6 | abstract class UltronLifecycleListener : LifecycleListener, AbstractListener(){ 7 | override fun after(operationResult: OperationResult) = Unit 8 | override fun afterFailure(operationResult: OperationResult) = Unit 9 | override fun afterSuccess(operationResult: OperationResult) = Unit 10 | override fun before(operation: Operation) = Unit 11 | } 12 | -------------------------------------------------------------------------------- /ultron-compose/src/androidMain/kotlin/com/atiurin/ultron/extensions/SemanticsMatcherExt.android.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.extensions 2 | 3 | import android.os.Build 4 | import androidx.annotation.RequiresApi 5 | import androidx.compose.ui.graphics.ImageBitmap 6 | import androidx.compose.ui.test.SemanticsMatcher 7 | import com.atiurin.ultron.core.compose.nodeinteraction.UltronComposeSemanticsNodeInteraction 8 | import com.atiurin.ultron.core.compose.nodeinteraction.captureToImage 9 | 10 | @RequiresApi(Build.VERSION_CODES.O) 11 | fun SemanticsMatcher.captureToImage(): ImageBitmap = UltronComposeSemanticsNodeInteraction(this).captureToImage() -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | google { 5 | mavenContent { 6 | includeGroupAndSubgroups("androidx") 7 | includeGroupAndSubgroups("com.android") 8 | includeGroupAndSubgroups("com.google") 9 | } 10 | } 11 | gradlePluginPortal() 12 | mavenCentral() 13 | } 14 | } 15 | 16 | 17 | //rootProject.name = "Ultron" 18 | include(":sample-app") 19 | include(":ultron-android") 20 | include(":ultron-compose") 21 | include(":ultron-allure") 22 | include(":ultron-common") 23 | include(":composeApp") 24 | -------------------------------------------------------------------------------- /ultron-allure/src/main/java/com/atiurin/ultron/allure/condition/AllureConditionExecutorWrapper.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.allure.condition 2 | 3 | import com.atiurin.ultron.allure.step.step 4 | import com.atiurin.ultron.testlifecycle.setupteardown.Condition 5 | import com.atiurin.ultron.testlifecycle.setupteardown.ConditionExecutorWrapper 6 | 7 | class AllureConditionExecutorWrapper : ConditionExecutorWrapper { 8 | override fun execute(condition: Condition) { 9 | val stepName = condition.name.ifEmpty { 10 | "${condition.counter} - ${condition.key}" 11 | } 12 | step(stepName) { condition.actions() } 13 | } 14 | } -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable-anydpi/ic_attach_file.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /ultron-allure/src/main/java/com/atiurin/ultron/allure/condition/AllureConditionsExecutor.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.allure.condition 2 | 3 | import com.atiurin.ultron.allure.step.step 4 | import com.atiurin.ultron.testlifecycle.setupteardown.Condition 5 | import com.atiurin.ultron.testlifecycle.setupteardown.DefaultConditionsExecutor 6 | 7 | class AllureConditionsExecutor : DefaultConditionsExecutor() { 8 | override fun execute(conditions: List, keys: List, description: String) { 9 | val stepName = description.ifEmpty { "Conditions" } 10 | step(stepName) { super.execute(conditions, keys, description) } 11 | } 12 | } -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/log/UltronLogUtil.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.log 2 | 3 | object UltronLogUtil { 4 | const val testStageDelimiter = "========================================================================================================" 5 | const val stepDelimiter = "********************************************************************" 6 | 7 | fun logTextBlock(text: String, logLevel: LogLevel = LogLevel.I, delimiter: String = testStageDelimiter) { 8 | UltronLog.log(logLevel, delimiter) 9 | UltronLog.log(logLevel, text) 10 | UltronLog.log(logLevel, delimiter) 11 | } 12 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M" 2 | org.gradle.caching=true 3 | org.gradle.configuration-cache=true 4 | 5 | kotlin.code.style=official 6 | android.useAndroidX=true 7 | android.nonTransitiveRClass=true 8 | org.jetbrains.compose.experimental.wasm.enabled=true 9 | org.jetbrains.compose.experimental.jscanvas.enabled=true 10 | org.jetbrains.compose.experimental.macos.enabled=true 11 | kotlin.mpp.androidSourceSetLayoutVersion=2 12 | kotlin.mpp.enableCInteropCommonization=true 13 | kotlin.native.cacheKind=none 14 | 15 | 16 | GROUP=com.atiurin 17 | POM_ARTIFACT_ID=ultron 18 | VERSION_NAME=2.6.2 19 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Build and deploy docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | github-pages: 10 | runs-on: ubuntu-22.04 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: actions/setup-node@v4 14 | with: 15 | node-version: 22 16 | - run: npm install 17 | working-directory: ./docs 18 | - run: npm run build 19 | working-directory: ./docs 20 | 21 | - name: Deploy to GitHub Pages 22 | uses: peaceiris/actions-gh-pages@v3 23 | with: 24 | github_token: ${{ secrets.GITHUB_TOKEN }} 25 | publish_dir: ./docs/build -------------------------------------------------------------------------------- /docs/src/components/HomepageFeatures/styles.module.css: -------------------------------------------------------------------------------- 1 | .features { 2 | display: flex; 3 | align-items: center; 4 | padding: 2rem 0; 5 | width: 100%; 6 | } 7 | 8 | .featureSvg { 9 | height: 150px; 10 | width: 150px; 11 | padding: 10px; 12 | overflow: 'hidden'; 13 | display: 'flex'; 14 | justify-content: center; 15 | } 16 | 17 | .imageContainer { 18 | width: 150px; 19 | height: 150px; 20 | /* display: flex; */ 21 | padding: 10px; 22 | margin-left: auto; 23 | margin-right: auto; 24 | 25 | /* overflow: hidden; */ 26 | } 27 | 28 | .imageContainer img { 29 | width: 100%; 30 | height: 100%; 31 | object-fit: cover; 32 | border-radius: 10px; 33 | } -------------------------------------------------------------------------------- /ultron-android/src/main/kotlin/com/atiurin/ultron/core/espresso/EspressoOperationResult.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.espresso 2 | 3 | import com.atiurin.ultron.core.common.Operation 4 | import com.atiurin.ultron.core.common.OperationIterationResult 5 | import com.atiurin.ultron.core.common.OperationResult 6 | 7 | class EspressoOperationResult( 8 | override val operation: T, 9 | override val success: Boolean, 10 | override val exceptions: List = emptyList(), 11 | override var description: String, 12 | override var operationIterationResult: OperationIterationResult?, 13 | override val executionTimeMs: Long 14 | ) : OperationResult -------------------------------------------------------------------------------- /sample-app/src/main/java/com/atiurin/sampleapp/idlingresources/Holder.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.idlingresources 2 | 3 | import androidx.annotation.VisibleForTesting 4 | 5 | open class Holder(private val constructor: () -> T) { 6 | 7 | @Volatile 8 | private var instance: T? = null 9 | 10 | @VisibleForTesting 11 | fun getInstanceFromTest(): T? { 12 | return when { 13 | instance != null -> instance 14 | else -> synchronized(this) { 15 | instance = constructor() 16 | instance 17 | } 18 | } 19 | } 20 | 21 | fun getInstanceFromApp(): T? { 22 | return instance 23 | } 24 | } -------------------------------------------------------------------------------- /ultron-android/src/main/kotlin/com/atiurin/ultron/core/uiautomator/UiAutomatorOperationResult.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.uiautomator 2 | 3 | import com.atiurin.ultron.core.common.Operation 4 | import com.atiurin.ultron.core.common.OperationIterationResult 5 | import com.atiurin.ultron.core.common.OperationResult 6 | 7 | class UiAutomatorOperationResult( 8 | override val operation: T, 9 | override val success: Boolean, 10 | override val exceptions: List = emptyList(), 11 | override var description: String = "", 12 | override var operationIterationResult: OperationIterationResult?, 13 | override val executionTimeMs: Long 14 | ) : OperationResult -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable/ic_menu_camera.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /ultron-android/src/main/kotlin/com/atiurin/ultron/core/espresso/action/EspressoActionType.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.espresso.action 2 | 3 | import com.atiurin.ultron.core.common.UltronOperationType 4 | 5 | enum class EspressoActionType : UltronOperationType { 6 | CLICK, LONG_CLICK, DOUBLE_CLICK, 7 | CLICK_TOP_LEFT, CLICK_TOP_RIGHT, CLICK_TOP_CENTER, CLICK_BOTTOM_CENTER, CLICK_BOTTOM_LEFT, CLICK_BOTTOM_RIGHT, CLICK_CENTER_RIGHT, CLICK_CENTER_LEFT, 8 | TYPE_TEXT, REPLACE_TEXT, CLEAR_TEXT, PRESS_KEY, 9 | SWIPE_LEFT, SWIPE_RIGHT, SWIPE_UP, SWIPE_DOWN, SCROLL, 10 | CLOSE_SOFT_KEYBOARD, PRESS_BACK, OPEN_ACTION_BAR_OVERFLOW_OR_OPTION_MENU, OPEN_CONTEXTUAL_ACTION_MODE_OVERFLOW_MENU 11 | } -------------------------------------------------------------------------------- /ultron-android/src/main/kotlin/com/atiurin/ultron/core/espressoweb/operation/WebOperationResult.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.espressoweb.operation 2 | 3 | import com.atiurin.ultron.core.common.Operation 4 | import com.atiurin.ultron.core.common.OperationIterationResult 5 | import com.atiurin.ultron.core.common.OperationResult 6 | 7 | class WebOperationResult( 8 | override val operation: T, 9 | override val success: Boolean, 10 | override val exceptions: List = emptyList(), 11 | override var description: String, 12 | override var operationIterationResult: OperationIterationResult?, 13 | override val executionTimeMs: Long 14 | ) : OperationResult 15 | -------------------------------------------------------------------------------- /ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/core/compose/operation/ComposeOperationResult.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.compose.operation 2 | 3 | import com.atiurin.ultron.core.common.Operation 4 | import com.atiurin.ultron.core.common.OperationIterationResult 5 | import com.atiurin.ultron.core.common.OperationResult 6 | 7 | class ComposeOperationResult( 8 | override val operation: T, 9 | override val success: Boolean, 10 | override val exceptions: List = emptyList(), 11 | override var description: String, 12 | override var operationIterationResult: OperationIterationResult?, 13 | override val executionTimeMs: Long 14 | ) : OperationResult -------------------------------------------------------------------------------- /ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/extensions/SemanticsSelectorExt.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.extensions 2 | 3 | import androidx.compose.ui.test.SelectionResult 4 | import androidx.compose.ui.test.SemanticsMatcher 5 | import androidx.compose.ui.test.SemanticsSelector 6 | 7 | fun SemanticsSelector.addFindNodeInTreeSelector( 8 | selectorName: String, 9 | matcher: SemanticsMatcher 10 | ): SemanticsSelector { 11 | return SemanticsSelector( 12 | "(${this.description}).$selectorName(${matcher.description})", 13 | requiresExactlyOneNode = false, 14 | chainedInputSelector = this 15 | ) { nodes -> 16 | SelectionResult(nodes.findNodeInTree(matcher)) 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/com/atiurin/samplekmp/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.samplekmp 2 | 3 | import App 4 | import android.os.Bundle 5 | import androidx.activity.ComponentActivity 6 | import androidx.activity.compose.setContent 7 | import androidx.activity.enableEdgeToEdge 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.tooling.preview.Preview 10 | 11 | class MainActivity : ComponentActivity() { 12 | override fun onCreate(savedInstanceState: Bundle?) { 13 | enableEdgeToEdge() 14 | super.onCreate(savedInstanceState) 15 | 16 | setContent { 17 | App() 18 | } 19 | } 20 | } 21 | 22 | @Preview 23 | @Composable 24 | fun AppAndroidPreview() { 25 | App() 26 | } -------------------------------------------------------------------------------- /sample-app/src/androidTest/java/com/atiurin/sampleapp/framework/Log.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.framework 2 | 3 | import android.os.SystemClock 4 | import android.util.Log 5 | 6 | object Log { 7 | const val LOG_TAG = "Ultron" 8 | fun info(message: String) = Log.i(LOG_TAG, message) 9 | fun debug(message: String) = Log.d(LOG_TAG, message) 10 | fun error(message: String, name: String) = Log.e(LOG_TAG, message) 11 | fun warn(message: String) = Log.w(LOG_TAG, message) 12 | fun time(desc: String, block: () -> R) : R{ 13 | val startTime = SystemClock.elapsedRealtime() 14 | val result = block() 15 | debug("$desc duration ${SystemClock.elapsedRealtime() - startTime} ms") 16 | return result 17 | } 18 | } -------------------------------------------------------------------------------- /sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/UiObject2FriendsListPage.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.pages 2 | 3 | import androidx.test.uiautomator.By 4 | import com.atiurin.sampleapp.R 5 | import com.atiurin.sampleapp.data.repositories.ContactRepositoty 6 | import com.atiurin.ultron.core.uiautomator.uiobject2.UltronUiObject2.Companion.by 7 | import com.atiurin.ultron.core.uiautomator.uiobject2.UltronUiObject2.Companion.byResId 8 | import com.atiurin.ultron.page.Page 9 | 10 | object UiObject2FriendsListPage : Page() { 11 | val list = byResId(R.id.recycler_friends) 12 | val topElement = by(By.text(ContactRepositoty.getFirst().name)) 13 | val bottomElement = by(By.text(ContactRepositoty.getLast().name)) 14 | } -------------------------------------------------------------------------------- /ultron-android/src/main/kotlin/com/atiurin/ultron/core/espressoweb/operation/EspressoWebOperationType.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.espressoweb.operation 2 | 3 | import com.atiurin.ultron.core.common.UltronOperationType 4 | 5 | enum class EspressoWebOperationType : 6 | UltronOperationType { 7 | //element 8 | WEB_CLICK, WEB_GET_TEXT, WEB_REPLACE_TEXT, 9 | WEB_CLEAR_ELEMENT, WEB_KEYS, 10 | WEB_SCROLL_INTO_VIEW, WEB_ASSERT_THAT, 11 | WEB_EXISTS, WEB_HAS_TEXT, WEB_CONTAINS_TEXT, WEB_HAS_ATTRIBUTE, 12 | 13 | //elements list 14 | WEB_FIND_MULTIPLE_ELEMENTS, 15 | 16 | //document 17 | WEB_VIEW_ASSERT_THAT, WEB_EVAL_JS_SCRIPT, 18 | WEB_SELECT_ACTIVE_ELEMENT, WEB_SELECT_FRAME_BY_INDEX, WEB_SELECT_FRAME_BY_ID_OR_NAME 19 | } -------------------------------------------------------------------------------- /sample-app/src/androidTest/java/com/atiurin/sampleapp/framework/ultronext/UltronEspressoWebExt.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.framework.ultronext 2 | 3 | import androidx.test.espresso.web.webdriver.DriverAtoms 4 | import com.atiurin.ultron.core.espressoweb.webelement.UltronWebElement 5 | 6 | // add action on wenView 7 | fun UltronWebElement.appendText(text: String) = apply { 8 | executeOperation( 9 | getUltronWebActionOperation( 10 | webInteractionBlock = { 11 | webInteractionBlock().perform(DriverAtoms.webKeys(text)) 12 | }, 13 | name = "${elementInfo.name} appendText '$text'", 14 | description = "${elementInfo.name} appendText '$text' during $timeoutMs ms" 15 | ) 16 | ) 17 | } -------------------------------------------------------------------------------- /sample-app/src/main/java/com/atiurin/sampleapp/async/GetContacts.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.async 2 | 3 | import com.atiurin.sampleapp.MyApplication.CONTACTS_LOADING_TIMEOUT_MS 4 | import com.atiurin.sampleapp.data.entities.Contact 5 | import com.atiurin.sampleapp.data.repositories.CONTACTS 6 | import kotlinx.coroutines.delay 7 | 8 | class GetContacts(val delayMs: Long = CONTACTS_LOADING_TIMEOUT_MS) : UseCase, UseCase.None>() { 9 | 10 | override suspend fun run(params: None): Either> { 11 | return try { 12 | delay(delayMs) 13 | val contacts = CONTACTS 14 | Success(contacts) 15 | } catch (e: Exception) { 16 | Failure(e) 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /ultron-android/src/main/kotlin/com/atiurin/ultron/custom/espresso/assertion/ExistsEspressoViewAssertion.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.custom.espresso.assertion 2 | 3 | import android.view.View 4 | import androidx.test.espresso.NoMatchingViewException 5 | import androidx.test.espresso.ViewAssertion 6 | import com.atiurin.ultron.exceptions.UltronAssertionException 7 | import com.atiurin.ultron.exceptions.UltronOperationException 8 | 9 | class ExistsEspressoViewAssertion : ViewAssertion { 10 | override fun check(view: View?, noViewFoundException: NoMatchingViewException?) { 11 | if (view == null){ 12 | val ex = noViewFoundException ?: UltronAssertionException("View does not exist in hierarchy") 13 | throw ex 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /ultron-android/src/main/kotlin/com/atiurin/ultron/core/uiautomator/uiobject/UiAutomatorUiSelectorOperationExecutor.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.uiautomator.uiobject 2 | 3 | import com.atiurin.ultron.core.common.Operation 4 | import com.atiurin.ultron.core.config.UltronConfig 5 | import com.atiurin.ultron.core.uiautomator.UiAutomatorOperationExecutor 6 | import kotlin.reflect.KClass 7 | 8 | class UiAutomatorUiSelectorOperationExecutor( 9 | operation: UiAutomatorUiSelectorOperation 10 | ) : UiAutomatorOperationExecutor(operation) { 11 | override fun getAllowedExceptions(operation: Operation): List> { 12 | return UltronConfig.UiAutomator.UiObjectConfig.allowedExceptions.map { it.kotlin } 13 | } 14 | } -------------------------------------------------------------------------------- /sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso_web/BaseWebViewTest.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.tests.espresso_web 2 | 3 | import androidx.test.core.app.ActivityScenario 4 | import com.atiurin.sampleapp.activity.WebViewActivity 5 | import com.atiurin.sampleapp.pages.WebViewPage 6 | import com.atiurin.sampleapp.tests.BaseTest 7 | import com.atiurin.ultron.testlifecycle.setupteardown.SetUpRule 8 | 9 | abstract class BaseWebViewTest : BaseTest() { 10 | val page = WebViewPage() 11 | 12 | private val startActivity = SetUpRule().add { 13 | ActivityScenario.launch(WebViewActivity::class.java) 14 | // UltronWebDocument.forceJavascriptEnabled() 15 | } 16 | 17 | init { 18 | ruleSequence.addLast(startActivity) 19 | } 20 | } -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/assertion/SoftAssertion.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.common.assertion 2 | 3 | import com.atiurin.ultron.core.config.UltronCommonConfig 4 | import com.atiurin.ultron.log.UltronLog 5 | 6 | fun softAssertion(failOnException: Boolean = true, block: () -> Unit){ 7 | UltronLog.info("Start soft assertion context") 8 | with(UltronCommonConfig.testContext){ 9 | softAssertion = true 10 | block() 11 | softAssertion = false 12 | if (failOnException){ 13 | softAnalyzer.verify() 14 | } 15 | } 16 | UltronLog.info("Finish soft assertion context") 17 | } 18 | 19 | fun verifySoftAssertions(){ 20 | UltronCommonConfig.testContext.softAnalyzer.verify() 21 | } -------------------------------------------------------------------------------- /ultron-android/src/main/kotlin/com/atiurin/ultron/core/espresso/action/EspressoActionExecutor.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.espresso.action 2 | 3 | import com.atiurin.ultron.core.common.Operation 4 | import com.atiurin.ultron.core.config.UltronConfig 5 | import com.atiurin.ultron.core.espresso.EspressoOperationExecutor 6 | import com.atiurin.ultron.core.espresso.UltronEspressoOperation 7 | import kotlin.reflect.KClass 8 | 9 | internal class EspressoActionExecutor( 10 | operation: UltronEspressoOperation 11 | ) : EspressoOperationExecutor(operation) { 12 | override fun getAllowedExceptions(operation: Operation): List> { 13 | return UltronConfig.Espresso.ViewActionConfig.allowedExceptions.map { it.kotlin } 14 | } 15 | } -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/test/context/DefaultUltronTestContext.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.test.context 2 | 3 | import com.atiurin.ultron.core.common.resultanalyzer.DefaultSoftAssertionOperationResultAnalyzer 4 | import com.atiurin.ultron.core.common.resultanalyzer.OperationResultAnalyzer 5 | 6 | open class DefaultUltronTestContext : UltronTestContext { 7 | override var softAssertion: Boolean = false 8 | override val softAnalyzer = DefaultSoftAssertionOperationResultAnalyzer() 9 | 10 | override fun wrapAnalyzerIfSoftAssertion(analyzer: OperationResultAnalyzer): OperationResultAnalyzer { 11 | return if (softAssertion) softAnalyzer.apply { 12 | originalAnalyzer = analyzer 13 | } else analyzer 14 | } 15 | } -------------------------------------------------------------------------------- /ultron-common/src/androidMain/kotlin/com/atiurin/ultron/core/config/UltronAndroidCommonConfig.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.config 2 | 3 | import com.atiurin.ultron.testlifecycle.setupteardown.ConditionExecutorWrapper 4 | import com.atiurin.ultron.testlifecycle.setupteardown.ConditionsExecutor 5 | import com.atiurin.ultron.testlifecycle.setupteardown.DefaultConditionExecutorWrapper 6 | import com.atiurin.ultron.testlifecycle.setupteardown.DefaultConditionsExecutor 7 | 8 | object UltronAndroidCommonConfig { 9 | class Conditions { 10 | companion object { 11 | var conditionExecutorWrapper: ConditionExecutorWrapper = DefaultConditionExecutorWrapper() 12 | var conditionsExecutor: ConditionsExecutor = DefaultConditionsExecutor() 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable/ic_menu_share.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /ultron-android/src/main/kotlin/com/atiurin/ultron/core/espresso/assertion/EspressoAssertionExecutor.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.espresso.assertion 2 | 3 | import com.atiurin.ultron.core.common.Operation 4 | import com.atiurin.ultron.core.config.UltronConfig 5 | import com.atiurin.ultron.core.espresso.EspressoOperationExecutor 6 | import com.atiurin.ultron.core.espresso.UltronEspressoOperation 7 | import kotlin.reflect.KClass 8 | 9 | internal class EspressoAssertionExecutor( 10 | operation: UltronEspressoOperation 11 | ) : EspressoOperationExecutor(operation) { 12 | override fun getAllowedExceptions(operation: Operation): List> { 13 | return UltronConfig.Espresso.ViewAssertionConfig.allowedExceptions.map { it.kotlin } 14 | } 15 | } -------------------------------------------------------------------------------- /ultron-android/src/main/kotlin/com/atiurin/ultron/core/espresso/assertion/EspressoAssertionType.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.espresso.assertion 2 | 3 | import com.atiurin.ultron.core.common.UltronOperationType 4 | 5 | enum class EspressoAssertionType : 6 | UltronOperationType { 7 | IS_DISPLAYED, IS_NOT_DISPLAYED, IS_COMPLETELY_DISPLAYED, IS_DISPLAYING_AT_LEAST, 8 | IS_VISIBLE, 9 | DOES_NOT_EXIST, EXISTS, 10 | IS_ENABLED, IS_NOT_ENABLED, 11 | IS_SELECTED, IS_NOT_SELECTED, 12 | IS_CLICKABLE, IS_NOT_CLICKABLE, 13 | IS_CHECKED, IS_NOT_CHECKED, 14 | IS_FOCUSABLE, IS_NOT_FOCUSABLE, HAS_FOCUS, 15 | IS_JS_ENABLED, 16 | HAS_TEXT, CONTAINS_TEXT, 17 | HAS_CONTENT_DESCRIPTION, CONTENT_DESCRIPTION_CONTAINS_TEXT, 18 | ASSERT_MATCHES, IDENTIFY_RECYCLER_VIEW 19 | } -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/log/ULogger.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.log 2 | 3 | abstract class ULogger { 4 | var id: String 5 | 6 | constructor(id: String){ 7 | this.id = id 8 | } 9 | constructor(){ 10 | this.id = this::class.simpleName.orEmpty() 11 | } 12 | 13 | abstract fun info(message: String): Any 14 | abstract fun info(message: String, throwable: Throwable): Any 15 | abstract fun debug(message: String): Any 16 | abstract fun debug(message: String, throwable: Throwable): Any 17 | abstract fun warn(message: String): Any 18 | abstract fun warn(message: String, throwable: Throwable): Any 19 | abstract fun error(message: String): Any 20 | abstract fun error(message: String, throwable: Throwable): Any 21 | } -------------------------------------------------------------------------------- /ultron-android/src/main/kotlin/com/atiurin/ultron/core/espresso/action/UltronEspressoActionParams.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.espresso.action 2 | 3 | import android.view.View 4 | import com.atiurin.ultron.core.common.CommonOperationType 5 | import com.atiurin.ultron.core.common.UltronOperationType 6 | import org.hamcrest.Matcher 7 | import org.hamcrest.Matchers 8 | 9 | data class UltronEspressoActionParams( 10 | val operationName: String, 11 | val operationDescription: String, 12 | val operationType: UltronOperationType = CommonOperationType.DEFAULT, 13 | val viewActionConstraints: Matcher = Matchers.any(View::class.java), 14 | val viewActionDescription: String = "Anonymous ViewAction: specify params in perform/execute method to provide custom info about this action" 15 | ) 16 | -------------------------------------------------------------------------------- /sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/uiautomator/UltronUiAutomatorPerfTest.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.tests.uiautomator 2 | 3 | import android.os.SystemClock 4 | import com.atiurin.sampleapp.framework.Log 5 | import com.atiurin.sampleapp.pages.UiObject2ElementsPage 6 | import com.atiurin.sampleapp.tests.UiElementsTest 7 | import org.junit.Test 8 | 9 | class UltronUiAutomatorPerfTest: UiElementsTest() { 10 | val page = UiObject2ElementsPage() 11 | 12 | @Test 13 | fun perfTest(){ 14 | val startTime = SystemClock.elapsedRealtime() 15 | for (i in 1..200){ 16 | page.button.click() 17 | page.eventStatus.textContains(i.toString()) 18 | } 19 | Log.debug("Duration ${SystemClock.elapsedRealtime() - startTime} ms") 20 | } 21 | } -------------------------------------------------------------------------------- /ultron-allure/src/main/java/com/atiurin/ultron/allure/attachment/AllureDirectoryUtil.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.allure.attachment 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import io.qameta.allure.kotlin.util.PropertiesUtils 5 | import java.io.File 6 | 7 | object AllureDirectoryUtil { 8 | 9 | fun getResultsDirectoryName(): String = PropertiesUtils.resultsDirectoryPath 10 | 11 | /** 12 | * From Allure source code 13 | * see [https://github.com/allure-framework/allure-kotlin/blob/master/allure-kotlin-android/src/main/kotlin/io/qameta/allure/android/AllureAndroidLifecycle.kt] 14 | */ 15 | fun getOriginalResultsDirectory(): File { 16 | return File(InstrumentationRegistry.getInstrumentation().targetContext.filesDir, getResultsDirectoryName()) 17 | } 18 | } -------------------------------------------------------------------------------- /ultron-android/src/main/kotlin/com/atiurin/ultron/core/uiautomator/uiobject2/UiAutomatorBySelectorActionExecutor.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.uiautomator.uiobject2 2 | 3 | import com.atiurin.ultron.core.common.Operation 4 | import com.atiurin.ultron.core.config.UltronConfig 5 | import com.atiurin.ultron.core.uiautomator.UiAutomatorOperation 6 | import com.atiurin.ultron.core.uiautomator.UiAutomatorOperationExecutor 7 | import kotlin.reflect.KClass 8 | 9 | class UiAutomatorBySelectorActionExecutor( 10 | action: UiAutomatorBySelectorAction 11 | ) : UiAutomatorOperationExecutor(action) { 12 | override fun getAllowedExceptions(operation: Operation): List> { 13 | return UltronConfig.UiAutomator.UiObject2Config.allowedExceptions.map { it.kotlin } 14 | } 15 | } -------------------------------------------------------------------------------- /ultron-android/src/main/kotlin/com/atiurin/ultron/core/uiautomator/uiobject2/UiAutomatorBySelectorAssertionExecutor.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.uiautomator.uiobject2 2 | 3 | import com.atiurin.ultron.core.common.Operation 4 | import com.atiurin.ultron.core.config.UltronConfig 5 | import com.atiurin.ultron.core.uiautomator.UiAutomatorOperation 6 | import com.atiurin.ultron.core.uiautomator.UiAutomatorOperationExecutor 7 | import kotlin.reflect.KClass 8 | 9 | class UiAutomatorBySelectorAssertionExecutor( 10 | assertion: UiAutomatorBySelectorAssertion 11 | ) : UiAutomatorOperationExecutor(assertion) { 12 | override fun getAllowedExceptions(operation: Operation): List> { 13 | return UltronConfig.UiAutomator.UiObject2Config.allowedExceptions.map { it.kotlin } 14 | } 15 | } -------------------------------------------------------------------------------- /sample-app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/core/compose/list/IndexComposeItemExecutor.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.compose.list 2 | 3 | import androidx.compose.ui.test.SemanticsMatcher 4 | import com.atiurin.ultron.core.compose.nodeinteraction.UltronComposeSemanticsNodeInteraction 5 | 6 | class IndexComposeItemExecutor( 7 | val ultronComposeList: UltronComposeList, 8 | val index: Int 9 | ) : ComposeItemExecutor { 10 | override fun scrollToItem(offset: Int) { 11 | ultronComposeList.scrollToIndex(index) 12 | } 13 | override fun getItemInteraction() : UltronComposeSemanticsNodeInteraction = ultronComposeList.onVisibleItem(index) 14 | override fun getItemChildInteraction(childMatcher: SemanticsMatcher): UltronComposeSemanticsNodeInteraction = 15 | ultronComposeList.onVisibleItemChild(index, childMatcher) 16 | } -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/listeners/LifecycleListener.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.listeners 2 | 3 | import com.atiurin.ultron.core.common.Operation 4 | import com.atiurin.ultron.core.common.OperationResult 5 | 6 | internal interface LifecycleListener{ 7 | /** 8 | * executed before any action or assertion 9 | */ 10 | fun before(operation: Operation) 11 | /** 12 | * called when action or assertion has been executed successfully 13 | */ 14 | fun afterSuccess(operationResult: OperationResult) 15 | 16 | /** 17 | * called when action or assertion failed 18 | */ 19 | fun afterFailure(operationResult: OperationResult) 20 | 21 | /** 22 | * called in any case of action or assertion result 23 | */ 24 | fun after(operationResult: OperationResult) 25 | } -------------------------------------------------------------------------------- /ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/core/compose/ComposeTestEnvironment.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.compose 2 | 3 | import androidx.compose.ui.test.MainTestClock 4 | import androidx.compose.ui.test.SemanticsNodeInteractionsProvider 5 | import androidx.compose.ui.unit.Density 6 | 7 | interface ComposeTestEnvironment { 8 | val provider: SemanticsNodeInteractionsProvider 9 | /** 10 | * Current device screen's density. 11 | */ 12 | val density: Density 13 | 14 | /** 15 | * Clock that drives frames and recompositions in compose tests. 16 | */ 17 | val mainClock: MainTestClock 18 | } 19 | 20 | data class UltronComposeTestEnvironment( 21 | override val provider: SemanticsNodeInteractionsProvider, 22 | override val density: Density, 23 | override val mainClock: MainTestClock 24 | ): ComposeTestEnvironment -------------------------------------------------------------------------------- /ultron-common/src/shared/kotlin/com/atiurin/ultron/log/UltronLog.shared.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.log 2 | 3 | /** 4 | * Not implemented yet 5 | */ 6 | actual fun getFileLogger(): UltronFileLogger { 7 | return object : UltronFileLogger() { 8 | override fun getLogFilePath(): String = "" 9 | override fun clearFile() = Unit 10 | override fun info(message: String) = Unit 11 | override fun info(message: String, throwable: Throwable) = Unit 12 | override fun debug(message: String) = Unit 13 | override fun debug(message: String, throwable: Throwable) = Unit 14 | override fun warn(message: String) = Unit 15 | override fun warn(message: String, throwable: Throwable) = Unit 16 | override fun error(message: String) = Unit 17 | override fun error(message: String, throwable: Throwable) = Unit 18 | } 19 | } -------------------------------------------------------------------------------- /ultron-android/src/main/kotlin/com/atiurin/ultron/core/espressoweb/operation/WebInteractionOperationExecutor.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.espressoweb.operation 2 | 3 | import com.atiurin.ultron.core.common.Operation 4 | import com.atiurin.ultron.core.common.ResultDescriptor 5 | import com.atiurin.ultron.core.config.UltronConfig 6 | import kotlin.reflect.KClass 7 | 8 | internal class WebInteractionOperationExecutor( 9 | operation: WebInteractionOperation 10 | ) : WebOperationExecutor>(operation) { 11 | override fun getAllowedExceptions(operation: Operation): List> { 12 | return UltronConfig.Espresso.WebInteractionOperationConfig.allowedExceptions.map { 13 | it.kotlin 14 | } 15 | } 16 | 17 | override val descriptor: ResultDescriptor 18 | get() = ResultDescriptor() 19 | } 20 | -------------------------------------------------------------------------------- /ultron-android/src/main/kotlin/com/atiurin/ultron/core/uiautomator/UiAutomatorActionType.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.uiautomator 2 | 3 | import com.atiurin.ultron.core.common.UltronOperationType 4 | 5 | enum class UiAutomatorActionType : 6 | UltronOperationType { 7 | CLICK, CLICK_AND_WAIT_FOR_NEW_WINDOW, CLICK_TOP_LEFT, CLICK_BOTTOM_RIGHT, 8 | LONG_CLICK, LONG_CLICK_BOTTOM_RIGHT, LONG_CLICK_TOP_LEFT, 9 | DRAG, FLING, PINCH_CLOSE, PINCH_OPEN, PINCH_OUT, PINCH_IN, 10 | ADD_TEXT, REPLACE_TEXT, CLEAR_TEXT, GET_TEXT, LEGACY_SET_TEXT, 11 | GET_APPLICATION_PACKAGE, GET_BOUNDS, GET_VISIBLE_BOUNDS, GET_VISIBLE_CENTER, GET_CLASS_NAME, GET_CONTENT_DESCRIPTION, GET_RESOURCE_NAME, 12 | GET_PARENT, GET_CHILDREN, GET_CHILD, GET_CHILD_COUNT, GET_FROM_PARENT, FIND_OBJECT, FIND_OBJECTS, 13 | SWIPE, SWIPE_LEFT, SWIPE_RIGHT, SWIPE_UP, SWIPE_DOWN, SCROLL, PERFORM 14 | } -------------------------------------------------------------------------------- /ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/core/compose/UltronUiTest.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.compose 2 | 3 | import androidx.compose.ui.test.ComposeUiTest 4 | import androidx.compose.ui.test.ExperimentalTestApi 5 | import androidx.compose.ui.test.runComposeUiTest 6 | import kotlin.coroutines.CoroutineContext 7 | import kotlin.coroutines.EmptyCoroutineContext 8 | 9 | @OptIn(ExperimentalTestApi::class) 10 | fun runUltronUiTest( 11 | effectContext: CoroutineContext = EmptyCoroutineContext, 12 | block: ComposeUiTest.() -> Unit 13 | ) { 14 | runComposeUiTest(effectContext){ 15 | ComposeTestContainer.init( 16 | UltronComposeTestEnvironment( 17 | provider = this, 18 | mainClock = this.mainClock, 19 | density = this.density 20 | ) 21 | ) 22 | block() 23 | } 24 | } -------------------------------------------------------------------------------- /ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/extensions/SemanticsNodeExt.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.extensions 2 | 3 | import androidx.compose.ui.semantics.SemanticsNode 4 | import androidx.compose.ui.test.SemanticsMatcher 5 | 6 | fun Iterable.findNodeInTree(matcher: SemanticsMatcher): List { 7 | val targetNodes = mutableListOf() 8 | this.forEach { node -> 9 | targetNodes.addAll(node.findNodeInTree(matcher)) 10 | } 11 | return targetNodes 12 | } 13 | 14 | fun SemanticsNode.findNodeInTree(matcher: SemanticsMatcher): List { 15 | val targetNodes = mutableListOf() 16 | if (matcher.matches(this)) { 17 | targetNodes.add(this) 18 | return targetNodes 19 | } else { 20 | targetNodes.addAll(this.children.findNodeInTree(matcher)) 21 | } 22 | return targetNodes 23 | } -------------------------------------------------------------------------------- /sample-app/src/main/java/com/atiurin/sampleapp/activity/BusyActivity.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.activity 2 | 3 | import android.app.Activity 4 | import android.os.Bundle 5 | import android.os.Handler 6 | import android.os.Looper 7 | 8 | class BusyActivity : Activity(){ 9 | private val handler = Handler(Looper.getMainLooper()) 10 | private val busyRunnable = object : Runnable { 11 | override fun run() { 12 | // Post a delayed runnable to keep the main thread busy indefinitely 13 | handler.postDelayed(this, 0) 14 | } 15 | } 16 | 17 | override fun onCreate(savedInstanceState: Bundle?) { 18 | super.onCreate(savedInstanceState) 19 | // Start the busy loop 20 | handler.post(busyRunnable) 21 | } 22 | 23 | override fun onDestroy() { 24 | super.onDestroy() 25 | handler.removeCallbacks(busyRunnable) 26 | } 27 | } -------------------------------------------------------------------------------- /ultron-allure/src/main/java/com/atiurin/ultron/allure/runner/ScreenshotAttachRunListener.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.allure.runner 2 | 3 | import com.atiurin.ultron.allure.config.AllureAttachStrategy 4 | import com.atiurin.ultron.allure.screenshot.AllureScreenshot 5 | import com.atiurin.ultron.extensions.fullTestName 6 | import com.atiurin.ultron.runner.UltronRunListener 7 | import org.junit.runner.notification.Failure 8 | 9 | class ScreenshotAttachRunListener(val policies: Set) : UltronRunListener() { 10 | 11 | val screenshot = AllureScreenshot() 12 | 13 | override fun testFailure(failure: Failure) { 14 | if (policies.contains(AllureAttachStrategy.TEST_FAILURE)){ 15 | screenshot.takeAndAttach("$prefix${failure.description.fullTestName()}") 16 | } 17 | } 18 | companion object{ 19 | private const val prefix = "screenshot_" 20 | } 21 | } -------------------------------------------------------------------------------- /ultron-allure/src/main/java/com/atiurin/ultron/allure/runner/WindowHierarchyAttachRunListener.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.allure.runner 2 | 3 | import com.atiurin.ultron.allure.config.AllureAttachStrategy 4 | import com.atiurin.ultron.allure.hierarchy.AllureHierarchyDumper 5 | import com.atiurin.ultron.extensions.fullTestName 6 | import com.atiurin.ultron.runner.UltronRunListener 7 | import org.junit.runner.notification.Failure 8 | 9 | class WindowHierarchyAttachRunListener(val policies: Set) : UltronRunListener() { 10 | val dumper = AllureHierarchyDumper() 11 | 12 | override fun testFailure(failure: Failure) { 13 | if (policies.contains(AllureAttachStrategy.TEST_FAILURE)){ 14 | dumper.dumpAndAttach("$prefix${failure.description.fullTestName()}") 15 | } 16 | } 17 | companion object{ 18 | private const val prefix = "hierarchy_" 19 | } 20 | } -------------------------------------------------------------------------------- /sample-app/src/main/java/com/atiurin/sampleapp/activity/ComposeRouterActivity.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.activity 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.activity.enableEdgeToEdge 7 | import androidx.compose.foundation.ExperimentalFoundationApi 8 | import androidx.compose.material.ExperimentalMaterialApi 9 | import androidx.compose.ui.unit.ExperimentalUnitApi 10 | import com.atiurin.sampleapp.compose.app.App 11 | 12 | class ComposeRouterActivity : ComponentActivity() { 13 | @ExperimentalMaterialApi 14 | @ExperimentalUnitApi 15 | @ExperimentalFoundationApi 16 | override fun onCreate(savedInstanceState: Bundle?) { 17 | enableEdgeToEdge() 18 | super.onCreate(savedInstanceState) 19 | 20 | setContent { 21 | App() 22 | } 23 | } 24 | } 25 | 26 | 27 | -------------------------------------------------------------------------------- /sample-app/src/main/res/layout/content_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 17 | 18 | -------------------------------------------------------------------------------- /ultron-common/src/androidMain/kotlin/com/atiurin/ultron/extensions/AnyExt.android.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.extensions 2 | 3 | inline fun Any.getProperty(propertyName: String): T? { 4 | return try { 5 | val property = this.javaClass.getDeclaredField(propertyName) 6 | property.isAccessible = true 7 | property.get(this) as T 8 | } catch (ex: Throwable) { null } 9 | } 10 | 11 | inline fun Any.getMethodResult(methodName: String, vararg args: Any?): T? { 12 | return try { 13 | val method = this.javaClass.getDeclaredMethod(methodName) 14 | method.isAccessible = true 15 | method.invoke(this, *args) as T 16 | } catch (ex: Throwable) { null } 17 | } 18 | 19 | fun Class<*>.isAssignedFrom(klasses: List>): Boolean{ 20 | klasses.forEach { 21 | if (it.isAssignableFrom(this)) return true 22 | } 23 | return false 24 | } -------------------------------------------------------------------------------- /docs/sidebars.ts: -------------------------------------------------------------------------------- 1 | import type {SidebarsConfig} from '@docusaurus/plugin-content-docs'; 2 | 3 | /** 4 | * Creating a sidebar enables you to: 5 | - create an ordered group of docs 6 | - render a sidebar for each doc of that group 7 | - provide next/previous navigation 8 | 9 | The sidebars can be generated from the filesystem, or explicitly defined here. 10 | 11 | Create as many sidebars as you want. 12 | */ 13 | const sidebars: SidebarsConfig = { 14 | // By default, Docusaurus generates a sidebar from the docs folder structure 15 | tutorialSidebar: [{type: 'autogenerated', dirName: '.'}], 16 | 17 | // But you can create a sidebar manually 18 | /* 19 | tutorialSidebar: [ 20 | 'intro', 21 | 'hello', 22 | { 23 | type: 'category', 24 | label: 'Tutorial', 25 | items: ['tutorial-basics/create-a-document'], 26 | }, 27 | ], 28 | */ 29 | }; 30 | 31 | export default sidebars; 32 | -------------------------------------------------------------------------------- /sample-app/src/main/java/com/atiurin/sampleapp/compose/SimpleOutlinedText.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.compose 2 | 3 | import androidx.compose.foundation.text.selection.SelectionContainer 4 | import androidx.compose.material.OutlinedTextField 5 | import androidx.compose.material.Text 6 | import androidx.compose.runtime.* 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.semantics.semantics 9 | import androidx.compose.ui.semantics.testTag 10 | 11 | @Composable 12 | fun SimpleOutlinedText(defaultValue: String = "", myTestTag: String = "outlinedText") { 13 | var text by remember { mutableStateOf(defaultValue) } 14 | SelectionContainer { 15 | OutlinedTextField( 16 | value = text, 17 | onValueChange = { text = it }, 18 | label = { Text("Label") }, 19 | modifier = Modifier.semantics { testTag = myTestTag }, 20 | 21 | ) 22 | } 23 | } -------------------------------------------------------------------------------- /ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/core/compose/ComposeTestContainer.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.compose 2 | 3 | import androidx.compose.ui.test.SemanticsNodeInteractionsProvider 4 | import com.atiurin.ultron.exceptions.UltronException 5 | 6 | object ComposeTestContainer { 7 | private lateinit var testEnvironment: ComposeTestEnvironment 8 | 9 | fun init(testEnvironment: ComposeTestEnvironment) { 10 | this.testEnvironment = testEnvironment 11 | } 12 | 13 | val isInitialized : Boolean 14 | get() = ::testEnvironment.isInitialized 15 | 16 | fun getProvider(): SemanticsNodeInteractionsProvider = this.testEnvironment.provider 17 | 18 | fun withComposeTestEnvironment(block: (ComposeTestEnvironment) -> T): T { 19 | if (!isInitialized) throw UltronException("ComposeTestContainer isn't initialized!") 20 | return block(testEnvironment) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ultron-compose/src/jvmMain/kotlin/com/atiurin/ultron/core/compose/UltronUiTest.jvm.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.compose 2 | 3 | import androidx.compose.ui.test.ExperimentalTestApi 4 | import androidx.compose.ui.test.SkikoComposeUiTest 5 | import kotlin.coroutines.CoroutineContext 6 | import kotlin.coroutines.EmptyCoroutineContext 7 | 8 | @OptIn(ExperimentalTestApi::class) 9 | fun runDesktopUltronUiTest( 10 | width: Int = 1024, 11 | height: Int = 768, 12 | effectContext: CoroutineContext = EmptyCoroutineContext, 13 | block: SkikoComposeUiTest.() -> Unit 14 | ) { 15 | SkikoComposeUiTest(width, height, effectContext).runTest { 16 | ComposeTestContainer.init( 17 | UltronComposeTestEnvironment( 18 | provider = this, 19 | mainClock = this.mainClock, 20 | density = this.density 21 | ) 22 | ) 23 | block() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /sample-app/src/main/java/com/atiurin/sampleapp/activity/ProfileActivity.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.activity 2 | 3 | import android.os.Bundle 4 | import android.widget.EditText 5 | import androidx.activity.enableEdgeToEdge 6 | import androidx.appcompat.app.AppCompatActivity 7 | import com.atiurin.sampleapp.R 8 | import com.atiurin.sampleapp.data.repositories.CURRENT_USER 9 | import com.atiurin.sampleapp.view.CircleImageView 10 | 11 | class ProfileActivity : AppCompatActivity(){ 12 | override fun onCreate(savedInstanceState: Bundle?) { 13 | enableEdgeToEdge() 14 | super.onCreate(savedInstanceState) 15 | setContentView(R.layout.activity_profile) 16 | val avatar = findViewById(R.id.avatar) 17 | avatar.setImageDrawable(getDrawable(CURRENT_USER.avatar)) 18 | val name = findViewById(R.id.et_username) 19 | name.hint = CURRENT_USER.name 20 | } 21 | } -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/listeners/UltronListenerUtil.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.listeners 2 | 3 | import com.atiurin.ultron.core.config.UltronCommonConfig 4 | 5 | 6 | fun executeWithoutListeners(block: () -> T): T { 7 | UltronCommonConfig.isListenersOn = false 8 | val result = block.invoke() 9 | UltronCommonConfig.isListenersOn = true 10 | return result 11 | } 12 | 13 | fun executeWithListeners(block: () -> T): T { 14 | UltronCommonConfig.isListenersOn = true 15 | return block.invoke() 16 | } 17 | 18 | fun executableWithoutListeners(block: () -> T): () -> T = { executeWithoutListeners(block) } 19 | fun executableWithListeners(block: () -> T): () -> T = { executeWithListeners(block) } 20 | 21 | fun (() -> T).setListenersState(value: Boolean): () -> T { 22 | return if(value) executableWithListeners(this) 23 | else executableWithoutListeners(this) 24 | } 25 | -------------------------------------------------------------------------------- /ultron-allure/src/main/java/com/atiurin/ultron/allure/config/AllureConfigParams.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.allure.config 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import com.atiurin.ultron.allure.attachment.AllureDirectoryUtil 5 | import com.atiurin.ultron.allure.attachment.AttachUtil 6 | import java.io.File 7 | 8 | data class AllureConfigParams( 9 | var addScreenshotPolicy: MutableSet = mutableSetOf( 10 | AllureAttachStrategy.TEST_FAILURE, 11 | AllureAttachStrategy.OPERATION_FAILURE 12 | ), 13 | var addHierarchyPolicy: MutableSet = mutableSetOf( 14 | AllureAttachStrategy.TEST_FAILURE, 15 | AllureAttachStrategy.OPERATION_FAILURE 16 | ), 17 | var attachUltronLog: Boolean = true, 18 | var attachLogcat: Boolean = true, 19 | var addConditionsToReport: Boolean = true, 20 | var detailedAllureReport: Boolean = true 21 | ) -------------------------------------------------------------------------------- /sample-app/src/androidTest/java/com/atiurin/sampleapp/framework/utils/TimeUtils.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.framework.utils 2 | 3 | import android.annotation.SuppressLint 4 | import java.time.Clock 5 | import java.time.Instant 6 | import java.time.LocalDate 7 | import java.time.ZoneId 8 | import java.time.ZoneOffset 9 | import java.time.format.DateTimeFormatter 10 | 11 | object TimeUtils { 12 | @SuppressLint("NewApi") 13 | fun formatTimestamp(timestamp: Long): String { 14 | val instant = Instant.ofEpochMilli(timestamp) 15 | val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") 16 | .withZone(ZoneId.systemDefault()) 17 | return formatter.format(instant) 18 | } 19 | 20 | fun getTimestampStartOfDay(): Long { 21 | return LocalDate.now(Clock.systemUTC()) 22 | .atStartOfDay() 23 | .toInstant(ZoneOffset.UTC) 24 | .toEpochMilli() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator. 4 | 5 | ### Installation 6 | 7 | ``` 8 | $ yarn 9 | ``` 10 | 11 | ### Local Development 12 | 13 | ``` 14 | $ yarn start 15 | ``` 16 | 17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ### Build 20 | 21 | ``` 22 | $ yarn build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ### Deployment 28 | 29 | Using SSH: 30 | 31 | ``` 32 | $ USE_SSH=true yarn deploy 33 | ``` 34 | 35 | Not using SSH: 36 | 37 | ``` 38 | $ GIT_USER= yarn deploy 39 | ``` 40 | 41 | 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. 42 | -------------------------------------------------------------------------------- /ultron-common/src/androidMain/kotlin/com/atiurin/ultron/log/UltronLogcatLogger.android.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.log 2 | 3 | import android.util.Log 4 | 5 | class UltronLogcatLogger : ULogger(){ 6 | companion object { 7 | const val LOG_TAG = "Ultron" 8 | } 9 | override fun info(message: String) = Log.i(LOG_TAG, message) 10 | override fun info(message: String, throwable: Throwable) = Log.i(LOG_TAG, message, throwable) 11 | override fun debug(message: String) = Log.d(LOG_TAG, message) 12 | override fun debug(message: String, throwable: Throwable) = Log.d(LOG_TAG, message, throwable) 13 | override fun warn(message: String) = Log.w(LOG_TAG, message) 14 | override fun warn(message: String, throwable: Throwable) = Log.w(LOG_TAG, message, throwable) 15 | override fun error(message: String) = Log.e(LOG_TAG, message) 16 | override fun error(message: String, throwable: Throwable) = Log.e(LOG_TAG, message, throwable) 17 | } -------------------------------------------------------------------------------- /sample-app/src/main/java/com/atiurin/sampleapp/activity/SplashActivity.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.activity 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import androidx.activity.enableEdgeToEdge 6 | import androidx.appcompat.app.AppCompatActivity 7 | import com.atiurin.sampleapp.managers.AccountManager 8 | 9 | class SplashActivity : AppCompatActivity() { 10 | 11 | override fun onCreate(savedInstanceState: Bundle?) { 12 | enableEdgeToEdge() 13 | super.onCreate(savedInstanceState) 14 | 15 | val accountManager = AccountManager(applicationContext) 16 | if (accountManager.isLogedIn()){ 17 | val intent = Intent(applicationContext, MainActivity::class.java) 18 | startActivity(intent) 19 | }else{ 20 | val intent = Intent(applicationContext, LoginActivity::class.java) 21 | startActivity(intent) 22 | } 23 | finish() 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /sample-app/src/main/java/com/atiurin/sampleapp/compose/screen/DatePickerScreen.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.compose.screen 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.runtime.Composable 5 | import com.atiurin.sampleapp.compose.DatePickerDocked 6 | 7 | @Composable 8 | fun DatePickerScreen() { 9 | Column { 10 | DatePickerDocked() 11 | 12 | // val modalDate = remember { mutableStateOf("No modal date selected") } 13 | // val showModal = remember { mutableStateOf(false) } 14 | // Text(modalDate.value) 15 | // if (showModal.value){ 16 | // DatePickerModal({ date -> 17 | // date?.let { modalDate.value = convertMillisToDate(date) } 18 | // showModal.value = false 19 | // }) { 20 | // showModal.value = false 21 | // } 22 | // } 23 | } 24 | 25 | } 26 | 27 | @Composable 28 | fun ShowModalButton(){ 29 | 30 | } -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/listeners/LogLifecycleListener.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.listeners 2 | 3 | import com.atiurin.ultron.core.common.Operation 4 | import com.atiurin.ultron.core.common.OperationResult 5 | import com.atiurin.ultron.log.UltronLog 6 | 7 | class LogLifecycleListener : UltronLifecycleListener() { 8 | override fun before(operation: Operation) { 9 | UltronLog.info("Start execution of ${operation.name}") 10 | UltronLog.info("Element info: ${operation.elementInfo}") 11 | } 12 | 13 | override fun afterSuccess(operationResult: OperationResult) { 14 | UltronLog.info("Successfully executed ${operationResult.operation.name}") 15 | } 16 | 17 | override fun afterFailure(operationResult: OperationResult) { 18 | UltronLog.error("Failed ${operationResult.operation.name}. with description: \n" + 19 | "${operationResult.description} ") 20 | } 21 | } -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/resultanalyzer/SoftAssertionOperationResultAnalyzer.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.common.resultanalyzer 2 | 3 | interface SoftAssertionOperationResultAnalyzer : OperationResultAnalyzer { 4 | /** 5 | * Clears all previously caught exceptions, effectively resetting the internal state. 6 | * Use this method when starting a new set of assertions to ensure 7 | * that previous exceptions do not affect the current verification process. 8 | */ 9 | fun clear() 10 | 11 | /** 12 | * Verifies whether any exceptions were caught during previous operations. 13 | * If there were caught exceptions, this method throws a general exception summarizing them. 14 | * Use this method at the end of your test or operation to ensure that all assertions passed. 15 | * 16 | * @throws Exception if one or more exceptions were previously caught. 17 | */ 18 | fun verify() 19 | } -------------------------------------------------------------------------------- /sample-app/src/main/java/com/atiurin/sampleapp/managers/PrefsManager.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.managers 2 | 3 | import android.content.Context.MODE_PRIVATE 4 | import android.content.Context 5 | 6 | 7 | class PrefsManager(val context: Context){ 8 | val PREFS_NAME = "MyPrefsFileName" 9 | 10 | fun savePref(key: String, value: String){ 11 | val editor = context.getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit() 12 | editor.putString(key, value) 13 | editor.apply() 14 | } 15 | 16 | fun getPref(key: String) : String{ 17 | val prefs = context.getSharedPreferences(PREFS_NAME, MODE_PRIVATE) 18 | var value = prefs.getString(key, null) 19 | if (value == null) value = "" 20 | return value 21 | } 22 | 23 | fun remove(key: String){ 24 | val editor = context.getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit() 25 | editor.remove(key) 26 | editor.commit() 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /ultron-android/src/main/kotlin/com/atiurin/ultron/core/uiautomator/UiAutomatorAssertionType.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.uiautomator 2 | 3 | import com.atiurin.ultron.core.common.UltronOperationType 4 | 5 | enum class UiAutomatorAssertionType : 6 | UltronOperationType { 7 | IS_DISPLAYED, IS_NOT_DISPLAYED, IS_COMPLETELY_DISPLAYED, IS_DISPLAYING_AT_LEAST, 8 | IS_ENABLED, IS_NOT_ENABLED, 9 | IS_CLICKABLE, IS_NOT_CLICKABLE, 10 | IS_LONG_CLICKABLE, IS_NOT_LONG_CLICKABLE, 11 | IS_CHECKED, IS_NOT_CHECKED, 12 | IS_CHECKABLE, IS_NOT_CHECKABLE, 13 | IS_FOCUSABLE, IS_NOT_FOCUSABLE, IS_FOCUSED, IS_NOT_FOCUSED, 14 | IS_SELECTED, IS_NOT_SELECTED, 15 | IS_SCROLLABLE, IS_NOT_SCROLLABLE, 16 | HAS_TEXT, TEXT_CONTAINS, TEST_IS_NULL_OR_EMPTY, TEST_IS_NOT_NULL_OR_EMPTY, 17 | HAS_CONTENT_DESCRIPTION, CONTENT_DESCRIPTION_CONTAINS_TEXT, CONTENT_DESCRIPTION_IS_NULL_OR_EMPTY, CONTENT_DESCRIPTION_IS_NOT_NULL_OR_EMPTY, 18 | ASSERT_THAT, EXISTS, NOT_EXISTS 19 | } -------------------------------------------------------------------------------- /sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso_web/UltronWebUiBlockTest.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.tests.espresso_web 2 | 3 | import com.atiurin.sampleapp.pages.uiblock.WebElementUiBlockScreen 4 | import org.junit.Test 5 | 6 | class UltronWebUiBlockTest : BaseWebViewTest() { 7 | @Test 8 | fun webUiBlock(){ 9 | WebElementUiBlockScreen { 10 | teacherBlock.name.exists().hasText("Socrates") 11 | teacherBlock.uiBlock.exists() 12 | studentWithoutDesc.name.exists().hasText("Plato") 13 | } 14 | } 15 | 16 | @Test 17 | fun uiBlockFactoryTest(){ 18 | WebElementUiBlockScreen { 19 | persons.student.name.hasText("Plato") 20 | } 21 | } 22 | 23 | @Test 24 | fun childUiBlockCreation(){ 25 | WebElementUiBlockScreen { 26 | persons.teacher.name.hasText("Socrates") 27 | persons.studentWithoutDesc.name.hasText("Plato") 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /ultron-common/src/androidMain/kotlin/com/atiurin/ultron/hierarchy/UiDeviceHierarchyDumper.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.hierarchy 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.uiautomator.UiDevice 5 | import com.atiurin.ultron.log.UltronLog 6 | import java.io.File 7 | 8 | class UiDeviceHierarchyDumper : HierarchyDumper { 9 | private val uiDevice: UiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) 10 | override fun dumpFullWindowHierarchy(file: File): HierarchyDumpResult { 11 | var isSuccess = false 12 | runCatching { 13 | uiDevice.dumpWindowHierarchy(file) 14 | }.onFailure { 15 | UltronLog.error("Couldn't dump window hierarchy. ${it.message}") 16 | }.onSuccess { 17 | UltronLog.debug("Window hierarchy is dumped to ${file.absolutePath}") 18 | isSuccess = true 19 | } 20 | return HierarchyDumpResult(isSuccess, file) 21 | } 22 | } -------------------------------------------------------------------------------- /ultron-common/src/androidMain/kotlin/com/atiurin/ultron/utils/ActivityUtil.android.kt.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.utils 2 | 3 | import android.app.Activity 4 | import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry 5 | import androidx.test.runner.lifecycle.Stage 6 | import com.atiurin.ultron.log.UltronLog 7 | 8 | object ActivityUtil { 9 | 10 | fun getResumedActivity(): Activity? { 11 | var resumedActivity: Activity? = null 12 | 13 | val findResumedActivity = { 14 | val resumedActivities = ActivityLifecycleMonitorRegistry.getInstance() 15 | .getActivitiesInStage(Stage.RESUMED) 16 | if (resumedActivities.iterator().hasNext()) { 17 | resumedActivity = resumedActivities.iterator().next() 18 | } 19 | } 20 | 21 | runOnUiThread { findResumedActivity() } 22 | 23 | resumedActivity ?: UltronLog.error("No resumed activity found") 24 | return resumedActivity 25 | } 26 | } -------------------------------------------------------------------------------- /sample-app/src/main/java/com/atiurin/sampleapp/async/task/CompatAsyncTask.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.async.task 2 | 3 | import android.os.AsyncTask 4 | 5 | @Suppress("DEPRECATION") 6 | class CompatAsyncTask : AsyncTask() { 7 | 8 | companion object { 9 | const val COMPAT_ASYNC_TASK_TIME_EXECUTION = 5000 10 | const val ASYNC = "ASYNC" 11 | } 12 | 13 | @Deprecated("Suppress") 14 | override fun doInBackground(vararg params: Void?): Void? { 15 | val startTime = System.currentTimeMillis() 16 | while (!isCancelled && System.currentTimeMillis() - startTime < COMPAT_ASYNC_TASK_TIME_EXECUTION) { 17 | Thread.sleep(1000) 18 | } 19 | return null 20 | } 21 | 22 | @Deprecated("Suppress") 23 | override fun onPostExecute(result: Void?) {} 24 | 25 | fun start() { 26 | executeOnExecutor(THREAD_POOL_EXECUTOR) 27 | } 28 | 29 | fun stop() { 30 | cancel(true) 31 | } 32 | } 33 | 34 | -------------------------------------------------------------------------------- /ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/core/compose/list/MatcherComposeItemExecutor.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.compose.list 2 | 3 | import androidx.compose.ui.test.SemanticsMatcher 4 | import com.atiurin.ultron.core.compose.nodeinteraction.UltronComposeSemanticsNodeInteraction 5 | 6 | class MatcherComposeItemExecutor( 7 | val ultronComposeList: UltronComposeList, 8 | val itemMatcher: SemanticsMatcher 9 | ) : ComposeItemExecutor { 10 | override fun scrollToItem(offset: Int) { 11 | ultronComposeList.scrollToNode(itemMatcher) 12 | } 13 | 14 | override fun getItemInteraction(): UltronComposeSemanticsNodeInteraction { 15 | scrollToItem() 16 | return ultronComposeList.onItem(itemMatcher) 17 | } 18 | 19 | override fun getItemChildInteraction(childMatcher: SemanticsMatcher): UltronComposeSemanticsNodeInteraction { 20 | scrollToItem() 21 | return ultronComposeList.onItemChild(itemMatcher, childMatcher) 22 | } 23 | } -------------------------------------------------------------------------------- /ultron-common/src/androidMain/kotlin/com/atiurin/ultron/testlifecycle/setupteardown/DefaultConditionsExecutor.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.testlifecycle.setupteardown 2 | 3 | import com.atiurin.ultron.core.config.UltronAndroidCommonConfig 4 | import com.atiurin.ultron.log.UltronLog 5 | import kotlin.reflect.KClass 6 | 7 | open class DefaultConditionsExecutor : ConditionsExecutor { 8 | override val conditionExecutor: ConditionExecutorWrapper by lazy { UltronAndroidCommonConfig.Conditions.conditionExecutorWrapper } 9 | override fun before(name: String, ruleClass: KClass<*>) { 10 | UltronLog.info("Execute ${ruleClass.simpleName} '$name' conditions") 11 | } 12 | override fun execute(conditions: List, keys: List, description: String) { 13 | conditions 14 | .sortedBy { it.counter } 15 | .filter { it.key in keys } 16 | .forEach { condition -> 17 | conditionExecutor.execute(condition) 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /ultron-android/src/main/kotlin/com/atiurin/ultron/custom/espresso/base/IterableUtils.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.custom.espresso.base 2 | 3 | import org.hamcrest.Matcher 4 | 5 | internal fun filter(iterable: Iterable, matcher: Matcher): Iterable { 6 | return iterable.filter { matcher.matches(it) } 7 | } 8 | 9 | internal fun filterToList(iterable: Iterable, matcher: Matcher): List { 10 | return filter(iterable, matcher).toList() 11 | } 12 | 13 | internal fun joinToString(iterable: Iterable, delimiter: String): String { 14 | return iterable.joinToString(separator = delimiter) 15 | } 16 | 17 | internal fun toArray(iterator: Iterator, clazz: Class): Array { 18 | val arrayList = ArrayList() 19 | while (iterator.hasNext()) { 20 | arrayList.add(iterator.next()) 21 | } 22 | return arrayList.toArray( 23 | java.lang.reflect.Array.newInstance( 24 | clazz, 25 | arrayList.size 26 | ) as Array 27 | ) 28 | } -------------------------------------------------------------------------------- /ultron-compose/src/androidMain/kotlin/com/atiurin/ultron/core/compose/listeners/ComposDebugListener.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.compose.listeners 2 | 3 | import com.atiurin.ultron.core.common.Operation 4 | import com.atiurin.ultron.core.common.OperationResult 5 | import com.atiurin.ultron.core.compose.ComposeTestContainer 6 | import com.atiurin.ultron.core.compose.ComposeTestContainer.withComposeTestEnvironment 7 | import com.atiurin.ultron.listeners.UltronLifecycleListener 8 | 9 | class ComposDebugListener(private val advanceFrameAmount: Int = 10) : UltronLifecycleListener() { 10 | override fun after(operationResult: OperationResult) { 11 | super.after(operationResult) 12 | if (android.os.Debug.isDebuggerConnected() && ComposeTestContainer.isInitialized){ 13 | withComposeTestEnvironment { env -> 14 | repeat(advanceFrameAmount) { 15 | env.mainClock.advanceTimeByFrame() 16 | } 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /docs/docs/common/boolean.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 5 3 | --- 4 | 5 | # Boolean result 6 | 7 | While using the **Ultron** framework you always can get the result of any operation as boolean value. 8 | 9 | ```kotlin 10 | object SomePage : Page{ 11 | private val composeElement = hasTestTag("some_tag") 12 | private val espressoElement = withId(R.id.espressoId) 13 | private val espressoWebViewElement = xpath("some_xpath") 14 | private val uiautomatorElement = byResId(R.id.uiatomatorId) 15 | } 16 | ``` 17 | All these elements have `isSuccess` method that allows us to get boolean result. 18 | In case of false it could be executed to long (5 sec by default). So it reasonable to specify custom timeout for some operations. 19 | ```kotlin 20 | composeElement.isSuccess { withTimeout(1_000).assertIsDisplayed() } 21 | espressoElement.isSuccess { withTimeout(2_000).isDisplayed() } 22 | uiautomatorElement.isSuccess { withTimeout(2_000).isDisplayed() } 23 | espressoWebViewElement.isSuccess { withTimeout(2_000).exists() } 24 | ``` 25 | -------------------------------------------------------------------------------- /ultron-allure/src/main/java/com/atiurin/ultron/allure/hierarchy/AllureHierarchyDumper.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.allure.hierarchy 2 | 3 | import com.atiurin.ultron.allure.attachment.AttachUtil 4 | import com.atiurin.ultron.hierarchy.HierarchyDumper 5 | import com.atiurin.ultron.hierarchy.UiDeviceHierarchyDumper 6 | import com.atiurin.ultron.log.UltronLog 7 | import com.atiurin.ultron.utils.createCacheFile 8 | 9 | class AllureHierarchyDumper { 10 | private val dumper: HierarchyDumper = UiDeviceHierarchyDumper() 11 | 12 | fun dumpAndAttach(name: String = "window_hierarchy"): Boolean { 13 | val tempFile = createCacheFile() 14 | val result = dumper.dumpFullWindowHierarchy(tempFile) 15 | val fileName = AttachUtil.attachFile( 16 | name = "$name${result.mimeType.extension}", 17 | file = tempFile, 18 | mimeType = result.mimeType 19 | ) 20 | UltronLog.info("WindowHierarchy file '$fileName' has attached to Allure report") 21 | return result.isSuccess 22 | } 23 | } -------------------------------------------------------------------------------- /sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/DefaultComponentActivityTest.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.tests.compose 2 | 3 | import androidx.compose.material.Text 4 | import androidx.compose.ui.Modifier 5 | import androidx.compose.ui.semantics.semantics 6 | import androidx.compose.ui.semantics.testTag 7 | import androidx.compose.ui.test.hasTestTag 8 | import com.atiurin.ultron.core.compose.createDefaultUltronComposeRule 9 | import com.atiurin.ultron.extensions.assertIsDisplayed 10 | import org.junit.Rule 11 | import org.junit.Test 12 | 13 | class DefaultComponentActivityTest { 14 | @get:Rule 15 | val composeRule = createDefaultUltronComposeRule() 16 | 17 | @Test 18 | fun setContent() { 19 | val testTagValue = "testTag" 20 | composeRule.setContent { 21 | Text(text = "Hello, world!", modifier = Modifier.semantics { testTag = testTagValue }) 22 | } 23 | hasTestTag(testTagValue) 24 | .assertIsDisplayed() 25 | .assertTextEquals("Hello, world!") 26 | } 27 | } -------------------------------------------------------------------------------- /ultron-android/src/main/kotlin/com/atiurin/ultron/custom/espresso/matcher/ElementWithAttributeMatcher.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.custom.espresso.matcher 2 | 3 | import org.hamcrest.Description 4 | import org.hamcrest.Matcher 5 | import org.hamcrest.TypeSafeMatcher 6 | import org.w3c.dom.Element 7 | 8 | open class ElementWithAttributeMatcher( 9 | val attributeName: String, 10 | val attributeValueMatcher: Matcher 11 | ) : 12 | TypeSafeMatcher() { 13 | override fun matchesSafely(element: Element): Boolean { 14 | return attributeValueMatcher.matches(element.getAttribute(attributeName)) 15 | } 16 | 17 | override fun describeTo(description: Description) { 18 | description.appendText("with text content: ") 19 | attributeValueMatcher.describeTo(description) 20 | } 21 | 22 | companion object { 23 | fun withAttribute(attributeName: String, attributeValueMatcher: Matcher) = 24 | ElementWithAttributeMatcher(attributeName, attributeValueMatcher) 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /sample-app/src/main/java/com/atiurin/sampleapp/async/UseCase.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.async 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.coroutineScope 5 | import kotlinx.coroutines.launch 6 | 7 | 8 | /** 9 | * Base class for a `coroutine` use case. 10 | */ 11 | abstract class UseCase where Type : Any { 12 | 13 | /** 14 | * Runs the actual logic of the use case. 15 | */ 16 | abstract suspend fun run(params: Params): Either 17 | 18 | suspend operator fun invoke(params: Params, onSuccess: (Type) -> Unit, onFailure: (Exception) -> Unit) { 19 | val result = run(params) 20 | coroutineScope { 21 | launch(Dispatchers.Main) { 22 | result.fold( 23 | failed = { onFailure(it) }, 24 | succeeded = { onSuccess(it) } 25 | ) 26 | } 27 | } 28 | } 29 | 30 | /** 31 | * Placeholder for a use case that doesn't need any input parameters. 32 | */ 33 | object None 34 | } -------------------------------------------------------------------------------- /ultron-allure/src/main/java/com/atiurin/ultron/allure/UltronAllureTestRunner.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.allure 2 | 3 | import android.app.Instrumentation 4 | import android.os.Bundle 5 | import com.atiurin.ultron.allure.runner.UltronAllureRunInformer 6 | import com.atiurin.ultron.allure.runner.UltronTestRunListener 7 | import com.atiurin.ultron.extensions.putArguments 8 | import com.atiurin.ultron.runner.UltronRunInformer 9 | import io.qameta.allure.android.runners.AllureAndroidJUnitRunner 10 | 11 | open class UltronAllureTestRunner : AllureAndroidJUnitRunner() { 12 | val informer: UltronRunInformer = UltronAllureRunInformer() 13 | 14 | override fun onCreate(arguments: Bundle) { 15 | arguments.putArguments("listener", UltronTestRunListener::class.qualifiedName!!) 16 | super.onCreate(arguments) 17 | } 18 | } 19 | 20 | fun Instrumentation.getRunInformer() : UltronRunInformer { 21 | return requireNotNull((this as? UltronAllureTestRunner)?.informer) { 22 | "Set testInstrumentationRunner = ${UltronAllureTestRunner::class.qualifiedName}" 23 | } 24 | } -------------------------------------------------------------------------------- /sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/WebViewPage.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.pages 2 | 3 | import com.atiurin.ultron.core.espressoweb.webelement.UltronWebElements.Companion.classNames 4 | import com.atiurin.ultron.core.espressoweb.webelement.UltronWebElement.Companion.className 5 | import com.atiurin.ultron.core.espressoweb.webelement.UltronWebElement.Companion.id 6 | import com.atiurin.ultron.page.Page 7 | 8 | class WebViewPage : Page() { 9 | companion object{ 10 | const val BUTTON2_TITLE = "button2 clicked" 11 | const val APPLE_LINK_TEXT = "Apple" 12 | const val APPLE_LINK_HREF = "fake_link.html" 13 | 14 | } 15 | val textInput = id("text_input") 16 | val buttonUpdTitle = id("button1") 17 | val buttonSetTitle2 = id("button2") 18 | val buttonSetTitleActive = id("button3") 19 | val title = id("title") 20 | val titleWithCss = className("css_title") 21 | val appleLink = id("apple_link") 22 | val buttons = classNames("button") 23 | val notExistedElement = id("Not existed element") 24 | } 25 | -------------------------------------------------------------------------------- /sample-app/src/main/java/com/atiurin/sampleapp/idlingresources/AbstractIdlingResource.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.idlingresources 2 | 3 | import androidx.annotation.Nullable 4 | import androidx.test.espresso.IdlingResource 5 | import java.util.concurrent.atomic.AtomicBoolean 6 | import androidx.test.espresso.IdlingResource.ResourceCallback 7 | 8 | abstract class AbstractIdlingResource : IdlingResource { 9 | @Nullable 10 | @Volatile 11 | private var mCallback: ResourceCallback? = null 12 | private val mIsIdleNow = AtomicBoolean(true) 13 | override fun getName(): String { 14 | return this.javaClass.name 15 | } 16 | 17 | override fun isIdleNow(): Boolean { 18 | return mIsIdleNow.get() 19 | } 20 | 21 | override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) { 22 | mCallback = callback 23 | } 24 | 25 | fun setIdleState(isIdleNow: Boolean) { 26 | mIsIdleNow.set(isIdleNow) 27 | if (isIdleNow && mCallback != null) { 28 | mCallback?.onTransitionToIdle() 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /ultron-android/src/main/kotlin/com/atiurin/ultron/core/espresso/recyclerview/RecyclerViewItemExecutor.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.espresso.recyclerview 2 | 3 | import android.view.View 4 | import androidx.recyclerview.widget.RecyclerView 5 | import androidx.test.espresso.Espresso.onView 6 | import androidx.test.espresso.ViewInteraction 7 | import com.atiurin.ultron.core.espresso.UltronEspressoInteraction 8 | import org.hamcrest.Matcher 9 | 10 | interface RecyclerViewItemExecutor { 11 | fun scrollToItem(offset: Int = 0) 12 | fun getItemMatcher(): Matcher 13 | fun getItemViewHolder(): RecyclerView.ViewHolder? 14 | fun getItemInteraction(): UltronEspressoInteraction = UltronEspressoInteraction(onView(getItemMatcher())) 15 | fun getItemChildMatcher(childMatcher: Matcher): Matcher 16 | fun getItemChildInteraction(childInteraction: UltronEspressoInteraction): UltronEspressoInteraction = UltronEspressoInteraction( 17 | onView((getItemChildMatcher(childInteraction.getInteractionMatcher()!!))) 18 | ) 19 | } -------------------------------------------------------------------------------- /composeApp/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso_web/UltronWebElementsTest.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.tests.espresso_web 2 | 3 | import com.atiurin.sampleapp.framework.Log 4 | import com.atiurin.ultron.core.espressoweb.webelement.UltronWebElements.Companion.classNames 5 | import org.junit.Assert 6 | import org.junit.Test 7 | 8 | class UltronWebElementsTest : BaseWebViewTest() { 9 | @Test 10 | fun getSizeTest() { 11 | val buttonsAmount = classNames("button").getSize() 12 | Assert.assertTrue(buttonsAmount == 3) 13 | } 14 | 15 | @Test 16 | fun getSize_notExistedElement() { 17 | Log.debug(">>>" + classNames("not_existed_classname").getSize()) 18 | // AssertUtils.assertException { } 19 | } 20 | 21 | @Test 22 | fun getListElementTest(){ 23 | classNames("link").getElements() 24 | .find { ultronWebElement -> 25 | ultronWebElement.isSuccess { 26 | withTimeout(100).hasText("Apple") 27 | } 28 | }?.webClick() 29 | page.title.containsText("apple") 30 | } 31 | } -------------------------------------------------------------------------------- /sample-app/src/main/java/com/atiurin/sampleapp/activity/CustomClicksActivity.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.activity 2 | 3 | import android.os.Bundle 4 | import androidx.activity.enableEdgeToEdge 5 | import androidx.appcompat.app.AppCompatActivity 6 | import com.atiurin.sampleapp.R 7 | import com.atiurin.sampleapp.async.task.CompatAsyncTask 8 | import com.atiurin.sampleapp.async.task.CompatAsyncTask.Companion.ASYNC 9 | 10 | class CustomClicksActivity : AppCompatActivity() { 11 | 12 | private val compatAsyncTask = CompatAsyncTask() 13 | 14 | override fun onCreate(savedInstanceState: Bundle?) { 15 | enableEdgeToEdge() 16 | super.onCreate(savedInstanceState) 17 | setContentView(R.layout.activity_custom_clicks) 18 | if(shouldBeAsyncTaskStart()) { 19 | startCompatAsyncTask() 20 | } 21 | } 22 | 23 | fun shouldBeAsyncTaskStart(): Boolean = intent.getBooleanExtra(ASYNC, false) 24 | 25 | fun startCompatAsyncTask() { 26 | compatAsyncTask.start() 27 | } 28 | 29 | fun stopCompatAsyncTask() { 30 | compatAsyncTask.stop() 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/CollectionInteractionTest.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.tests.compose 2 | 3 | import androidx.compose.ui.test.hasTestTag 4 | import com.atiurin.sampleapp.activity.ComposeListActivity 5 | import com.atiurin.sampleapp.compose.contactNameTestTag 6 | import com.atiurin.sampleapp.compose.contactsListTestTag 7 | import com.atiurin.sampleapp.data.repositories.CONTACTS 8 | import com.atiurin.sampleapp.tests.BaseTest 9 | import com.atiurin.ultron.core.compose.createSimpleUltronComposeRule 10 | import com.atiurin.ultron.core.compose.operation.UltronComposeCollectionInteraction.Companion.allNodes 11 | import org.junit.Rule 12 | import org.junit.Test 13 | 14 | class CollectionInteractionTest: BaseTest() { 15 | @get:Rule 16 | val composeRule = createSimpleUltronComposeRule() 17 | val list = hasTestTag(contactsListTestTag) 18 | @Test 19 | fun allNodes_getByIndex(){ 20 | val index = 4 21 | val contact = CONTACTS[index] 22 | allNodes(hasTestTag(contactNameTestTag), true).get(index).assertTextContains(contact.name) 23 | } 24 | } -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/listeners/AbstractListenersContainer.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.listeners 2 | 3 | import kotlin.reflect.KClass 4 | 5 | abstract class AbstractListenersContainer { 6 | private var listeners: MutableList = mutableListOf() 7 | 8 | open fun getListeners(): List { 9 | return listeners 10 | } 11 | 12 | fun addListener(listener: T) { 13 | val exist = listeners.find { it.id == listener.id } 14 | exist?.let { listeners.remove(it) } 15 | listeners.add(listener) 16 | } 17 | 18 | fun clearListeners() { 19 | listeners.clear() 20 | } 21 | 22 | fun removeListener(listenerId: String) { 23 | val exist = listeners.find { it.id == listenerId } 24 | if (exist != null) { 25 | listeners.remove(exist) 26 | } 27 | } 28 | 29 | fun removeListener(listenerClass: KClass) { 30 | val exist = listeners.find { it.id == listenerClass.simpleName } 31 | if (exist != null) { 32 | listeners.remove(exist) 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/core/compose/operation/UltronComposeOperation.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.compose.operation 2 | 3 | import com.atiurin.ultron.core.common.* 4 | import com.atiurin.ultron.core.common.assertion.DefaultOperationAssertion 5 | import com.atiurin.ultron.core.common.assertion.OperationAssertion 6 | 7 | class UltronComposeOperation( 8 | val operationBlock: () -> Unit, 9 | override val name: String, 10 | override val type: UltronOperationType, 11 | override val description: String, 12 | override val timeoutMs: Long, 13 | override val assertion: OperationAssertion = DefaultOperationAssertion(""){}, 14 | override val elementInfo: ElementInfo = DefaultElementInfo() 15 | ) : Operation { 16 | override fun execute(): OperationIterationResult { 17 | var success = true 18 | var exception: Throwable? = null 19 | try { 20 | operationBlock() 21 | }catch (error: Throwable){ 22 | success = false 23 | exception = error 24 | } 25 | return DefaultOperationIterationResult(success, exception) 26 | } 27 | } -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/annotations/ExperimentalUltronApi.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.annotations 2 | 3 | import kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS 4 | import kotlin.annotation.AnnotationTarget.CLASS 5 | import kotlin.annotation.AnnotationTarget.CONSTRUCTOR 6 | import kotlin.annotation.AnnotationTarget.FIELD 7 | import kotlin.annotation.AnnotationTarget.FUNCTION 8 | import kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE 9 | import kotlin.annotation.AnnotationTarget.PROPERTY 10 | import kotlin.annotation.AnnotationTarget.PROPERTY_GETTER 11 | import kotlin.annotation.AnnotationTarget.PROPERTY_SETTER 12 | import kotlin.annotation.AnnotationTarget.TYPEALIAS 13 | import kotlin.annotation.AnnotationTarget.VALUE_PARAMETER 14 | 15 | @RequiresOptIn 16 | @MustBeDocumented 17 | @Target( 18 | CLASS, 19 | ANNOTATION_CLASS, 20 | PROPERTY, 21 | FIELD, 22 | LOCAL_VARIABLE, 23 | VALUE_PARAMETER, 24 | CONSTRUCTOR, 25 | FUNCTION, 26 | PROPERTY_GETTER, 27 | PROPERTY_SETTER, 28 | TYPEALIAS 29 | ) 30 | @Retention(AnnotationRetention.BINARY) 31 | public annotation class ExperimentalUltronApi -------------------------------------------------------------------------------- /ultron-common/src/androidMain/kotlin/com/atiurin/ultron/testlifecycle/setupteardown/SetUpRule.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.testlifecycle.setupteardown 2 | 3 | import org.junit.runner.Description 4 | 5 | open class SetUpRule(override val name: String = "") : ConditionRule(name) { 6 | override fun starting(description: Description) { 7 | val keys = mutableListOf().apply { this.addAll(commonConditionKeys) } 8 | val method = description.testClass.getMethod(getMethodName(description.methodName)) 9 | if (method.isAnnotationPresent(SetUp::class.java)) { 10 | val setUpAnnotation = method.getAnnotation(SetUp::class.java) 11 | if (setUpAnnotation != null) { 12 | keys.addAll(setUpAnnotation.value.toList()) //get the list of keys in annotation SetUp 13 | } 14 | conditionsExecutor.before(name, this::class) 15 | conditionsExecutor.execute(conditions, keys, name) 16 | } else { 17 | conditionsExecutor.before(name, this::class) 18 | conditionsExecutor.execute(conditions, commonConditionKeys, name) 19 | } 20 | super.starting(description) 21 | } 22 | } -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/file/MimeType.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.file 2 | 3 | enum class MimeType(val extension: String, val value: String) { 4 | /** JPEG images */ 5 | JPEG(".jpeg", "image/jpeg"), 6 | 7 | /** Portable Network Graphics */ 8 | PNG(".png", "image/png"), 9 | 10 | /** WEBP image */ 11 | WEBP(".webp", "image/webp"), 12 | 13 | JSON(".json", "application/json"), 14 | 15 | /** Adobe Portable Document Format (PDF) */ 16 | PDF(".pdf", "application/pdf"), 17 | 18 | /** MP4 audio */ 19 | MP4(".mp4", "audio/mp4"), 20 | 21 | /** AVI: Audio Video Interleave */ 22 | AVI(".avi", "video/x-msvideo"), 23 | 24 | /** MPEG Video */ 25 | MPEG(".mpeg", "video/mpeg"), 26 | 27 | /** Cascading Style Sheets (CSS) */ 28 | CSS(".css", "text/css"), 29 | 30 | /** Comma-separated values (CSV) */ 31 | CSV(".csv", "text/csv"), 32 | 33 | /** HyperText Markup Language (HTML) */ 34 | HTML(".html", "text/html"), 35 | 36 | /** Text, (generally ASCII or ISO 8859-n) */ 37 | PLAIN_TEXT(".txt", "text/plain"), 38 | 39 | /** YAML */ 40 | YAML(".yaml", "text/yaml"), 41 | 42 | XML(".xml", "text/xml") 43 | } 44 | -------------------------------------------------------------------------------- /sample-app/src/main/java/com/atiurin/sampleapp/activity/UiBlockActivity.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.activity 2 | 3 | import android.os.Bundle 4 | import android.widget.LinearLayout 5 | import android.widget.TextView 6 | import androidx.appcompat.app.AppCompatActivity 7 | import com.atiurin.sampleapp.R 8 | import com.atiurin.sampleapp.data.repositories.CONTACTS 9 | 10 | class UiBlockActivity : AppCompatActivity() { 11 | override fun onCreate(savedInstanceState: Bundle?) { 12 | super.onCreate(savedInstanceState) 13 | setContentView(R.layout.activity_uiblock) 14 | val contactItem1: LinearLayout = this.findViewById(R.id.contact_item_1) 15 | val contactItem2: LinearLayout = this.findViewById(R.id.contact_item_2) 16 | contactItem1.findViewById(R.id.name).text = CONTACTS[0].name 17 | contactItem1.findViewById(R.id.status).text = CONTACTS[0].status 18 | contactItem2.findViewById(R.id.name).text = CONTACTS[1].name 19 | contactItem2.findViewById(R.id.status).text = CONTACTS[1].status 20 | } 21 | 22 | override fun onSupportNavigateUp(): Boolean { 23 | onBackPressed() 24 | return true 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /sample-app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 14 | 19 | 20 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /sample-app/src/main/java/com/atiurin/sampleapp/activity/WebViewActivity.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.activity 2 | 3 | import android.os.Build 4 | import android.os.Bundle 5 | import android.text.Editable 6 | import android.text.TextWatcher 7 | import android.view.View 8 | import android.view.View.* 9 | import android.webkit.WebView 10 | import android.widget.* 11 | import androidx.activity.enableEdgeToEdge 12 | import androidx.appcompat.app.AppCompatActivity 13 | import com.atiurin.sampleapp.R 14 | 15 | class WebViewActivity : AppCompatActivity() { 16 | override fun onCreate(savedInstanceState: Bundle?) { 17 | enableEdgeToEdge() 18 | super.onCreate(savedInstanceState) 19 | setContentView(R.layout.activity_webview) 20 | val webView: WebView = findViewById(R.id.webview) 21 | webView.settings.javaScriptEnabled = true 22 | val customHtml = applicationContext.assets.open("webview.html").reader().readText() 23 | webView.loadData(customHtml, "text/html", "UTF-8") 24 | } 25 | 26 | override fun onSupportNavigateUp(): Boolean { 27 | onBackPressed() 28 | return true 29 | } 30 | 31 | override fun onResume() { 32 | super.onResume() 33 | } 34 | } -------------------------------------------------------------------------------- /ultron-common/src/androidMain/kotlin/com/atiurin/ultron/testlifecycle/setupteardown/TearDownRule.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.testlifecycle.setupteardown 2 | 3 | import org.junit.runner.Description 4 | 5 | open class TearDownRule(override val name: String = "") : ConditionRule(name), RuleSequenceTearDown { 6 | override fun finished(description: Description) { 7 | val keys = mutableListOf().apply { this.addAll(commonConditionKeys) } 8 | val method = description.testClass.getMethod(getMethodName(description.methodName)) 9 | if (method.isAnnotationPresent(TearDown::class.java)) { 10 | val tearDownAnnotation = method.getAnnotation(TearDown::class.java) 11 | if (tearDownAnnotation != null) { 12 | keys.addAll(tearDownAnnotation.value.toList()) //get the list of keys in annotation TearDown 13 | } 14 | conditionsExecutor.before(name, this::class) 15 | conditionsExecutor.execute(conditions, keys, name) 16 | } else { 17 | conditionsExecutor.before(name, this::class) 18 | conditionsExecutor.execute(conditions, commonConditionKeys, name) 19 | } 20 | super.finished(description) 21 | } 22 | } -------------------------------------------------------------------------------- /sample-app/src/main/res/layout/app_bar_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 14 | 15 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ultron-allure/src/main/java/com/atiurin/ultron/allure/runner/UltronTestRunListener.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.allure.runner 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import com.atiurin.ultron.allure.getRunInformer 5 | import org.junit.runner.Description 6 | import org.junit.runner.Result 7 | import org.junit.runner.notification.Failure 8 | import org.junit.runner.notification.RunListener 9 | 10 | class UltronTestRunListener : RunListener() { 11 | private val informer = InstrumentationRegistry.getInstrumentation().getRunInformer() 12 | 13 | override fun testRunStarted(description: Description) = informer.testRunStarted(description) 14 | override fun testStarted(description: Description) = informer.testStarted(description) 15 | override fun testFinished(description: Description) = informer.testFinished(description) 16 | override fun testFailure(failure: Failure) = informer.testFailure(failure) 17 | override fun testAssumptionFailure(failure: Failure) = informer.testAssumptionFailure(failure) 18 | override fun testIgnored(description: Description) = informer.testIgnored(description) 19 | override fun testRunFinished(result: Result) = informer.testRunFinished(result) 20 | } 21 | 22 | -------------------------------------------------------------------------------- /ultron-common/src/androidMain/kotlin/com/atiurin/ultron/runner/UltronRunInformer.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.runner 2 | 3 | import com.atiurin.ultron.listeners.AbstractListenersContainer 4 | import org.junit.runner.Description 5 | import org.junit.runner.Result 6 | import org.junit.runner.notification.Failure 7 | 8 | abstract class UltronRunInformer : AbstractListenersContainer(), RunListener { 9 | override fun testRunStarted(description: Description) = getListeners().forEach { it.testRunStarted(description) } 10 | override fun testStarted(description: Description) = getListeners().forEach { it.testStarted(description) } 11 | override fun testFinished(description: Description) = getListeners().forEach { it.testFinished(description) } 12 | override fun testFailure(failure: Failure) { 13 | getListeners().forEach { it.testFailure(failure) } 14 | } 15 | override fun testAssumptionFailure(failure: Failure) = getListeners().forEach { it.testAssumptionFailure(failure) } 16 | override fun testIgnored(description: Description) = getListeners().forEach { it.testIgnored(description) } 17 | override fun testRunFinished(result: Result) = getListeners().forEach { it.testRunFinished(result) } 18 | } -------------------------------------------------------------------------------- /sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/TreeTest.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.tests.compose 2 | 3 | import androidx.compose.ui.test.onRoot 4 | import androidx.compose.ui.test.printToString 5 | import com.atiurin.sampleapp.activity.ComposeElementsActivity 6 | import com.atiurin.sampleapp.pages.ComposeElementsPage 7 | import com.atiurin.sampleapp.tests.BaseTest 8 | import com.atiurin.ultron.allure.attachment.AttachUtil 9 | import com.atiurin.ultron.core.compose.createSimpleUltronComposeRule 10 | import com.atiurin.ultron.file.MimeType 11 | import com.atiurin.ultron.log.UltronLog 12 | import com.atiurin.ultron.utils.createCacheFile 13 | import org.junit.Test 14 | 15 | class TreeTest : BaseTest() { 16 | val page = ComposeElementsPage 17 | val composeRule = createSimpleUltronComposeRule() 18 | init { 19 | ruleSequence.add(composeRule) 20 | } 21 | @Test 22 | fun generateSemanticsTreeTest(){ 23 | val node = composeRule.onRoot(useUnmergedTree = true).printToString() 24 | val file = createCacheFile("tree_", ".log") 25 | file.writeText(node) 26 | val fileName = AttachUtil.attachFile(file, MimeType.PLAIN_TEXT) 27 | UltronLog.error(node) 28 | } 29 | } -------------------------------------------------------------------------------- /ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/test/TestMethod.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.test 2 | 3 | import com.atiurin.ultron.core.config.UltronCommonConfig 4 | import com.atiurin.ultron.core.test.context.UltronTestContext 5 | 6 | class TestMethod(testContext: UltronTestContext) { 7 | init { 8 | UltronCommonConfig.testContext = testContext 9 | } 10 | 11 | private var beforeTest: TestMethod.() -> Unit = {} 12 | private var afterTest: TestMethod.() -> Unit = {} 13 | private var test: TestMethod.() -> Unit = {} 14 | 15 | internal fun attack() { 16 | var throwable: Throwable? = null 17 | beforeTest() 18 | runCatching(test).onFailure { ex -> 19 | throwable = ex 20 | } 21 | runCatching(afterTest).onFailure { ex -> 22 | throwable?.let { throw it } 23 | throw ex 24 | } 25 | throwable?.let { throw it } 26 | } 27 | 28 | fun before(block: TestMethod.() -> Unit) = apply { 29 | beforeTest = block 30 | } 31 | 32 | fun after(block: TestMethod.() -> Unit) = apply { 33 | afterTest = block 34 | } 35 | 36 | fun go(block: TestMethod.() -> Unit) = apply { 37 | test = block 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /sample-app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 16 | 17 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/UiObjectElementsPage.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.pages 2 | 3 | import com.atiurin.sampleapp.R 4 | import com.atiurin.ultron.core.uiautomator.uiobject.UltronUiObject.Companion.uiResId 5 | 6 | class UiObjectElementsPage { 7 | val notExistedObject = uiResId(R.id.send_button) 8 | val button = uiResId(R.id.button1) 9 | val eventStatus = uiResId(R.id.last_event_status) 10 | val radioGroup = uiResId(R.id.radio_group_visibility) 11 | val radioVisibleButton = uiResId(R.id.radio_visible) 12 | val radioInvisibleButton = uiResId(R.id.radio_invisible) 13 | val radioGoneButton = uiResId(R.id.radio_gone) 14 | val checkBoxClickable = uiResId(R.id.checkbox_clickable) 15 | val checkBoxEnabled = uiResId(R.id.checkbox_enable) 16 | val checkBoxSelected = uiResId(R.id.checkbox_selected) 17 | val checkBoxFocusable = uiResId(R.id.checkbox_focusable) 18 | val checkBoxJsEnabled = uiResId(R.id.checkbox_js_enabled) 19 | val editTextContentDesc = uiResId(R.id.et_contentDesc) 20 | val webView = uiResId(R.id.webview) 21 | val appCompatTextView = uiResId(R.id.app_compat_text) 22 | val swipableImageView = uiResId(R.id.swipe_image_view) 23 | val emptyImageView = uiResId(R.id.empty_image_view) 24 | } -------------------------------------------------------------------------------- /ultron-allure/src/main/java/com/atiurin/ultron/allure/attachment/AttachUtil.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.allure.attachment 2 | 3 | import com.atiurin.ultron.file.MimeType 4 | import io.qameta.allure.kotlin.Allure 5 | import io.qameta.allure.kotlin.AllureLifecycle 6 | import java.io.File 7 | import java.io.InputStream 8 | 9 | object AttachUtil { 10 | /** 11 | * @return allure file name 12 | */ 13 | fun attachFile(file: File, mimeType: MimeType) = attachFile( 14 | name = file.name, 15 | file = file, 16 | mimeType = mimeType 17 | ) 18 | 19 | /** 20 | * @return allure file name 21 | */ 22 | fun attachFile(name: String, file: File, mimeType: MimeType) = Allure.lifecycle.writeFile( 23 | name = name, 24 | stream = file.inputStream(), 25 | type = mimeType.value, 26 | fileExtension = mimeType.extension 27 | ) 28 | } 29 | 30 | fun AllureLifecycle.writeFile(name: String, stream: InputStream, type: String?, fileExtension: String?): String { 31 | val source = prepareAttachment( 32 | name = name, 33 | type = type, 34 | fileExtension = fileExtension 35 | ) 36 | writeAttachment( 37 | attachmentSource = source, 38 | stream = stream 39 | ) 40 | return source 41 | } -------------------------------------------------------------------------------- /ultron-allure/src/main/java/com/atiurin/ultron/allure/runner/UltronLogAttachRunListener.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.allure.runner 2 | 3 | import com.atiurin.ultron.allure.attachment.AttachUtil 4 | import com.atiurin.ultron.allure.config.UltronAllureConfig 5 | import com.atiurin.ultron.core.config.UltronCommonConfig 6 | import com.atiurin.ultron.file.MimeType 7 | import com.atiurin.ultron.log.UltronLog 8 | import com.atiurin.ultron.runner.UltronRunListener 9 | import org.junit.runner.notification.Failure 10 | import java.io.File 11 | 12 | class UltronLogAttachRunListener : UltronRunListener() { 13 | override fun testFailure(failure: Failure) { 14 | if (UltronAllureConfig.params.attachUltronLog ){ 15 | if (!UltronCommonConfig.logToFile){ 16 | UltronLog.error("Ultron doesn't log into file. " + 17 | "Change config param UltronConfig.edit { logToFile = true }" 18 | ) 19 | return 20 | } 21 | val fileName = AttachUtil.attachFile( 22 | file = File(UltronLog.fileLogger.getLogFilePath()), 23 | mimeType = MimeType.PLAIN_TEXT 24 | ) 25 | UltronLog.info("Ultron log file '$fileName' has attached to Allure report") 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/RunUltronUiTest.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.sampleapp.tests.compose 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.material.Button 5 | import androidx.compose.material.Text 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.platform.testTag 8 | import androidx.compose.ui.test.ExperimentalTestApi 9 | import androidx.compose.ui.test.hasTestTag 10 | import com.atiurin.ultron.core.compose.runUltronUiTest 11 | import com.atiurin.ultron.extensions.assertTextContains 12 | import com.atiurin.ultron.extensions.isSuccess 13 | import org.junit.Test 14 | import kotlin.test.assertTrue 15 | 16 | @OptIn(ExperimentalTestApi::class) 17 | class RunUltronUiTest { 18 | 19 | @Test 20 | fun useUnmergedTreeConfigTest() = runUltronUiTest { 21 | val testTag = "element" 22 | setContent { 23 | Column { 24 | Button(onClick = {}, modifier = Modifier.testTag(testTag)) { 25 | Text("Text1") 26 | Text("Text2") 27 | } 28 | } 29 | } 30 | assertTrue ("Ultron operation success should be true") { 31 | hasTestTag(testTag).isSuccess { assertTextContains("Text1") } 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /ultron-android/src/main/kotlin/com/atiurin/ultron/core/espresso/recyclerview/RecyclerViewScrollToPositionViewAction.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.core.espresso.recyclerview 2 | 3 | import android.view.View 4 | import androidx.recyclerview.widget.RecyclerView 5 | import androidx.test.espresso.UiController 6 | import androidx.test.espresso.ViewAction 7 | import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom 8 | import androidx.test.espresso.matcher.ViewMatchers.isDisplayed 9 | import com.atiurin.ultron.extensions.instantScrollToPosition 10 | import kotlinx.coroutines.delay 11 | import kotlinx.coroutines.runBlocking 12 | import org.hamcrest.Matcher 13 | import org.hamcrest.Matchers.allOf 14 | 15 | internal class RecyclerViewScrollToPositionViewAction ( 16 | private val position: Int 17 | ) : ViewAction { 18 | override fun getConstraints(): Matcher { 19 | return allOf(isAssignableFrom(RecyclerView::class.java), isDisplayed()) 20 | } 21 | 22 | override fun getDescription(): String { 23 | return "scroll RecyclerView to position: $position" 24 | } 25 | 26 | override fun perform(uiController: UiController, view: View) { 27 | val recyclerView = view as RecyclerView 28 | recyclerView.instantScrollToPosition(position, 0.5f) 29 | runBlocking { delay(10) } 30 | } 31 | } -------------------------------------------------------------------------------- /ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/extensions/SemanticsNodeInteractionExt.kt: -------------------------------------------------------------------------------- 1 | package com.atiurin.ultron.extensions 2 | 3 | import androidx.compose.ui.semantics.SemanticsNode 4 | import androidx.compose.ui.semantics.SemanticsPropertyKey 5 | import androidx.compose.ui.test.SemanticsNodeInteraction 6 | 7 | fun SemanticsNodeInteraction.getConfigField(name: String): Any? { 8 | for ((key, value) in this.fetchSemanticsNode().config) { 9 | if (key.name == name) { 10 | return value 11 | } 12 | } 13 | return null 14 | } 15 | 16 | fun SemanticsNodeInteraction.getOneOfConfigFields(names: List): Any? { 17 | names.forEach { name -> 18 | val value = getConfigField(name) 19 | value?.let { return it } 20 | } 21 | return null 22 | } 23 | 24 | fun SemanticsNodeInteraction.requireSemantics( 25 | node: SemanticsNode, 26 | vararg properties: SemanticsPropertyKey<*>, 27 | errorMessage: () -> String 28 | ) { 29 | val missingProperties = properties.filter { it !in node.config } 30 | if (missingProperties.isNotEmpty()) { 31 | val msg = "${errorMessage()}, the node is missing [${missingProperties.joinToString()}]" 32 | throw AssertionError(msg) 33 | } 34 | } 35 | 36 | expect fun SemanticsNodeInteraction.getSelectorDescription(): String -------------------------------------------------------------------------------- /sample-app/src/main/res/layout/activity_uiblock.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | 20 | 21 | 27 | 28 | 29 | 30 | 31 | --------------------------------------------------------------------------------