├── 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 |
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 |
15 |
16 |
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 |
--------------------------------------------------------------------------------