[
  {
    "path": ".github/workflows/ci-pipeline.yml",
    "content": "name: MultiplatformCI\n\non:\n  push:\n    branches: [ master ]\n  pull_request:\n    branches: [ master ]\n\njobs:\n  compileKotlin:\n    runs-on: macos-latest\n    steps:\n    - uses: actions/checkout@v4\n    - uses: actions/setup-java@v4\n      with:\n        distribution: 'adopt'\n        java-version: '17'\n\n    - name: Compile framework\n      run: ./gradlew compileDebugKotlin compileDebugKotlinAndroid compileKotlinDesktop compileKotlinIosArm64 compileKotlinIosSimulatorArm64 compileKotlinJs compileKotlinWasmJs\n"
  },
  {
    "path": ".github/workflows/docs.yml",
    "content": "name: Build and deploy docs\n\non:\n  push:\n    branches:\n      - master\n\njobs:\n  github-pages:\n    runs-on: ubuntu-22.04\n    steps:\n      - uses: actions/checkout@v3\n      - uses: actions/setup-node@v4\n        with:\n          node-version: 22\n      - run: npm install\n        working-directory: ./docs\n      - run: npm run build\n        working-directory: ./docs\n\n      - name: Deploy to GitHub Pages\n        uses: peaceiris/actions-gh-pages@v3\n        with:\n          github_token: ${{ secrets.GITHUB_TOKEN }}\n          publish_dir: ./docs/build"
  },
  {
    "path": ".github/workflows/maven_central_publish.yml",
    "content": "name: Publish\n\npermissions:\n  contents: read\n\non:\n  push:\n    branches:\n      - 'release/*'\n\njobs:\n  publish:\n    name: Publish to Maven Central Portal\n    runs-on: macos-latest\n    strategy:\n      matrix:\n        include:\n          - target: :ultron-common:publishToMavenCentral\n          - target: :ultron-compose:publishToMavenCentral\n          - target: :ultron-android:publishToMavenCentral\n          - target: :ultron-allure:publishToMavenCentral\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v3\n\n      - name: Validate Gradle Wrapper\n        uses: gradle/wrapper-validation-action@v1\n\n      - name: Setup JDK 17\n        uses: actions/setup-java@v3\n        with:\n          java-version: '17'\n          distribution: \"zulu\"\n\n      - name: Import GPG key\n        uses: crazy-max/ghaction-import-gpg@v6\n        with:\n          gpg_private_key: ${{ secrets.OSSRH_GPG_SECRET_KEY }}\n          passphrase: ${{ secrets.OSSRH_GPG_SECRET_KEY_PASSWORD }}\n      - name: Publish to MavenCentral\n        run: ./gradlew \"${{ matrix.target }}\" --no-configuration-cache\n        env:\n          ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_USER }}\n          ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_TOKEN }}\n          ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.SIGNING_KEY_ID }}\n          ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }}\n          ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_KEY_CONTENTS }}\n"
  },
  {
    "path": ".gitignore",
    "content": "*.iml\n.gradle\n/local.properties\n/.idea\n/.idea/caches\n/.idea/libraries\n/.idea/modules.xml\n/.idea/workspace.xml\n/.idea/navEditor.xml\n/.idea/assetWizardSettings.xml\n.DS_Store\nbuild/\n/captures\n.externalNativeBuild\n/allure-results\n/.kotlin"
  },
  {
    "path": "LICENSE",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License."
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n<img src=\"https://user-images.githubusercontent.com/12834123/252489846-db6cb0f8-6b28-4ae4-bceb-8b5907f1d59f.png#gh-light-mode-only\" width=600>\n<img src=\"https://user-images.githubusercontent.com/12834123/252498170-61e5a440-c2b5-42ea-8bfb-91ee12248422.png#gh-dark-mode-only\" width=600>\n</p>\n\n<div align=\"center\">\n\n[![Documentation][documentation-badge]][documentation]\n[![Releases][releases-badge]][releases]\n[![Telegram][telegram-badge]][telegram]\n\n</div>\n\nUltron is the simplest framework to develop UI tests for **Android** & **Compose Multiplatform**.\n\nIt's constructed upon the Espresso, UI Automator and Compose UI testing frameworks. Ultron introduces a range of remarkable new features. Furthermore, Ultron puts you in complete control of your tests!\n\nYou don't need to learn any new classes or special syntax. All magic actions and assertions are provided from crunch. Ultron can be easially customised and extended. Wish you exclusively stable tests!\n\n## What are the benefits of using the framework?\n\n- Page/Screen Object pattern support\n- Exceptional simplification for [**Compose UI tests**](https://open-tool.github.io/ultron/docs/compose/index)\n- Out-of-the-box generation of [**Allure report**](https://open-tool.github.io/ultron/docs/common/allure) (Now, for Android UI tests only)\n- A straightforward and expressive syntax\n- Ensured **Stability** for all actions and assertions\n- Complete control over every action and assertion\n- Incredible interaction with lists: [**RecyclerView**](./android/recyclerview.md) and [**Compose LazyList**](https://open-tool.github.io/ultron/docs/compose/lazylist).\n- An **Architectural** approach to developing UI tests (search \"Best practice\")\n- An incredible mechanism for setups and teardowns (You can even set up preconditions for a single test within a test class, without affecting the others)\n- [The ability to effortlessly extend the framework with your own operations](https://open-tool.github.io/ultron/docs/common/extension)\n- Accelerated UI Automator operations\n- Ability to monitor each stage of operation execution with [Listeners](https://open-tool.github.io/ultron/docs/common/listeners)\n- [Custom operation assertions](https://open-tool.github.io/ultron/docs/common/customassertion)\n\n***\n### Documentation\nThe framework offers an excellent [documentation](https://open-tool.github.io/ultron/docs/) that addresses the majority of significant usage scenarios.\n\n### A few words about syntax\n\nThe standard syntax provided by Google is intricate and not intuitive. This is especially evident when dealing with **LazyList** and **RecyclerView** interactions.\n\nLet's explore some examples:\n\n#### 1. Simple compose operation (refer to the doc [here](https://open-tool.github.io/ultron/docs/compose/index))\n\n_Compose framework_\n\n```kotlin\ncomposeTestRule.onNode(hasTestTag(\"Continue\")).performClick()\ncomposeTestRule.onNodeWithText(\"Welcome\").assertIsDisplayed()\n```\n_Ultron_\n\n```kotlin\nhasTestTag(\"Continue\").click()\nhasText(\"Welcome\").assertIsDisplayed()\n```\n\n#### 2. Compose list operation (refer to the [doc](https://open-tool.github.io/ultron/docs/compose/lazylist))\n\n_Compose framework_\n\n```kotlin\nval itemMatcher = hasText(contact.name)\ncomposeRule\n    .onNodeWithTag(contactsListTestTag)\n    .performScrollToNode(itemMatcher)\n    .onChildren()\n    .filterToOne(itemMatcher)\n    .assertTextContains(contact.name)\n```\n\n_Ultron_\n\n```kotlin\ncomposeList(hasTestTag(contactsListTestTag))\n    .item(hasText(contact.name))\n    .assertTextContains(contact.name)\n```\n#### 3. Simple Espresso assertion and action.\n\n_Espresso_\n\n```kotlin\nonView(withId(R.id.send_button)).check(isDisplayed()).perform(click())\n```\n_Ultron_\n\n```kotlin\nwithId(R.id.send_button).isDisplayed().click()\n```\nThis presents a cleaner approach. Ultron's operation names mirror Espresso's, while also providing additional operations.\n\nRefer to the [doc](https://open-tool.github.io/ultron/docs/android/espress) for further details.\n\n#### 4. Action on RecyclerView list item\n\n_Espresso_\n\n```kotlin\nonView(withId(R.id.recycler_friends))\n    .perform(\n        RecyclerViewActions\n            .actionOnItem<RecyclerView.ViewHolder>(\n                hasDescendant(withText(\"Janice\")),\n                click()\n            )\n        )\n```\n_Ultron_\n\n```kotlin\nwithRecyclerView(R.id.recycler_friends)\n    .item(hasDescendant(withText(\"Janice\")))\n    .click()\n```\n\nExplore the [doc](https://open-tool.github.io/ultron/docs/android/espress) to unveil Ultron's magic with RecyclerView interactions.\n\n#### 5. Espresso WebView operations\n\n_Espresso_\n\n```kotlin\nonWebView()\n    .withElement(findElement(Locator.ID, \"text_input\"))\n    .perform(webKeys(newTitle))\n    .withElement(findElement(Locator.ID, \"button1\"))\n    .perform(webClick())\n    .withElement(findElement(Locator.ID, \"title\"))\n    .check(webMatches(getText(), containsString(newTitle)))\n```\n\n_Ultron_\n\n```kotlin\nid(\"text_input\").webKeys(newTitle)\nid(\"button1\").webClick()\nid(\"title\").hasText(newTitle)\n```\n\nRefer to the [doc](https://open-tool.github.io/ultron/docs/android/webview) for more details.\n\n#### 6. UI Automator operations\n\n_UI Automator_\n\n```kotlin\nval device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())\ndevice\n    .findObject(By.res(\"com.atiurin.sampleapp:id\", \"button1\"))\n    .click()\n```\n\n_Ultron_\n\n```kotlin\nbyResId(R.id.button1).click() \n```\nRefer to the [doc](https://open-tool.github.io/ultron/docs/android/uiautomator)\n***\n### Acquiring the result of any operation as Boolean value\n\n```kotlin\nval isButtonDisplayed = withId(R.id.button).isSuccess { isDisplayed() }\nif (isButtonDisplayed) {\n    //do some reasonable actions\n}\n```\n***\n### Why are all Ultron actions and assertions more stable?\n\nThe framework captures a list of specified exceptions and attempts to repeat the operation during a timeout period (default is 5 seconds). Of course, you have the ability to customize the list of handled exceptions. You can also set a custom timeout for any operation.\n\n```kotlin\nwithId(R.id.result).withTimeout(10_000).hasText(\"Passed\")\n```\n***\n## 3 steps to develop a test using Ultron\n\nWe advocate for a proper test framework architecture, division of responsibilities between layers, and other best practices. Therefore, when using Ultron, we recommend the following approach:\n\n1. Create a Page Object and specify screen UI elements as `Matcher<View>` objects.\n\n```kotlin\nobject ChatPage : Page<ChatPage>() {\n    private val messagesList = withId(R.id.messages_list)\n    private val clearHistoryBtn = withText(\"Clear history\")\n    private val inputMessageText = withId(R.id.message_input_text)\n    private val sendMessageBtn = withId(R.id.send_button)\n}\n```\n\nIt's recommended to make all Page Objects as `object` and descendants of Page class.\nThis allows for the utilization of convenient Kotlin features. It also helps you to keep Page Objects stateless.\n\n2. Describe user step methods in Page Object.\n\n```kotlin\nobject ChatPage : Page<ChatPage>() {\n    fun sendMessage(text: String) = apply {\n        inputMessageText.typeText(text)\n        sendMessageBtn.click()\n        getMessageListItem(text).text\n             .isDisplayed()\n             .hasText(text)\n    }\n\n    fun clearHistory() = apply {\n        openContextualActionModeOverflowMenu()\n        clearHistoryBtn.click()\n    }\n}\n```\nRefer to the full code sample [ChatPage.class](https://github.com/open-tool/ultron/blob/master/sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/ChatPage.kt)\n\n3. Call user steps in test\n\n```kotlin\n    @Test\n    fun friendsItemCheck(){\n        FriendsListPage {\n            assertName(\"Janice\")\n            assertStatus(\"Janice\",\"Oh. My. God\")\n        }\n    }\n    @Test\n    fun sendMessage(){\n        FriendsListPage.openChat(\"Janice\")\n        ChatPage {\n            clearHistory()\n            sendMessage(\"test message\")\n        }\n    }\n```\nRefer to the full code sample [DemoEspressoTest.class](https://github.com/open-tool/ultron/blob/master/sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso/DemoEspressoTest.kt)\n\nIn essence, your project's architecture will look like this:\n\n[acrchitecture](https://github.com/open-tool/ultron/assets/12834123/b0882d34-a18d-4f1f-959b-f75796d11036)\n\n***\n## Allure report\n\nUltron has built in support to generate artifacts for Allure reports. Just apply the recommended configuration and set testIntrumentationRunner.\n\nFor the complete guide, refer to the [Allure description](https://open-tool.github.io/ultron/docs/common/allure)\n\n```kotlin\n@BeforeClass @JvmStatic\nfun setConfig() {\n    UltronConfig.applyRecommended()\n    UltronAllureConfig.applyRecommended()\n    UltronComposeConfig.applyRecommended() \n}\n```\n![allure](https://github.com/open-tool/ultron/assets/12834123/c05c813a-ece6-45e6-a04f-e1c92b82ffb1)\n\n![allure compose](https://github.com/open-tool/ultron/assets/12834123/1f751f3d-fc58-4874-a850-acd9181bfb70)\n\n## Add Ultron to your project\n\nGradle\n```groovy\nrepositories {\n    mavenCentral()\n}\n\ndependencies {\n    androidTestImplementation 'com.atiurin:ultron-android:<latest_version>'\n    androidTestImplementation 'com.atiurin:ultron-allure:<latest_version>'\n    androidTestImplementation 'com.atiurin:ultron-compose:<latest_version>'\n}\n```\nPlease, read [gradle dependencies management](https://open-tool.github.io/ultron/docs/intro/dependencies) doc.\n\n<!--\nLink References\n-->\n\n[telegram-badge]:https://img.shields.io/badge/Chat-Telegram-0088CC?style=for-the-badge\n[documentation-badge]:https://img.shields.io/badge/Documentation-233a60?style=for-the-badge\n[releases-badge]:https://img.shields.io/github/release/open-tool/ultron.svg?style=for-the-badge\n\n[telegram]:https://t.me/ultron_framework\n[documentation]:https://open-tool.github.io/ultron/\n[releases]:https://github.com/open-tool/ultron/releases\n"
  },
  {
    "path": "build.gradle.kts",
    "content": "import org.jetbrains.compose.internal.utils.getLocalProperty\n\nbuildscript {\n    extra.apply {\n        set(\"RELEASE_REPOSITORY_URL\", \"https://central.sonatype.com/api/v1/publisher\")\n        set(\"SNAPSHOT_REPOSITORY_URL\", \"https://central.sonatype.com/api/v1/publisher\")\n    }\n\n    repositories {\n        google()\n        mavenCentral()\n        mavenLocal()\n    }\n    dependencies {\n        classpath(Plugins.kotlinGradle)\n        classpath(Plugins.androidToolsBuildGradle)\n        classpath(Plugins.androidMavenGradle)\n        classpath(Plugins.dokka)\n    }\n}\n\nplugins {\n    //trick: for the same plugin versions in all sub-modules\n    alias(libs.plugins.androidLibrary) apply false\n    alias(libs.plugins.kotlinMultiplatform) apply false\n    alias(libs.plugins.androidApplication) apply false\n    alias(libs.plugins.jetbrainsCompose) apply false\n    alias(libs.plugins.kotlinJvm) apply false\n    alias(libs.plugins.compose.compiler) apply false\n    alias(libs.plugins.kotlinAndroid) apply false\n    alias(libs.plugins.vanniktech.mavenPublish) apply false\n}\n\nallprojects {\n    repositories {\n        google()\n        mavenCentral()\n        mavenLocal()\n        gradlePluginPortal()\n    }\n}"
  },
  {
    "path": "buildSrc/build.gradle.kts",
    "content": "import org.gradle.kotlin.dsl.`kotlin-dsl`\n\nplugins {\n    `kotlin-dsl`\n}\n\nrepositories {\n    google()\n    mavenCentral()\n    gradlePluginPortal()\n}\n"
  },
  {
    "path": "buildSrc/src/main/kotlin/Versions.kt",
    "content": "object Versions {\n    const val kotlin = \"2.1.21\"\n    const val androidToolsBuildGradle = \"8.3.1\"\n    const val androidMavenGradlePlugin = \"2.1\"\n    const val dokkaPlugin = \"1.9.20\"\n\n    const val recyclerView = \"1.2.1\"\n    const val espresso = \"3.6.1\"\n    const val uiautomator = \"2.2.0\"\n    const val accessibility = \"4.0.0\"\n    const val hamcrestCore = \"2.2\"\n    const val compose = \"1.7.0\"\n    const val androidXTest = \"1.4.0\"\n    const val junit = \"4.13.2\"\n    const val allure = \"2.4.0\"\n    //sample-app\n\n    const val coroutines = \"1.4.2\"\n    const val ktx = \"1.6.0\"\n    const val supportV4 = \"1.0.0\"\n    const val appcompat = \"1.3.1\"\n    const val material = \"1.4.0\"\n    const val material3 = \"1.3.1\"\n    const val constraintlayout = \"2.1.4\"\n    const val cardview = \"1.0.0\"\n    const val robolectric = \"4.8.1\"\n    const val mockito = \"3.9.0\"\n    const val activityCompose = \"1.8.2\"\n\n    const val junitExt = \"1.1.2\"\n}\n\nobject Plugins {\n    val androidToolsBuildGradle = \"com.android.tools.build:gradle:${Versions.androidToolsBuildGradle}\"\n    val androidMavenGradle = \"com.github.dcendents:android-maven-gradle-plugin:${Versions.androidMavenGradlePlugin}\"\n    val kotlinGradle = \"org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.kotlin}\"\n    val dokka = \"org.jetbrains.dokka:dokka-gradle-plugin:${Versions.dokkaPlugin}\"\n}\n\nobject Libs {\n    val kotlinStdlib = \"org.jetbrains.kotlin:kotlin-stdlib:${Versions.kotlin}\"\n    val kotlinReflect = \"org.jetbrains.kotlin:kotlin-reflect:${Versions.kotlin}\"\n    val espressoCore = \"androidx.test.espresso:espresso-core:${Versions.espresso}\"\n    val espressoContrib = \"androidx.test.espresso:espresso-contrib:${Versions.espresso}\"\n    val espressoWeb = \"androidx.test.espresso:espresso-web:${Versions.espresso}\"\n    val uiautomator = \"androidx.test.uiautomator:uiautomator:${Versions.uiautomator}\"\n    val accessibility =\n        \"com.google.android.apps.common.testing.accessibility.framework:accessibility-test-framework:${Versions.accessibility}\"\n    val hamcrestCore = \"org.hamcrest:hamcrest-core:${Versions.hamcrestCore}\"\n    val recyclerView = \"androidx.recyclerview:recyclerview:${Versions.recyclerView}\"\n    val androidXRunner = \"androidx.test:runner:${Versions.androidXTest}\"\n    val composeUiTest = \"androidx.compose.ui:ui-test-junit4:${Versions.compose}\"\n    val junit = \"junit:junit:${Versions.junit}\"\n    // allure\n    val allureCommon = \"io.qameta.allure:allure-kotlin-commons:${Versions.allure}\"\n    val allureModel = \"io.qameta.allure:allure-kotlin-model:${Versions.allure}\"\n    val allureJunit4 = \"io.qameta.allure:allure-kotlin-junit4:${Versions.allure}\"\n    val allureAndroid = \"io.qameta.allure:allure-kotlin-android:${Versions.allure}\"\n\n    // sample-app\n    val coroutines = \"org.jetbrains.kotlinx:kotlinx-coroutines-android:${Versions.coroutines}\"\n    val androidXKtx = \"androidx.core:core-ktx:${Versions.ktx}\"\n    val supportV4 = \"androidx.legacy:legacy-support-v4:${Versions.supportV4}\"\n    val appcompat = \"androidx.appcompat:appcompat:${Versions.appcompat}\"\n    val material = \"com.google.android.material:material:${Versions.material}\"\n    val material3 = \"androidx.compose.material3:material3-android:${Versions.material3}\"\n    val constraintLayout = \"androidx.constraintlayout:constraintlayout:${Versions.constraintlayout}\"\n    val cardview = \"androidx.cardview:cardview:${Versions.cardview}\"\n\n    // sample-app compose\n    val composeUi = \"androidx.compose.ui:ui:${Versions.compose}\"\n    val composeUiTooling = \"androidx.compose.ui:ui-tooling:${Versions.compose}\" // Tooling support (Previews, etc.)\n    val composeFoundation = \"androidx.compose.foundation:foundation:${Versions.compose}\" // Foundation (Border, Background, Box, Image, Scroll, shapes, animations, etc.)\n    val composeMaterial = \"androidx.compose.material:material:${Versions.compose}\"\n    val composeMaterialIconsCore = \"androidx.compose.material:material-icons-core:${Versions.compose}\" // Material design icons\n    val composeMaterialIconsExtend = \"androidx.compose.material:material-icons-extended:${Versions.compose}\"\n    val activityCompose = \"androidx.activity:activity-compose:${Versions.activityCompose}\"\n\n    // sample-app test\n    val robolectric = \"org.robolectric:robolectric:${Versions.robolectric}\"\n    val mockito = \"org.mockito:mockito-core:${Versions.mockito}\"\n    val androidXTextCore = \"androidx.test:core:${Versions.androidXTest}\"\n\n    //sample-app androidTest\n    val espressoIdlingResource = \"androidx.test.espresso:espresso-idling-resource:${Versions.espresso}\"\n    val espressoIntents = \"androidx.test.espresso:espresso-intents:${Versions.espresso}\"\n    val espressoAccessibility = \"androidx.test.espresso:espresso-accessibility:${Versions.espresso}\"\n    val espressoConcurrent = \"androidx.test.espresso.idling:idling-concurrent:${Versions.espresso}\"\n    val androidXRules = \"androidx.test:rules:${Versions.androidXTest}\"\n    val androidXTruth = \"androidx.test.ext:truth:${Versions.androidXTest}\"\n    val androidXJunit = \"androidx.test.ext:junit:${Versions.junitExt}\"\n}"
  },
  {
    "path": "composeApp/build.gradle.kts",
    "content": "import org.jetbrains.compose.ExperimentalComposeLibrary\nimport org.jetbrains.compose.desktop.application.dsl.TargetFormat\nimport org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi\nimport org.jetbrains.kotlin.gradle.dsl.JvmTarget\nimport org.jetbrains.kotlin.gradle.plugin.KotlinSourceSetTree\nimport org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl\n\nplugins {\n    alias(libs.plugins.kotlinMultiplatform)\n    alias(libs.plugins.androidApplication)\n    alias(libs.plugins.jetbrainsCompose)\n    alias(libs.plugins.compose.compiler)\n}\n\nkotlin {\n    androidTarget {\n        @OptIn(ExperimentalKotlinGradlePluginApi::class)\n        compilerOptions {\n            jvmTarget.set(JvmTarget.JVM_17)\n        }\n        @OptIn(ExperimentalKotlinGradlePluginApi::class)\n        instrumentedTestVariant {\n            sourceSetTree.set(KotlinSourceSetTree.test)\n\n            dependencies {\n                implementation(libs.androidx.ui.test.junit4.android)\n                debugImplementation(libs.androidx.ui.test.manifest)\n                implementation(project(\":ultron-compose\"))\n            }\n        }\n    }\n    \n    jvm(\"desktop\")\n    listOf(\n        iosX64(),\n        iosArm64(),\n        iosSimulatorArm64()\n    ).forEach { iosTarget ->\n        iosTarget.binaries.framework {\n            baseName = \"ComposeApp\"\n            isStatic = true\n        }\n    }\n    js(IR){\n        browser()\n    }\n    @OptIn(ExperimentalWasmDsl::class)\n    wasmJs(){\n        browser {\n            testTask(Action {\n                useKarma {\n                    useChromeHeadless()\n                    useConfigDirectory(project.projectDir.resolve(\"karma.config.d\").resolve(\"wasm\"))\n                }\n            })\n        }\n    }\n\n    sourceSets {\n        val desktopMain by getting\n\n        androidMain.dependencies {\n            implementation(compose.preview)\n            implementation(libs.androidx.activity.compose)\n        }\n        commonMain.dependencies {\n            implementation(compose.runtime)\n            implementation(compose.foundation)\n            implementation(compose.material)\n            implementation(compose.ui)\n            implementation(compose.components.resources)\n            implementation(compose.components.uiToolingPreview)\n            implementation(libs.androidx.lifecycle.runtime.compose)\n            implementation(libs.androidx.lifecycle.viewmodel.compose)\n            implementation(libs.androidx.navigation.compose)\n        }\n        commonTest.dependencies {\n            @OptIn(ExperimentalComposeLibrary::class)\n            implementation(compose.uiTest)\n            implementation(kotlin(\"test\"))\n            implementation(project(\":ultron-compose\"))\n        }\n        desktopMain.dependencies {\n            implementation(compose.desktop.currentOs)\n        }\n        val desktopTest by getting {\n            dependencies {\n                implementation(compose.desktop.uiTestJUnit4)\n                implementation(compose.desktop.currentOs)\n            }\n        }\n        @OptIn(ExperimentalWasmDsl::class)\n        wasmJs()\n        val wasmJsTest by getting\n    }\n}\n\nandroid {\n    namespace = \"com.atiurin.samplekmp\"\n    compileSdk = libs.versions.android.compileSdk.get().toInt()\n\n    sourceSets[\"main\"].manifest.srcFile(\"src/androidMain/AndroidManifest.xml\")\n    sourceSets[\"main\"].res.srcDirs(\"src/androidMain/res\")\n    sourceSets[\"main\"].resources.srcDirs(\"src/commonMain/resources\")\n\n    defaultConfig {\n        applicationId = \"com.atiurin.samplekmp\"\n        minSdk = libs.versions.android.minSdk.get().toInt()\n        targetSdk = libs.versions.android.targetSdk.get().toInt()\n        versionCode = 1\n        versionName = \"1.0\"\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n    packaging {\n        resources {\n            excludes += \"/META-INF/{AL2.0,LGPL2.1}\"\n        }\n    }\n    buildTypes {\n        getByName(\"release\") {\n            isMinifyEnabled = false\n        }\n    }\n    compileOptions {\n        sourceCompatibility = JavaVersion.VERSION_17\n        targetCompatibility = JavaVersion.VERSION_17\n    }\n    buildFeatures {\n        compose = true\n    }\n//    dependencies {\n//        debugImplementation(compose.uiTooling)\n//    }\n\n}\n\n\ncompose.desktop {\n    application {\n        mainClass = \"MainKt\"\n\n        nativeDistributions {\n            targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)\n            packageName = \"com.atiurin.samplekmp\"\n            packageVersion = \"1.0.0\"\n        }\n    }\n}\n"
  },
  {
    "path": "composeApp/karma.config.d/wasm/config.js",
    "content": "// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file\n// This file provides karma.config.d configuration to run tests with k/wasm\n\nconst path = require(\"path\");\n\nconfig.browserConsoleLogOptions.level = \"debug\";\n\nconst basePath = config.basePath;\nconst projectPath = path.resolve(basePath, \"..\", \"..\", \"..\", \"..\");\nconst generatedAssetsPath = path.resolve(projectPath, \"build\", \"karma-webpack-out\")\n\nconst debug = message => console.log(`[karma-config] ${message}`);\n\ndebug(`karma basePath: ${basePath}`);\ndebug(`karma generatedAssetsPath: ${generatedAssetsPath}`);\n\nconfig.proxies[\"/\"] = path.resolve(basePath, \"kotlin\");\n\nconfig.files = [\n    {pattern: path.resolve(generatedAssetsPath, \"**/*\"), included: false, served: true, watched: false},\n    {pattern: path.resolve(basePath, \"kotlin\", \"**/*.png\"), included: false, served: true, watched: false},\n    {pattern: path.resolve(basePath, \"kotlin\", \"**/*.gif\"), included: false, served: true, watched: false},\n    {pattern: path.resolve(basePath, \"kotlin\", \"**/*.ttf\"), included: false, served: true, watched: false},\n    {pattern: path.resolve(basePath, \"kotlin\", \"**/*.txt\"), included: false, served: true, watched: false},\n    {pattern: path.resolve(basePath, \"kotlin\", \"**/*.json\"), included: false, served: true, watched: false},\n    {pattern: path.resolve(basePath, \"kotlin\", \"**/*.xml\"), included: false, served: true, watched: false},\n].concat(config.files);\n\nfunction KarmaWebpackOutputFramework(config) {\n    // This controller is instantiated and set during the preprocessor phase.\n    const controller = config.__karmaWebpackController;\n\n    // only if webpack has instantiated its controller\n    if (!controller) {\n        console.warn(\n            \"Webpack has not instantiated controller yet.\\n\" +\n            \"Check if you have enabled webpack preprocessor and framework before this framework\"\n        )\n        return\n    }\n\n    config.files.push({\n        pattern: `${controller.outputPath}/**/*`,\n        included: false,\n        served: true,\n        watched: false\n    })\n}\n\nconst KarmaWebpackOutputPlugin = {\n    'framework:webpack-output': ['factory', KarmaWebpackOutputFramework],\n};\n\nconfig.plugins.push(KarmaWebpackOutputPlugin);\nconfig.frameworks.push(\"webpack-output\");"
  },
  {
    "path": "composeApp/src/androidMain/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <application\n        android:allowBackup=\"true\"\n        android:icon=\"@mipmap/ic_launcher\"\n        android:label=\"@string/app_name\"\n        android:roundIcon=\"@mipmap/ic_launcher_round\"\n        android:supportsRtl=\"true\"\n        android:theme=\"@android:style/Theme.Material.Light.NoActionBar\">\n        <activity\n            android:exported=\"true\"\n            android:configChanges=\"orientation|screenSize|screenLayout|keyboardHidden|mnc|colorMode|density|fontScale|fontWeightAdjustment|keyboard|layoutDirection|locale|mcc|navigation|smallestScreenSize|touchscreen|uiMode\"\n            android:name=\".MainActivity\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\" />\n\n                <category android:name=\"android.intent.category.LAUNCHER\" />\n            </intent-filter>\n        </activity>\n    </application>\n\n</manifest>"
  },
  {
    "path": "composeApp/src/androidMain/kotlin/Platform.android.kt",
    "content": "import android.os.Build\n\nclass AndroidPlatform : Platform {\n    override val name: String = \"Android ${Build.VERSION.SDK_INT}\"\n}\n\nactual fun getPlatform(): Platform = AndroidPlatform()"
  },
  {
    "path": "composeApp/src/androidMain/kotlin/com/atiurin/samplekmp/MainActivity.kt",
    "content": "package com.atiurin.samplekmp\n\nimport App\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport androidx.activity.enableEdgeToEdge\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.tooling.preview.Preview\n\nclass MainActivity : ComponentActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        enableEdgeToEdge()\n        super.onCreate(savedInstanceState)\n\n        setContent {\n            App()\n        }\n    }\n}\n\n@Preview\n@Composable\nfun AppAndroidPreview() {\n    App()\n}"
  },
  {
    "path": "composeApp/src/androidMain/res/drawable/ic_launcher_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n    android:viewportWidth=\"108\"\n    android:viewportHeight=\"108\">\n    <path\n        android:fillColor=\"#3DDC84\"\n        android:pathData=\"M0,0h108v108h-108z\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M9,0L9,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,0L19,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M29,0L29,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M39,0L39,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M49,0L49,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M59,0L59,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M69,0L69,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M79,0L79,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M89,0L89,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M99,0L99,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,9L108,9\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,19L108,19\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,29L108,29\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,39L108,39\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,49L108,49\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,59L108,59\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,69L108,69\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,79L108,79\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,89L108,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,99L108,99\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,29L89,29\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,39L89,39\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,49L89,49\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,59L89,59\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,69L89,69\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,79L89,79\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M29,19L29,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M39,19L39,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M49,19L49,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M59,19L59,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M69,19L69,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M79,19L79,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n</vector>"
  },
  {
    "path": "composeApp/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:aapt=\"http://schemas.android.com/aapt\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n    android:viewportWidth=\"108\"\n    android:viewportHeight=\"108\">\n    <path android:pathData=\"M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z\">\n        <aapt:attr name=\"android:fillColor\">\n            <gradient\n                android:endX=\"85.84757\"\n                android:endY=\"92.4963\"\n                android:startX=\"42.9492\"\n                android:startY=\"49.59793\"\n                android:type=\"linear\">\n                <item\n                    android:color=\"#44000000\"\n                    android:offset=\"0.0\" />\n                <item\n                    android:color=\"#00000000\"\n                    android:offset=\"1.0\" />\n            </gradient>\n        </aapt:attr>\n    </path>\n    <path\n        android:fillColor=\"#FFFFFF\"\n        android:fillType=\"nonZero\"\n        android:pathData=\"M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z\"\n        android:strokeWidth=\"1\"\n        android:strokeColor=\"#00000000\" />\n</vector>"
  },
  {
    "path": "composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@drawable/ic_launcher_background\" />\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\" />\n</adaptive-icon>"
  },
  {
    "path": "composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@drawable/ic_launcher_background\" />\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\" />\n</adaptive-icon>"
  },
  {
    "path": "composeApp/src/androidMain/res/values/strings.xml",
    "content": "<resources>\n    <string name=\"app_name\">sample-kmp</string>\n</resources>"
  },
  {
    "path": "composeApp/src/commonMain/composeResources/drawable/compose-multiplatform.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"600dp\"\n    android:height=\"600dp\"\n    android:viewportWidth=\"600\"\n    android:viewportHeight=\"600\">\n  <path\n      android:pathData=\"M301.21,418.53C300.97,418.54 300.73,418.56 300.49,418.56C297.09,418.59 293.74,417.72 290.79,416.05L222.6,377.54C220.63,376.43 219,374.82 217.85,372.88C216.7,370.94 216.09,368.73 216.07,366.47L216.07,288.16C216.06,287.32 216.09,286.49 216.17,285.67C216.38,283.54 216.91,281.5 217.71,279.6L199.29,268.27L177.74,256.19C175.72,260.43 174.73,265.23 174.78,270.22L174.79,387.05C174.85,393.89 178.57,400.2 184.53,403.56L286.26,461.02C290.67,463.51 295.66,464.8 300.73,464.76C300.91,464.76 301.09,464.74 301.27,464.74C301.24,449.84 301.22,439.23 301.22,439.23L301.21,418.53Z\"\n      android:fillColor=\"#041619\"\n      android:fillType=\"nonZero\"/>\n  <path\n      android:pathData=\"M409.45,242.91L312.64,188.23C303.64,183.15 292.58,183.26 283.68,188.51L187.92,245C183.31,247.73 179.93,251.62 177.75,256.17L177.74,256.19L199.29,268.27L217.71,279.6C217.83,279.32 217.92,279.02 218.05,278.74C218.24,278.36 218.43,277.98 218.64,277.62C219.06,276.88 219.52,276.18 220.04,275.51C221.37,273.8 223.01,272.35 224.87,271.25L289.06,233.39C290.42,232.59 291.87,231.96 293.39,231.51C295.53,230.87 297.77,230.6 300,230.72C302.98,230.88 305.88,231.73 308.47,233.2L373.37,269.85C375.54,271.08 377.49,272.68 379.13,274.57C379.68,275.19 380.18,275.85 380.65,276.53C380.86,276.84 381.05,277.15 381.24,277.47L397.79,266.39L420.34,252.93L420.31,252.88C417.55,248.8 413.77,245.35 409.45,242.91Z\"\n      android:fillColor=\"#37BF6E\"\n      android:fillType=\"nonZero\"/>\n  <path\n      android:pathData=\"M381.24,277.47C381.51,277.92 381.77,278.38 382.01,278.84C382.21,279.24 382.39,279.65 382.57,280.06C382.91,280.88 383.19,281.73 383.41,282.59C383.74,283.88 383.92,285.21 383.93,286.57L383.93,361.1C383.96,363.95 383.35,366.77 382.16,369.36C381.93,369.86 381.69,370.35 381.42,370.83C379.75,373.79 377.32,376.27 374.39,378L310.2,415.87C307.47,417.48 304.38,418.39 301.21,418.53L301.22,439.23C301.22,439.23 301.24,449.84 301.27,464.74C306.1,464.61 310.91,463.3 315.21,460.75L410.98,404.25C419.88,399 425.31,389.37 425.22,379.03L425.22,267.85C425.17,262.48 423.34,257.34 420.34,252.93L397.79,266.39L381.24,277.47Z\"\n      android:fillColor=\"#3870B2\"\n      android:fillType=\"nonZero\"/>\n  <path\n      android:pathData=\"M177.75,256.17C179.93,251.62 183.31,247.73 187.92,245L283.68,188.51C292.58,183.26 303.64,183.15 312.64,188.23L409.45,242.91C413.77,245.35 417.55,248.8 420.31,252.88L420.34,252.93L498.59,206.19C494.03,199.46 487.79,193.78 480.67,189.75L320.86,99.49C306.01,91.1 287.75,91.27 273.07,99.95L114.99,193.2C107.39,197.69 101.81,204.11 98.21,211.63L177.74,256.19L177.75,256.17ZM301.27,464.74C301.09,464.74 300.91,464.76 300.73,464.76C295.66,464.8 290.67,463.51 286.26,461.02L184.53,403.56C178.57,400.2 174.85,393.89 174.79,387.05L174.78,270.22C174.73,265.23 175.72,260.43 177.74,256.19L98.21,211.63C94.86,218.63 93.23,226.58 93.31,234.82L93.31,427.67C93.42,438.97 99.54,449.37 109.4,454.92L277.31,549.77C284.6,553.88 292.84,556.01 301.2,555.94L301.2,555.8C301.39,543.78 301.33,495.26 301.27,464.74Z\"\n      android:strokeWidth=\"10\"\n      android:fillColor=\"#00000000\"\n      android:strokeColor=\"#083042\"\n      android:fillType=\"nonZero\"/>\n  <path\n      android:pathData=\"M498.59,206.19L420.34,252.93C423.34,257.34 425.17,262.48 425.22,267.85L425.22,379.03C425.31,389.37 419.88,399 410.98,404.25L315.21,460.75C310.91,463.3 306.1,464.61 301.27,464.74C301.33,495.26 301.39,543.78 301.2,555.8L301.2,555.94C309.48,555.87 317.74,553.68 325.11,549.32L483.18,456.06C497.87,447.39 506.85,431.49 506.69,414.43L506.69,230.91C506.6,222.02 503.57,213.5 498.59,206.19Z\"\n      android:strokeWidth=\"10\"\n      android:fillColor=\"#00000000\"\n      android:strokeColor=\"#083042\"\n      android:fillType=\"nonZero\"/>\n  <path\n      android:pathData=\"M301.2,555.94C292.84,556.01 284.6,553.88 277.31,549.76L109.4,454.92C99.54,449.37 93.42,438.97 93.31,427.67L93.31,234.82C93.23,226.58 94.86,218.63 98.21,211.63C101.81,204.11 107.39,197.69 114.99,193.2L273.07,99.95C287.75,91.27 306.01,91.1 320.86,99.49L480.67,189.75C487.79,193.78 494.03,199.46 498.59,206.19C503.57,213.5 506.6,222.02 506.69,230.91L506.69,414.43C506.85,431.49 497.87,447.39 483.18,456.06L325.11,549.32C317.74,553.68 309.48,555.87 301.2,555.94Z\"\n      android:strokeWidth=\"10\"\n      android:fillColor=\"#00000000\"\n      android:strokeColor=\"#083042\"\n      android:fillType=\"nonZero\"/>\n</vector>"
  },
  {
    "path": "composeApp/src/commonMain/kotlin/App.kt",
    "content": "import androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.navigationBarsPadding\nimport androidx.compose.foundation.layout.statusBarsPadding\nimport androidx.compose.material.Button\nimport androidx.compose.material.MaterialTheme\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.semantics.semantics\nimport androidx.compose.ui.semantics.testTag\nimport org.jetbrains.compose.resources.painterResource\nimport org.jetbrains.compose.ui.tooling.preview.Preview\nimport ui.screens.ContactsListScreen\nimport ultron.composeapp.generated.resources.Res\nimport ultron.composeapp.generated.resources.compose_multiplatform\n\n\n@Composable\n@Preview\nfun App() {\n    MaterialTheme {\n        var showContent by remember { mutableStateOf(false) }\n        Column(Modifier.fillMaxWidth().navigationBarsPadding().statusBarsPadding(), horizontalAlignment = Alignment.CenterHorizontally) {\n            Button(onClick = { showContent = !showContent }) {\n                Text(\"Click me!\")\n            }\n            AnimatedVisibility(showContent) {\n                val greeting = remember { Greeting().greet() }\n                Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {\n                    Image(painterResource(Res.drawable.compose_multiplatform), null)\n                    Text(\"Compose: $greeting\", modifier = Modifier.semantics { testTag = \"greeting\" })\n                }\n            }\n            ContactsListScreen()\n        }\n    }\n}"
  },
  {
    "path": "composeApp/src/commonMain/kotlin/Greeting.kt",
    "content": "class Greeting {\n    private val platform = getPlatform()\n\n    fun greet(): String {\n        return \"Hello, ${platform.name}!\"\n    }\n}"
  },
  {
    "path": "composeApp/src/commonMain/kotlin/Platform.kt",
    "content": "interface Platform {\n    val name: String\n}\n\nexpect fun getPlatform(): Platform"
  },
  {
    "path": "composeApp/src/commonMain/kotlin/repositories/ContactRepository.kt",
    "content": "package repositories\n\nobject ContactRepository {\n    fun getContact(id: Int) : Contact {\n        return contacts.find { it.id == id }!!\n    }\n\n    fun getFirst(): Contact {\n        return contacts.first()\n    }\n    fun getLast() : Contact {\n        return contacts.last()\n    }\n    fun all() = contacts.toList()\n\n    private val contacts = CONTACTS\n}"
  },
  {
    "path": "composeApp/src/commonMain/kotlin/repositories/Storage.kt",
    "content": "package repositories\n\n\ndata class Contact( val id: Int,val name: String, val status: String, val avatar: Int)\ndata class User( val id: Int,val name: String, val avatar: Int, val login: String, val password: String)\n\nval CURRENT_USER = User(1, \"Joey Tribbiani\", Avatars.JOEY.drawable, \"joey\", \"1234\")\n\nval CONTACTS = arrayListOf(\n    Contact(2, \"Chandler Bing\", \"Joey doesn't share food!\", Avatars.CHANDLER.drawable),\n    Contact(3, \"Ross Geller\", \"UNAGI\", Avatars.ROSS.drawable),\n    Contact(4, \"Rachel Green\", \"I got off the plane!\", Avatars.RACHEL.drawable),\n    Contact(5, \"Phoebe Buffay\", \"Smelly cat, smelly cat..\", Avatars.PHOEBE.drawable),\n    Contact(6, \"Monica Geller\", \"I need to clean up..\", Avatars.MONICA.drawable),\n    Contact(7, \"Gunther\", \"They were on break :(\", Avatars.GUNTHER.drawable),\n    Contact(8, \"Janice\", \"Oh. My. God\", Avatars.JANICE.drawable),\n    Contact(9, \"Bob\", \"I wanna drink\", Avatars.DEFAULT.drawable),\n    Contact(10, \"Marty McFly\", \"Back to the ...\", Avatars.DEFAULT.drawable),\n    Contact(12, \"Emmet Brown\", \"Time fluid capacitor\", Avatars.DEFAULT.drawable),\n    Contact(13, \"Friend1\", \"Time fluid capacitor\", Avatars.DEFAULT.drawable),\n    Contact(14, \"Friend2\", \"Time fluid capacitor\", Avatars.DEFAULT.drawable),\n    Contact(15, \"Friend3\", \"Time fluid capacitor\", Avatars.DEFAULT.drawable),\n    Contact(16, \"Friend4\", \"Time fluid capacitor\", Avatars.DEFAULT.drawable),\n    Contact(17, \"Friend5\", \"Time fluid capacitor\", Avatars.DEFAULT.drawable),\n    Contact(18, \"Friend6\", \"Time fluid capacitor\", Avatars.DEFAULT.drawable),\n    Contact(19, \"Friend7\", \"Time fluid capacitor\", Avatars.DEFAULT.drawable),\n    Contact(20, \"Friend8\", \"Time fluid capacitor\", Avatars.DEFAULT.drawable),\n    Contact(21, \"Friend9\", \"Time fluid capacitor\", Avatars.DEFAULT.drawable),\n    Contact(22, \"Friend10\", \"Time fluid capacitor\", Avatars.DEFAULT.drawable),\n    Contact(23, \"Friend11\", \"Time fluid capacitor\", Avatars.DEFAULT.drawable),\n    Contact(24, \"Friend12\", \"Time fluid capacitor\", Avatars.DEFAULT.drawable),\n    Contact(25, \"Friend13\", \"Time fluid capacitor\", Avatars.DEFAULT.drawable),\n    Contact(26, \"Friend14\", \"Time fluid capacitor\", Avatars.DEFAULT.drawable),\n    Contact(27, \"Friend15\", \"Time fluid capacitor\", Avatars.DEFAULT.drawable),\n    Contact(28, \"Friend16\", \"Time fluid capacitor\", Avatars.DEFAULT.drawable),\n    Contact(29, \"Friend17\", \"Time fluid capacitor\", Avatars.DEFAULT.drawable),\n    Contact(30, \"Friend18\", \"Time fluid capacitor\", Avatars.DEFAULT.drawable),\n    Contact(31, \"Friend19\", \"Time fluid capacitor\", Avatars.DEFAULT.drawable),\n    Contact(32, \"Friend20\", \"Time fluid capacitor\", Avatars.DEFAULT.drawable)\n)\n\nenum class Avatars(val drawable: Int) {\n    CHANDLER(0),\n    ROSS(1),\n    MONICA(2),\n    RACHEL(3),\n    PHOEBE(4),\n    GUNTHER(5),\n    JOEY(6),\n    JANICE(7),\n    DEFAULT(8)\n}"
  },
  {
    "path": "composeApp/src/commonMain/kotlin/ui/screens/ContactsListScreen.kt",
    "content": "package ui.screens\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.material.Divider\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.platform.testTag\nimport androidx.compose.ui.semantics.contentDescription\nimport androidx.compose.ui.semantics.semantics\nimport androidx.compose.ui.semantics.testTag\nimport androidx.compose.ui.unit.TextUnit\nimport androidx.compose.ui.unit.TextUnitType\nimport androidx.compose.ui.unit.dp\nimport repositories.ContactRepository\nimport kotlinx.coroutines.async\nimport repositories.Contact\n\n@Composable\nfun ContactsListScreen() {\n    Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {\n        val scope = rememberCoroutineScope()\n        var contactItems by remember { mutableStateOf(emptyList<Contact>()) }\n        var text by remember { mutableStateOf(\"Loading ...\") }\n\n        scope.async {\n            contactItems = loadContacts()\n            text = \"Contacts loaded\"\n        }\n\n        Text(text)\n        LazyColumn(\n            contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),\n            verticalArrangement = Arrangement.spacedBy(4.dp),\n            modifier = Modifier.semantics {\n                contentDescription = \"contactsListContentDesc\"\n                testTag = \"contactsListTestTag\"\n            }\n        ) {\n            items(contactItems) { contact -> ContactItem(contact) }\n        }\n    }\n}\n\n@Composable\nfun ContactItem(contact: Contact) {\n    Box(modifier = Modifier.testTag(\"contactItem=${contact.id}\")) {\n        Column {\n            Row {\n                Column {\n                    Text(contact.name, Modifier.semantics { testTag = \"contactNameTestTag\" }, fontSize = TextUnit(20f, TextUnitType.Sp))\n                    Spacer(modifier = Modifier.height(8.dp))\n                    Text(text = contact.status, Modifier.semantics { testTag = \"contactStatusTestTag\" }, fontSize = TextUnit(16f, TextUnitType.Sp))\n                    Spacer(modifier = Modifier.height(8.dp))\n                }\n            }\n        }\n        Spacer(modifier = Modifier.height(8.dp))\n        Divider(color = Color.Black)\n    }\n\n}\n\nsuspend fun loadContacts(): List<Contact> {\n//    delay(1000)\n    return ContactRepository.all()\n}"
  },
  {
    "path": "composeApp/src/commonTest/kotlin/BaseInteractionTest.kt",
    "content": "\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.material.Button\nimport androidx.compose.material.Text\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.testTag\nimport androidx.compose.ui.test.ExperimentalTestApi\nimport androidx.compose.ui.test.hasTestTag\nimport androidx.compose.ui.test.hasText\nimport com.atiurin.ultron.core.common.options.TextContainsOption\nimport com.atiurin.ultron.core.compose.config.UltronComposeConfig\nimport com.atiurin.ultron.core.compose.nodeinteraction.click\nimport com.atiurin.ultron.core.compose.runUltronUiTest\nimport com.atiurin.ultron.extensions.assertIsDisplayed\nimport com.atiurin.ultron.extensions.isSuccess\nimport com.atiurin.ultron.extensions.withAssertion\nimport com.atiurin.ultron.extensions.withTimeout\nimport com.atiurin.ultron.extensions.withUseUnmergedTree\nimport kotlin.test.AfterTest\nimport kotlin.test.Test\nimport kotlin.test.assertFalse\nimport kotlin.test.assertTrue\n\n@OptIn(ExperimentalTestApi::class)\nclass BaseInteractionTest {\n    @Test\n    fun test() = runUltronUiTest {\n        setContent {\n            App()\n        }\n        hasText(\"Click me!\").withAssertion() {\n            hasTestTag(\"greeting\")\n                .assertIsDisplayed()\n                .assertTextContains(\"Compose: Hello,\", option = TextContainsOption(substring = true))\n        }.click()\n    }\n\n    @Test\n    fun useUnmergedTreeConfigTest() = runUltronUiTest {\n        val testTag = \"element\"\n        setContent {\n            Column {\n                Button(onClick = {}, modifier = Modifier.testTag(testTag)) {\n                    Text(\"Text1\")\n                    Text(\"Text2\")\n                }\n            }\n        }\n        UltronComposeConfig.params.useUnmergedTree = true\n        assertFalse(\"Ultron operation success should be false\") {\n            hasTestTag(testTag).isSuccess { withTimeout(1000).assertTextContains(\"Text1\") }\n        }\n        assertTrue (\"Ultron operation success should be true\") {\n            hasTestTag(testTag).withUseUnmergedTree(false).isSuccess { assertTextContains(\"Text1\") }\n        }\n    }\n\n    @AfterTest\n    fun disableUseUnmergedTree(){\n        UltronComposeConfig.params.useUnmergedTree = false\n    }\n}"
  },
  {
    "path": "composeApp/src/commonTest/kotlin/ExampleTest.kt",
    "content": "\nimport androidx.compose.material.Button\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.testTag\nimport androidx.compose.ui.test.ExperimentalTestApi\nimport androidx.compose.ui.test.assertTextEquals\nimport androidx.compose.ui.test.onNodeWithTag\nimport androidx.compose.ui.test.performClick\nimport androidx.compose.ui.test.runComposeUiTest\nimport kotlin.test.Test\n\nclass ExampleTest {\n    @OptIn(ExperimentalTestApi::class)\n    @Test\n    fun myTest() = runComposeUiTest {\n        setContent {\n            var text by remember { mutableStateOf(\"Hello\") }\n            Text(\n                text = text,\n                modifier = Modifier.testTag(\"text\")\n            )\n            Button(\n                onClick = { text = \"Compose\" },\n                modifier = Modifier.testTag(\"button\")\n            ) {\n                Text(\"Click me\")\n            }\n        }\n\n        // Tests the declared UI with assertions and actions of the Compose Multiplatform testing API\n        onNodeWithTag(\"text\").assertTextEquals(\"Hello\")\n        onNodeWithTag(\"button\").performClick()\n        onNodeWithTag(\"text\").assertTextEquals(\"Compose\")\n    }\n}"
  },
  {
    "path": "composeApp/src/commonTest/kotlin/ListTest.kt",
    "content": "import androidx.compose.ui.test.ExperimentalTestApi\nimport androidx.compose.ui.test.hasTestTag\nimport com.atiurin.ultron.core.compose.list.UltronComposeListItem\nimport com.atiurin.ultron.core.compose.list.composeList\nimport com.atiurin.ultron.core.compose.runUltronUiTest\nimport com.atiurin.ultron.page.Screen\nimport repositories.ContactRepository\nimport kotlin.test.Test\n\n@OptIn(ExperimentalTestApi::class)\nclass ListTest {\n\n    @Test\n    fun testList() = runUltronUiTest {\n        setContent {\n            App()\n        }\n        composeList(hasTestTag(\"contactsListTestTag\"))\n            .assertIsDisplayed().assertNotEmpty()\n            .firstVisibleItem().assertIsDisplayed()\n    }\n\n    @Test\n    fun testListItemChildElements() = runUltronUiTest {\n        setContent {\n            App()\n        }\n        val contact = ContactRepository.getFirst()\n        ListScreen {\n            list.assertContentDescriptionEquals(contactsListContentDesc)\n            list.getFirstVisibleItem<ListScreen.ListItem>().apply {\n                name.assertIsDisplayed().assertTextContains(contact.name)\n                status.assertIsDisplayed().assertTextContains(contact.status)\n            }\n        }\n    }\n}\n\nobject ListScreen : Screen<ListScreen>() {\n    const val contactsListTestTag = \"contactsListTestTag\"\n    const val contactsListContentDesc = \"contactsListContentDesc\"\n    val list = composeList(\n        listMatcher = hasTestTag(contactsListTestTag),\n        initBlock = {\n            registerItem { ListItem() }\n        }\n    )\n\n    class ListItem : UltronComposeListItem() {\n        val name by child { hasTestTag(\"contactNameTestTag\") }\n        val status by child { hasTestTag(\"contactStatusTestTag\") }\n    }\n}"
  },
  {
    "path": "composeApp/src/commonTest/kotlin/UltronTestFlowTest.kt",
    "content": "import com.atiurin.ultron.annotations.ExperimentalUltronApi\nimport com.atiurin.ultron.core.test.UltronTest\nimport com.atiurin.ultron.log.UltronLog\nimport kotlin.test.Test\nimport kotlin.test.assertEquals\nimport kotlin.test.assertTrue\n\nclass UltronTestFlowTest : UltronTest() {\n    companion object {\n        var order = 0\n        var beforeFirstTestCounter = 0\n        var commonBeforeOrder = -1\n        var commonAfterOrder = -1\n        var afterOrder = -1\n    }\n\n    @OptIn(ExperimentalUltronApi::class)\n    override val beforeFirstTest = {\n        beforeFirstTestCounter++\n        UltronLog.info(\"Before Class\")\n    }\n\n    override val beforeTest = {\n        commonBeforeOrder = order\n        order++\n        UltronLog.info(\"Before test common\")\n    }\n    override val afterTest = {\n        commonAfterOrder = order\n        order++\n        assertTrue(afterOrder < commonAfterOrder, message = \"CommonAfter block should run after 'after' test block\")\n        UltronLog.info(\"After test common\")\n    }\n\n    @Test\n    fun someTest1() = test {\n        var beforeOrder = -1\n        var goOrder = -1\n        order++\n        before {\n            assertTrue(beforeFirstTestCounter == 1, message = \"beforeFirstTest block should run before all test\")\n            beforeOrder = order\n            order++\n            UltronLog.info(\"Before TestMethod 1\")\n        }.go {\n            goOrder = order\n            order++\n            UltronLog.info(\"Run TestMethod 1\")\n        }.after {\n            afterOrder = order\n            order++\n            assertTrue(commonBeforeOrder < beforeOrder, message = \"beforeOrder block should run after commonBefore block\")\n            assertTrue(beforeOrder < goOrder, message = \"Before block should run before 'go'\")\n            assertTrue(goOrder < afterOrder, message = \"After block should run after 'go'\")\n        }\n    }\n\n    @Test\n    fun someTest2() = test(suppressCommonBefore = true) {\n        before {\n            UltronLog.info(\"Before TestMethod 2\")\n        }.after {\n            UltronLog.info(\"After TestMethod 2\")\n        }.go {\n            assertTrue(beforeFirstTestCounter == 1, message = \"beforeFirstTest block should run only once\")\n            UltronLog.info(\"Run TestMethod 2\")\n        }\n    }\n\n    @Test\n    fun simpleTest() = test {\n        assertTrue(beforeFirstTestCounter == 1, message = \"beforeFirstTest block should run only once\")\n        UltronLog.info(\"UltronTest simpleTest\")\n    }\n\n\n    @Test\n    fun afterBlockExecutedOnFailedTest() {\n        var isAfterExecuted = false\n        runCatching {\n            test {\n                go {\n                    throw RuntimeException(\"test exception\")\n                }\n                after {\n                    isAfterExecuted = true\n                }\n            }\n        }\n        assertTrue(isAfterExecuted)\n    }\n\n    @Test\n    fun testExceptionMessageThrownOnFailedTest() {\n        val testExceptionMessage = \"test exception\"\n        runCatching {\n            test {\n                go {\n                    throw RuntimeException(testExceptionMessage)\n                }\n                after {\n                    throw RuntimeException(\"Another after exception\")\n                }\n            }\n        }.onFailure { ex ->\n            assertEquals(ex.message, testExceptionMessage)\n        }\n    }\n}"
  },
  {
    "path": "composeApp/src/commonTest/kotlin/UltronTestFlowTest2.kt",
    "content": "import com.atiurin.ultron.annotations.ExperimentalUltronApi\nimport com.atiurin.ultron.core.test.UltronTest\nimport com.atiurin.ultron.log.UltronLog\nimport kotlin.test.Test\nimport kotlin.test.assertEquals\nimport kotlin.test.assertTrue\n\nclass UltronTestFlowTest2 : UltronTest() {\n    companion object {\n        var order = 0\n        var beforeFirstTestCounter = 0\n    }\n\n    @OptIn(ExperimentalUltronApi::class)\n    override val beforeFirstTest = {\n        beforeFirstTestCounter++\n        order++\n        UltronLog.info(\"Before Class\")\n    }\n\n    @Test\n    fun someTest1() = test {\n        var beforeOrder = -1\n        var afterOrder = -1\n        var goOrder = -1\n        order++\n        before {\n            assertEquals(1, beforeFirstTestCounter, message = \"beforeFirstTest block should run before all test\")\n            beforeOrder = order\n            order++\n            UltronLog.info(\"Before TestMethod 1\")\n        }.go {\n            goOrder = order\n            order++\n            UltronLog.info(\"Run TestMethod 1\")\n        }.after {\n            afterOrder = order\n            assertTrue(beforeOrder < goOrder, message = \"Before block should run before 'go'\")\n            assertTrue(goOrder < afterOrder, message = \"After block should run after 'go'\")\n        }\n    }\n\n    @Test\n    fun someTest2() = test(suppressCommonBefore = true) {\n        before {\n            UltronLog.info(\"Before TestMethod 2\")\n        }.after {\n            UltronLog.info(\"After TestMethod 2\")\n        }.go {\n            assertEquals(1, beforeFirstTestCounter, message = \"beforeFirstTest block should run before all test\")\n            UltronLog.info(\"Run TestMethod 2\")\n        }\n    }\n}"
  },
  {
    "path": "composeApp/src/desktopMain/kotlin/Platform.jvm.kt",
    "content": "class JVMPlatform: Platform {\n    override val name: String = \"Java ${System.getProperty(\"java.version\")}\"\n}\n\nactual fun getPlatform(): Platform = JVMPlatform()"
  },
  {
    "path": "composeApp/src/desktopMain/kotlin/main.kt",
    "content": "import androidx.compose.ui.window.Window\nimport androidx.compose.ui.window.application\n\nfun main() = application {\n    Window(\n        onCloseRequest = ::exitApplication,\n        title = \"sample-kmp\",\n    ) {\n        App()\n    }\n}"
  },
  {
    "path": "composeApp/src/desktopTest/kotlin/DesktopSampleTest.kt",
    "content": "\nimport androidx.compose.material.Button\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.testTag\nimport androidx.compose.ui.test.ExperimentalTestApi\nimport androidx.compose.ui.test.hasTestTag\nimport com.atiurin.ultron.core.compose.runDesktopUltronUiTest\nimport com.atiurin.ultron.core.test.UltronTest\nimport com.atiurin.ultron.extensions.assertTextEquals\nimport com.atiurin.ultron.extensions.click\nimport com.atiurin.ultron.page.Page\nimport org.junit.Test\n\nclass DesktopSampleTest : UltronTest()  {\n    @OptIn(ExperimentalTestApi::class)\n    @Test\n    fun myTest() = test {\n        runDesktopUltronUiTest {\n            setContent {\n                var text by remember { mutableStateOf(\"Hello\") }\n\n                Text(\n                    text = text,\n                    modifier = Modifier.testTag(\"text\")\n                )\n                Button(\n                    onClick = { text = \"Compose\" },\n                    modifier = Modifier.testTag(\"button\")\n                ) {\n                    Text(\"Click me\")\n                }\n            }\n\n            SamplePage {\n                someStep()\n            }\n        }\n    }\n}\n\nobject SamplePage : Page<SamplePage>() {\n    private val text = hasTestTag(\"text\")\n    private val button = hasTestTag(\"button\")\n\n    fun someStep(){\n        text.assertTextEquals(\"Hello\")\n        button.click()\n        text.assertTextEquals(\"Compose\")\n    }\n}"
  },
  {
    "path": "composeApp/src/iosMain/kotlin/MainViewController.kt",
    "content": "import androidx.compose.ui.window.ComposeUIViewController\n\nfun MainViewController() = ComposeUIViewController { App() }"
  },
  {
    "path": "composeApp/src/iosMain/kotlin/Platform.ios.kt",
    "content": "import platform.UIKit.UIDevice\n\nclass IOSPlatform: Platform {\n    override val name: String = UIDevice.currentDevice.systemName() + \" \" + UIDevice.currentDevice.systemVersion\n}\n\nactual fun getPlatform(): Platform = IOSPlatform()"
  },
  {
    "path": "composeApp/src/iosTest/kotlin/IOSSampleTest.kt",
    "content": "import androidx.compose.material.Button\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.testTag\nimport androidx.compose.ui.test.ExperimentalTestApi\nimport androidx.compose.ui.test.hasTestTag\nimport com.atiurin.ultron.core.compose.runUltronUiTest\nimport com.atiurin.ultron.extensions.assertTextEquals\nimport com.atiurin.ultron.extensions.click\nimport com.atiurin.ultron.page.Page\nimport kotlin.test.Test\n\nclass IOSSampleTest {\n    @OptIn(ExperimentalTestApi::class)\n    @Test\n    fun sampleTest() = runUltronUiTest {\n        setContent {\n            var text by remember { mutableStateOf(\"Hello\") }\n\n            Text(\n                text = text,\n                modifier = Modifier.testTag(\"text\")\n            )\n            Button(\n                onClick = { text = \"Compose\" },\n                modifier = Modifier.testTag(\"button\")\n            ) {\n                Text(\"Click me\")\n            }\n        }\n\n        SamplePage {\n            someStep()\n        }\n    }\n}\n\nobject SamplePage : Page<SamplePage>() {\n    private val text = hasTestTag(\"text\")\n    private val button = hasTestTag(\"button\")\n\n    fun someStep(){\n        text.assertTextEquals(\"Hello\")\n        button.click()\n        text.assertTextEquals(\"Compose\")\n    }\n}"
  },
  {
    "path": "composeApp/src/jsMain/kotlin/Platform.js.kt",
    "content": "actual fun getPlatform(): Platform {\n    TODO(\"Not yet implemented\")\n}"
  },
  {
    "path": "composeApp/src/jsTest/kotlin/JsSampleTest.kt",
    "content": "import androidx.compose.material.Button\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.testTag\nimport androidx.compose.ui.test.ExperimentalTestApi\nimport androidx.compose.ui.test.assertTextEquals\nimport androidx.compose.ui.test.hasText\nimport androidx.compose.ui.test.onNodeWithTag\nimport androidx.compose.ui.test.performClick\nimport androidx.compose.ui.test.runComposeUiTest\nimport com.atiurin.ultron.core.compose.runUltronUiTest\nimport com.atiurin.ultron.extensions.assertTextEquals\nimport com.atiurin.ultron.extensions.click\nimport kotlin.test.Test\n\nclass JsSampleTest {\n    @OptIn(ExperimentalTestApi::class)\n    @Test\n    fun myTest() = runUltronUiTest {\n        setContent {\n            var text by remember { mutableStateOf(\"Hello\") }\n            Text(\n                text = text,\n                modifier = Modifier.testTag(\"text\")\n            )\n            Button(\n                onClick = { text = \"Compose\" },\n                modifier = Modifier.testTag(\"button\")\n            ) {\n                Text(\"Click me\")\n            }\n        }\n        hasText(\"text\").assertTextEquals(\"Hello\")\n\n        // Tests the declared UI with assertions and actions of the Compose Multiplatform testing API\n        onNodeWithTag(\"text\").assertTextEquals(\"Hello\")\n        onNodeWithTag(\"button\").performClick()\n        onNodeWithTag(\"text\").assertTextEquals(\"Compose123123\")\n    }\n}"
  },
  {
    "path": "composeApp/src/wasmJsMain/kotlin/Platform.wasmJs.kt",
    "content": "class WasmPlatform: Platform {\n    override val name: String = \"Web with Kotlin/Wasm\"\n}\n\nactual fun getPlatform(): Platform = WasmPlatform()"
  },
  {
    "path": "composeApp/src/wasmJsMain/kotlin/main.kt",
    "content": "import androidx.compose.ui.ExperimentalComposeUiApi\nimport androidx.compose.ui.window.ComposeViewport\nimport kotlinx.browser.document\n\n@OptIn(ExperimentalComposeUiApi::class)\nfun main() {\n    ComposeViewport(document.body!!) {\n        App()\n    }\n}"
  },
  {
    "path": "composeApp/src/wasmJsMain/resources/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>sample-kmp</title>\n    <link type=\"text/css\" rel=\"stylesheet\" href=\"styles.css\">\n    <script type=\"application/javascript\" src=\"composeApp.js\"></script>\n</head>\n<body>\n</body>\n</html>"
  },
  {
    "path": "composeApp/src/wasmJsMain/resources/styles.css",
    "content": "html, body {\n    width: 100%;\n    height: 100%;\n    margin: 0;\n    padding: 0;\n    overflow: hidden;\n}"
  },
  {
    "path": "docs/.gitignore",
    "content": "# Dependencies\n/node_modules\n\n# Production\n/build\n\n# Generated files\n.docusaurus\n.cache-loader\n\n# Misc\n.DS_Store\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n"
  },
  {
    "path": "docs/README.md",
    "content": "# Website\n\nThis website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator.\n\n### Installation\n\n```\n$ yarn\n```\n\n### Local Development\n\n```\n$ yarn start\n```\n\nThis command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.\n\n### Build\n\n```\n$ yarn build\n```\n\nThis command generates static content into the `build` directory and can be served using any static contents hosting service.\n\n### Deployment\n\nUsing SSH:\n\n```\n$ USE_SSH=true yarn deploy\n```\n\nNot using SSH:\n\n```\n$ GIT_USER=<Your GitHub username> yarn deploy\n```\n\nIf you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.\n"
  },
  {
    "path": "docs/babel.config.js",
    "content": "module.exports = {\n  presets: [require.resolve('@docusaurus/core/lib/babel/preset')],\n};\n"
  },
  {
    "path": "docs/docs/android/_category_.json",
    "content": "{\n  \"label\": \"Android\",\n  \"position\": 3,\n  \"collapsed\": false\n}\n"
  },
  {
    "path": "docs/docs/android/espress.md",
    "content": "---\nsidebar_position: 1\n---\n\n# Espresso\n\n## How to use?\n\nSimple espresso operation looks like this\n\n```kotlin\nonView(withId(R.id.send_button)).check(isDisplayed()).perform(click())\n```\nthe same with **Ultron**\n\n```kotlin\nwithId(R.id.send_button).isDisplayed().click()\n```\nNames of all Ultron operations are the same as espresso one. There are a lot of additional operations those simplifies test development.\n\n```kotlin\n//------ actions ------ \nclick()\ndoubleClick()\nlongClick()\ntypeText(text: String)\nreplaceText(text: String)\nclearText()\npressKey(keyCode: Int)\npressKey(key: EspressoKey)\ncloseSoftKeyboard()\nswipeLeft()\nswipeRight()\nswipeUp()\nswipeDown()\nscrollTo()\nperform(viewAction: ViewAction)          // execute custom espresso action as Ultron one\nperform(params: UltronEspressoActionParams? = null, block: (uiController: UiController, view: View) -> Unit)\n<T> execute(params: UltronEspressoActionParams? = null, block: (uiController: UiController, view: View) -> T): T\n\n//------ get View property actions ------ \ngetText() : String?\ngetContentDescription() : String?\ngetDrawable() : Drawable?\n\n//------ assertions ------ \nexists()\ndoesNotExist()\nisDisplayed()\nisNotDisplayed()\nisCompletelyDisplayed()\nisDisplayingAtLeast(percentage: Int)\ndoesNotExist()\nisEnabled()\nisNotEnabled()\nisSelected()\nisNotSelected()\nisClickable()\nisNotClickable()\nisChecked()\nisNotChecked()\nisFocusable()\nisNotFocusable()\nhasFocus()\nisJavascriptEnabled()\nhasText(text: String) \nhasText(resourceId: Int)\nhasText(stringMatcher: Matcher<String>)\ntextContains(text: String)\nhasContentDescription(text: String)\nhasContentDescription(resourceId: Int)\nhasContentDescription(charSequenceMatcher: Matcher<CharSequence>) \ncontentDescriptionContains(text: String)\nassertMatches(condition: Matcher<View>) // execute custom espresso assertion as Ultron one\nhasDrawable(@DrawableRes resourceId: Int)\nhasAnyDrawable()\nhasCurrentTextColor(@ColorRes colorRes: Int)\nhasCurrentHintTextColor(@ColorRes colorRes: Int)\nhasShadowColor(@ColorRes colorRes: Int)\nhasHighlightColor(@ColorRes colorRes: Int)\nassertMatches(params: UltronEspressoAssertionParams? = null, block: (view: View) -> Boolean)\n//------ general ------ \nwithTimeout(timeoutMs: Long)                     // set custom timeout for operations\nwithResultHandler(resultHandlerBlock)            // set custom result handler and process operation result \nwithAssertion(assertion: OperationAssertion)     // define custom assertion of action success\nwithAssertion(name: String = \"\", isListened: Boolean = false, block: () -> Unit)\n\n//------ custom clicks ------\nclickTopLeft(offsetX: Int, offsetY: Int)\nclickTopCenter(offsetY: Int)\nclickTopRight(offsetX: Int, offsetY: Int)\nclickCenterRight(offsetX: Int)\nclickBottomRight(offsetX: Int, offsetY: Int)\nclickBottomCenter(offsetY: Int)\nclickBottomLeft(offsetX: Int, offsetY: Int)\nclickCenterLeft(offsetX: Int)\n\n```\n\n## Best practice\n\nSpecify page elements as properties of PageObject class.\n\n```kotlin\nobject SomePage : Page<SomePage>() {\n    private val button = withId(R.id.button1)\n    private val eventStatus = withId(R.id.last_event_status)\n}\n```\n\nUse this properties in page steps\n```kotlin\nobject SomePage : Page<SomePage>() {\n    //page elements\n    fun someUserStepOnPage(expectedEventText: String){\n         button.click()\n         eventStatus.hasText(expectedEventText)\n    }\n}\n```\n## Custom timeout for any operation\n\n```kotlin\nwithId(R.id.last_event_status).withTimeout(10_000).isDisplayed()\n```\nThere are 2 ways of using custom timeout:\n- Specify it for page property and it will be applied for all operations with this element\n```kotlin\nobject SomePage : Page<SomePage>() {\n    private val eventStatus = withId(R.id.last_event_status).withTimeout(10_000)\n}\n```\n- Specify it inside special step there the element operation could take more time. This timeout value will be applied only once for single operation.\n```kotlin\nobject SomePage : Page<SomePage>() {\n    fun someLongUserStep(expectedEventText: String){\n         longRequestButton.click()\n         eventStatus.withTimeout(20_000).hasText(expectedEventText)\n    }\n}\n```\n## Boolean operation result\n\nThere is `isSuccess` method that allows us to get the result of any operation as boolean value. In case of false it could be executed to long (5 sec by default). So it reasonable to specify custom timeout for some operations.\n\n```kotlin\nval isButtonDisplayed = withId(R.id.button).isSuccess { withTimeout(2_000).isDisplayed() }\nif (isButtonDisplayed) {\n    //do some reasonable actions\n}\n```\n\n## Dialog and popup\n\nTo execute any operation inside dialog or popup with espresso you have to specify correct root element\n```kotlin\nonView(withText(\"OK\"))).inRoot(isDialog()).perform(click())\nonView(withText(\"Cancel\")).inRoot(isPlatformPopup()).perform(click())\n```\nHere is a point we need to put our minds on.\n\n**Ultron extends not only `Matcher<View>` object but also `ViewInteraction` and `DataInteraction` objects**\n\n`onView(withText(\"OK\"))).inRoot(isDialog())` returns _ViewInteraction_. Therefore it's possible to use Ultron operations with dialogs.\n\nSo the best way would be a following\n\n```kotlin\nobject DialogPage : Page<DialogPage>() {\n    val okButton = onView(withText(R.string.ok_button))).inRoot(isDialog())\n    val cancelButton = onView(withText(R.string.cancel_button))).inRoot(isDialog())\n}\n...\nfun someUserStepInsideSomePage(){\n    DialogPage.okButton.click()\n    somePageElement.isDisplayed()\n}\n```\n## Extend framework with your own ViewActions and ViewAssertions\n\nUnder the hood all espresso Ultron operations are described in `UltronEspressoInteraction` class. That is why you just need to extend this class using [kotlin extension function](https://kotlinlang.org/docs/extensions.html), e.g.\n```kotlin\nfun <T> UltronEspressoInteraction<T>.appendText(text: String) = apply {\n    executeAction(\n        operationBlock = getInteractionActionBlock(AppendTextAction(text)),\n        name = \"Append text '$text' to ${getInteractionMatcher()}\",\n        description = \"${interaction!!::class.simpleName} APPEND_TEXT to ${getInteractionMatcher()} during $timeoutMs ms\",\n    )\n}\n```\n`AppendTextAction` is a custom ViewAction, smth like that\n```kotlin\nclass AppendTextAction(private val value: String) : ViewAction {\n    override fun getConstraints() = allOf(isDisplayed(), isAssignableFrom(TextView::class.java))\n    override fun perform(uiController: UiController, view: View) {\n        (view as TextView).apply {\n            this.text = \"$text$value\"\n        }\n        uiController.loopMainThreadUntilIdle()\n    }\n    ...\n}\n```\n\nTo make your custom operation 100% native for Ultron framework it's required to add 3 lines more\n\n```kotlin\n//support action for all Matcher<View>\nfun Matcher<View>.appendText(text: String) = UltronEspressoInteraction(onView(this)).appendText(text)\n\n//support action for all ViewInteractions\nfun ViewInteraction.appendText(text: String) = UltronEspressoInteraction(this).appendText(text)\n\n//support action for all DataInteractions\nfun DataInteraction.appendText(text: String) =  UltronEspressoInteraction(this).appendText(text)\n```\nFinally you are able to use this custom operation\n```kotlin\nwithId(R.id.text_input).appendText(\"some text to append\")\n```\nView sample code [UltronEspressoExt](https://github.com/open-tool/ultron/blob/master/sample-app/src/androidTest/java/com/atiurin/sampleapp/framework/ultronext/UltronEspressoExt.kt)\n\n## Get any property of any View\n\nThere are several build in methods that extends `Matcher<View>, ViewInteraction, DataInteraction`:\n```kotlin\ngetText() : String?\ngetContentDescription() : String?\ngetDrawable() : Drawable?\n```\nAnd you are able to get any other property. There is an example how it could be done - [GetTextAction](https://github.com/alex-tiurin/ultron/blob/master/ultron/src/main/java/com/atiurin/ultron/custom/espresso/action/GetTextAction.kt)"
  },
  {
    "path": "docs/docs/android/recyclerview.md",
    "content": "---\nsidebar_position: 3\n---\n\n# RecyclerView\n\n## Terms\nBefore we go forward we need to define some terms:\n- RecyclerView - list of some items (a standard Android framework class). Ultron has a class that wraps an interaction with RecyclerView - `UltronRecyclerView`. \n- RecyclerViewItem - single item of RecyclerView list (there is a class `UltronRecyclerViewItem`)\n- RecyclerViewItem.child - child element of RecyclerViewItem (just a term, there is no special class to work with child elements). So _RecyclerViewItem.child_ could be considered as a simple android View.\n\n![Terms](https://user-images.githubusercontent.com/12834123/107883156-4008d000-6efe-11eb-9764-8c57e767e5e2.png)\n\n## UltronRecyclerView\n\nCreate an instance of `UltronRecyclerView` using the method `withRecyclerView(..)` method:\n\n```kotlin\nwithRecyclerView(R.id.recycler_friends).assertSize(CONTACTS.size)\n```\n\n### Parameters for `withRecyclerView` method\n\nThe withRecyclerView method allows creating an instance of UltronRecyclerView with customizable parameters:\n\n- `recyclerViewMatcher: Matcher`/ `resourceId: Int`, - A Matcher / @IntegerRes that identifies the target RecyclerView in the layout.\n- `loadTimeout: Long` - The maximum time (in milliseconds) to wait for RecyclerView items to load. The default value is defined by `UltronConfig.Espresso.RECYCLER_VIEW_LOAD_TIMEOUT`.\n- `itemSearchLimit: Int` - The maximum number of items to search through when locating an item in the RecyclerView. The default value is defined by `UltronConfig.Espresso.RECYCLER_VIEW_ITEM_SEARCH_LIMIT`.\n- `operationsTimeoutMs: Long` - The maximum time (in milliseconds) to wait for operations on RecyclerView to complete. The default value is defined by `UltronConfig.Espresso.RECYCLER_VIEW_OPERATIONS_TIMEOUT`.\n- `implementation: UltronRecyclerViewImpl`- Specifies the implementation of UltronRecyclerView to use. The default value is `UltronConfig.Espresso.RECYCLER_VIEW_IMPLEMENTATION`, which is set in the configuration.\n\n\n### UltronRecyclerViewImpl\n\n`UltronRecyclerViewImpl` has two available modes:\n\n- `STANDARD`: This is the default implementation. It is already 4 times faster than the previous version. When multiple identical child elements are found within an UltronRecyclerViewItem, the first matching element is selected without throwing an AmbiguousViewMatcherException.\n\n- `PERFORMANCE`: Optimized for higher performance. However, if multiple child elements matching the same criteria are found, an exception (AmbiguousViewMatcherException) will be thrown.\n\nThe choice of implementation affects not only performance but also how child elements of `UltronRecyclerViewItem` are handled.\nFor now, the actual difference in performance between these modes is minimal, but it could be highly valuable if your RecyclerView item contains many child elements.\n\n### _Best practice_ - save `UltronRecyclerView` as page class properties   \n\n```kotlin\nobject FriendsListPage : Page<FriendsListPage>() {\n    // param loadTimeout in ms specifies a time of waiting while RecyclerView items will be loaded\n    val recycler = withRecyclerView(R.id.recycler_friends, loadTimeout = 10_000L) \n    fun someStep(){\n        recycler.assertEmpty()\n        recycler.hasContentDescription(\"Description\")\n    }\n}\n```\n\n`UltronRecyclerView` api\n\n```kotlin\n// ----- assertions -----\nassertEmpty()                                 // Asserts RecyclerView has no item\nassertSize(expected: Int)                     // Asserts RecyclerView list has [expected] items count during\nassertHasItemAtPosition(position: Int)        // Asserts RecyclerView list has item at [position]\nassertMatches(matcher: Matcher<View>)         // Assert RecyclerView matches custom condition\nassertItemNotExist(matcher: Matcher<View>, timeoutMs: Long) // watch java doc to understand how it works\nassertItemNotExistImmediately(matcher: Matcher<View>, timeoutMs: Long)\nisDisplayed()\nisNotDisplayed()\ndoesNotExist()\nisEnabled()\nisNotEnabled()\nhasContentDescription(contentDescription: String)\nhasContentDescription(resourceId: Int)\nhasContentDescription(charSequenceMatcher: Matcher<CharSequence>)\ncontentDescriptionContains(text: String)\n// ----- item providers for simple UltronRecyclerViewItem -----\n// all item provider methods has params [autoScroll: Boolean = true, scrollOffset: Int = 0]. It's shown only once but all of them has it\nitem(matcher: Matcher<View>, autoScroll: Boolean = true, scrollOffset: Int = 0): UltronRecyclerViewItem \nitem(position: Int, ..): UltronRecyclerViewItem \nfirstItem(..): UltronRecyclerViewItem\nlastItem(..): UltronRecyclerViewItem\n\n// Sometimes it is impossible to provide unique matcher for RecyclerView item\n// There is a set of methods to access not unique items by matcher and index\n// index is a value from 0 to lastIndex of matched items\nitemMatched(matcher: Matcher<View>, index: Int): UltronRecyclerViewItem\nfirstItemMatched(matcher: Matcher<View>, ..): UltronRecyclerViewItem\nlastItemMatched(matcher: Matcher<View>, ..): UltronRecyclerViewItem\n\n// ----- item providers for UltronRecyclerViewItem subclasses -----\n// following methods return a generic type T which is a subclass of UltronRecyclerViewItem\ngetItem(matcher: Matcher<View>, autoScroll: Boolean = true, scrollOffset: Int = 0): T  \ngetItem(position: Int, ..): T  \ngetFirstItem(..): T \ngetLastItem(..): T\n\n// ----- in case it's impossible to define unique matcher for `UltronRecyclerViewItem` -----\ngetItemMatched(matcher: Matcher<View>, index: Int, ..): T\ngetFirstItemMatched(matcher: Matcher<View>, ..): T\ngetLastItemMatched(matcher: Matcher<View>, ..): T\n```\n## UltronRecyclerViewItem\n\n`UltronRecyclerView` provides an access to `UltronRecyclerViewItem`. \n\n### Simple Item\n\nIf you don't need to interact with item child just use methods like `item`, `firstItem`, `lastItem`, `itemMatched` and etc\n\n```kotlin\nrecycler.item(position = 10, autoScroll = true).click() // find item at position 10 and scroll to this item \nrecycler.item(matcher = hasDescendant(withText(\"Janice\"))).isDisplayed()\nrecycler.firstItem().click() //take first RecyclerView item\nrecycler.lastItem().isCompletelyDisplayed()\n\n// if it's impossible to specify unique matcher for target item\nval matcher = hasDescendant(withText(\"Friend\"))\nrecycler.itemMatched(matcher, index = 2).click() //return 3rd matched item, because index starts from zero\nrecycler.firstItemMatched(matcher).isDisplayed()\nrecycler.lastItemMatched(matcher).isDisplayed()\nrecycler.getItemsAdapterPositionList(matcher) // return positions of all matched items\n```\nYou don't need to worry about scroll to item. By default autoscroll in all item accessor method equals true.\n\n### Complex item with children\n\nIt's often required to interact with item child. The best solution will be to describe children as properties of `UltronRecyclerViewItem` subclass.\n\n```kotlin\nclass FriendRecyclerItem : UltronRecyclerViewItem() {\n    val avatar by child { withId(R.id.avatar) }\n    val name by child { withId(R.id.tv_name) }\n    val status by child { withId(R.id.tv_status) }\n}\n```\n**Note: you have to use delegated initialisation with `by child`.**\n\nNow you're able to get `FriendRecyclerItem` object using methods `getItem`, `getFirstItem`, `getLastItem` etc\n\n```kotlin\nrecycler.getItem<FriendRecyclerItem>(position = 10, autoScroll = true).status.hasText(\"UNAGI\")\nrecycler.getItem<FriendRecyclerItem>(matcher = hasDescendant(withText(\"Janice\"))).status.textContains(\"Oh. My\")\nrecycler.getFirstItem<FriendRecyclerItem>().avatar.click() //take first RecyclerView item\nrecycler.getLastItem<FriendRecyclerItem>().isCompletelyDisplayed()\n\n// if it's impossible to specify unique matcher for target item\nval matcher = hasDescendant(withText(containsString(\"Friend\")))\nrecycler.getItemMatched<FriendRecyclerItem>(matcher, index = 2).name.click() //return 3rd matched item, because index starts from zero\nrecycler.getFirstItemMatched<FriendRecyclerItem>(matcher).name.hasText(\"Friend1\")\nrecycler.getLastItemMatched<FriendRecyclerItem>(matcher).avatar.isDisplayed()\n```\n### _Best practice_ - add a method to Page class that returns `FriendRecyclerItem` \n\n```kotlin\nobject FriendsListPage : Page<FriendsListPage>() {\n    val recycler = withRecyclerView(R.id.recycler_friends)\n    fun getListItem(contactName: String): FriendRecyclerItem {\n        return recycler.getItem(hasDescendant(allOf(withId(R.id.tv_name), withText(contactName))))\n    }\n    fun getListItem(positions: Int): FriendRecyclerItem {\n        return recycler.getItem(positions)\n    }\n}\n```\nuse `getListItem` inside `FriendsListPage` steps\n```kotlin\nobject FriendsListPage : Page<FriendsListPage>() {\n    ...\n    fun assertStatus(name: String, status: String) = apply {\n        getListItem(name).status.hasText(status).isDisplayed()\n    }\n}\n```\n\n`UltronRecyclerViewItem` api\n\n```kotlin\n//actions \nscrollToItem(offset: Int = 0)\nclick()\nlongClick()\ndoubleClick()\nswipeUp()\nswipeDown()\nswipeLeft()\nswipeRight()\nperform(viewAction: ViewAction)\n\n//assertions\nisDisplayed()\nisNotDisplayed()\nisCompletelyDisplayed()\nisDisplayingAtLeast(percentage: Int)\nisClickable()\nisNotClickable()\nisEnabled()\nisNotEnabled()\nassertMatches(condition: Matcher<View>)\nhasContentDescription(contentDescription: String)\nhasContentDescription(resourceId: Int)\nhasContentDescription(charSequenceMatcher: Matcher<CharSequence>)\ncontentDescriptionContains(text: String)\n\n//general\ngetViewHolder(): RecyclerView.ViewHolder?\ngetChild(childMatcher: Matcher<View>): Matcher<View> //return matcher to a child element\nwithTimeout(timeoutMs: Long) //set custom timeout for the next operation\nwithResultHandler(..) // allows you to process action on item by your own way\n\n// click options\nclickTopLeft(offsetX: Int = 0, offsetY: Int = 0)\nclickTopCenter(offsetY: Int)\nclickTopRight(offsetX: Int = 0, offsetY: Int = 0)\nclickCenterRight(offsetX: Int = 0)\nclickBottomRight(offsetX: Int = 0, offsetY: Int = 0)\nclickBottomCenter(offsetY: Int = 0)\nclickBottomLeft(offsetX: Int = 0, offsetY: Int = 0)\nclickCenterLeft(offsetX: Int = 0)\n```\n"
  },
  {
    "path": "docs/docs/android/rootview.md",
    "content": "---\nsidebar_position: 5\n---\n\n# withSuitableRoot\n\nMethod allows to avoiding nontrivial element lookup exceptions\n\nIn some cases, we encounter non-trivial exceptions in finding elements that are part of the Espresso framework. Such problems and their solution will be considered.\n\n# Waited for the root of the view hierarchy to have window focus and not request layout for 10 seconds.\n\nIf you observe such an exception, then this indicates a complex problem for testing the user interface. One of the well-known reasons is that programmers add their views to the application context, and not to the activity or fragment. At phase of view interaction creation, Espresso assigns a root view where your matcher will be matched. Unfortunately, the views attached to the application context may not have the same root view that was set at the time view interaction was created. To solve this problem, the following solution was created:\n\n```kotlin\nval toolbarTitle = withId(R.id.toolbar_title)\n\nfun assertToolbarTitleWithSuitableRoot(text: String) {\n    toolbarTitle.withSuitableRoot().hasText(text)\n}\n```\n\nwithSuitableRoot() extension returns a view interaction with the correct root view in which the element you are looking for will be  located. If the root view is not found, the test will be interrupted with espresso exception - NoMatchingRootException: Matcher ...did not match any of the following roots...\n\nYou can also use the root matcher to set the root for Espresso view interaction.\n\n```kotlin\nval toolbarTitle = withId(R.id.toolbar_title)\nonView(toolbarTitle).inRoot(withSuitableRoot(toolbarTitle)).check {\n    // Your checks here\n}\n```\n\nThe same works for UltronRecyclerViewItem:\n\n```kotlin\nval recycler = withRecyclerView(R.id.recycler_friends)\n\nclass FriendRecyclerItem : UltronRecyclerViewItem() {\n    val name by child { withId(R.id.tv_name) }\n    val status by child { withId(R.id.tv_status) }\n    val avatar by child { withId(R.id.avatar) }\n}\n\nfun getListItem(positions: Int): FriendRecyclerItem {\n    return recycler.getItem(positions)\n}\n\n// Usage:\n\ngetListItem(0).withSuitableRoot().isDisplayed()\ngetListItem(0).name.withSuitableRoot().isDisplayed().click()\n```\n"
  },
  {
    "path": "docs/docs/android/testconditions.md",
    "content": "---\nsidebar_position: 6\n---\n\n# Test Conditions Management\n\nIt is a feature that includes 3 parts\n\n- RuleSequence\n- SetUpRule & TearDownRule\n- @SetUp @TearDown annotations\n\nAdditional feature - UltronActivityRule for launch Activity before test and finish after\n\nRuleSequence + SetUps & TearDowns for tests = full control of your tests\n\n- control the execution of pre- and postconditions of each test\n- control the moment of activity launching. It is one of the most  important point in android automation.\n- don't write @Before and @After methods by changing it to the lambdas of SetUpRule or TearDownRule object\n- combine conditions of your test using annotations\n\n## RuleSequence\n\nThis rule is a modern replacement of JUnit 4 *RuleChain*. It allows to control an order of rules execution.\n\nThe RuleChain is not flexible. It is unpleasant to use RuleChain especially with class inheritance. That's why\n[RuleSequence](https://github.com/alex-tiurin/ultron/blob/master/ultron/src/main/java/com/atiurin/ultron/testlifecycle/rulesequence/RuleSequence.kt)\nhas been created.\n\nThe order of rules execution depends on its addition order.\nRuleSequence contains three rules lists with their own priority.\n- first - rules from this list will be executed first of all\n- normal - rules will be added to this list by default\n- last - rules from this list will be executed last\n\nIt is recommended to create `RuleSequence` in `BaseTest`. You will be able to add rules to `RuleSequence` in `BaseTest` and in `BaseTest` subclasses.\n\n```kotlin\nabstract class BaseTest {\n    val setupRule = SetUpRule(name = \"some name\").add {\n            // some resonable precondition for all tests, eg login or smth like that\n        }\n\n    @get:Rule\n    open val ruleSequence = RuleSequence(setupRule)\n}\n```\nIt's better to add rules in subclasses inside `init` section.\n```kotlin\nclass DemoTest : BaseTest() {\n    private val activityRule = ActivityScenarioRule(MainActivity::class.java)\n\n    init {\n        ruleSequence.addLast(activityRule)\n    }\n}\n```\n**Note**: while using `RuleSequence`(as it was with `RuleChain`) you don't need to specify `@get:Rule` annotation for other rules.\n\nFull code sample:\n- [BaseTest](https://github.com/alex-tiurin/ultron/blob/master/sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/BaseTest.kt)\n- [DemoEspressoTest](https://github.com/alex-tiurin/ultron/blob/master/sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso/DemoEspressoTest.kt)\n\nTo learn more about order of rules execution see [Deep dive into rules order with RuleSequence](https://github.com/alex-tiurin/ultron/wiki/Deep-dive-into-rules-order-with-RuleSequence)\n\n\n## SetUpRule\n\nThis rule allows you to specify lambdas which will be definitely invoked before a test is started.\nMoreover in combination with **RuleSequence** setup lambdas could be invoked before an activity is launched.\n\n### Precondition for each tests\n\nAdd lambda to `SetUpRule` without any string key and it will be executed before each test in class.\n\n```kotlin\nopen val setupRule = SetUpRule(\"Login user rule\")\n    .add(name = \"Login valid user $CURRENT_USER\") {\n        Log.info(\"Login valid user will be executed before any test is started\")\n        AccountManager(InstrumentationRegistry.getInstrumentation().targetContext).login(\n            CURRENT_USER.login, CURRENT_USER.password\n        )\n    }\n```\n### Precondition for specific test\n\n1. add lambda with string key to `SetUpRule`\n2. add `@SetUp` annotation with specified key to desired test\n\n```kotlin\nsetupRule.add(FIRST_CONDITION){ \n    Log.info(\"$FIRST_CONDITION setup, executed for test with annotation @SetUp(FIRST_CONDITION)\")  \n}\n\n@SetUp(FIRST_CONDITION)\n@Test\nfun someTest() {\n    // some test steps\n}\n```\n\n**Attention**: dont forget to add `SetUpRule` to `RuleSequence`\n\n```kotlin\nruleSequence.add(setupRule)\n```\n\n## TearDownRule\n\nThis rule allows you to specify lambdas which will be definitely invoked after a test is finished.\n\n### Postcondition for all tests\n\nAdd lambda to `TearDownRule` without any string key and it will be executed after each test in class.\n\n```kotlin\nopen val tearDownRule = TearDownRule(name = \"Logout user from app\")\n    .add {\n        AccountManager(InstrumentationRegistry.getInstrumentation().targetContext).logout()\n    }\n```\n### Postcondition for specific test\n\n1. add lambda with string key to `TearDownRule`\n2. add `@TearDown` annotation with specified key to desired test\n\n```kotlin\ntearDownRule.add (LAST_CONDITION){ \n    Log.info(\"$LAST_CONDITION tearDown, executed for test with annotation @TearDown(LAST_CONDITION)\")  \n}\n\n@TearDown(LAST_CONDITION)\n@Test\nfun someTest() {\n    // some test steps\n}\n```\n\n**Attention**: dont forget to add `TearDownRule` to `RuleSequence`\n\n```kotlin\nruleSequence.addLast(tearDownRule)\n```\n\n## Add your SetUps and TearDowns to Allure report\n\nLets clearly define a term **condition**. It's any code block that you've `add` for`SetUpRule` or `TearDownRule`.\n\nFor example:\n```kotlin\nSetUpRule(name = \"sample set up\").add { \n   //codition code\n}\n```\nIt's possible to add all SetUps and TearDowns to Allure report with applying a recommended config:\n\n```kotlin\nUltronAllureConfig.applyRecommended()\n```\n\nYou can read about Allure configuration  [here](../common/allure.md)\n\nWhat it gives us:\n\n- Rule `name` param will be used as name of Allure step.\n\n```kotlin\nSetUpRule(name = \"External step name\").add {...}\n```\n\n- Condition `name` param will be used as a name of inner step\n\n```kotlin\nSetUpRule(name = \"External step name\").add(name = \"Internal step name\") { \n   //condition code\n}\n```\n\n## UltronActivityRule\n\nTo start the activity you can use UltronActivityRule instead of `androidx.test.ext.junit.rules.ActivityScenarioRule`\n\nThe rule has the following advantages:\n\n- finish all activities in RESUMED, PAUSED and STOPPED stage after test\n- does not await idle state for finish activity (fix infinity test execution in case AppNotIdleException)\n- has setup and teardown step in allure report\n\n```kotlin\nval activityRule = UltronActivityRule(YourActivity::class.java)\nruleSequence.add(activityRule)\n```\n\n"
  },
  {
    "path": "docs/docs/android/uiautomator.md",
    "content": "---\nsidebar_position: 4\n---\n\n# UI Automator \n\n**Ultron** makes UI Automator actions and assertions much more stable and simple. It wraps both UiObject and UiObject2.\n\n# Speed up all UI Automator operations\n\n**Ultron** operation could be significantly faster then UI Automator one. To accelerate all operations add single line of code in tests precondition.\n\n```kotlin\n@BeforeClass\n@JvmStatic\nfun speedUpAutomator() {\n    UltronConfig.UiAutomator.speedUp()\n    //or apply the config\n    UltronConfig.apply {\n        accelerateUiAutomator = true\n    }\n}\n```\n\n# How to use?\n\nCompare following code snippets.\n\n_UI Automator_\n\n```kotlin\nval device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())\ndevice.wait(\n    Until.findObject(\n        By.res(\"com.atiurin.sampleapp:id\", \"button1\")\n    ), 5_000\n).click()\nval uiObject2 = device.wait(\n    Until.findObject(\n        By.res(\"com.atiurin.sampleapp:id\", \"last_event_status\")\n    ), 5_000\n)\nuiObject2.text = \"Ultron\"\nAssert.assertEquals(\"Ultron\", uiObject2.text)\n```\n_Ultron_\n\n```kotlin\nbyResId(R.id.button1).click()\nbyResId(R.id.last_event_status).replaceText(\"Ultron\").hasText(\"Ultron\")\n```\n\nThe last one looks a little bit better :)\n\n`byResId(R.id.button1)` actually returns `UltronUiObject2`.\n\nWhile the framework tries to execute UI Automator operation, it catches a list of specified exceptions and tries to repeat the operation during the timeout (5 seconds by default). Of course, you are able to customize the list of processed exceptions. It is also possible to specify a custom timeout for any operation. The configuration process for this part of the framework is explained below.\n\n## `UltronUiObject2` api\n\nThere are factory methods to create `UltronUiObject2`.\n\n```kotlin\nbyResId(@IntegerRes resourceId: Int): UltronUiObject2 // specify element with target application resourceId\nby(bySelector: BySelector): UltronUiObject2           // eg by(By.res(\"com.android.camera2\",\"shutter_button\"))\n```\n\nTo describe UI element with text or content description use following approach\n\n```kotlin\nval textElement = by(By.text(\"some text\"))\nval contentDescElement = by(By.desc(\"Content desc\"))\n```\n\n`UltronUiObject2` has all methods of standart UiObject2 and also provide a lot of new features.\n\n```kotlin\n// data providers\ngetParent(): UltronUiObject2?                  // return this object's parent, or null if it has no parent\ngetChildren(): List<UltronUiObject2>           // return a collection of the child elements directly under this object. Empty list if no child exist\ngetChildCount(): Int\nfindObject(bySelector: BySelector): UltronUiObject2? // searches all elements under this object and returns the first object to match the criteria\nfindObjects(bySelector: BySelector): List<UltronUiObject2>  // searches all elements under this object and returns all objects that match the criteria\ngetApplicationPackage(): String?               // return the package name of the app that this object belongs to\ngetText(): String?                             // return view.text or null if view has no text\ngetClassName(): String                         // return the class name of the view represented by this object\ngetVisibleBounds(): Rect?                      // return the visible bounds of this object in screen coordinates\ngetVisibleCenter(): Point?                     // return a point in the center of the visible bounds of this object\ngetResourceName(): String?                     // return the fully qualified resource name for this object's id\ngetContentDescription(): String?               // return the content description for this object\n\n//actions\nclick(duration: Long = 0)                      // A basic click is a touch down and touch up over the same point with no delay.\nlongClick()\nclear()                                        // Clears the text content if object is an editable field\naddText(text: String)                          // Add the text content if object is an editable field\nlegacySetText(text: String)                    // Set the text content by sending individual key codes\nreplaceText(text: String)                      // Set the text content if object is an editable field\ndrag(dest: Point, speed: Int = DEFAULT_DRAG_SPEED) // Drags object to the specified location\npinchClose(percent: Float, speed: Int = DEFAULT_PINCH_SPEED) // Performs a pinch close gesture on this object\nswipe(direction: Direction, percent: Float, speed: Int = DEFAULT_SWIPE_SPEED) // Performs a swipe gesture on this object\nswipeUp()\nswipeDown()\nswipeLeft()\nswipeRight()\nscroll(direction: Direction, percent: Float, speed: Int = DEFAULT_SCROLL_SPEED) // Performs a scroll gesture on this object\nscrollUp()\nscrollDown()\nscrollLeft()\nscrollRight()\nfling(direction: Direction, speed: Int = DEFAULT_FLING_SPEED)         // Performs a fling gesture on this object\nperform(actionBlock: UiObject2.() -> Unit, actionDescription: String) // custom action on UiObject2\n\n//asserts\nhasText(textMatcher: Matcher<String>)\nhasText(text: String)\ntextContains(textSubstring: String)\ntextIsNullOrEmpty()\ntextIsNotNullOrEmpty()\nhasContentDescription(contentDescMatcher: Matcher<String>)\nhasContentDescription(contentDesc: String)\ncontentDescriptionContains(contentDescSubstring: String)\ncontentDescriptionIsNullOrEmpty()\ncontentDescriptionIsNotNullOrEmpty()\nisCheckable()\nisNotCheckable()\nisChecked()\nisNotChecked()\nisClickable()\nisNotClickable()\nisEnabled()\nisNotEnabled()\nisFocusable()\nisNotFocusable()\nisFocused()\nisNotFocused()\nisLongClickable()\nisNotLongClickable()\nisScrollable()\nisNotScrollable()\nisSelected()\nisNotSelected()\nisDisplayed()\nisNotDisplayed()\nassertThat(assertBlock: UiObject2.() -> Boolean, assertionDescription: String) // custom assertion of UiObject2\n\n//------ general ------ \nwithTimeout(timeoutMs: Long)                     // set custom timeout for operations\nwithResultHandler(resultHandlerBlock)            // set custom result handler and process operation result \nwithAssertion(assertion: OperationAssertion)     // define custom assertion of action success\nwithAssertion(name: String = \"\", isListened: Boolean = false, block: () -> Unit)\n```\n\n## `UltronUiObject` api\n\nAs it was mentioned before **Ultron** wraps UiObject too. There is a set of static methods to create `UltronUiObject`.\n\n```kotlin\nuiResId(@IntegerRes resourceId: Int): UltronUiObject // specify element with target application resourceId\nui(uiSelector: UiSelector): UltronUiObject\n```\n\nIt has all methods of standart UiObject and also provide a lot of new features. As `UltronUiObject` has almost the same api as `UltronUiObject2` we don't list it.\n\n## Best practice\n\nSpecify page elements as properties of PageObject class.\n\n```kotlin\nobject SomePage : Page<SomePage>() {\n    private val button = byResId(R.id.button1)\n    private val eventStatus = byResId(R.id.last_event_status)\n}\n```\n\nUse this properties in page steps\n```kotlin\nobject SomePage : Page<SomePage>() {\n    //page elements\n    fun someUserStepOnPage(expectedEventText: String){\n         button.click()\n         eventStatus.hasText(expectedEventText)\n    }\n}\n```\n## Custom timeout for any operation\n\n```kotlin\nbyResId(R.id.last_event_status).withTimeout(10_000).isDisplayed()\n```\nThere are 2 ways of using custom timeout:\n- Specify it for page property and it will be applied for all operations with this element\n```kotlin\nobject SomePage : Page<SomePage>() {\n    private val eventStatus = byResId(R.id.last_event_status).withTimeout(10_000)\n}\n```\n- Specify it inside special step there the element operation could take more time. This timeout value will be applied only once for single operation.\n```kotlin\nobject SomePage : Page<SomePage>() {\n    fun someLongUserStep(expectedEventText: String){\n         longRequestButton.click()\n         eventStatus.withTimeout(20_000).hasText(expectedEventText)\n    }\n}\n```\n## Boolean operation result\n\nThere is `isSuccess` method that allows us to get the result of any operation as boolean value. In case of false it could be executed to long (5 sec by default). So it's resonable to specify custom timeout for some operations.\n\n```kotlin\nval isButtonDisplayed = byResId(R.id.button).isSuccess { withTimeout(2_000).isDisplayed() }\nif (isButtonDisplayed) {\n    //do some reasonable actions\n}\n```\n\n## Extend framework with your own action and assertion\n\nIt's described in another page [here](../common/extension.md#ui-automator)\n"
  },
  {
    "path": "docs/docs/android/webview.md",
    "content": "---\nsidebar_position: 2\n---\n\n# WebView\n\nThere are 3 different objects to interact with.\n\n* `UltronWebDocument` - wraps operations with WebView DOM document (execute JS script and etc).\n* `UltronWebElement` - represents a DOM element. Provides operations with element (`webClick`, `replaceText`, `exists` etc)\n* `UltronWebElements` - represents a list of similar WebElements.\n\n## How to use?\n\n### UltronWebDocument\n\nIt contains a set of static methods. For example\n```kotlin\nUltronWebDocument.evalJS(\"document.getElementById(\\\"title\\\").innerHTML = '$title';\")\nUltronWebDocument.assertThat(\n            webContent(\n                elementById(\n                    \"apple_link\",\n                    withTextContent(\"Apple\")\n                )\n            )\n        )\n```\nFull list:\n\n```kotlin\nforceJavascriptEnabled(webViewMatcher, timeoutMs, ..)            // performs a force enable of Javascript on a WebView\nevalJS(script: String, webViewMatcher, timeoutMs, ..)            // evaluate JS on webView\nassertThat(WebAssertion, webViewMatcher, ..)                     // use any webAssertion to assert it safely\nselectActiveElement(..): ElementReference                        // finds the currently active element in the document\nselectFrameByIndex(index: Int, ..): WindowReference              // selects a subframe of the currently selected window by it's index\nselectFrameByIdOrName(idOrName: String, ..): WindowReference     // selects a subframe of the current window by it's name or id\n```\n\n### UltronWebElement\n`UltronWebElement` has a list of factory methods that help us to create an instance of UltronWebElement. Full list is here - [UltronWebElement](https://github.com/open-tool/ultron/blob/603150ab12a703a19245ad08a48b036ce562dfd8/ultron/src/main/java/com/atiurin/ultron/core/espressoweb/webelement/UltronWebElement.kt#L311)\n\n```kotlin\nimport com.atiurin.ultron.core.espressoweb.webelement.UltronWebElement.Companion.id\n//other imports\n\nid(\"text_input\").webKeys(\"Ultron\")\nclassName(\"css_button\").webClick()\nxpath(\"some_xpath_link\").hasAttribute(\"href\", \"https://github.com/alex-tiurin/ultron\")\n```\nIt's preferable to use `id` or `xpath` to create `UltronWebElement` instance because they provide very profitable method `hasAttribute`\n\nFull operations list\n\n```kotlin\n//actions\nclearElement()                                // clears content from an editable element\nreplaceText(String)                           // simulates javascript clear and key events sent to a certain element\nwebKeys(String)                               // simulates javascript key events sent to a certain element\ngetText()                                     // returns the visible text beneath a given DOM element\nwebScrollIntoView()                           // executes scroll to view\nwebScrollIntoViewBoolean()                    // returns if the desired element is in view after scrolling\nwebClick()                                    // simulates the javascript events to click on a particular element\n\n//assertions\ncontainsText(String)                          // asserts that DOM element contains visible text beneath it self \nexists()                                      // asserts that element exists in webView\nhasText(String)                               // asserts that DOM element has visible text beneath it self\nhasAttribute(String, Matcher<String>)         // assert any html attribute value\nassertThat(WebAssertion)                      // use any webAssertion to assert it safely \n\nisSuccess(block: UltronWebElement.() -> Unit) // transforms any action or assertion to Boolean value \nreset()                                       // removes the Element and Window references from this interaction\n//------ general ------ \nwithTimeout(timeoutMs: Long)                  // set custom timeout\nwithResultHandler(resultHandlerBlock)         // provides the ability to process operation result in custom way\nwithContextual(UltronWebElement)              // set a parent element\nwithAssertion(assertion: OperationAssertion)  // define custom assertion of action success\nwithAssertion(name: String = \"\", isListened: Boolean = false, block: () -> Unit)\n```\n\n### UltronWebElements\n\nIt helps to find similar elements.\n```kotlin\nclassNames(\"link\").getElements()\n   .find { ultronWebElement ->\n        ultronWebElement.isSuccess {\n            withTimeout(100).hasText(\"Apple\")\n        }\n   }?.webClick()\n```\nIt has only 2 usable methods\n\n```kotlin\ngetElements(): List<UltronWebElement>\ngetSize(): Int\n```\n## Boolean operation result\n\nThere is `isSuccess` method that allows us to get the result of any operation as boolean value. In case of false it could be executed to long (5 sec by default). So it reasonable to specify custom timeout for some operations.\n\n```kotlin\nval isWebElementExist = xpath(\"some_xpath\").isSuccess { withTimeout(2_000).exists() }\nif (isWebElementExist) {\n    //do some reasonable actions\n}\n```\n## Best practice\n\nSpecify web elements as properties of PageObject class.\n\n```kotlin\nobject WebViewPage : Page<WebViewPage>() {\n    private val button = id(\"button\")\n    private val textInput = id(\"text_input\")\n    private val title = xpath(\"some_xpath\")\n}\n```\n\nUse this properties in page steps\n```kotlin\nobject WebViewPage : Page<WebViewPage>() {\n    //page elements\n    fun someUserStepOnWebView(expectedEventText: String){\n         textInput.replaceText(expectedEventText)\n         button.webClick()\n         title.hasText(expectedEventText)\n    }\n}\n```\n\n## Extend framework with your own Web operations\n\n\nIt's described in another page [here](../common/extension.md#espresso-web)\n"
  },
  {
    "path": "docs/docs/common/_category_.json",
    "content": "{\n  \"label\": \"Common\",\n  \"position\": 4,\n  \"collapsed\": false\n}\n"
  },
  {
    "path": "docs/docs/common/allure.md",
    "content": "---\nsidebar_position: 1\n---\n\n# Allure\n\nUltron can generate artifacts for Allure report only for Android UI tests. \n\nJust set Ultron `testInstrumentationRunner` in your app build.gradle file ([example build.gradle.kts](https://github.com/open-tool/ultron/blob/master/sample-app/build.gradle.kts#L14))\n\n```kotlin\nandroid {\n    defaultConfig {\n        testInstrumentationRunner = \"com.atiurin.ultron.allure.UltronAllureTestRunner\"\n        ...\n    }\n```\nand apply recommended config in your BaseTest class ([example BaseTest](https://github.com/open-tool/ultron/blob/master/sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/BaseTest.kt#L31)).\n\n```kotlin\n@BeforeClass @JvmStatic\nfun setConfig() {\n    UltronConfig.applyRecommended()\n    UltronAllureConfig.applyRecommended()\n}\n```\n\n## Custom results directory\n\nUltron allows you to specify the directory where the Allure results will be stored.\nBy default, the results are stored in the `<app_directory>/files/allure-results` directory in the root of the project.\nYou can change this directory by calling `UltronAllureConfig.setAllureResultsDirectory()`\n\n```kotlin\n@BeforeClass @JvmStatic\nfun setConfig() {\n    ...\n    UltronAllureConfig.applyRecommended()\n    UltronAllureConfig.setAllureResultsDirectory(Environment.DIRECTORY_DOWNLOADS)\n}\n```\n\n## Ultron Allure report contains:\n- Detailed report about all operations in your test\n- Logcat file (in case of failure)\n- Screenshot (in case of failure)\n- Ultron log file (in case of failure)\n\nYou also can add any artifact you need. It will be described later.\n\n![allure](https://github.com/open-tool/ultron/assets/12834123/c05c813a-ece6-45e6-a04f-e1c92b82ffb1)\n\n***\n## Ultron `step`\nUltron wraps Allure `step` method into it's own one. \n\nIt's recommended to use Ultron method cause it will provide more info to report in future releases.\n\n### Best practice\n\nWraps all steps with Ultron `step` method e.g.\n\n```kotlin\nobject ChatPage: Page<ChatPage>(){\n    ...\n    fun sendMessage(text: String) = apply {\n        step(\"Send message with text '$text\") {\n            inputMessageText.typeText(text)\n            sendMessageBtn.click()\n            this.getMessageListItem(text).text\n                .isDisplayed()\n                .hasText(text)\n        }\n    }\n\n    fun assertMessageTextAtPosition(position: Int, text: String) = apply {\n        step(\"Assert item at position $position has text '$text'\"){\n            this.getListItemAtPosition(position).text.isDisplayed().hasText(text)\n        }\n    }\n}\n```\n\n## Custom config\n\n```kotlin\nUltronConfig.apply {\n    this.operationTimeoutMs = 10_000\n    this.logToFile = false\n    this.accelerateUiAutomator = false\n}\nUltronAllureConfig.apply {\n    this.attachUltronLog = false\n    this.attachLogcat = false\n    this.detailedAllureReport = false\n    this.addConditionsToReport = false\n    this.addScreenshotPolicy = mutableSetOf(\n        AllureAttachStrategy.TEST_FAILURE,      // attach screenshot at the end of failed test\n        AllureAttachStrategy.OPERATION_FAILURE, // attach screenshot once operation failed\n        AllureAttachStrategy.OPERATION_SUCCESS  // attach screenshot for each operation\n    )\n}\nUltronComposeConfig.apply {\n    this.operationTimeoutMs = 7_000\n    ...\n}\n```\n## Add detailed info about your conditions to report\n\nUltron provides cool feature called [Test condition management](../android/testconditions.md) \n\nWith recommended config all conditions will be added to Allure report automatically. The `name` of rule and condition is used as Allure `step` name.\n\nFor example this code \n\n```kotlin\n    val setupRule = SetUpRule(\"Login user rule\")\n        .add(name = \"Login valid user $CURRENT_USER\") {\n            AccountManager(InstrumentationRegistry.getInstrumentation().targetContext).login(\n                CURRENT_USER.login, CURRENT_USER.password\n            )\n        }\n```\n\ngenerate following marked steps \n\n![conditions](https://user-images.githubusercontent.com/12834123/232789449-1b6a0bc8-5c68-4dd3-836c-8d39696ce8dd.png)\n\n## How to add custom artifacts to Allure report?\n\n### Write artifact to report\n\nThe framework has special methods to write your artifacts into report.\n\n`createCacheFile` - creates temp file to write the content ([see InstrumentationUtil.kt](https://github.com/open-tool/ultron/blob/master/ultron/src/main/java/com/atiurin/ultron/utils/InstrumentationUtil.kt))\\\n\n`AttachUtil.attachFile(...)` - to attach file to report [see AttachUtil](https://github.com/open-tool/ultron/blob/master/ultron-allure/src/main/java/com/atiurin/ultron/allure/attachment/AttachUtil.kt)\n\nYou method can looks like\n\n```kotlin\nfun addMyArtifactToAllure(){\n    val tempFile = createCacheFile()\n    val result = writeContentToFile(tempFile)\n    val fileName = AttachUtil.attachFile(\n        name = \"file_name.xml\",\n        file = tempFile,\n        mimeType = \"text/xml\"\n    )\n}\n```\n`writeContentToFile(tempFile)` - you should implement it.\n\n### Manage artifact creation\n\nYou can attach artifact using 2 types of Ultron listeners:\n\n- [UltronLifecycleListener](https://github.com/open-tool/ultron/blob/master/ultron/src/main/java/com/atiurin/ultron/listeners/UltronLifecycleListener.kt) - once Ultron operation finished with any result. Sample - [ScreenshotAttachListener.kt](https://github.com/open-tool/ultron/blob/master/ultron-allure/src/main/java/com/atiurin/ultron/allure/listeners/ScreenshotAttachListener.kt)\n\n- [UltronRunListener](https://github.com/open-tool/ultron/blob/master/ultron/src/main/java/com/atiurin/ultron/runner/UltronRunListener.kt) which is inherited from [RunListener](https://github.com/open-tool/ultron/blob/master/ultron/src/main/java/com/atiurin/ultron/runner/RunListener.kt). This type can be used to add artifact in different test lifecycle state. Sample - [WindowHierarchyAttachRunListener.kt](https://github.com/open-tool/ultron/blob/master/ultron-allure/src/main/java/com/atiurin/ultron/allure/runner/WindowHierarchyAttachRunListener.kt)\n\nRefer to the [Listeners doc page](../common/listeners.md) for details."
  },
  {
    "path": "docs/docs/common/boolean.md",
    "content": "---\nsidebar_position: 5\n---\n\n# Boolean result\n\nWhile using the **Ultron** framework you always can get the result of any operation as boolean value. \n\n```kotlin\nobject SomePage : Page<SomePage>{\n    private val composeElement = hasTestTag(\"some_tag\")\n    private val espressoElement = withId(R.id.espressoId)\n    private val espressoWebViewElement = xpath(\"some_xpath\")\n    private val uiautomatorElement = byResId(R.id.uiatomatorId)\n}\n```\nAll these elements have `isSuccess` method that allows us to get boolean result. \nIn case of false it could be executed to long (5 sec by default). So it reasonable to specify custom timeout for some operations.\n```kotlin\ncomposeElement.isSuccess { withTimeout(1_000).assertIsDisplayed() }\nespressoElement.isSuccess { withTimeout(2_000).isDisplayed() }\nuiautomatorElement.isSuccess { withTimeout(2_000).isDisplayed() }\nespressoWebViewElement.isSuccess { withTimeout(2_000).exists() }\n```\n"
  },
  {
    "path": "docs/docs/common/customassertion.md",
    "content": "---\nsidebar_position: 6\n---\n\n# Custom assertions\n\nOur applications are not perfect. It's often happens, that some action has no result. Mostly, this\nproblem connected with bad app design and test device freeze.\n\nAll Ultron operations (Espresso, Web, UiAutomator and Compose) has an ability to be asserted by\ncustom logic.\n\nFor example, you need to assert that some element appears after click. If it's not you need to\nrepeat the click action.\n\nYou can do it like:\n\n```kotlin\nbutton.withAssertion(\"Assert smth is displayed\") {\n    title.isDisplayed()\n}.click()\n```\n\n`\"Assert smth is displayed\"` - is the name of assertion an you will see it in case of exception.\n\nYou can skip it and write shorter:\n```kotlin\nbutton.withAssertion {\n    title.isDisplayed()\n}.click()\n```\n\nBy default all Ultron operations inside assertion block are not logged in logcat, but don't worry!\nYou will see it result in case of exception.\n\nIf you want to have everything in logcat, use `isListened` param\n\n```kotlin\nbutton.withAssertion(isListened = true) { .. }\n```\n\n### Few words about timeouts\n\n**Please note: it is really important to understand the timeouts of operation and assertion.**\n\n* `withAssertion {..}` may double the time of failure. This happens because an operation is executed\n  at least twice. And the assertion block is also executed twice. It's required to make sure that the\n  failure is a proper failure.\n\n* In case an operation is executed successfully but an assertion fails you may have several interactions of the operation and the assertion.\n\n* You may restrict the assertion time by using Ultron `withTimeout()` method, e.g.\n\n```kotlin\nbutton.withAssertion {\n    title.withTimeout(3_000L).isDisplayed()\n}.click()\n```\n\n* You still can extend operation timeout\n```kotlin\nbutton.withTimeout(10_000L).withAssertion {\n    title.withTimeout(2_000L).isDisplayed()\n}.click()\n```\n\n\n\n"
  },
  {
    "path": "docs/docs/common/extension.md",
    "content": "---\nsidebar_position: 3\n---\n\n# Ultron Extension\n\nUltron leverages the power of [Kotlin extension functions](https://kotlinlang.org/docs/extensions.html).\n\nYou can extend the framework by using its native approach along with your custom operations.\n\n## Compose\n***\nTo enhance the Compose part of the framework, follow these steps:\n- Create an extension method for `UltronComposeSemanticsNodeInteraction`. This method should encapsulate the logic of the operation.\n- Create `SemanticsMatcher` extension method to invoke the method with the operation logic.\n\nTwo methods facilitate this process:\n\n- `perform`: This evaluates the operation and returns updated `UltronComposeSemanticsNodeInteraction` object.\n\n```kotlin\nfun UltronComposeSemanticsNodeInteraction.hasAnyChildren() = perform {\n    Assert.assertTrue(\"SemanticsNode has any children\", it.fetchSemanticsNode().children.isNotEmpty())\n}\n\nfun SemanticsMatcher.hasAnyChildren() = UltronComposeSemanticsNodeInteraction(this).hasAnyChildren()\n```\n\n- `execute`: This evaluates the operation and returns the operation's result.\n```kotlin\nfun UltronComposeSemanticsNodeInteraction.getWidth(): Int = execute {\n    it.fetchSemanticsNode().size.width\n}\n\nfun SemanticsMatcher.getWidth(): Int = UltronComposeSemanticsNodeInteraction(this).getWidth()\n```\n\n### Customize operation info\n\nYou can provide additional information to the framework using `UltronComposeOperationParams` for both the `perform` and `execute` methods.\n\n```kotlin\nfun UltronComposeSemanticsNodeInteraction.getWidth(): Int = execute(\n    UltronComposeOperationParams(\n        operationName = \"Get width of '${semanticsNodeInteraction.getDescription()}'\",\n        operationDescription = \"Compose get width of '${semanticsNodeInteraction.getDescription()}' during $timeoutMs ms\",\n        operationType = CustomComposeOperationType.GET_WIDTH\n    )\n) {\n    it.fetchSemanticsNode().size.width\n}\n```\n\n## Espresso \n***\nFor Espresso operations, extend `UltronEspressoInteraction` class. There are 3 methods that help us: \n\n- `perform`: This evaluates the action and returns an updated  `UltronEspressoInteraction` object.\n\n```kotlin\nfun <T> UltronEspressoInteraction<T>.appendText(value: String) = perform { _, view ->\n    val textView = (view as TextView)\n    textView.text = \"${textView.text}$value\"\n}\n```\n\n- `execute`: This evaluates the action and returns the result of the operation.\n```kotlin\nfun <T> UltronEspressoInteraction<T>.getText(): String = execute { _, view ->\n    (view as TextView).text.toString()\n}\n```\n\n- `assertMatches`: This evaluates the assertion and returns an updated `UltronEspressoInteraction` object.\n\n```kotlin\nfun <T> UltronEspressoInteraction<T>.assertChecked(expectedState: Boolean) = assertMatches { view ->\n    // block returns Boolean defining whether assertion failed or succeded\n    (view as CheckBox).isChecked == expectedState\n}\n```\nTo make your custom operation fully native, extend `Matcher<View>`, `ViewInteraction`, `DataInteraction`:\n\n```kotlin\n//support action for all Matcher<View>\nfun Matcher<View>.appendText(text: String) = UltronEspressoInteraction(onView(this)).appendText(text)\n\n//support action for all ViewInteractions\nfun ViewInteraction.appendText(text: String) = UltronEspressoInteraction(this).appendText(text)\n\n//support action for all DataInteractions\nfun DataInteraction.appendText(text: String) =  UltronEspressoInteraction(this).appendText(text)\n```\n\nYou are able to use this custom operation\n```kotlin\nwithId(R.id.text_input).appendText(\"some text to append\")\n```\n\n### Customize action info\n\nYou can provide additional information to the framework using  `UltronEspressoActionParams` for both the `perform` and `execute` methods.\n\n```kotlin\nfun <T> UltronEspressoInteraction<T>.getText(): String = execute(\n    UltronEspressoActionParams(\n        operationName = \"GetText from TextView with '${getInteractionMatcher()}'\",\n        operationDescription = \"${interaction.simpleClassName()} action '${CustomEspressoActionType.GET_TEXT}' of '${getInteractionMatcher()}' with root '${getInteractionRootMatcher()}' during ${getActionTimeout()} ms\",\n        operationType = CustomEspressoActionType.GET_TEXT,\n        viewActionDescription = \"getting text from TextView\",\n        viewActionConstraints = isAssignableFrom(TextView::class.java)\n    )\n) { _, view ->\n    (view as TextView).text.toString()\n}\n```\n\n### Customize assertion info\n\nYou can provide additional information to the framework using  `UltronEspressoAssertionParams` for the `assertChecked` method.\n\n```kotlin\nfun <T> UltronEspressoInteraction<T>.assertChecked(expectedState: Boolean) = assertMatches (\n    UltronEspressoAssertionParams(\n        operationName = \"Assert CheckBox isChecked = '$expectedState'\",\n        operationDescription = \"Assert CheckBox isChecked = '$expectedState' during $timeoutMs ms\",\n        operationType = EspressoAssertionType.IS_CHECKED,\n    )\n){ view ->\n    (view as CheckBox).isChecked == expectedState\n}\n```\n\n## Espresso Web\n***\n\nFor Espresso Web operations, extend the `UltronWebElement` class.\n\n```kotlin\n// add action on wenView\nfun UltronWebElement.appendText(text: String) = apply {\n        executeOperation(\n            getUltronWebActionOperation (\n                webInteractionBlock = {\n                    webInteractionBlock().perform(DriverAtoms.webKeys(text))\n                },\n                name = \"WebElement(${locator.type} = '$value') appendText '$text'\",\n                description = \"WebElement(${locator.type} = '$value') appendText '$text' during $timeoutMs ms\"\n            )\n        )\n    }\n```\n\nUse it like\n```kotlin\nid(\"text_input\").appendText(\"some text\")\n```\n\nIn case you need to add an assertion, use `getUltronWebAssertionOperation()` instead of `getUltronWebActionOperation()`\n\n```kotlin\n// add assertion on webView\nfun UltronWebElement.appendText(text: String) = apply {\n        executeOperation(\n            getUltronWebAssertionOperation (...)\n        )\n    }\n```\n\n## UI Automator\n***\n\nFor UI Automator operations, extend either `UltronUiObject2` or `UltronUiObject` class.\n\n```kotlin\n//actually, UltronUiObject2 already has the same method addText\n// this is just an example of how to extend UltronUiObject2\nfun UltronUiObject2.appendText(appendText: String) = apply {\n        executeAction(\n            actionBlock = { uiObject2ProviderBlock()!!.text += appendText },\n            name = \"AppendText '$appendText' to $selectorDesc\",\n            description = \"UiObject2 action '${UiAutomatorActionType.ADD_TEXT}' $selectorDesc appendText '$appendText' during $timeoutMs ms\"\n        )\n    }\n```\nUse this new ability like:\n```kotlin\nobject SomePage : Page<SomePage>() {\n    private val search = byResId(R.id.search)  \n    fun someUserStep(prefixText: String){\n         search.addPrefixText(prefix)\n    }\n}\n```\nThe same approach applies to adding custom assertions:\n\n```kotlin\n// actually it is not required to create custom  UltronOperationType, but could be useful later\nenum class CustomUltronOperations : UltronOperationType {\n    ASSERT_HAS_ANY_CHILD\n}\n// add extension function to UltronUiObject2 that calls `executeAssertion`\nfun UltronUiObject2.assertHasAnyChild() = apply {\n    executeAssertion(\n        assertionBlock = { uiObject2ProviderBlock()!!.childCount > 0 },\n        name = \"Assert $selectorDesc has any child\",\n        type = CustomUltronOperations.ASSERT_HAS_ANY_CHILD,\n        description = \"UiObject2 assertion '${CustomUltronOperations.ASSERT_HAS_ANY_CHILD}' of $selectorDesc during $timeoutMs ms\"\n    )\n}\n```\nUse this new ability like:\n```kotlin\nobject SomePage : Page<SomePage>() {\n    private val searchResult = byResId(R.id.search_result)\n    fun someUserStep(prefixText: String){\n        search.addPrefixText(prefix)\n        searchResult.assertHasAnyChild()\n    }\n}\n```\n\n\n\n"
  },
  {
    "path": "docs/docs/common/listeners.md",
    "content": "---\nsidebar_position: 4\n---\n\n# Listeners\n\nThe framework has 2 types of listeners: UltronLifecycleListener & UltronRunListener\n\n## UltronLifecycleListener\n\nThis one allows you to listen all stages of **Operation execution**. \n\n```kotlin\nabstract class UltronLifecycleListener {\n    /**\n     * executed before any action or assertion\n     */\n    override fun before(operation: Operation) = Unit\n\n    /**\n     * called when action or assertion failed\n     */\n    override fun afterFailure(operationResult: OperationResult<Operation>) = Unit\n    /**\n     * called when action or assertion has been executed successfully\n     */\n    override fun afterSuccess(operationResult: OperationResult<Operation>) = Unit\n    /**\n     * called in any case of action or assertion result\n     */\n    override fun after(operationResult: OperationResult<Operation>) = Unit    \n}\n```\n`Operation` object contains all info about operation (name, description, type, timeout)\n\n`OperationResult` object contains all info about operation result (success, all exceptions that occured and exception that was thrown, description etc) and also has a reference to `Operation`.\n\nAll listener methods will be executed before an exception will be thrown. It gives you a guarantee that all exceptions in your tests will be processed  as you want.\n\n### Log operation example\n\nFor instance, here is a listener that logs everything to Ultron log.\n```kotlin\nclass LogLifecycleListener : UltronLifecycleListener() {\n    override fun before(operation: Operation) {\n        UltronLog.info(\"Start execution of ${operation.name}\")\n    }\n\n    override fun afterSuccess(operationResult: OperationResult<Operation>) {\n        UltronLog.info(\"Successfully executed ${operationResult.operation.name}\")\n    }\n\n    override fun afterFailure(operationResult: OperationResult<Operation>) {\n        UltronLog.error(\"Failed ${operationResult.operation.name} with description: \\n\" +\n                \"${operationResult.description} \")\n    }\n}\n```\n\nYou can create you own custom listener in the same way.\n\n```kotlin\nclass CustomLifecycleListener : UltronLifecycleListener() {...}\n```\n\nAdd new listener for Ultron operations using `UltronCommonConfig.addListener()`.\n\n```kotlin\nabstract class BaseTest {\n    companion object {\n        @BeforeClass @JvmStatic\n        fun configureUltron() {\n            UltronCommonConfig.addListener(CustomLifecycleListener())\n        }\n    }\n}\n```\n\n### Configuration\n\nBasically we already know how to add new listener. But there are other options to configure Ultron listeners.\n\nFirst of all Ultron by default already has [LogLifecycleListener](https://github.com/alex-tiurin/ultron/blob/master/ultron/src/main/java/com/atiurin/ultron/listeners/LogLifecycleListener.kt) that writes some usable info to logcat.\n\n### Lifecycles\n\nUltron has 4 different lifecycles that watch for different operations.\n- UltronEspressoOperationLifecycle\n- UltronWebLifecycle (WebView operations)\n- UltronUiAutomatorLifecycle\n- UltronComposeOperationLifecycle\n\nIt is possible to add listener for any of these lifecycles.\n\n`UltronUiAutomatorLifecycle.addListener(CustomLifecycleListener())`\n\nIn this case `CustomLifecycleListener` will be applied only for UI Automator operations.\n\n### Exclude operation from listeners monitor\n\nUltron allows it to exclude operation from all listeners. This option is based on operation type.\n\nFor example, you've created a new operation\n\n```kotlin\nenum class CustomUltronOperations : UltronOperationType {\n   ASSERT_HAS_ANY_CHILD\n}\nfun UltronUiObject2.assertHasAnyChild() = apply {\n    executeAssertion(\n            assertionBlock = { uiObject2ProviderBlock()!!.childCount > 0 },\n            name = \"Assert $selectorDesc has any child\",\n            type = CustomUltronOperations.ASSERT_HAS_ANY_CHILD,\n            description = \"UiObject2 assertion '${CustomUltronOperations.ASSERT_HAS_ANY_CHILD}' of $selectorDesc during $timeoutMs ms\",\n            timeoutMs = timeoutMs,\n            resultHandler = resultHandler\n    )\n}\n```\nAnd you would like to exclude it from listeners for any reason no matter why.\n\nAdd single line to Ultron configuration function.\n\n```kotlin\nabstract class BaseTest {\n    companion object {\n        @BeforeClass @JvmStatic\n        fun configureUltron() {\n            ... \n            UltronCommonConfig.operationsExcludedFromListeners.add(CustomUltronOperations.ASSERT_HAS_ANY_CHILD)\n        }\n    }\n}\n```\n\n## UltronRunListener\n\nAllows you to add listener for Test Lifecycle. See [RunListener](https://github.com/open-tool/ultron/blob/master/ultron/src/main/java/com/atiurin/ultron/runner/RunListener.kt).\n\nIt is available in case you use `ultron-allure` and set `testInstrumentationRunner`.\n\n```kotlin\ntestInstrumentationRunner = \"com.atiurin.ultron.allure.UltronAllureTestRunner\"\n```\n\nIt could be used, for instance, to attach your custom application log to Allure Report.\n\n```kotlin\nclass AppLogAttachRunListener() : UltronRunListener() {\n    override fun testFailure(failure: Failure) {\n        val logFile: File = AppLogProvider.provide()\n        val fileName = AttachUtil.attachFile(\n            name = \"app_log_file\",\n            file = logFile,\n            mimeType = MimeType.PLAIN_TEXT\n        )\n    }\n}\n```\n\nAdd custom RunListener to Allure config.\n```kotlin\n@BeforeClass @JvmStatic\nfun configureUltron() {\n    ...\n    UltronAllureConfig.addRunListener(AppLogAttachRunListener())\n}\n```\n\n## ComposDebugListener\n\nIf you have had issues debugging Compose tests, such as not seeing UI changes on the screen immediately, Ultron can help you fix this. Simply add ComposeDebugListener to the framework configuration.\n\n```kotlin\n@BeforeClass @JvmStatic\nfun configureUltron() {\n    ...\n    UltronCommonConfig.addListener(ComposDebugListener())\n}\n```\n"
  },
  {
    "path": "docs/docs/common/resulthandler.md",
    "content": "---\nsidebar_position: 7\n---\n\n# Result handler\n\n**Ultron** allows you to process the result of any operation in your own custom way. It provides full info to do that.\n\nLet's loot at the example\n\n```kotlin\nobject SomePage : Page<SomePage>{\n    private val espressoElement = withId(R.id.espressoId)\n    private val espressoWebViewElement = xpath(\"some_xpath\")\n    private val uiautomatorElement = byResId(R.id.uiatomatorId)\n    private val composeElement = hasTestTag(\"some_tag\")\n}\n```\nNow, we want to catch the result of operation and do smth reasonable. There is a method that opens the door - `withResultHandler`\n```kotlin\nespressoElement.withResultHandler { operationResult ->\n    // smth that make sense\n}\n```\nWhat it gives to us?\n\n![resultHandler](https://user-images.githubusercontent.com/12834123/113351564-bc872f00-9343-11eb-925a-432dbc191b32.png)\n\n**_A little explanation in case you would like to be more familiar with **Ultron** framework_**\n\nThere is an entity which we call `ResultHandler`. By default all Ultron operations has the same `ResultHandler`. \nIt catches the result of operation and asks `OperationResultAnalyzer` to analyze the result. \nIn case `operationResult.success` is `false` the result analyzer throws catched exception.\n\n## How to use?\nThere are 2 ways of using custom ResultHandler:\n- Specify it for page property and it will be applied for all operations with this element\n```kotlin\nobject SomePage : Page<SomePage>() {\n    private val eventStatus = withId(R.id.last_event_status).withResultHandler { operationResult ->\n        // smth that make sense\n    }\n}\n```\n- Specify it inside special step there the element operation should be processed in different way.\nThis ResultHandler will be applied only once for single operation.\n\n```kotlin\nobject SomePage : Page<SomePage>() {\n    fun someSpecificUserStep(expectedEventText: String){\n         eventStatus.withResultHandler { operationResult ->\n             // smth that make sense\n         }.hasText(expectedEventText)\n    }\n}\n```"
  },
  {
    "path": "docs/docs/common/uiblock.md",
    "content": "---\nsidebar_position: 2\n---\n\n# UI Block\n\nUI blocks are a powerful tool for describing and interacting with user interface elements. They allow you to define UI elements within the context of their parent blocks, rather than the entire screen, which makes tests more readable, maintainable, and reliable.\n\nFor example, consider a UI block that represents a user’s name and status. We can define this block once and reuse it across different screens.\n\n![UI Block](/img/uiblock.png)\n\nWe can describe this block and use it on different screens. \n\n_Supported: Compose (CMP & Android), Espresso, Espresso Web, UiAutomator (UiObject2)_\n\n## Compose\n***\nCreate a class that inherits from `UltronComposeUiBlock`.\n\n```kotlin\n\nclass ContactCard(blockMatcher: SemanticsMatcher, blockDescription: String) \n    : UltronComposeUiBlock(blockMatcher, blockDescription) {\n    val name = child(hasTestTag(contactNameTag)).withName(\"Name in '$blockDescription'\")\n    val status = child(hasTestTag(contactStatusTag))\n}\n```\n\n`UltronComposeUiBlock` accepts two parameters:\n\n- `blockMatcher` – describes how to locate this block in the Compose element tree. This is a required parameter and must always be provided.\n- `blockDescription` – a description of the block that clearly identifies the UI container. This parameter is optional, with a default value of `blockDescription = \"\"`.\n\n**Note**: To describe child elements of a UI block, you need to use the `child()` method.\n\nAs shown in the example above, we added a custom name to the `name` field. If an error occurs, this name will appear in the description of the element we tried to interact with. We recommend including the value of `blockDescription` in the element description. This provides better context about the specific element being checked (or any other operation performed).\n\nThe next step is to integrate the block into a screen.\n\n```kotlin\nobject SomeComposeScreen : Screen<SomeComposeScreen>(){\n    val card = ContactCard(hasTestTag(contactCardTag), \"SomeComposeScreen contact card\")\n    \n    fun assertContactCard(contact: Contact){\n        softAssertion {\n            card.name.assertTextEquals(contact.name)\n            card.status.assertTextEquals(contact.status)\n        }\n    }\n}\n```\n\nAs seen in `SomeComposeScreen`, we no longer need to know how to locate `name` and `status`. It's enough to describe how to locate the parent UI block – `ContactCard`.\n\nIn addition to individual UI elements, child blocks can also represent other UI blocks. To describe a child UI block, you can use one of the overloaded `child` methods.\n\n- In Multiplatform, only the method requiring an explicit approach to creating the child block is available.\n- In Android, you can simplify this further using reflection.\n\n### Compose Multiplatform\n\n```kotlin\nclass ProfileBlock(blockMatcher: SemanticsMatcher, blockDescription: String) \n    : UltronComposeUiBlock(blockMatcher, blockDescription) {\n    val card = child(\n        childMatcher = hasTestTag(contactCardTag),\n        uiBlockFactory = { updatedMatcher ->\n            ContactCard(\n                blockMatcher = updatedMatcher, \n                blockDescription = \"Contact card '$blockDescription'\"\n            )\n        }\n    )\n}\n```\n\nThis method offers greater flexibility for creating child UI blocks.  \n`updatedMatcher` – an updated matcher used to locate the `ContactCard` only within the `ProfileBlock`.\n\n### Compose Android Only\n\nReflection capabilities in Android are more advanced than in Multiplatform, allowing for simpler descriptions of child UI blocks.\n\n```kotlin\nclass ProfileBlock(blockMatcher: SemanticsMatcher, blockDescription: String) \n    : UltronComposeUiBlock(blockMatcher, blockDescription) {\n    val card = child(\n        ContactCard(\n            blockMatcher = hasTestTag(contactCardTag), \n            blockDescription = \"Contact card '$blockDescription'\"\n        )\n    )\n}\n```\n\nThere are limitations to using this method:\n\nThe class must meet the following conditions to be instantiated:\n1. It must not be a nested or inner class. It should be defined at the top level or as a file-level class.\n2. It must have one of the following constructors:\n    - A constructor with one parameter of type *SemanticsMatcher*.\n    - A constructor with two parameters: `blockMatcher` of type *SemanticsMatcher* and `blockDescription` of type *String*.\n\nWe can use the `ProfileBlock` on the screen.\n\n```kotlin\nobject SomeComposeScreen : Screen<SomeComposeScreen>(){\n    val profile = ProfileBlock(hasTestTag(profileTag), \"SomeComposeScreen profile card\")\n    \n    fun assertContactCardInProfile(contact: Contact){\n        softAssertion {\n            profile.card.name.assertTextEquals(contact.name)\n            profile.card.status.assertTextEquals(contact.status)\n        }\n    }\n}\n```\n\nThe `UltronComposeUiBlock` class has a `uiBlock` property, which facilitates proper interaction with block elements.\n\n```kotlin\nobject SomeComposeScreen : Screen<SomeComposeScreen>(){\n    val profile = ProfileBlock(hasTestTag(profileTag), \"SomeComposeScreen profile block\")\n    \n    fun assertProfileContactIsDisplayed(){\n        profile.card.uiBlock.assertIsDisplayed()\n    }\n}\n```\n\n## Espresso\n***\n\nCreate a class that inherits from `UltronEspressoUiBlock`\n\n```kotlin\nclass ContactCard(blockMatcher: Matcher<View>, blockDescription: String) \n    : UltronEspressoUiBlock(blockMatcher, blockDescription) {\n    val name = child(withId(R.id.name)).withName(\"Name in '$blockDescription'\")\n    val status = child(withId(R.id.name))\n}\n```\nAdd the block to the screen.\n\n```kotlin\nobject SomeEspressoScreen : Screen<SomeEspressoScreen>(){\n    val card = ContactCard(withId(R.id.card), \"SomeComposeScreen contact card\")\n    \n    fun assertContactCard(contact: Contact){\n        softAssertion {\n            card.name.hasText(contact.name)\n            card.status.hasText(contact.status)\n        }\n    }\n}\n```\nUsing reflection simplifies the implementation of child UI blocks by automating instantiation under specific conditions.\n\nChild UI block with reflection. \n```kotlin\nclass ProfileBlock(blockMatcher: Matcher<View>, blockDescription: String) \n    : UltronEspressoUiBlock(blockMatcher, blockDescription) {\n    val card = child(\n        ContactCard(\n            blockMatcher = withId(R.id.contactCard), \n            blockDescription = \"Contact card of '$blockDescription'\"\n        )\n    )\n}\n```\nChild UI block with factory method\n```kotlin\nclass ProfileBlock(blockMatcher: Matcher<View>, blockDescription: String) \n    : UltronEspressoUiBlock(blockMatcher, blockDescription) {\n    val card = child(\n        childMatcher = withId(R.id.contactCard),\n        uiBlockFactory = { updatedMatcher ->\n            ContactCard(\n                blockMatcher = updatedMatcher, \n                blockDescription = \"Contact card '$blockDescription'\"\n            )\n        }\n    )\n}\n```\nDefine block on screen\n```kotlin\nobject SomeEspressoScreen : Screen<SomeEspressoScreen>(){\n    val profile = ProfileBlock(withId(R.id.profileBlock), \"SomeEspressoScreen profile block\")\n    \n    fun assertContactCardInProfile(contact: Contact){\n        softAssertion {\n            profile.uiBlock.isDisplayed()\n            profile.card.uiBlock.isDisplayed()\n            profile.card.name.hasText(contact.name)\n            profile.card.status.hasText(contact.status)\n        }\n    }\n}\n```\n\n## Espresso Web\n***\n\nCreate a class that inherits from `UltronWebElementUiBlock`\n\n```kotlin\nclass WebContactCard(blockElement: UltronWebElement, blockDescription: String)\n    : UltronWebElementUiBlock(blockElement, blockDescription){\n    val name = child(id(\"name\")).withName(\"Name in '$blockDescription'\")\n    val status = child(className(\"status\"))\n}\n```\nAdd the block to the screen.\n\n```kotlin\nobject SomeWebScreen : Screen<SomeWebScreen>(){\n    val card = WebContactCard(id(\"card\"), \"SomeWebScreen contact card\")\n    \n    fun assertContactCard(contact: Contact){\n        softAssertion {\n            card.name.hasText(contact.name)\n            card.status.hasText(contact.status)\n        }\n    }\n}\n```\nChild UI block with reflection\n```kotlin\nclass WebProfileBlock(blockMatcher: UltronWebElement, blockDescription: String) \n    : UltronWebElementUiBlock(blockMatcher, blockDescription) {\n    val card = child(\n        WebContactCard(\n            blockElement = id(\"card\"), \n            blockDescription = \"Contact card of '$blockDescription'\"\n        )\n    )\n}\n```\nChild UI block with factory method\n```kotlin\nclass WebProfileBlock(blockMatcher: UltronWebElement, blockDescription: String) \n    : UltronWebElementUiBlock(blockMatcher, blockDescription) {\n    val card = child(\n        childMatcher = id(\"card\"),\n        uiBlockFactory = { updatedElement ->\n            ContactCard(\n                blockElement = updatedElement, \n                blockDescription = \"Contact card '$blockDescription'\"\n            )\n        }\n    )\n}\n```\n\n## UiAutomator\n***\nOnly **UiObject2** is supported.\n\nCreate a class that inherits from `UltronUiObject2UiBlock`\n\n```kotlin\nclass UiAutomatorContactCard(blockDesc: String, blockSelector: () -> BySelector)\n    : UltronUiObject2UiBlock(blockDesc, blockSelector){\n    val name = child(bySelector(R.id.name)).withName(\"Name in '$blockDesc'\")\n    val status = child(By.desc(\"status content desc\"))\n}\n```\nAdd the block to the screen.\n\n```kotlin\nobject SomeUiAutomatorScreen : Screen<SomeUiAutomatorScreen>(){\n    val card = UiAutomatorContactCard(\n       blockDesc=\"SomeUiAutomatorScreen contact card\",\n       blockSelector=bySelector(R.id.card)\n    )\n    \n    fun assertContactCard(contact: Contact){\n        softAssertion {\n            card.name.hasText(contact.name)\n            card.status.hasText(contact.status)\n        }\n    }\n}\n```\nChild UI block with reflection\n```kotlin\nclass UiAutomatorProfileBlock(blockDesc: String, blockSelector: () -> BySelector)\n   : UltronUiObject2UiBlock(blockDesc, blockSelector){\n    val card = child(\n        UiAutomatorContactCard(\n            blockDesc = \"Contact card of '$blockDesc'\",\n            blockSelector = { bySelector(R.id.card) }\n        )\n    )\n}\n```\nChild UI block with factory method\n```kotlin\nclass UiAutomatorProfileBlock(blockDesc: String, blockSelector: () -> BySelector)\n   : UltronUiObject2UiBlock(blockDesc, blockSelector){\n    val card = child(\n        selector = bySelector(R.id.card),\n        description = \"Contact card of '$desc'\",\n        uiBlockFactory = { desc, selector ->\n            UiAutomatorContactCard(desc, selector)\n        }\n    )\n}\n```"
  },
  {
    "path": "docs/docs/common/ultrontest.md",
    "content": "---\nsidebar_position: 2\n---\n\n# UltronTest\n\n`UltronTest` is a powerful base class provided by the Ultron framework that enables the definition of common preconditions and postconditions for tests. By extending this class, you can streamline test setup and teardown, ensuring consistent execution across your test suite.\n\n## Features of `UltronTest`\n\n- **Pre-Test Actions:** Define actions to be executed before each test.\n- **Post-Test Actions:** Define actions to be executed after each test.\n- **Lifecycle Management:** Execute code once before all tests in a class using `beforeFirstTest`.\n- **Customizable Test Execution:** Suppress pre-test or post-test actions when needed.\n\n### Example\n\nHere is an example of using `UltronTest`:\n\n```kotlin\nclass SampleUltronFlowTest : UltronTest() {\n\n    @OptIn(ExperimentalUltronApi::class)\n    override val beforeFirstTest = {\n        UltronLog.info(\"Before Class\")\n    }\n\n    override val beforeTest = {\n        UltronLog.info(\"Before test common\")\n    }\n\n    override val afterTest = {\n        UltronLog.info(\"After test common\")\n    }\n\n    /**\n     * The order of method execution is as follows::\n     * beforeFirstTest, beforeTest, before, go, after, afterTest\n     */\n    @Test\n    fun someTest1() = test {\n        before {\n            UltronLog.info(\"Before TestMethod 1\")\n        }.go {\n            UltronLog.info(\"Run TestMethod 1\")\n        }.after {\n            UltronLog.info(\"After TestMethod 1\")\n        }\n    }\n    \n    /**\n     * An order of methods execution is follow: before, go, after\n     * `beforeFirstTest` - Not executed, as it is only run once and was already executed before `someTest1`.\n     * `beforeTest` - Not executed because it was suppressed using `suppressCommonBefore`.\n     * `afterTest` - Not executed because it was suppressed using `suppressCommonAfter`.\n     */\n    @Test\n    fun someTest2() = test(\n        suppressCommonBefore = true,\n        suppressCommonAfter = true\n    ) {\n        before {\n            UltronLog.info(\"Before TestMethod 2\")\n        }.go {\n            UltronLog.info(\"Run TestMethod 2\")\n        }.after {\n            UltronLog.info(\"After TestMethod 2\")\n        }\n    }\n\n    /**\n     * An order of methods execution is follow: beforeTest, test, afterTest\n     * `beforeFirstTest` - Not executed, since it was executed before `someTest1`\n     */\n    @Test\n    fun someTest3() = test {\n        UltronLog.info(\"UltronTest simpleTest\")\n    }\n}\n```\n\n### Key Methods\n\n- **`beforeFirstTest`**: Code executed once before all tests in a class.\n- **`beforeTest`**: Code executed before each test.\n- **`afterTest`**: Code executed after each test.\n- **`test`**: Executes a test with options to suppress pre-test or post-test actions.\n\n### Key Features of the `test` Method\n\n- **Test Context Recreation:**  \n  The `test` method automatically recreates the `UltronTestContext` for each test execution, ensuring a clean and isolated state for the test context.\n\n- **Soft Assertion Reset:**  \n  Any exceptions captured during `softAssertions` in the previous test are cleared at the start of each new `test` execution, maintaining a clean state.\n\n- **Lifecycle Management:**\n  It invokes `beforeTest` and `afterTest` methods around your test logic unless explicitly suppressed.\n---\n\n### Purpose of `before`, `go`, and `after`\n- **`before`:** Defines preconditions or setup actions that must be performed before the main test logic is executed.\nThese actions might include preparing data, navigating to a specific screen, or setting up the environment.\n  ```kotlin\n  before {\n      UltronLog.info(\"Setting up preconditions for TestMethod 2\")\n  }\n  ```\n\n- **`go`:** Encapsulates the core logic or actions of the test. This is where the actual operations being tested are performed, such as interacting with UI elements or executing specific functionality.\n  ```kotlin\n  go {\n      UltronLog.info(\"Executing the main logic of TestMethod 2\")\n  }\n  ```\n\n- **`after`:** Block is used for postconditions or cleanup actions that need to occur after the main test logic has executed. This might include verifying results, resetting the environment, or clearing resources.\n  ```kotlin\n  after {\n      UltronLog.info(\"Cleaning up after TestMethod 2\")\n  }\n  ```\n\nThese methods help clearly separate test phases, making tests easier to read and maintain.\n\n## Using `softAssertion` for Flexible Error Handling\n\nThe `softAssertion` mechanism in Ultron allows tests to catch and verify multiple exceptions during their execution without failing immediately. This feature is particularly useful for validating multiple conditions within a single test.\n### Example of `softAssertion`\n\n```kotlin\nclass SampleTest : UltronTest() {\n    @Test\n    fun softAssertionTest() {\n        softAssertion(failOnException = false) {\n            hasText(\"NotExistText\").withTimeout(100).assertIsDisplayed()\n            hasTestTag(\"NotExistTestTag\").withTimeout(100).assertHasClickAction()\n        }\n        verifySoftAssertions()\n    }\n}\n```\n\nThe `softAssertion` mechanism does not inherently depend on `UltronTest`.\nYou can use `softAssertion` independently of the `UltronTest` base class. However, in such cases, you must manually clear exceptions between tests to ensure they do not persist across test executions.\n```kotlin\nclass SampleTest {\n    @Test\n    fun softAssertionTest() {\n        UltronCommonConfig.testContext.softAnalyzer.clear()\n        softAssertion() {\n            //assert smth\n        }\n    }\n}\n```\n\n### Explanation\n\n- **Fail on Exception:** By default (`failOnException = true`), `softAssertion` will throw an exception after completing all operations within its block if any failures occur.\n- **Manual Verification:** If `failOnException` is set to `false`, you can explicitly verify all caught exceptions at the end of the test using `verifySoftAssertions()`.\n\nThis approach ensures granular control over how exceptions are handled and reported, making it easier to analyze and debug test failures.\n\n---\n\n## Benefits of `UltronTest` usage\n\n- Simplifies test setup and teardown with consistent preconditions and postconditions.\n- Enhances error handling by allowing multiple assertions within a single test.\n- Improves test readability and maintainability.\n\nBy leveraging `UltronTest` and `softAssertion`, you can build robust and flexible UI tests for your applications.\n\n"
  },
  {
    "path": "docs/docs/compose/_category_.json",
    "content": "{\n  \"label\": \"Compose\",\n  \"position\": 2,\n  \"collapsed\": false\n}\n"
  },
  {
    "path": "docs/docs/compose/android.md",
    "content": "---\nsidebar_position: 2\n---\n\n# Android\n\nNote: it's possible to use Multiplatform approach using methods `runComposeUiTest` and `runUltronUiTest` for Android UI tests. \nYou can read about it in [multiplatform description](multiplatform.md)\n\n## Android Compose testing API\n\nTypical Android test looks smth like this:\n\n```kotlin\nclass ComposeContentTest {\n    @get:Rule\n    val composeTestRule = createComposeRule()\n    @Test\n    fun myTest() {\n        composeTestRule.setContent { .. } \n        composeTestRule.onNode(hasTestTag(\"Continue\")).performClick()\n        composeTestRule.onNodeWithText(\"Welcome\").assertIsDisplayed()\n    }\n}\n```\nYou can read more about it in [official documentation](https://developer.android.com/jetpack/compose/testing)\n\nSo, all compose testing APIs are provided by `composeTestRule`. It's definitely uncomfortable. Moreover, in case your UI loading takes some time, e.g. in integration test, an assertion or an action fails.\n\nIf you need to launch an Activity it's required to use another factory method to create Compose TestRule - `createAndroidComposeRule<A>`\n\n```kotlin\nclass ActivityComposeTest {\n    @get:Rule\n    val composeTestRule = createAndroidComposeRule<YourActivity>()\n    @Test\n    fun myTest() {\n        composeTestRule.onNode(hasTestTag(\"Continue\")).performClick()\n        composeTestRule.onNodeWithText(\"Welcome\").assertIsDisplayed()\n    }\n}\n```\n\n_**Ultron**_ framework solves all these problems and do a lot more.\n\n## Ultron Compose\n\nJust create compose rule using Ultron static method\n\n```kotlin\n@get:Rule\nval composeTestRule = createDefaultUltronComposeRule()\n```\nAfter that you're able to perform stable compose operations in **ANY** class. Just create a `SemanticsMatcher`(like `hasTestTag(\"smth\")`) and call an operation on it. e.g.\n```kotlin\nhasTestTag(\"Continue\").click()\nhasText(\"Welcome\").assertIsDisplayed()\n```\n\n`SemanticsMatcher` object is used in Android Compose testing framework to find a target node to interact with.\n\nTo launch an Activity use `createUltronComposeRule<A>` or `createSimpleUltronComposeRule<A>`\n\n```kotlin\n@get:Rule\nval composeTestRule = createUltronComposeRule<YourActivity>()\n```\n\n`createSimpleUltronComposeRule<A>` used `UltronActivityRule` for launch and finish activity. You can read more in testconditions chapter\n"
  },
  {
    "path": "docs/docs/compose/api.md",
    "content": "---\nsidebar_position: 4\n---\n\n# Ultron Compose API\n\nThe framework provides an extended API for Compose UI testing. Basically, it's available for `SemanticsMatcher` object. It could be created by functions like `hasTestTag()`,  `hasText()` and etc.\n```kotlin\n//config\nfun withTimeout(timeoutMs: Long)  // to change an operation timeout from default one\nfun withResultHandler(resultHandler: (ComposeOperationResult<UltronComposeOperation>) -> Unit) // provide a scope to modify operation result processing\nfun <T> isSuccess(action: UltronComposeSemanticsNodeInteraction.() -> T): Boolean\nfun withAssertion(assertion: OperationAssertion)\nfun withAssertion(name: String = \"\", isListened: Boolean = false, block: () -> Unit)\nfun withUseUnmergedTree(value: Boolean) \nfun withName(name: String) // specify custom name for UI element, it'll be visible in log, exception, and step name for detailed allure report\nfun withDescription(description: String) // analog of fun withName(name: String) for matchers of UltronComposeList, UltronComposeListItem, and child of UltronComposeListItem\nfun withMetaInfo(meta: Any) // allows association of custom info with UI element\n\n//actions\nfun click(option: ClickOption? = null)\nfun clickCenterLeft(option: ClickOption? = null)\nfun clickCenterRight(option: ClickOption? = null)\nfun clickTopCenter(option: ClickOption? = null)\nfun clickTopLeft(option: ClickOption? = null)\nfun clickTopRight(option: ClickOption? = null)\nfun clickBottomCenter(option: ClickOption? = null)\nfun clickBottomLeft(option: ClickOption? = null)\nfun clickBottomRight(option: ClickOption? = null)\nfun longClick(option: LongClickOption? = null)\nfun longClickCenterLeft(option: LongClickOption? = null)\nfun longClickCenterRight(option: LongClickOption? = null)\nfun longClickTopCenter(option: LongClickOption? = null)\nfun longClickTopLeft(option: LongClickOption? = null)\nfun longClickTopRight(option: LongClickOption? = null)\nfun longClickBottomCenter(option: LongClickOption? = null)\nfun longClickBottomLeft(option: LongClickOption? = null)\nfun longClickBottomRight(option: LongClickOption? = null)\nfun doubleClick(option: DoubleClickOption? = null)\nfun doubleClickCenterLeft(option: DoubleClickOption? = null)\nfun doubleClickCenterRight(option: DoubleClickOption? = null)\nfun doubleClickTopCenter(option: DoubleClickOption? = null)\nfun doubleClickTopLeft(option: DoubleClickOption? = null)\nfun doubleClickTopRight(option: DoubleClickOption? = null)\nfun doubleClickBottomCenter(option: DoubleClickOption? = null)\nfun doubleClickBottomLeft(option: DoubleClickOption? = null)\nfun doubleClickBottomRight(option: DoubleClickOption? = null)\nfun swipeDown(option: ComposeSwipeOption? = null)\nfun swipeUp(option: ComposeSwipeOption? = null)\nfun swipeLeft(option: ComposeSwipeOption? = null)\nfun swipeRight(option: ComposeSwipeOption? = null)\nfun scrollTo()\nfun scrollToIndex(index: Int)\nfun scrollToKey(key: String)\nfun scrollToNode(matcher: SemanticsMatcher)\nfun imeAction()\nfun pressKey(keyEvent: KeyEvent)\nfun getText(): String?\nfun inputText(text: String)\nfun typeText(text: String)\nfun inputTextSelection(selection: TextRange)\nfun setSelection(startIndex: Int = 0, endIndex: Int = 0, traversalMode: Boolean)\nfun selectText(range: TextRange)\nfun clearText()\nfun replaceText(text: String)\nfun copyText()\nfun pasteText()\nfun cutText()\nfun setText(text: String)\nfun setText(text: AnnotatedString)\nfun collapse()\nfun expand()\nfun dismiss()\nfun setProgress(value: Float)\nfun captureToImage(): ImageBitmap\n\nfun performMouseInput(block: MouseInjectionScope.() -> Unit)\nfun performSemanticsAction(key: SemanticsPropertyKey<AccessibilityAction<() -> Boolean>>) \nfun perform(params: UltronComposeOperationParams? = null, block: (SemanticsNodeInteraction) -> Unit)\nfun <T> execute(params: UltronComposeOperationParams? = null, block: (SemanticsNodeInteraction) -> T): T\n\nfun getNode(): SemanticsNode\nfun <T> getNodeConfigProperty(key: SemanticsPropertyKey<T>): T\n\n//asserts\nfun assertIsDisplayed()\nfun assertIsNotDisplayed() \nfun assertExists()\nfun assertDoesNotExist()\nfun assertIsEnabled() \nfun assertIsNotEnabled() \nfun assertIsFocused() \nfun assertIsNotFocused() \nfun assertIsSelected() \nfun assertIsNotSelected()\nfun assertIsSelectable()\nfun assertIsOn() \nfun assertIsOff() \nfun assertIsToggleable() \nfun assertHasClickAction() \nfun assertHasNoClickAction() \nfun assertTextEquals(vararg expected: String, option: TextEqualsOption? = null)\nfun assertTextContains(expected: String, option: TextContainsOption? = null)\nfun assertContentDescriptionEquals(vararg expected: String)\nfun assertContentDescriptionContains(expected: String, option: ContentDescriptionContainsOption? = null)\nfun assertValueEquals(expected: String) \nfun assertRangeInfoEquals(range: ProgressBarRangeInfo)\nfun assertHeightIsAtLeast(minHeight: Dp) \nfun assertHeightIsEqualTo(expectedHeight: Dp)\nfun assertWidthIsAtLeast(minWidth: Dp) \nfun assertWidthIsEqualTo(expectedWidth: Dp) \nfun assertMatches(matcher: SemanticsMatcher, messagePrefixOnError: (() -> String)? = null) \n```\n\n### _Best practice_ \n\n> Use Page Object pattern. Specify page elements as properties of Page class\n\n```kotlin\nobject SomePage : Page<SomePage>() {\n    private val button = hasTestTag(ComposeTestTags.button)\n    private val eventStatus = hasTestTag(ComposeTestTags.eventStatus)\n}\n```\n\nHere `ComposeTestTags` could be an object that stores testTag constants.\n\nUse this properties in page steps\n\n```kotlin\nobject SomePage : Page<SomePage>() {\n    //page elements\n    fun someUserStepOnPage(expectedEventText: String) = apply {\n         button.click()\n         eventStatus.assertTextContains(expectedEventText)\n    }\n}\n```\n\nIt's possible to use term `Screen` instead of `Page`. They are equals.\n\n```kotlin\nobject SomeScreen : Screen<SomeScreen>() { ... }\n```\n\n\n## Extend framework with your own compose operations\n\nUnder the hood all Ultron compose operations are described in `UltronComposeSemanticsNodeInteraction` class. That is why you just need to extend this class using [kotlin extension function](https://kotlinlang.org/docs/extensions.html), e.g.\n\n```kotlin\n//new semantic matcher for assertion\nfun hasProgress(value: Float): SemanticsMatcher = SemanticsMatcher.expectValue(GetProgress, value)\n\n//add new operation\nfun UltronComposeSemanticsNodeInteraction.assertProgress(expected: Float) = apply {\n    executeOperation(\n        operationBlock = { semanticsNodeInteraction.assert(hasProgress(expected)) },\n        name = \"Assert '${semanticsNodeInteraction.getDescription()}' has progress $expected\",\n        description = \"Compose assertProgress = $expected in '${semanticsNodeInteraction.getDescription()}' during $timeoutMs ms\",\n    )\n}\n\n//extend SemanticsMatcher with your new operation\nfun SemanticsMatcher.assertProgress(expected: Float) = UltronComposeSemanticsNodeInteraction(this).assertProgress(expected)\n```\nHow to use\n```kotlin\nval progress = 0.7f\nhasTestTag(ComposeElementsActivity.progressBar).setProgress(progress).assertProgress(progress)\n```\nYou may ask what is `GetProgress`?\n\nThis is a feature of Compose framework. It's available to extend you app with custom SemanticsPropertyKey. Define it in app and assert it in tests.\n\n```kotlin\n//application code\n@Composable\nfun LinearProgressBar(statusState: MutableState<String>){\n    val progressState = remember {\n        mutableStateOf(0f)\n    }\n    LinearProgressIndicator(progress = progressState.value, modifier =\n    Modifier\n        .semantics {\n            testTag = ComposeElementsActivity.progressBar\n            setProgress { value ->\n                progressState.value = value\n                statusState.value = \"set progress $value\"\n                true\n            }\n            progressBarRangeInfo = ProgressBarRangeInfo(progressState.value, 0f..progressState.value, 100)\n        }\n        .getProgress(progressState.value)\n        .progressSemantics()\n    )\n}\n\nval GetProgress = SemanticsPropertyKey<Float>(\"ProgressValue\")\nvar SemanticsPropertyReceiver.getProgress by GetProgress\n\nfun Modifier.getProgress(progress: Float): Modifier {\n    return semantics { getProgress = progress }\n}\n```\n\n\n"
  },
  {
    "path": "docs/docs/compose/index.md",
    "content": "# Compose\n\nThere are two types of UI tests you can write with Compose.\n\n1. Kotlin Multiplatform UI test ([Kotlin documentation](https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-test.html))\n2. Platform Specific JUnit-based tests ([Android documentation](https://developer.android.com/develop/ui/compose/testing))\n\nUltron supports both types of UI tests and make it`s development much easier."
  },
  {
    "path": "docs/docs/compose/lazylist.md",
    "content": "---\nsidebar_position: 5\n---\n\n# LazyList\n\n## Ultron LazyColumn/LazyRow\n\nIt's pretty much familiar with `UltronRecyclerView` approach. The difference is in internal structure of `RecyclerView `and `LazyColumn/LazyRow`.\nDue to implementation features of LazyColumn/LazyRow we can't predict where matched item is located in list without scrolling (actually we can but it takes additional efforts from development)\n\nBefore we go forward we need to clarify some terms:\n\n- ComposeList - list of some items. It's typically implemented in application as LazyColumnt or LazyRow. Ultron has a class that wraps an interaction with list - `UltronComposeList`.\n- ComposeListItem - single item of ComposeList (there is a class `UltronComposeListItem`)\n- ComposeListItemChild - child element of ComposeListItem (just a term, there is no special class to work with child elements). So _ComposeListItemChild_ could be considered as a simple compose node.\n\n![lazyColumn](https://user-images.githubusercontent.com/12834123/188237127-32e501ca-ae8b-4cd4-8114-e3e17843dc55.PNG)\n\n***\n## UltronComposeList\n\nCreate an instance of `UltronComposeList` by calling a method `composeList(..)`\n\n```kotlin\ncomposeList(hasTestTag(contactsListTestTag)).assertNotEmpty()\n```\n### _Best practice_ - define `UltronComposeList` object as page class property\n\n```kotlin\nobject ContactsListPage : Page<ContactsListPage >() {\n   val lazyList = composeList(hasContentDescription(contactsListContentDesc))\n    fun someStep(){\n        lazyList.assertNotEmpty() \n        lazyList.assertContentDescriptionEquals(contactsListContentDesc)\n    }\n}\n```\n\n### `UltronComposeList` API\n```kotlin\nwithTimeout(timeoutMs: Long) // defines a timeout for all operations \n//assertions\nfun assertIsDisplayed() \nfun assertIsNotDisplayed()\nfun assertExists() \nfun assertDoesNotExist()\nfun assertContentDescriptionEquals(vararg expected: String)\nfun assertContentDescriptionContains(expected: String, option: ContentDescriptionContainsOption? = null)\nfun assertNotEmpty()\nfun assertEmpty()\nfun assertVisibleItemsCount(expected: Int) \n\n//item providers for simple UltronComposeListItem\nfun item(matcher: SemanticsMatcher): UltronComposeListItem\nfun visibleItem(index: Int): UltronComposeListItem\nfun firstVisibleItem(): UltronComposeListItem\nfun lastVisibleItem(): UltronComposeListItem\n\n// ----- item providers for UltronComposeListItem subclasses -----\n// following methods return a generic type T which is a subclass of UltronComposeListItem\nfun getItem(matcher: SemanticsMatcher): T\nfun getVisibleItem(index: Int): T\nfun getFirstVisibleItem(): T \nfun getLastVisibleItem(): T\n\n//interaction provider\nvisibleChild(matcher: SemanticsMatcher)  // provides an interaction on visible matched item\n\n//actions\nfun getVisibleItemsCount(): Int\nfun scrollToNode(itemMatcher: SemanticsMatcher)\nfun scrollToIndex(index: Int) \nfun scrollToKey(key: Any)\n/**\n* Provide a scope with references to list SemanticsNode and SemanticsNodeInteraction.\n* It is possible to evaluate any action or assertion on this node.\n*/\nfun <T> performOnList(block: (SemanticsNode, SemanticsNodeInteraction) -> T): T\n```\n\n### useUnmergedTree\nIt is really important to understand the difference btwn merged and unmerged tree. There is a property `useUnmergedTree` that defines a behaviour.\n```kotlin\ncomposeList(hasTestTag(contactsListTestTag), useUnmergedTree = false)\n```\n\n- By default `UltronComposeList` uses unmerged tree (`useUnmergedTree = true`). All child elements contain info in seperate nodes.\n- In case we use merged tree (`useUnmergedTree = false`) all child elements of item is merged to single node. So you're not able to identify a text value of concrete child.\n\nWhy it's important? Cause you need to use different SemanticsMatchers to find appropriate child.\n\n```kotlin\nmergedTreeList.item(hasText(contact.name)) // contact.name could be placed in wrong child\nunmergedList.item(hasAnyDescendant(hasText(contact.name) and hasTestTag(contactNameTestTag))) //it's longer but certainly provides target node\n```\n***\n## UltronComposeListItem\n`UltronComposeList` provides an access to `UltronComposeListItem`\n\nThere is a set of methods to create `UltronComposeListItem`. It's listed upper in `UltronComposeList` api.\n\n### Simple `UltronComposeListItem`\n\nIf you don't need to interact with item child just use methods like  `item`, `firstItem`, `visibleItem`, `firstVisibleItem`, `lastVisibleItem`\n```kotlin\nlistWithMergedTree.item(hasText(contact.name)).assertTextContains(contact.name)\nlistWithMergedTree.firstVisibleItem()\n    .assertIsDisplayed()\n    .assertTextContains(contact.name)\n    .assertTextContains(contact.status)\n```\nYou don't need to worry about scroll to item. It's executed automatically.\n\n### Complex `UltronComposeListItem` with children\n\nIt's often required to interact with item child. The best solution will be to describe children as properties of UltronComposeListItem subclass.\n```kotlin\nclass ComposeFriendListItem : UltronComposeListItem(){\n    val name by child { hasTestTag(contactNameTestTag) }\n    val status by child { hasTestTag(contactStatusTestTag) }\n}\n```\n**Note: you have to use delegated initialisation with `by child`.**\n\nFor Compose Multiplatform project you need to register Item class instances with `initBlock` param:\n\n```kotlin\ncomposeList(.., initBlock = {\n    registerItem { ComposeFriendListItem() }\n    registerItem { AnotherListItem() }\n})\n```\nIt is required cause Kotlin Multiplatfor Project has limited reflation API for different platforms.\n\nYou don't need to register Items for Android UI tests.\n\nNow you're able to get `ComposeFriendListItem` object using methods `getItem`, `getVisibleItem`, `getFirstVisibleItem`, `getLastVisibleItem`\n\n```kotlin\nlazyList.getFirstVisibleItem<ComposeFriendListItem>()\nlazyList.getVisibleItem<ComposeFriendListItem>(index)\nlazyList.getItem<ComposeFriendListItem>(hasTestTag(..))\n```\n\n### _Best practice_\n\n> Add a method to `Page` class that returns `UltronComposeListItem` subclass\n\nMark such methods with `private` visibility modifier. e.g. `getContactItem`\n```kotlin\nobject ComposeListPage : Page<ComposeListPage>() {\n    private val lazyList = composeList(hasContentDescription(contactsListContentDesc), ..)\n    private fun getContactItem(contact: Contact): ComposeFriendListItem = lazyList.getItem(hasTestTag(contact.id))\n\n    class ComposeFriendListItem : UltronComposeListItem(){\n        val name by lazy { getChild(hasTestTag(contactNameTestTag)) }\n        val status by lazy { getChild(hasTestTag(contactStatusTestTag)) }\n    }\n}\n```\nUse `getContactItem` in `Page` steps like `assertContactStatus`\n```kotlin\nobject ComposeListPage : Page<ComposeListPage>() {\n    private fun getContactItem(contact: Contact): ComposeFriendListItem = lazyList.getItem(hasTestTag(contact.id))\n    ...\n    fun assertContactStatus(contact: Contact) = apply {\n         getContactItem(contact).status.assertTextEquals(contact.status)\n    }\n}\n```\n\n## `UltronComposeListItem` API\n\nIt's pretty much the same as [simple node api](../compose/api.md), but extends it mostly for internal features.\n\n***\n## Efficient Strategies for Locating Items in Compose LazyList\n\nLet's start with approaches that you can use without additional efforts. For example, you have identified `LazyList` in your tests code like\n\n```kotlin\nval lazyList = composeList(listMatcher = hasTestTag(\"listTestTag\").withDescription(description = \"List of contacts\"), ..)\n\nclass ComposeListItem : UltronComposeListItem() {\n    val name by lazy { getChild(hasTestTag(contactNameTestTag).withDescription(description = \"Contact name\")) }\n    val status by lazy { getChild(hasTestTag(contactStatusTestTag).withDescription(description = \"Contact status\")) }\n}\n```\n\n### 1. `..visibleItem`\n\nThis is probably the most unstable approach. It's only suitable in case you didn't interact with `LazyList` and would like to reach an item that is on the screen.\n\nUse the following methods:\n\n```kotlin\nlazyList.firstVisibleItem()\nlazyList.visibleItem(index = 3)\nlazyList.lastVisibleItem()\n\nlazyList.getFirstVisibleItem<ComposeListItem>()\nlazyList.getVisibleItem<ComposeListItem>(index = 3)\nlazyList.getLastVisibleItem<ComposeListItem>()\n```\n\n### 2. Item by unique `SemanticsMatcher`\n\nA more stable way to find the item is to use `SemanticsMatcher`. It allows you to find the item not only on the screen.\n\n```kotlin\nval someText = \"Some unique text\"\nlazyList.item(hasAnyDescendant(hasText(someText).withDescription(description = someText)) \nlazyList.getItem<ComposeListItem>(hasAnyDescendant(hasText(\"Some unique text\")) \n```\n\n***\n\nThe next two approaches require additional code in the application. These are the most stable and preferable ways.\n\n### 3. Set up `positionPropertyKey`\n\nBy default, a compose list item doesn't have a property that stores its position in the list. We can add this property in a really simple way.\n\nHere is the application code:\n```kotlin\n// create custom SemanticsPropertyKey\nval ListItemPositionPropertyKey = SemanticsPropertyKey<Int>(\"ListItemPosition\")\nvar SemanticsPropertyReceiver.listItemPosition by ListItemPositionPropertyKey\n\n// specify it for item and store item index in this property\n@Composable\nfun ContactsListWithPosition(contacts: List<Contact>\n) {\n    LazyColumn(\n        modifier = Modifier.semantics { testTag = \"listTestTag\" }\n    ) {\n        itemsIndexed(contacts) { index, contact ->\n            Column(\n                modifier = Modifier.semantics {\n                    listItemPosition = index\n                }\n            ) {\n                // item content\n            }\n        }\n    }\n}\n```\n\nAfter that, you need to specify the custom `SemanticsPropertyKey` in the test code:\n\n```kotlin\nval lazyList = composeList(\n    listMatcher = hasTestTag(\"listTestTag\"),\n    positionPropertyKey = ListItemPositionPropertyKey\n)\n```\n\nIt allows you to reach the item by its position in the list:\n\n```kotlin\nlazyList.firstItem()\nlazyList.item(position = 25)\nlazyList.getFirstItem<ComposeListItem>()\nlazyList.getItem<ComposeListItem>(position = 7)\n```\n\n### 4. Set up item `testTag`\n\nIt is recommended to build `testTag` in a separate function based on data object. \n\nFor example, let's assume we have a `Contact` data class that stores data to be presented in the item.\n\n```kotlin\ndata class Contact(val id: Int, val name: String, val status: String, val avatar: String)\n```\n\nWe can create function to build `testTag` based on `contact.id`\n\n```kotlin\nfun getContactItemTestTag(contact: Contact) = \"contactId=${contact.id}\"\n```\n\nWe can use this function in the application code to specify `testTag` and in the test code to find the item by `testTag`:\n\n```kotlin\n// application code\n@Composable\nfun ContactsListWithPosition(contacts: List<Contact>\n) {\n    LazyColumn(\n        modifier = Modifier.semantics { testTag = \"listTestTag\" }\n    ) {\n        itemsIndexed(contacts) { index, contact ->\n            Column(\n                modifier = Modifier.semantics {\n                    listItemPosition = index\n                    testTag = getContactItemTestTag(contact)\n                }\n            ) {\n                // item content\n            }\n        }\n    }\n}\n\n//test code\nval lazyList = composeList(listMatcher = hasTestTag(\"listTestTag\"))\n\nlazyList.item(hasTestTag(getContactItemTestTag(contact)))\nlazyList.getItem<ComposeListItem>(hasTestTag(getContactItemTestTag(contact)))\n\n```"
  },
  {
    "path": "docs/docs/compose/multiplatform.md",
    "content": "---\nsidebar_position: 1\n---\n\n# Multiplatform\n\n> Multiplatform support is in Alpha state.\n\nCompose Multiplatform provides robust tools for building and testing UI components across various platforms. One significant aspect of this is the ability to write and run common tests for your UI elements ([official doc sample](https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-test.html#write-and-run-common-tests)).\n\n### `runComposeUiTest` vs `runUltronUiTest`\n\nWith standart Compose Testing framework you have to use `runComposeUiTest` method to interact with UI elements.\n\nHere is simplified basic test sample with Compose Multiplatform. Typically it's placed in common app module, like `composeApp/src/commonTest/kotlin`\n\n```kotlin\nclass ComposeExampleTest {\n    @Test\n    fun myTest() = runComposeUiTest {\n        setContent {\n            // reasonable UI content\n        }\n        onNode(hasTestTag(\"text\")).assertTextEquals(\"Hello\")\n        onNode(hasTestTag(\"button\")).performClick()\n        onNode(hasTestTag(\"text\")).assertTextEquals(\"Compose\")\n    }\n}\n```\n\nUsage of `runUltronUiTest` function simplifies the interaction syntax.  \n\n```kotlin\nclass UltronExampleTest {\n    @Test\n    fun myTest() = runUltronUiTest {\n        setContent {\n            // reasonable UI content\n        }\n        hasTestTag(\"text\").assertTextEquals(\"Hello\")\n        hasTestTag(\"button\").click()\n        hasTestTag(\"text\").assertTextEquals(\"Compose\")\n    }\n}\n```\nMore over it makes interactions more reliable and stable. \n\nAdditionally, it becomes possible to call these interactions **EVERYWHERE** you want, e.g. in **Page Objects**\n\n### Compose Page Object\n\nEveryone knows that **Page Object** pattern is a good pattern. But how to use it for multiplatform tests?\n\nWhile `runComposeUiTest` provides the context for interaction with UI elements, like calling `onNodeWithTag()`, moving this logic into a Page Object or any other class/method can lead to issues, as these don’t have direct access to the testing API. This is because the testing API is provided by an object called `SemanticsNodeInteractionProvider`, which needs to be passed into each object to call the testing API.\n\nHere’s an example of a modified test using the Page Object pattern:\n\n```kotlin\nclass PageObjectMultiplatformTest {\n    @Test\n    fun myTest() = runComposeUiTest {\n        setContent {\n            // reasonable UI content\n        }\n        ExamplePage(provider = this).someStep()\n    }\n}\n\nclass ExamplePage(val provider: SemanticsNodeInteractionsProvider){\n    fun someStep(){\n        provider.onNodeWithTag(\"text\").assertTextEquals(\"Hello\")\n        provider.onNodeWithTag(\"button\").performClick()\n        provider.onNodeWithTag(\"text\").assertTextEquals(\"Compose\")\n    }\n}\n```\n\n### Ultron Page Object\n\nUltron eliminates the need to pass `SemanticsNodeInteractionProvider` into each object. You only need to replace the `runComposeUiTest` method with `runUltronUiTest`.\n\n```kotlin\nclass UltronMultiplatformTest {\n    @Test\n    fun myTest() = runUltronUiTest {\n        setContent {\n            // reasonable UI content\n        }\n\n        UltronPage.someStep()\n    }\n}\n\nobject UltronPage {\n    fun someStep(){\n        hasTestTag(\"text\").assertTextEquals(\"Hello\")\n        hasTestTag(\"button\").click()\n        hasTestTag(\"text\").assertTextEquals(\"Compose\")\n    }\n}\n```"
  },
  {
    "path": "docs/docs/index.md",
    "content": "---\nsidebar_position: 1\n---\n\n# Introduction\n\n![Docusaurus themed image](/img/ultron_banner_light.png#gh-light-mode-only)![Docusaurus themed image](/img/ultron_banner_dark.png#gh-dark-mode-only)\n\nUltron is the simplest framework to develop UI tests for **Android** & **Compose Multiplatform**. \n\nIt's constructed upon the Espresso, UI Automator and Compose UI testing frameworks. Ultron introduces a range of remarkable new features. Furthermore, Ultron puts you in complete control of your tests! \n\nYou don't need to learn any new classes or special syntax. All magic actions and assertions are provided from crunch. Ultron can be easially customised and extended. \n\n## What are the benefits of using the framework?\n\n- Page/Screen Object pattern support\n- Exceptional simplification for [**Compose UI tests**](compose/index.md)\n- Out-of-the-box generation of [**Allure report**](./common/allure.md) (Now, for Android UI tests only)\n- A straightforward and expressive syntax\n- Ensured **Stability** for all actions and assertions\n- Complete control over every action and assertion\n- Incredible interaction with lists: [**RecyclerView**](./android/recyclerview.md) and [**Compose LazyList**](compose/lazylist.md).\n- An **Architectural** approach to developing UI tests (search \"Best practice\")\n- An incredible mechanism for setups and teardowns (You can even set up preconditions for a single test within a test class, without affecting the others)\n- [The ability to effortlessly extend the framework with your own operations](common/extension.md)\n- Accelerated UI Automator operations\n- Ability to monitor each stage of operation execution with [Listeners](common/listeners.md)\n- [Custom operation assertions](common/customassertion.md)\n***\n\n### A few words about syntax\n\nThe standard syntax provided by Google is intricate and not intuitive. This is especially evident when dealing with **LazyList** and **RecyclerView** interactions.\n\nLet's explore some examples:\n\n#### 1. Simple compose operation (refer to the doc [here](./compose/index.md))\n\n_Compose framework_\n\n```kotlin\ncomposeTestRule.onNode(hasTestTag(\"Continue\")).performClick()\ncomposeTestRule.onNodeWithText(\"Welcome\").assertIsDisplayed()\n```\n_Ultron_\n\n```kotlin\nhasTestTag(\"Continue\").click()\nhasText(\"Welcome\").assertIsDisplayed()\n```\n\n#### 2. Compose list operation (refer to the [doc](./compose/lazylist.md))\n\n_Compose framework_\n\n```kotlin\nval itemMatcher = hasText(contact.name)\ncomposeRule\n    .onNodeWithTag(contactsListTestTag)\n    .performScrollToNode(itemMatcher)\n    .onChildren()\n    .filterToOne(itemMatcher)\n    .assertTextContains(contact.name)\n```\n\n_Ultron_\n\n```kotlin\ncomposeList(hasTestTag(contactsListTestTag))\n    .item(hasText(contact.name))\n    .assertTextContains(contact.name)\n```\n#### 3. Simple Espresso assertion and action.\n\n_Espresso_\n\n```kotlin\nonView(withId(R.id.send_button)).check(isDisplayed()).perform(click())\n```\n_Ultron_\n\n```kotlin\nwithId(R.id.send_button).isDisplayed().click()\n```\nThis presents a cleaner approach. Ultron's operation names mirror Espresso's, while also providing additional operations. \n\nRefer to the [doc](./android/espress.md) for further details.\n\n#### 4. Action on RecyclerView list item\n\n_Espresso_\n\n```kotlin\nonView(withId(R.id.recycler_friends))\n    .perform(\n        RecyclerViewActions\n            .actionOnItem<RecyclerView.ViewHolder>(\n                hasDescendant(withText(\"Janice\")),\n                click()\n            )\n        )\n```\n_Ultron_\n\n```kotlin\nwithRecyclerView(R.id.recycler_friends)\n    .item(hasDescendant(withText(\"Janice\")))\n    .click()\n```\n\nExplore the [doc](./android/espress.md) to unveil Ultron's magic with RecyclerView interactions.\n\n#### 5. Espresso WebView operations \n\n_Espresso_\n\n```kotlin\nonWebView()\n    .withElement(findElement(Locator.ID, \"text_input\"))\n    .perform(webKeys(newTitle))\n    .withElement(findElement(Locator.ID, \"button1\"))\n    .perform(webClick())\n    .withElement(findElement(Locator.ID, \"title\"))\n    .check(webMatches(getText(), containsString(newTitle)))\n```\n\n_Ultron_\n\n```kotlin\nid(\"text_input\").webKeys(newTitle)\nid(\"button1\").webClick()\nid(\"title\").hasText(newTitle)\n```\n\nRefer to the [doc](./android/webview.md) for more details.\n\n#### 6. UI Automator operations\n\n_UI Automator_\n\n```kotlin\nval device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())\ndevice\n    .findObject(By.res(\"com.atiurin.sampleapp:id\", \"button1\"))\n    .click()\n```\n\n_Ultron_\n\n```kotlin\nbyResId(R.id.button1).click() \n```\nRefer to the [doc](./android/uiautomator.md) \n***\n### Acquiring the result of any operation as Boolean value\n\n```kotlin\nval isButtonDisplayed = withId(R.id.button).isSuccess { isDisplayed() }\nif (isButtonDisplayed) {\n    //do some reasonable actions\n}\n```\n***\n### Why are all Ultron actions and assertions more stable?\n\nThe framework captures a list of specified exceptions and attempts to repeat the operation during a timeout period (default is 5 seconds). Of course, you have the ability to customize the list of handled exceptions. You can also set a custom timeout for any operation.\n\n```kotlin\nwithId(R.id.result).withTimeout(10_000).hasText(\"Passed\")\n```\n***\n## 3 steps to develop a test using Ultron\n\nWe advocate for a proper test framework architecture, division of responsibilities between layers, and other best practices. Therefore, when using Ultron, we recommend the following approach:\n\n1. Create a Page Object and specify screen UI elements as `Matcher<View>` objects.\n\n```kotlin\nobject ChatPage : Page<ChatPage>() {\n    private val messagesList = withId(R.id.messages_list)\n    private val clearHistoryBtn = withText(\"Clear history\")\n    private val inputMessageText = withId(R.id.message_input_text)\n    private val sendMessageBtn = withId(R.id.send_button)\n}\n```\n\nIt's recommended to make all Page Objects as `object` and descendants of Page class.\nThis allows for the utilization of convenient Kotlin features. It also helps you to keep Page Objects stateless.\n\n2. Describe user step methods in Page Object.\n\n```kotlin\nobject ChatPage : Page<ChatPage>() {\n    fun sendMessage(text: String) = apply {\n        inputMessageText.typeText(text)\n        sendMessageBtn.click()\n        getMessageListItem(text).text\n             .isDisplayed()\n             .hasText(text)\n    }\n\n    fun clearHistory() = apply {\n        openContextualActionModeOverflowMenu()\n        clearHistoryBtn.click()\n    }\n}\n```\nRefer to the full code sample [ChatPage.class](https://github.com/open-tool/ultron/blob/master/sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/ChatPage.kt)\n\n3. Call user steps in test\n\n```kotlin\n    @Test\n    fun friendsItemCheck(){\n        FriendsListPage {\n            assertName(\"Janice\")\n            assertStatus(\"Janice\",\"Oh. My. God\")\n        }\n    }\n    @Test\n    fun sendMessage(){\n        FriendsListPage.openChat(\"Janice\")\n        ChatPage {\n            clearHistory()\n            sendMessage(\"test message\")\n        }\n    }\n```\nRefer to the full code sample [DemoEspressoTest.class](https://github.com/open-tool/ultron/blob/master/sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso/DemoEspressoTest.kt)\n\nIn essence, your project's architecture will look like this:\n\n[acrchitecture](https://github.com/open-tool/ultron/assets/12834123/b0882d34-a18d-4f1f-959b-f75796d11036)\n\n***\n## Allure report\n\nUltron has built in support to generate artifacts for Allure reports. Just apply the recommended configuration and set testIntrumentationRunner. \n\nFor the complete guide, refer to the [Allure description](./common/allure.md)\n\n```kotlin\n@BeforeClass @JvmStatic\nfun setConfig() {\n    UltronConfig.applyRecommended()\n    UltronAllureConfig.applyRecommended()\n    UltronComposeConfig.applyRecommended() \n}\n```\n![allure](https://github.com/open-tool/ultron/assets/12834123/c05c813a-ece6-45e6-a04f-e1c92b82ffb1)\n\n![allure compose](https://github.com/open-tool/ultron/assets/12834123/1f751f3d-fc58-4874-a850-acd9181bfb70)"
  },
  {
    "path": "docs/docs/intro/_category_.json",
    "content": "{\n  \"label\": \"Getting started\",\n  \"position\": 1,\n  \"collapsed\": false\n}\n"
  },
  {
    "path": "docs/docs/intro/configuration.md",
    "content": "---\nsidebar_position: 4\n---\n\n# Configuration\n\nEach library of the framework has it's own config onject. \n\n- `UltronComposeConfig` - ultron-compose\n- `UltronConfig` - ultron-android\n- `UltronAllureConfig` - ultron-allure\n- `UltronCommonConfig` - inside each library\n\nYou can use recommended configuration and just apply it in **BaseTest** class ([sample](https://github.com/open-tool/ultron/blob/master/sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/BaseTest.kt#L29)) :\n\n```kotlin\n@BeforeClass\n@JvmStatic\nfun config() {\n    UltronConfig.applyRecommended()\n    UltronAllureConfig.applyRecommended()\n    UltronComposeConfig.applyRecommended()\n}\n\n```\n\n### UltronComposeConfig\n***\nManages configurations for Compose part of the framework\n\n```kotlin\nUltronComposeConfig.apply {\n    operationTimeoutMs = 10_000\n    lazyColumnOperationTimeoutMs = 15_000\n    operationPollingTimeoutMs = 100\n    lazyColumnItemSearchLimit = 100\n    useUnmergedTree = true // set up this value as a default for all SemanticNodeInteractions\n}\n```\n\n### UltronCommonConfig\n***\nProvides an ability to config common parameters for your testing framework. \n\n```kotlin\nUltronCommonConfig.apply {\n    logToFile = true\n    operationTimeoutMs = 10_000\n    logDateFormat = \"MM-dd HH:mm:ss.SSS\"\n}\n```\n\nIt also gives an API to add/remove operations listeners\n\n```kotlin\nUltronCommonConfig.addListener(CustomListener())\n```\n\n### UltronConfig \n***\n`UltronConfig` object is responsible for configuring and managing settings related to the Espresso, EspressoWeb, and UiAutomator. \n\nYou can set custom main settings using `apply` method.\n\n```kotlin\nUltronConfig.apply {\n    accelerateUiAutomator = true\n    operationTimeoutMs = 10_000\n}\n```\n\n- `UltronConfig.Espresso` nested Object:\n\nManages configurations specific to the Espresso part of the framework.\nProvides settings related to timeouts, view matchers, result analyzers, and action/assertion configurations.\n\n```kotlin\nUltronConfig.Espresso.RECYCLER_VIEW_LOAD_TIMEOUT = 20_000\nUltronConfig.Espresso.RECYCLER_VIEW_OPERATIONS_TIMEOUT = 10_000\nUltronConfig.Espresso.RECYCLER_VIEW_ITEM_SEARCH_LIMIT = 100\nUltronConfig.Espresso.INCLUDE_VIEW_HIERARCHY_TO_EXCEPTION = true // false by default\nUltronConfig.Espresso.setResultAnalyzer { operationResult ->\n    // set custom operations result analyzer \n}\n```\n\n- `UltronConfig.Espresso.ViewActionConfig` and `UltronConfig.Espresso.ViewAssertionConfig` nested Objects:\n\nManage configurations for Espresso view actions and view assertions, respectively.\nProvide settings for allowed exceptions and result handlers.\n\n```kotlin\nUltronConfig.Espresso.ViewActionConfig.allowedExceptions.add(CustomViewException::class.java)\nUltronConfig.Espresso.ViewAssertionConfig.allowedExceptions.add(CustomViewException::class.java)\n```\n\n- `UltronConfig.Espresso.WebInteractionOperationConfig` nested Object:\n\nManages configurations for Espresso web interaction operations.\nProvides settings for allowed exceptions and result handlers.\n\n```kotlin\nUltronConfig.Espresso.WebInteractionOperationConfig.allowedExceptions.add(CustomJSException::class.java)\n```\n\n- `UltronConfig.UiAutomator` nested Object:\n\nManages configurations specific to the UiAutomator part of the framework.\nProvides settings related to timeouts, result analyzers, and UiDevice configurations.\n\n```kotlin\nUltronConfig.UiAutomator.OPERATION_TIMEOUT = 15_000\nval device = UltronConfig.UiAutomator.uiDevice\nUltronConfig.UiAutomator.UiObject2Config.allowedExceptions.add(CustomViewException::class.java)\n```\n\n- `UltronConfig.UiAutomator.UiObjectConfig` and `UltronConfig.UiAutomator.UiObject2Config` nested Objects:\n\nManage configurations for UiAutomator operations using UiSelector and BySelector, respectively.\nProvide settings for allowed exceptions and result handlers.\n\n### UltronAllureConfig\n***\n\nHelp us to configure Allure report.\n\n```kotlin\nUltronAllureConfig.apply {\n    addScreenshotPolicy =  mutableSetOf(\n        AllureAttachStrategy.TEST_FAILURE,\n        AllureAttachStrategy.OPERATION_FAILURE,\n        AllureAttachStrategy.OPERATION_SUCCESS\n    )\n    addHierarchyPolicy = mutableSetOf(\n        AllureAttachStrategy.TEST_FAILURE\n    )\n    attachLogcat = false\n    attachUltronLog = true\n    addConditionsToReport = true\n    detailedAllureReport = true\n}\n```\n\nIt also allow us to add or remove RunListener. \n\n```kotlin\nUltronAllureConfig.addRunListener(LogcatAttachRunListener())\nUltronAllureConfig.removeRunListener(LogcatAttachRunListener::class.java)\n```\n\n"
  },
  {
    "path": "docs/docs/intro/connect.md",
    "content": "---\nsidebar_position: 2\n---\n\n# Connect to project\n\nThe framework has three libraries that could be added as dependencies.\n\n- `com.atiurin:ultron-compose` - could be used both for Android application and Compose Multiplatform UI tests\n- `com.atiurin:ultron-android` - native Android UI tests based on Espresso(including web part) and UI Automator\n- `com.atiurin:ultron-allure` - Allure report support for Android application UI tests \n\nYou need **mavenCentral** repository.\n\n```kotlin\nrepositories {\n    mavenCentral()\n}\n```\n\n### Android application instrumented UI tests\n```kotlin\ndependencies {\n    androidTestImplementation(\"com.atiurin:ultron-compose:<latest_version>\")\n    androidTestImplementation(\"com.atiurin:ultron-android:<latest_version>\")\n    androidTestImplementation(\"com.atiurin:ultron-allure:<latest_version>\")\n}\n```\n\n### Compose Multiplatform UI tests\n\n```kotlin\nkotlin {\n    sourceSets {\n         commonTest.dependencies {\n            implementation(\"com.atiurin:ultron-compose:<latest_version>\")\n        }\n    }\n}\n```\nSince Multiplatform support in alpha state it's possible to have some problems with `commonTest` usage.\n\nIn this case you can specify dependencies in relevant part.\n```kotlin\nkotlin {\n    androidTarget {\n        @OptIn(ExperimentalKotlinGradlePluginApi::class)\n        instrumentedTestVariant {\n            ...\n            dependencies {\n                implementation(\"com.atiurin:ultron-compose:<latest_version>\")\n            }\n        }\n    }\n    sourceSets {\n        val desktopTest by getting {\n            dependencies {\n                implementation(\"com.atiurin:ultron-compose:<latest_version>\")\n            }\n        }\n    }\n}\n```"
  },
  {
    "path": "docs/docs/intro/dependencies.md",
    "content": "---\nsidebar_position: 3\n---\n\n# Dependencies Management\n\nUltron provides all the required dependencies in a transitive manner. That means you don't need to specify the Espresso or UI Automator library in your dependencies section in most cases.\n\nYou can find all Ultron dependencies in [Versions.kt](https://github.com/open-tool/ultron/blob/master/buildSrc/src/main/kotlin/Versions.kt).\n\n## Android Dependencies\n\nThe `com.atiurin:ultron-android:<latest_version>` library provides:\n\n```kotlin\ndependencies {\n    api(Libs.espressoCore)\n    api(Libs.espressoContrib)\n    api(Libs.espressoWeb)\n    api(Libs.accessibility)\n    api(Libs.hamcrestCore)\n    api(Libs.uiautomator)\n}\n```\n\nIf you need another Espresso library in dependencies. It's better to use the same Espresso version as Ultron. \n\nNow - [Ultron Espresso verion](https://github.com/open-tool/ultron/blob/master/buildSrc/src/main/kotlin/Versions.kt#L9) is `3.6.1`. \n\n## Allure Dependencies\n\nThe `com.atiurin:ultron-allure:<latest_version>` library provides all Allure dependencies.\n\n```kotlin\ndependencies {\n    api(Libs.allureAndroid)\n    api(Libs.allureCommon)\n    api(Libs.allureModel)\n    api(Libs.allureJunit4)\n    api(Libs.espressoCore)\n}\n```\n\n## Compose Dependencies\n\nThe `com.atiurin:ultron-compose:<latest_version>` library provides `androidx.compose.ui:ui-test-junit4`\n\n```kotlin\ndependencies {\n    api(Libs.composeUiTest)\n}\n```\n"
  },
  {
    "path": "docs/docusaurus.config.ts",
    "content": "import {themes as prismThemes} from 'prism-react-renderer';\nimport type {Config} from '@docusaurus/types';\nimport type * as Preset from '@docusaurus/preset-classic';\n\nconst config: Config = {\n  title: 'Ultron',\n  tagline: 'Compose Multiplatform and Android UI testing framework',\n  favicon: 'img/favicon.ico',\n  url: 'https://ultron.github.io',\n  // Set the /<baseUrl>/ pathname under which your site is served\n  // For GitHub pages deployment, it is often '/<projectName>/'\n  baseUrl: '/ultron/',\n\n  // GitHub pages deployment config.\n  // If you aren't using GitHub pages, you don't need these.\n  organizationName: 'Open-tool', // Usually your GitHub org/user name.\n  projectName: 'ultron', // Usually your repo name.\n\n  onBrokenLinks: 'throw',\n  onBrokenMarkdownLinks: 'warn',\n\n  // Even if you don't use internationalization, you can use this field to set\n  // useful metadata like html lang. For example, if your site is Chinese, you\n  // may want to replace \"en\" with \"zh-Hans\".\n  i18n: {\n    defaultLocale: 'en',\n    locales: ['en'],\n  },\n\n  presets: [\n    [\n      'classic',\n      {\n        docs: {\n          sidebarPath: './sidebars.ts',\n        },\n        theme: {\n          customCss: './src/css/custom.css',\n        },\n      } satisfies Preset.Options,\n    ],\n  ],\n\n  themeConfig: {\n    image: 'img/docusaurus-social-card.jpg',\n    navbar: {\n      title: 'Ultron',\n      logo: {\n        alt: 'Ultron Logo',\n        src: 'img/ultron_full_light.png',\n      },\n      items: [\n        {\n          type: 'docSidebar',\n          sidebarId: 'tutorialSidebar',\n          position: 'left',\n          label: 'Docs',\n        },\n        {\n          href: 'https://t.me/ultron_framework',\n          position: 'right',\n          className: 'header-telegram-link',\n          'aria-label': 'Telegram',\n        },\n        {\n          href: 'https://github.com/open-tool/ultron',\n          position: 'right',\n          className: 'header-github-link',\n          'aria-label': 'GitHub repository',\n        },\n        {\n          type: 'search',\n          position: 'right',\n        },\n      ],\n    },\n    prism: {\n      theme: prismThemes.github,\n      darkTheme: prismThemes.dracula,\n    },\n    algolia: {\n          appId: 'TLB3E9OO68',\n          apiKey: '06f26f943a74848657b1e5bec4c85aaf',\n          indexName: 'open-toolio',\n          contextualSearch: true,\n          searchParameters: {},\n          insights: false,\n        },\n    prism: {\n      theme: prismThemes.github,\n      darkTheme: prismThemes.dracula,\n    },\n  } satisfies Preset.ThemeConfig,\n\n  plugins: [\n    [\n      '@docusaurus/plugin-client-redirects',\n      {\n        fromExtensions: ['html', 'htm'], // /myPage.html -> /myPage\n        toExtensions: ['exe', 'zip'], // /myAsset -> /myAsset.zip (if latter exists)\n        redirects: [\n          {\n            to: '/docs/',\n            from: '/',\n          },\n        ],\n      },\n    ],\n  ],\n};\n\n\nexport default config;\n\n"
  },
  {
    "path": "docs/package.json",
    "content": "{\n  \"name\": \"my-website\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"docusaurus\": \"docusaurus\",\n    \"start\": \"docusaurus start\",\n    \"build\": \"docusaurus build\",\n    \"swizzle\": \"docusaurus swizzle\",\n    \"deploy\": \"docusaurus deploy\",\n    \"clear\": \"docusaurus clear\",\n    \"serve\": \"docusaurus serve\",\n    \"write-translations\": \"docusaurus write-translations\",\n    \"write-heading-ids\": \"docusaurus write-heading-ids\",\n    \"typecheck\": \"tsc\"\n  },\n  \"dependencies\": {\n    \"@docusaurus/core\": \"3.4.0\",\n    \"@docusaurus/plugin-client-redirects\": \"^3.4.0\",\n    \"@docusaurus/preset-classic\": \"3.4.0\",\n    \"@mdx-js/react\": \"^3.0.0\",\n    \"clsx\": \"^2.0.0\",\n    \"prism-react-renderer\": \"^2.3.0\",\n    \"react\": \"^18.0.0\",\n    \"react-dom\": \"^18.0.0\"\n  },\n  \"devDependencies\": {\n    \"@docusaurus/module-type-aliases\": \"3.4.0\",\n    \"@docusaurus/tsconfig\": \"3.4.0\",\n    \"@docusaurus/types\": \"3.4.0\",\n    \"typescript\": \"~5.2.2\"\n  },\n  \"browserslist\": {\n    \"production\": [\n      \">0.5%\",\n      \"not dead\",\n      \"not op_mini all\"\n    ],\n    \"development\": [\n      \"last 3 chrome version\",\n      \"last 3 firefox version\",\n      \"last 5 safari version\"\n    ]\n  },\n  \"engines\": {\n    \"node\": \">=18.0\"\n  }\n}\n"
  },
  {
    "path": "docs/sidebars.ts",
    "content": "import type {SidebarsConfig} from '@docusaurus/plugin-content-docs';\n\n/**\n * Creating a sidebar enables you to:\n - create an ordered group of docs\n - render a sidebar for each doc of that group\n - provide next/previous navigation\n\n The sidebars can be generated from the filesystem, or explicitly defined here.\n\n Create as many sidebars as you want.\n */\nconst sidebars: SidebarsConfig = {\n  // By default, Docusaurus generates a sidebar from the docs folder structure\n  tutorialSidebar: [{type: 'autogenerated', dirName: '.'}],\n\n  // But you can create a sidebar manually\n  /*\n  tutorialSidebar: [\n    'intro',\n    'hello',\n    {\n      type: 'category',\n      label: 'Tutorial',\n      items: ['tutorial-basics/create-a-document'],\n    },\n  ],\n   */\n};\n\nexport default sidebars;\n"
  },
  {
    "path": "docs/src/components/HomepageFeatures/index.tsx",
    "content": "import clsx from 'clsx';\nimport Heading from '@theme/Heading';\nimport styles from './styles.module.css';\n\ntype FeatureItem = {\n  title: string;\n  pict: JSX.Element;\n  description: JSX.Element;\n};\n\nconst FeatureList: FeatureItem[] = [\n  {\n    title: 'Simple',\n    pict: (\n      <img src={'img/kotlin.png'} alt=\"simplicity\"/> \n    ),\n    description: (\n      <>\n        <p>The simplest syntax for UI tests.</p>\n        <code>hasTestTag(\"elementId\").click()</code>\n      </>\n    ),\n  },\n  {\n    title: 'Stable',\n    pict: (\n      <img src={'img/stable.webp'} alt=\"stability\"/> \n    ),\n    description: (\n      <>\n        <p>No flaky tests</p>\n        <p>Auto-waits for UI elements</p>\n        <p>Automatic retries of failed interactions</p>\n        <p>Custom assertions of executed action</p>\n      </>\n    ),\n  },\n  {\n    title: 'Maintainable',\n    pict: (\n      <img src={'img/maintainability.webp'} alt=\"maintainability\"/> \n    ),\n    description: (\n      <>\n        <p>Page Object support</p>\n        <p>Allure support</p>\n        <p>Detailed logs</p>\n        <p>Extendable API</p>\n      </>\n    ),\n  },\n];\n\nfunction Feature({ title, pict, description }: FeatureItem) {\n  const imageStyle = `text--center padding-horiz--md ${styles.imageContainer} `\n  return (\n    <div className={clsx('col col--4')}>\n      <div className='text--center padding-horiz--md'>\n        <div className={styles.imageContainer}>\n          {pict}\n        </div>\n      </div>\n      <div className=\"text--center padding-horiz--md\">\n        <Heading as=\"h3\">{title}</Heading>\n        <div>{description}</div>\n      </div>\n    </div>\n  );\n}\n\nexport default function HomepageFeatures(): JSX.Element {\n  return (\n    <section className={styles.features}>\n      <div className=\"container\">\n        <div className=\"row\">\n          {FeatureList.map((props, idx) => (\n            <Feature key={idx} {...props} />\n          ))}\n        </div>\n      </div>\n    </section>\n  );\n}"
  },
  {
    "path": "docs/src/components/HomepageFeatures/styles.module.css",
    "content": ".features {\n  display: flex;\n  align-items: center;\n  padding: 2rem 0;\n  width: 100%;\n}\n\n.featureSvg {\n  height: 150px;\n  width: 150px;\n  padding: 10px;\n  overflow: 'hidden';\n  display: 'flex';\n  justify-content: center;\n}\n\n.imageContainer {\n  width: 150px;\n  height: 150px;\n  /* display: flex; */\n  padding: 10px;\n  margin-left: auto;\n  margin-right: auto;\n\n  /* overflow: hidden; */\n}\n\n.imageContainer img {\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n  border-radius: 10px;\n}"
  },
  {
    "path": "docs/src/css/custom.css",
    "content": "/**\n * Any CSS included here will be global. The classic template\n * bundles Infima by default. Infima is a CSS framework designed to\n * work well for content-centric websites.\n */\n\n/* You can override the default Infima variables here. */\n/* :root {\n  --ifm-color-primary: #2e8555;\n  --ifm-color-primary-dark: #29784c;\n  --ifm-color-primary-darker: #277148;\n  --ifm-color-primary-darkest: #205d3b;\n  --ifm-color-primary-light: #33925d;\n  --ifm-color-primary-lighter: #359962;\n  --ifm-color-primary-lightest: #3cad6e;\n  --ifm-code-font-size: 95%;\n  --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);\n} */\n\n\n:root {\n  --ifm-color-primary: #233a60;\n  --ifm-color-primary-dark: #29784c;\n  --ifm-color-primary-darker: #277148;\n  --ifm-color-primary-darkest: #205d3b;\n  --ifm-color-primary-light: #33925d;\n  --ifm-color-primary-lighter: #359962;\n  --ifm-color-primary-lightest: #3cad6e;\n  --ifm-code-font-size: 95%;\n  --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);\n}\n\n/* For readability concerns, you should choose a lighter palette in dark mode. */\n[data-theme='dark'] {\n  --ifm-color-primary: #5a85cf;\n  --ifm-color-primary-dark: #21af90;\n  --ifm-color-primary-darker: #1fa588;\n  --ifm-color-primary-darkest: #1a8870;\n  --ifm-color-primary-light: #29d5b0;\n  --ifm-color-primary-lighter: #32d8b4;\n  --ifm-color-primary-lightest: #4fddbf;\n  --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);\n}\n\n[data-theme='light'] img[src$='#gh-dark-mode-only'],\n[data-theme='dark'] img[src$='#gh-light-mode-only'] {\n  display: none;\n}\n\n.header-github-link::before {\n  content: '';\n  width: 24px;\n  height: 24px;\n  display: flex;\n  background-color: var(--ifm-navbar-link-color);\n  mask-image: url(\"data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E\");\n  transition: background-color var(--ifm-transition-fast)\n    var(--ifm-transition-timing-default);\n}\n\n.header-github-link:hover::before {\n  background-color: var(--ifm-navbar-link-hover-color);\n}\n\n.header-telegram-link {\n  display: flex;\n  align-items: center;\n}\n\n.header-telegram-link::before {\n  content: '';\n  display: inline-block;\n  width: 24px;\n  height: 24px;\n  background-image: url('/img/telegram-icon.svg');\n  background-size: contain;\n  background-repeat: no-repeat;\n  margin-right: 8px;\n}\n"
  },
  {
    "path": "docs/src/pages/index.module.css",
    "content": "/**\n * CSS files with the .module.css suffix will be treated as CSS modules\n * and scoped locally.\n */\n\n.heroBanner {\n  padding: 4rem 0;\n  text-align: center;\n  position: relative;\n  overflow: hidden;\n}\n\n@media screen and (max-width: 996px) {\n  .heroBanner {\n    padding: 2rem;\n  }\n}\n\n.buttons {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n"
  },
  {
    "path": "docs/src/pages/index.tsx",
    "content": "import clsx from 'clsx';\nimport Link from '@docusaurus/Link';\nimport useDocusaurusContext from '@docusaurus/useDocusaurusContext';\nimport Layout from '@theme/Layout';\nimport HomepageFeatures from '@site/src/components/HomepageFeatures';\nimport Heading from '@theme/Heading';\n\nimport styles from './index.module.css';\n\nfunction HomepageHeader() {\n  const {siteConfig} = useDocusaurusContext();\n  return (\n    <header className={clsx('hero hero--primary', styles.heroBanner)}>\n      <div className=\"container\">\n        <Heading as=\"h1\" className=\"hero__title\">\n          U L T R O N\n        </Heading>\n        <p className=\"hero__subtitle\">{siteConfig.tagline}</p>\n        <div className={styles.buttons}>\n          <Link\n            className=\"button button--secondary button--lg\"\n            to=\"/docs/\">\n            GET STARTED\n          </Link>\n        </div>\n      </div>\n    </header>\n  );\n}\n\nexport default function Home(): JSX.Element {\n  const {siteConfig} = useDocusaurusContext();\n  return (\n    <Layout\n      title='Home'\n      description=\"Description will go into a meta tag in <head />\">\n      <HomepageHeader />\n      <main>\n        <HomepageFeatures />\n      </main>\n    </Layout>\n  );\n}\n"
  },
  {
    "path": "docs/src/pages/markdown-page.md",
    "content": "---\ntitle: Markdown page example\n---\n\n# Markdown page example\n\nYou don't need React to write simple standalone pages.\n"
  },
  {
    "path": "docs/static/.nojekyll",
    "content": ""
  },
  {
    "path": "docs/tsconfig.json",
    "content": "{\n  // This file is not used in compilation. It is here just for a nice editor experience.\n  \"extends\": \"@docusaurus/tsconfig\",\n  \"compilerOptions\": {\n    \"baseUrl\": \".\"\n  }\n}\n"
  },
  {
    "path": "gradle/libs.versions.toml",
    "content": "[versions]\nagp = \"8.9.3\"\natomicfu = \"0.27.0\"\nkotlin = \"2.1.21\"\nandroidx-activityCompose = \"1.10.1\"\nkotlinxCoroutinesCore = \"1.10.2\"\nkotlinxDatetime = \"0.7.1\"\ncompose-plugin = \"1.8.2\"\nokio = \"3.11.0\"\nandroid-compileSdk = \"35\"\nandroid-minSdk = \"24\"\nandroid-targetSdk = \"35\"\nandroidx-lifecycle = \"2.8.4\"\nandroidx-navigation = \"2.7.0-alpha06\"\nmonitor = \"1.7.2\"\nuiTestJunit4Android = \"1.8.1\"\nlifecycleCommonJvm = \"2.9.2\"\ncoreKtx = \"1.6.1\"\n\n[libraries]\nandroidx-ui-test-junit4-android = { module = \"androidx.compose.ui:ui-test-junit4-android\", version.ref = \"uiTestJunit4Android\" }\nandroidx-ui-test-manifest = { module = \"androidx.compose.ui:ui-test-manifest\", version.ref = \"uiTestJunit4Android\" }\natomicfu = { module = \"org.jetbrains.kotlinx:atomicfu\", version.ref = \"atomicfu\" }\nkotlin-test = { module = \"org.jetbrains.kotlin:kotlin-test\", version.ref = \"kotlin\" }\nandroidx-activity-compose = { module = \"androidx.activity:activity-compose\", version.ref = \"androidx-activityCompose\" }\nkotlinx-coroutines-core = { module = \"org.jetbrains.kotlinx:kotlinx-coroutines-core\", version.ref = \"kotlinxCoroutinesCore\" }\nkotlinx-datetime = { module = \"org.jetbrains.kotlinx:kotlinx-datetime\", version.ref = \"kotlinxDatetime\" }\nokio = { module = \"com.squareup.okio:okio\", version.ref = \"okio\" }\n\nandroidx-monitor = { group = \"androidx.test\", name = \"monitor\", version.ref = \"monitor\" }\nandroidx-lifecycle-runtime-compose = { module = \"org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose\", version.ref = \"androidx-lifecycle\" }\nandroidx-lifecycle-viewmodel-compose = { module = \"org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose\", version.ref = \"androidx-lifecycle\" }\nandroidx-navigation-compose = { module = \"org.jetbrains.androidx.navigation:navigation-compose\", version.ref = \"androidx-navigation\" }\nandroidx-lifecycle-common-jvm = { group = \"androidx.lifecycle\", name = \"lifecycle-common-jvm\", version.ref = \"lifecycleCommonJvm\" }\nandroidx-core-ktx = { group = \"androidx.test\", name = \"core-ktx\", version.ref = \"coreKtx\" }\n\n[plugins]\nandroidApplication = { id = \"com.android.application\", version.ref = \"agp\" }\nandroidLibrary = { id = \"com.android.library\", version.ref = \"agp\" }\nkotlinAndroid = { id = \"org.jetbrains.kotlin.android\", version.ref = \"kotlin\" }\nkotlinMultiplatform = { id = \"org.jetbrains.kotlin.multiplatform\", version.ref = \"kotlin\" }\njetbrainsCompose = { id = \"org.jetbrains.compose\", version.ref = \"compose-plugin\" }\ncompose-compiler = { id = \"org.jetbrains.kotlin.plugin.compose\", version.ref = \"kotlin\" }\nkotlinJvm = { id = \"org.jetbrains.kotlin.jvm\", version.ref = \"kotlin\" }\nvanniktech-mavenPublish = { id = \"com.vanniktech.maven.publish\", version = \"0.30.0\" }\n"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "#Tue May 20 16:16:07 MSK 2025\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-8.14-bin.zip\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\n"
  },
  {
    "path": "gradle.properties",
    "content": "org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\\=\"-Xmx2048M\"\norg.gradle.caching=true\norg.gradle.configuration-cache=true\n\nkotlin.code.style=official\nandroid.useAndroidX=true\nandroid.nonTransitiveRClass=true\norg.jetbrains.compose.experimental.wasm.enabled=true\norg.jetbrains.compose.experimental.jscanvas.enabled=true\norg.jetbrains.compose.experimental.macos.enabled=true\nkotlin.mpp.androidSourceSetLayoutVersion=2\nkotlin.mpp.enableCInteropCommonization=true\nkotlin.native.cacheKind=none\n\n\nGROUP=com.atiurin\nPOM_ARTIFACT_ID=ultron\nVERSION_NAME=2.6.2\n"
  },
  {
    "path": "gradlew",
    "content": "#!/bin/sh\n\n#\n# Copyright  2015-2021 the original authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n##############################################################################\n#\n#   Gradle start up script for POSIX generated by Gradle.\n#\n#   Important for running:\n#\n#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is\n#       noncompliant, but you have some other compliant shell such as ksh or\n#       bash, then to run this script, type that shell name before the whole\n#       command line, like:\n#\n#           ksh Gradle\n#\n#       Busybox and similar reduced shells will NOT work, because this script\n#       requires all of these POSIX shell features:\n#         * functions;\n#         * expansions $var, ${var}, ${var:-default}, ${var+SET},\n#           ${var#prefix}, ${var%suffix}, and $( cmd );\n#         * compound commands having a testable exit status, especially case;\n#         * various built-in commands including command, set, and ulimit.\n#\n#   Important for patching:\n#\n#   (2) This script targets any POSIX shell, so it avoids extensions provided\n#       by Bash, Ksh, etc; in particular arrays are avoided.\n#\n#       The \"traditional\" practice of packing multiple parameters into a\n#       space-separated string is a well documented source of bugs and security\n#       problems, so this is (mostly) avoided, by progressively accumulating\n#       options in \"$@\", and eventually passing that to Java.\n#\n#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,\n#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;\n#       see the in-line comments for details.\n#\n#       There are tweaks for specific operating systems such as AIX, CygWin,\n#       Darwin, MinGW, and NonStop.\n#\n#   (3) This script is generated from the Groovy template\n#       https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt\n#       within the Gradle project.\n#\n#       You can find Gradle at https://github.com/gradle/gradle/.\n#\n##############################################################################\n\n# Attempt to set APP_HOME\n\n# Resolve links: $0 may be a link\napp_path=$0\n\n# Need this for daisy-chained symlinks.\nwhile\n    APP_HOME=${app_path%\"${app_path##*/}\"}  # leaves a trailing /; empty if no leading path\n    [ -h \"$app_path\" ]\ndo\n    ls=$( ls -ld \"$app_path\" )\n    link=${ls#*' -> '}\n    case $link in             #(\n      /*)   app_path=$link ;; #(\n      *)    app_path=$APP_HOME$link ;;\n    esac\ndone\n\nAPP_HOME=$( cd \"${APP_HOME:-./}\" && pwd -P ) || exit\n\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=${0##*/}\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS='\"-Xmx64m\" \"-Xms64m\"'\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=maximum\n\nwarn () {\n    echo \"$*\"\n} >&2\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n} >&2\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"$( uname )\" in                #(\n  CYGWIN* )         cygwin=true  ;; #(\n  Darwin* )         darwin=true  ;; #(\n  MSYS* | MINGW* )  msys=true    ;; #(\n  NONSTOP* )        nonstop=true ;;\nesac\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=$JAVA_HOME/jre/sh/java\n    else\n        JAVACMD=$JAVA_HOME/bin/java\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=java\n    which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif ! \"$cygwin\" && ! \"$darwin\" && ! \"$nonstop\" ; then\n    case $MAX_FD in #(\n      max*)\n        MAX_FD=$( ulimit -H -n ) ||\n            warn \"Could not query maximum file descriptor limit\"\n    esac\n    case $MAX_FD in  #(\n      '' | soft) :;; #(\n      *)\n        ulimit -n \"$MAX_FD\" ||\n            warn \"Could not set maximum file descriptor limit to $MAX_FD\"\n    esac\nfi\n\n# Collect all arguments for the java command, stacking in reverse order:\n#   * args from the command line\n#   * the main class name\n#   * -classpath\n#   * -D...appname settings\n#   * --module-path (only if needed)\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.\n\n# For Cygwin or MSYS, switch paths to Windows format before running java\nif \"$cygwin\" || \"$msys\" ; then\n    APP_HOME=$( cygpath --path --mixed \"$APP_HOME\" )\n    CLASSPATH=$( cygpath --path --mixed \"$CLASSPATH\" )\n\n    JAVACMD=$( cygpath --unix \"$JAVACMD\" )\n\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    for arg do\n        if\n            case $arg in                                #(\n              -*)   false ;;                            # don't mess with options #(\n              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath\n                    [ -e \"$t\" ] ;;                      #(\n              *)    false ;;\n            esac\n        then\n            arg=$( cygpath --path --ignore --mixed \"$arg\" )\n        fi\n        # Roll the args list around exactly as many times as the number of\n        # args, so each arg winds up back in the position where it started, but\n        # possibly modified.\n        #\n        # NB: a `for` loop captures its iteration list before it begins, so\n        # changing the positional parameters here affects neither the number of\n        # iterations, nor the values presented in `arg`.\n        shift                   # remove old arg\n        set -- \"$@\" \"$arg\"      # push replacement arg\n    done\nfi\n\n# Collect all arguments for the java command;\n#   * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of\n#     shell script including quotes and variable substitutions, so put them in\n#     double quotes to make sure that they get re-expanded; and\n#   * put everything else in single quotes, so that it's not re-expanded.\n\nset -- \\\n        \"-Dorg.gradle.appname=$APP_BASE_NAME\" \\\n        -classpath \"$CLASSPATH\" \\\n        org.gradle.wrapper.GradleWrapperMain \\\n        \"$@\"\n\n# Use \"xargs\" to parse quoted args.\n#\n# With -n1 it outputs one arg per line, with the quotes and backslashes removed.\n#\n# In Bash we could simply go:\n#\n#   readarray ARGS < <( xargs -n1 <<<\"$var\" ) &&\n#   set -- \"${ARGS[@]}\" \"$@\"\n#\n# but POSIX shell has neither arrays nor command substitution, so instead we\n# post-process each arg (as a line of input to sed) to backslash-escape any\n# character that might be a shell metacharacter, then use eval to reverse\n# that process (while maintaining the separation between arguments), and wrap\n# the whole thing up as a single \"set\" statement.\n#\n# This will of course break if any of these variables contains a newline or\n# an unmatched quote.\n#\n\neval \"set -- $(\n        printf '%s\\n' \"$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS\" |\n        xargs -n1 |\n        sed ' s~[^-[:alnum:]+,./:=@_]~\\\\&~g; ' |\n        tr '\\n' ' '\n    )\" '\"$@\"'\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@rem\r\n@rem Copyright 2015 the original author or authors.\r\n@rem\r\n@rem Licensed under the Apache License, Version 2.0 (the \"License\");\r\n@rem you may not use this file except in compliance with the License.\r\n@rem You may obtain a copy of the License at\r\n@rem\r\n@rem      https://www.apache.org/licenses/LICENSE-2.0\r\n@rem\r\n@rem Unless required by applicable law or agreed to in writing, software\r\n@rem distributed under the License is distributed on an \"AS IS\" BASIS,\r\n@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n@rem See the License for the specific language governing permissions and\r\n@rem limitations under the License.\r\n@rem\r\n\r\n@if \"%DEBUG%\" == \"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@rem  Gradle startup script for Windows\r\n@rem\r\n@rem ##########################################################################\r\n\r\n@rem Set local scope for the variables with windows NT shell\r\nif \"%OS%\"==\"Windows_NT\" setlocal\r\n\r\nset DIRNAME=%~dp0\r\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\r\nset APP_BASE_NAME=%~n0\r\nset APP_HOME=%DIRNAME%\r\n\r\n@rem Resolve any \".\" and \"..\" in APP_HOME to make it shorter.\r\nfor %%i in (\"%APP_HOME%\") do set APP_HOME=%%~fi\r\n\r\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r\nset DEFAULT_JVM_OPTS=\"-Xmx64m\" \"-Xms64m\"\r\n\r\n@rem Find java.exe\r\nif defined JAVA_HOME goto findJavaFromJavaHome\r\n\r\nset JAVA_EXE=java.exe\r\n%JAVA_EXE% -version >NUL 2>&1\r\nif \"%ERRORLEVEL%\" == \"0\" goto execute\r\n\r\necho.\r\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:findJavaFromJavaHome\r\nset JAVA_HOME=%JAVA_HOME:\"=%\r\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\r\n\r\nif exist \"%JAVA_EXE%\" goto execute\r\n\r\necho.\r\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:execute\r\n@rem Setup the command line\r\n\r\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\r\n\r\n\r\n@rem Execute Gradle\r\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %*\r\n\r\n:end\r\n@rem End local scope for the variables with windows NT shell\r\nif \"%ERRORLEVEL%\"==\"0\" goto mainEnd\r\n\r\n:fail\r\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r\nrem the _cmd.exe /c_ return code!\r\nif  not \"\" == \"%GRADLE_EXIT_CONSOLE%\" exit 1\r\nexit /b 1\r\n\r\n:mainEnd\r\nif \"%OS%\"==\"Windows_NT\" endlocal\r\n\r\n:omega\r\n"
  },
  {
    "path": "iosApp/Configuration/Config.xcconfig",
    "content": "TEAM_ID=\nBUNDLE_ID=com.atiurin.samplekmp.sample-kmp\nAPP_NAME=sample-kmp"
  },
  {
    "path": "iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json",
    "content": "{\n  \"colors\" : [\n    {\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}"
  },
  {
    "path": "iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"app-icon-1024.png\",\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"size\" : \"1024x1024\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "iosApp/iosApp/Assets.xcassets/Contents.json",
    "content": "{\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}"
  },
  {
    "path": "iosApp/iosApp/ContentView.swift",
    "content": "import UIKit\nimport SwiftUI\nimport ComposeApp\n\nstruct ComposeView: UIViewControllerRepresentable {\n    func makeUIViewController(context: Context) -> UIViewController {\n        MainViewControllerKt.MainViewController()\n    }\n\n    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}\n}\n\nstruct ContentView: View {\n    var body: some View {\n        ComposeView()\n                .ignoresSafeArea(.keyboard) // Compose has own keyboard handler\n    }\n}\n\n\n\n"
  },
  {
    "path": "iosApp/iosApp/Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>CFBundleDevelopmentRegion</key>\n\t<string>$(DEVELOPMENT_LANGUAGE)</string>\n\t<key>CFBundleExecutable</key>\n\t<string>$(EXECUTABLE_NAME)</string>\n\t<key>CFBundleIdentifier</key>\n\t<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>\n\t<key>CFBundleInfoDictionaryVersion</key>\n\t<string>6.0</string>\n\t<key>CFBundleName</key>\n\t<string>$(PRODUCT_NAME)</string>\n\t<key>CFBundlePackageType</key>\n\t<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>\n\t<key>CFBundleShortVersionString</key>\n\t<string>1.0</string>\n\t<key>CFBundleVersion</key>\n\t<string>1</string>\n\t<key>LSRequiresIPhoneOS</key>\n\t<true/>\n\t<key>CADisableMinimumFrameDurationOnPhone</key>\n\t<true/>\n\t<key>UIApplicationSceneManifest</key>\n\t<dict>\n\t\t<key>UIApplicationSupportsMultipleScenes</key>\n\t\t<false/>\n\t</dict>\n\t<key>UILaunchScreen</key>\n\t<dict/>\n\t<key>UIRequiredDeviceCapabilities</key>\n\t<array>\n\t\t<string>armv7</string>\n\t</array>\n\t<key>UISupportedInterfaceOrientations</key>\n\t<array>\n\t\t<string>UIInterfaceOrientationPortrait</string>\n\t\t<string>UIInterfaceOrientationLandscapeLeft</string>\n\t\t<string>UIInterfaceOrientationLandscapeRight</string>\n\t</array>\n\t<key>UISupportedInterfaceOrientations~ipad</key>\n\t<array>\n\t\t<string>UIInterfaceOrientationPortrait</string>\n\t\t<string>UIInterfaceOrientationPortraitUpsideDown</string>\n\t\t<string>UIInterfaceOrientationLandscapeLeft</string>\n\t\t<string>UIInterfaceOrientationLandscapeRight</string>\n\t</array>\n</dict>\n</plist>\n"
  },
  {
    "path": "iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json",
    "content": "{\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}"
  },
  {
    "path": "iosApp/iosApp/iOSApp.swift",
    "content": "import SwiftUI\n\n@main\nstruct iOSApp: App {\n\tvar body: some Scene {\n\t\tWindowGroup {\n\t\t\tContentView()\n\t\t}\n\t}\n}"
  },
  {
    "path": "iosApp/iosApp.xcodeproj/project.pbxproj",
    "content": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 60;\n\tobjects = {\n\n/* Begin PBXBuildFile section */\n\t\t058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557BA273AAA24004C7B11 /* Assets.xcassets */; };\n\t\t058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */; };\n\t\t2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iOSApp.swift */; };\n\t\t7555FF83242A565900829871 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* ContentView.swift */; };\n/* End PBXBuildFile section */\n\n/* Begin PBXFileReference section */\n\t\t058557BA273AAA24004C7B11 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = \"<group>\"; };\n\t\t058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = \"Preview Assets.xcassets\"; sourceTree = \"<group>\"; };\n\t\t2152FB032600AC8F00CF470E /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = \"<group>\"; };\n\t\t7555FF7B242A565900829871 /* sample-kmp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = \"sample-kmp.app\"; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t7555FF82242A565900829871 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = \"<group>\"; };\n\t\t7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = \"<group>\"; };\n\t\tAB3632DC29227652001CCB65 /* Config.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = \"<group>\"; };\n/* End PBXFileReference section */\n\n/* Begin PBXFrameworksBuildPhase section */\n\t\tB92378962B6B1156000C7307 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXFrameworksBuildPhase section */\n\n/* Begin PBXGroup section */\n\t\t058557D7273AAEEB004C7B11 /* Preview Content */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */,\n\t\t\t);\n\t\t\tpath = \"Preview Content\";\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t42799AB246E5F90AF97AA0EF /* Frameworks */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t);\n\t\t\tname = Frameworks;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t7555FF72242A565900829871 = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tAB1DB47929225F7C00F7AF9C /* Configuration */,\n\t\t\t\t7555FF7D242A565900829871 /* iosApp */,\n\t\t\t\t7555FF7C242A565900829871 /* Products */,\n\t\t\t\t42799AB246E5F90AF97AA0EF /* Frameworks */,\n\t\t\t);\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t7555FF7C242A565900829871 /* Products */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t7555FF7B242A565900829871 /* sample-kmp.app */,\n\t\t\t);\n\t\t\tname = Products;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t7555FF7D242A565900829871 /* iosApp */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t058557BA273AAA24004C7B11 /* Assets.xcassets */,\n\t\t\t\t7555FF82242A565900829871 /* ContentView.swift */,\n\t\t\t\t7555FF8C242A565B00829871 /* Info.plist */,\n\t\t\t\t2152FB032600AC8F00CF470E /* iOSApp.swift */,\n\t\t\t\t058557D7273AAEEB004C7B11 /* Preview Content */,\n\t\t\t);\n\t\t\tpath = iosApp;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tAB1DB47929225F7C00F7AF9C /* Configuration */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tAB3632DC29227652001CCB65 /* Config.xcconfig */,\n\t\t\t);\n\t\t\tpath = Configuration;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXGroup section */\n\n/* Begin PBXNativeTarget section */\n\t\t7555FF7A242A565900829871 /* iosApp */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget \"iosApp\" */;\n\t\t\tbuildPhases = (\n\t\t\t\tF36B1CEB2AD83DDC00CB74D5 /* Compile Kotlin Framework */,\n\t\t\t\t7555FF77242A565900829871 /* Sources */,\n\t\t\t\tB92378962B6B1156000C7307 /* Frameworks */,\n\t\t\t\t7555FF79242A565900829871 /* Resources */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t);\n\t\t\tname = iosApp;\n\t\t\tpackageProductDependencies = (\n\t\t\t);\n\t\t\tproductName = iosApp;\n\t\t\tproductReference = 7555FF7B242A565900829871 /* sample-kmp.app */;\n\t\t\tproductType = \"com.apple.product-type.application\";\n\t\t};\n/* End PBXNativeTarget section */\n\n/* Begin PBXProject section */\n\t\t7555FF73242A565900829871 /* Project object */ = {\n\t\t\tisa = PBXProject;\n\t\t\tattributes = {\n\t\t\t\tBuildIndependentTargetsInParallel = YES;\n\t\t\t\tLastSwiftUpdateCheck = 1130;\n\t\t\t\tLastUpgradeCheck = 1540;\n\t\t\t\tORGANIZATIONNAME = orgName;\n\t\t\t\tTargetAttributes = {\n\t\t\t\t\t7555FF7A242A565900829871 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 11.3.1;\n\t\t\t\t\t};\n\t\t\t\t};\n\t\t\t};\n\t\t\tbuildConfigurationList = 7555FF76242A565900829871 /* Build configuration list for PBXProject \"iosApp\" */;\n\t\t\tcompatibilityVersion = \"Xcode 15.0\";\n\t\t\tdevelopmentRegion = en;\n\t\t\thasScannedForEncodings = 0;\n\t\t\tknownRegions = (\n\t\t\t\ten,\n\t\t\t\tBase,\n\t\t\t);\n\t\t\tmainGroup = 7555FF72242A565900829871;\n\t\t\tpackageReferences = (\n\t\t\t);\n\t\t\tproductRefGroup = 7555FF7C242A565900829871 /* Products */;\n\t\t\tprojectDirPath = \"\";\n\t\t\tprojectRoot = \"\";\n\t\t\ttargets = (\n\t\t\t\t7555FF7A242A565900829871 /* iosApp */,\n\t\t\t);\n\t\t};\n/* End PBXProject section */\n\n/* Begin PBXResourcesBuildPhase section */\n\t\t7555FF79242A565900829871 /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */,\n\t\t\t\t058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXResourcesBuildPhase section */\n\n/* Begin PBXShellScriptBuildPhase section */\n\t\tF36B1CEB2AD83DDC00CB74D5 /* Compile Kotlin Framework */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t);\n\t\t\tname = \"Compile Kotlin Framework\";\n\t\t\toutputFileListPaths = (\n\t\t\t);\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"if [ \\\"YES\\\" = \\\"$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED\\\" ]; then\\n  echo \\\"Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \\\\\\\"YES\\\\\\\"\\\"\\n  exit 0\\nfi\\ncd \\\"$SRCROOT/..\\\"\\n./gradlew :composeApp:embedAndSignAppleFrameworkForXcode\\n\";\n\t\t};\n/* End PBXShellScriptBuildPhase section */\n\n/* Begin PBXSourcesBuildPhase section */\n\t\t7555FF77242A565900829871 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */,\n\t\t\t\t7555FF83242A565900829871 /* ContentView.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXSourcesBuildPhase section */\n\n/* Begin XCBuildConfiguration section */\n\t\t7555FFA3242A565B00829871 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_WEAK = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_TESTABILITY = YES;\n\t\t\t\tENABLE_USER_SCRIPT_SANDBOXING = NO;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_DYNAMIC_NO_PIC = NO;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_OPTIMIZATION_LEVEL = 0;\n\t\t\t\tGCC_PREPROCESSOR_DEFINITIONS = (\n\t\t\t\t\t\"DEBUG=1\",\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t);\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 15.3;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;\n\t\t\t\tMTL_FAST_MATH = YES;\n\t\t\t\tONLY_ACTIVE_ARCH = YES;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tSWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t7555FFA4242A565B00829871 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_WEAK = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_USER_SCRIPT_SANDBOXING = NO;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 15.3;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tMTL_FAST_MATH = YES;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tSWIFT_COMPILATION_MODE = wholemodule;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-O\";\n\t\t\t\tVALIDATE_PRODUCT = YES;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t7555FFA6242A565B00829871 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCODE_SIGN_IDENTITY = \"Apple Development\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tDEVELOPMENT_ASSET_PATHS = \"\\\"iosApp/Preview Content\\\"\";\n\t\t\t\tDEVELOPMENT_TEAM = \"${TEAM_ID}\";\n\t\t\t\tENABLE_PREVIEWS = YES;\n\t\t\t\tFRAMEWORK_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\\n$(SRCROOT)/../composeApp/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\",\n\t\t\t\t);\n\t\t\t\tINFOPLIST_FILE = iosApp/Info.plist;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 15.0;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tOTHER_LDFLAGS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"-framework\",\n\t\t\t\t\tComposeApp,\n\t\t\t\t);\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = \"${BUNDLE_ID}${TEAM_ID}\";\n\t\t\t\tPRODUCT_NAME = \"${APP_NAME}\";\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t7555FFA7242A565B00829871 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCODE_SIGN_IDENTITY = \"Apple Development\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tDEVELOPMENT_ASSET_PATHS = \"\\\"iosApp/Preview Content\\\"\";\n\t\t\t\tDEVELOPMENT_TEAM = \"${TEAM_ID}\";\n\t\t\t\tENABLE_PREVIEWS = YES;\n\t\t\t\tFRAMEWORK_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\\n$(SRCROOT)/../composeApp/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\",\n\t\t\t\t);\n\t\t\t\tINFOPLIST_FILE = iosApp/Info.plist;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 15.0;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tOTHER_LDFLAGS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"-framework\",\n\t\t\t\t\tComposeApp,\n\t\t\t\t);\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = \"${BUNDLE_ID}${TEAM_ID}\";\n\t\t\t\tPRODUCT_NAME = \"${APP_NAME}\";\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n/* End XCBuildConfiguration section */\n\n/* Begin XCConfigurationList section */\n\t\t7555FF76242A565900829871 /* Build configuration list for PBXProject \"iosApp\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t7555FFA3242A565B00829871 /* Debug */,\n\t\t\t\t7555FFA4242A565B00829871 /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget \"iosApp\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t7555FFA6242A565B00829871 /* Debug */,\n\t\t\t\t7555FFA7242A565B00829871 /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n/* End XCConfigurationList section */\n\t};\n\trootObject = 7555FF73242A565900829871 /* Project object */;\n}\n"
  },
  {
    "path": "prepare-emulator.bat",
    "content": "adb shell settings put global development_settings_enabled 1\nadb shell settings put global window_animation_scale 0.0\nadb shell settings put global transition_animation_scale 0.0\nadb shell settings put global animator_duration_scale 0.0"
  },
  {
    "path": "prepare-emulator.sh",
    "content": "adb shell settings put global development_settings_enabled 1\nadb shell settings put global window_animation_scale 0.0\nadb shell settings put global transition_animation_scale 0.0\nadb shell settings put global animator_duration_scale 0.0"
  },
  {
    "path": "sample-app/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "sample-app/build.gradle.kts",
    "content": "plugins {\n    id(\"com.android.application\")\n    id(\"kotlin-android\")\n    alias(libs.plugins.compose.compiler)\n}\n\nandroid {\n    namespace = \"com.atiurin.sampleapp\"\n    compileSdk = 35\n    defaultConfig {\n        applicationId = \"com.atiurin.sampleapp\"\n        minSdk = 21\n        targetSdk = 35\n        multiDexEnabled = true\n        testInstrumentationRunner = \"com.atiurin.sampleapp.framework.CustomTestRunner\"\n    }\n    compileOptions {\n        targetCompatibility = JavaVersion.VERSION_17\n        sourceCompatibility = JavaVersion.VERSION_17\n    }\n    buildFeatures {\n        compose = true\n        buildConfig = true\n    }\n    kotlinOptions {\n        jvmTarget = \"17\"\n    }\n    buildTypes {\n        debug {\n            isMinifyEnabled = false\n        }\n    }\n\n    packagingOptions {\n        resources.excludes.add(\"META-INF/DEPENDENCIES\")\n        resources.excludes.add(\"META-INF/LICENSE\")\n        resources.excludes.add(\"META-INF/LICENSE.txt\")\n        resources.excludes.add(\"META-INF/license.txt\")\n        resources.excludes.add(\"META-INF/NOTICE\")\n        resources.excludes.add(\"META-INF/NOTICE.txt\")\n        resources.excludes.add(\"META-INF/notice.txt\")\n        resources.excludes.add(\"META-INF/AL2.0\")\n        resources.excludes.add(\"META-INF/LGPL2.1\")\n        resources.excludes.add(\"META-INF/*.kotlin_module\")\n    }\n}\n\ndependencies {\n    implementation(project(\":ultron-compose\"))\n    implementation(project(\":ultron-allure\"))\n    implementation(project(\":ultron-android\"))\n    implementation(Libs.kotlinStdlib)\n    implementation(Libs.coroutines)\n    implementation(Libs.appcompat)\n    implementation(Libs.androidXKtx)\n    implementation(Libs.supportV4)\n    implementation(Libs.material)\n    implementation(Libs.material3)\n    implementation(Libs.constraintLayout)\n    implementation(Libs.recyclerView)\n    implementation(Libs.cardview)\n    implementation(Libs.espressoIdlingResource)\n    implementation(libs.androidx.navigation.compose)\n    //compose\n    implementation(Libs.composeUi)\n    implementation(Libs.composeUiTooling)\n    implementation(Libs.composeFoundation)\n    implementation(Libs.composeMaterial)\n    implementation(Libs.composeMaterialIconsCore)\n    implementation(Libs.composeMaterialIconsExtend)\n    implementation(Libs.activityCompose)\n\n    // AndroidJUnitRunner and JUnit Rules\n    testImplementation(Libs.junit)\n    testImplementation(Libs.robolectric)\n    testImplementation(Libs.mockito)\n    testImplementation(Libs.androidXTextCore)\n\n    androidTestImplementation(Libs.androidXRules)\n    androidTestImplementation(Libs.androidXTruth)\n    androidTestImplementation(Libs.androidXJunit)\n    // Espresso dependencies\n    androidTestImplementation(Libs.espressoIntents)\n    androidTestImplementation(Libs.espressoAccessibility)\n    androidTestImplementation(Libs.espressoConcurrent)\n}\n"
  },
  {
    "path": "sample-app/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile\n"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/framework/CustomTestRunner.kt",
    "content": "package com.atiurin.sampleapp.framework\n\nimport com.atiurin.ultron.allure.UltronAllureTestRunner\n\nclass CustomTestRunner : UltronAllureTestRunner() {}"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/framework/DummyMetaObject.kt",
    "content": "package com.atiurin.sampleapp.framework\n\ndata class DummyMetaObject(val value: String)"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/framework/Log.kt",
    "content": "package com.atiurin.sampleapp.framework\n\nimport android.os.SystemClock\nimport android.util.Log\n\nobject Log {\n    const val LOG_TAG = \"Ultron\"\n    fun info(message: String) = Log.i(LOG_TAG, message)\n    fun debug(message: String) = Log.d(LOG_TAG, message)\n    fun error(message: String, name: String) = Log.e(LOG_TAG, message)\n    fun warn(message: String) = Log.w(LOG_TAG, message)\n    fun <R> time(desc: String, block: () -> R) : R{\n        val startTime = SystemClock.elapsedRealtime()\n        val result = block()\n        debug(\"$desc duration ${SystemClock.elapsedRealtime() - startTime} ms\")\n        return result\n    }\n}"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/framework/ScreenshotLifecycleListener.kt",
    "content": "package com.atiurin.sampleapp.framework\n\nimport com.atiurin.ultron.core.common.Operation\nimport com.atiurin.ultron.core.common.OperationResult\nimport com.atiurin.ultron.listeners.UltronLifecycleListener\n\nclass ScreenshotLifecycleListener : UltronLifecycleListener(){\n    override fun before(operation: Operation) {\n    }\n\n    override fun after(operationResult: OperationResult<Operation>) {\n        operationResult.operation\n    }\n}"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/framework/ultronext/UltronComposeExt.kt",
    "content": "package com.atiurin.sampleapp.framework.ultronext\n\nimport androidx.compose.ui.semantics.SemanticsProperties\nimport androidx.compose.ui.test.SemanticsMatcher\nimport com.atiurin.ultron.core.compose.nodeinteraction.UltronComposeSemanticsNodeInteraction\nimport com.atiurin.ultron.core.compose.operation.UltronComposeOperationParams\nimport com.atiurin.ultron.extensions.assertMatches\n\n// ------ custom action definition ------\n// step 1: extend base class [UltronComposeSemanticsNodeInteraction] with custom action logic\nfun UltronComposeSemanticsNodeInteraction.getProgress() = execute(\n    UltronComposeOperationParams(\n        operationName = \"Get '${elementInfo.name}' current progress\",\n        operationDescription = \"Compose get current progress of '${elementInfo.name}' during $timeoutMs ms\"\n    )\n) {\n    getNodeConfigProperty(SemanticsProperties.ProgressBarRangeInfo).current\n}\n\n// step 2: extend [SemanticsMatcher] class with new action method\nfun SemanticsMatcher.getProgress(): Float = UltronComposeSemanticsNodeInteraction(this).getProgress()\n\n// ------ custom assertion definition ------\n// step 1: define custom matcher logic - use function, not a subclass (it's a compose way)\nfun hasProgress(expected: Float) = SemanticsMatcher(\n    description = \"ProgressBarRangeInfo.current = [$expected]\"\n) { node ->\n    val current = node.config[SemanticsProperties.ProgressBarRangeInfo].current\n    current == expected\n}\n// step 2: extend [UltronComposeSemanticsNodeInteraction] class with extension function and [assertMatches] for easier validation\n//fun UltronComposeSemanticsNodeInteraction.assertProgress(expected: Float) = assertMatches(hasProgress(expected))\n// step 3: extend [SemanticsMatcher] class with new assertion method\n//fun SemanticsMatcher.assertProgress(expected: Float) = UltronComposeSemanticsNodeInteraction(this).assertProgress(expected)\n\n// ------ custom ui element definition ------\n// just make a subclass of [UltronComposeSemanticsNodeInteraction] and add new method\nopen class ProgressBar(val matcher: SemanticsMatcher) : UltronComposeSemanticsNodeInteraction(matcher) {\n    fun assertProgress(expected: Float) = matcher.assertMatches(hasProgress(expected))\n}\n\nfun progressBar(block: () -> SemanticsMatcher): Lazy<ProgressBar> = lazy { ProgressBar(block()) }"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/framework/ultronext/UltronEspressoExt.kt",
    "content": "package com.atiurin.sampleapp.framework.ultronext\n\nimport android.view.View\nimport android.widget.CheckBox\nimport android.widget.TextView\nimport androidx.test.espresso.DataInteraction\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.ViewInteraction\nimport com.atiurin.ultron.core.espresso.UltronEspressoInteraction\nimport com.atiurin.ultron.core.espresso.action.UltronEspressoActionParams\nimport com.atiurin.ultron.core.espresso.recyclerview.UltronRecyclerView\nimport com.atiurin.ultron.core.espresso.recyclerview.UltronRecyclerViewItem\nimport org.hamcrest.Matcher\n\nfun <T> UltronEspressoInteraction<T>.appendText(value: String) = perform(\n    params = UltronEspressoActionParams(\n        operationName = \"Append text '$value' to ${getInteractionMatcher()}\",\n        operationDescription = \"Awesome description\"\n    )\n) { _, view ->\n    val textView = (view as TextView)\n    textView.text = \"${textView.text}$value\"\n}\n\n//support action for all Matcher<View>\nfun Matcher<View>.appendText(value: String) = UltronEspressoInteraction(onView(this)).appendText(value)\n\n//support action for all ViewInteractions\nfun ViewInteraction.appendText(text: String) = UltronEspressoInteraction(this).appendText(text)\n\n//support action for all DataInteractions\nfun DataInteraction.appendText(text: String) = UltronEspressoInteraction(this).appendText(text)\n\n//support action for RecyclerView list. appendText action is useless for RecyclerView.\n// This is just an example of how to add new behaviour for UltronRecyclerView\nfun UltronRecyclerView.appendText(text: String) = apply { recyclerViewMatcher.appendText(text) }\n\n//support action for RecyclerView item\nfun UltronRecyclerViewItem.appendText(text: String) = apply { getMatcher().appendText(text) }\n\n// assertion example\nfun <T> UltronEspressoInteraction<T>.assertChecked(expectedState: Boolean) = assertMatches { view ->\n    (view as CheckBox).isChecked == expectedState\n}\nfun Matcher<View>.assertChecked(expectedState: Boolean) = UltronEspressoInteraction(onView(this)).assertChecked(expectedState)\nfun ViewInteraction.assertChecked(expectedState: Boolean) = UltronEspressoInteraction(this).assertChecked(expectedState)\nfun DataInteraction.assertChecked(expectedState: Boolean) = UltronEspressoInteraction(this).assertChecked(expectedState)\n\nfun <T> UltronEspressoInteraction<T>.getViewSimple(): View = execute { _, view ->\n    view\n}\nfun Matcher<View>.getViewSimple() = UltronEspressoInteraction(onView(this)).getViewSimple()"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/framework/ultronext/UltronEspressoWebExt.kt",
    "content": "package com.atiurin.sampleapp.framework.ultronext\n\nimport androidx.test.espresso.web.webdriver.DriverAtoms\nimport com.atiurin.ultron.core.espressoweb.webelement.UltronWebElement\n\n// add action on wenView\nfun UltronWebElement.appendText(text: String) = apply {\n    executeOperation(\n        getUltronWebActionOperation(\n            webInteractionBlock = {\n                webInteractionBlock().perform(DriverAtoms.webKeys(text))\n            },\n            name = \"${elementInfo.name} appendText '$text'\",\n            description = \"${elementInfo.name} appendText '$text' during $timeoutMs ms\"\n        )\n    )\n}"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/framework/ultronext/UltronUiAutomatorExt.kt",
    "content": "package com.atiurin.sampleapp.framework.ultronext\n\nimport com.atiurin.ultron.core.common.UltronOperationType\nimport com.atiurin.ultron.core.uiautomator.UiAutomatorActionType\nimport com.atiurin.ultron.core.uiautomator.uiobject2.UltronUiObject2\n\n//actually, UltronUiObject2 already has the same method addText\n// this is just an example of how to extend UltronUiObject2\nfun UltronUiObject2.appendText(appendText: String) = apply {\n    executeAction(\n        actionBlock = { uiObject2ProviderBlock()!!.text += appendText },\n        name = \"AppendText '$appendText' to ${elementInfo.name}\",\n        description = \"UiObject2 action '${UiAutomatorActionType.ADD_TEXT}' ${elementInfo.name} appendText '$appendText' during $timeoutMs ms\"\n    )\n}\n\nenum class CustomUltronOperations : UltronOperationType {\n    ADD_TEXT_PREFIX, ASSERT_HAS_ANY_CHILD\n}\n// add extension function to UltronUiObject2 that calls `executeAssertion`\nfun UltronUiObject2.assertHasAnyChild() = apply {\n    executeAssertion(\n        assertionBlock = { uiObject2ProviderBlock()!!.childCount > 0 },\n        name = \"Assert ${elementInfo.name} has any child\",\n        type = CustomUltronOperations.ASSERT_HAS_ANY_CHILD,\n        description = \"UiObject2 assertion '${CustomUltronOperations.ASSERT_HAS_ANY_CHILD}' of ${elementInfo.name} during $timeoutMs ms\"\n    )\n}\n"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/framework/utils/AssertUtils.kt",
    "content": "package com.atiurin.sampleapp.framework.utils\n\nimport org.junit.Assert\n\nobject AssertUtils {\n    fun assertException(expected: Boolean = true, block: () -> Unit) {\n        var exceptionOccurs = false\n        try {\n            block()\n        } catch (ex: Throwable) {\n//            throw ex\n            exceptionOccurs = true\n        }\n        Assert.assertEquals(expected, exceptionOccurs)\n    }\n\n    fun assertExecTimeMoreThen(time: Long, block: () -> Unit) {\n        val startTime = System.currentTimeMillis()\n        try {\n            block()\n        } catch (ex: Throwable) {\n        }\n        Assert.assertTrue(System.currentTimeMillis() - startTime >= time)\n    }\n\n    fun assertExecTimeLessThen(time: Long, block: () -> Unit) {\n        val startTime = System.currentTimeMillis()\n        try {\n            block()\n        } catch (ex: Throwable) {\n        }\n        Assert.assertTrue(System.currentTimeMillis() - startTime <= time)\n    }\n\n    fun assertExecTimeBetween(minTime: Long, maxTime: Long, block: () -> Unit) {\n        val startTime = System.currentTimeMillis()\n        try {\n            block()\n        } catch (ex: Throwable) {\n        }\n        val execTime = System.currentTimeMillis() - startTime\n        Assert.assertTrue(\"ExecTime in $minTime .. $maxTime, but actual $execTime\", execTime in minTime..maxTime)\n    }\n}"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/framework/utils/EspressoUtil.kt",
    "content": "package com.atiurin.sampleapp.framework.utils\n\nimport androidx.test.espresso.Espresso\nimport androidx.test.platform.app.InstrumentationRegistry\n\nobject EspressoUtil {\n//    fun openOptionsMenu() = apply {\n//        Espresso.openActionBarOverflowOrOptionsMenu(InstrumentationRegistry.getInstrumentation().context)\n//    }\n}"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/framework/utils/TestDataUtils.kt",
    "content": "package com.atiurin.sampleapp.framework.utils\n\nimport androidx.test.platform.app.InstrumentationRegistry\n\nobject TestDataUtils {\n    fun getResourceString(resourceId: Int): String {\n        return InstrumentationRegistry.getInstrumentation().targetContext.resources.getString(resourceId)\n    }\n}"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/framework/utils/TimeUtils.kt",
    "content": "package com.atiurin.sampleapp.framework.utils\n\nimport android.annotation.SuppressLint\nimport java.time.Clock\nimport java.time.Instant\nimport java.time.LocalDate\nimport java.time.ZoneId\nimport java.time.ZoneOffset\nimport java.time.format.DateTimeFormatter\n\nobject TimeUtils {\n    @SuppressLint(\"NewApi\")\n    fun formatTimestamp(timestamp: Long): String {\n        val instant = Instant.ofEpochMilli(timestamp)\n        val formatter = DateTimeFormatter.ofPattern(\"yyyy-MM-dd HH:mm:ss\")\n            .withZone(ZoneId.systemDefault())\n        return formatter.format(instant)\n    }\n\n    fun getTimestampStartOfDay(): Long {\n        return LocalDate.now(Clock.systemUTC())\n            .atStartOfDay()\n            .toInstant(ZoneOffset.UTC)\n            .toEpochMilli()\n    }\n}\n"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/ChatPage.kt",
    "content": "package com.atiurin.sampleapp.pages\n\nimport android.view.View\nimport androidx.test.espresso.matcher.ViewMatchers.*\nimport com.atiurin.sampleapp.R\nimport com.atiurin.ultron.allure.step.step\nimport com.atiurin.ultron.page.Page\nimport com.atiurin.ultron.core.espresso.recyclerview.UltronRecyclerViewItem\nimport com.atiurin.ultron.core.espresso.recyclerview.withRecyclerView\nimport com.atiurin.ultron.core.espresso.UltronEspresso\nimport com.atiurin.ultron.custom.espresso.matcher.withSuitableRoot\nimport com.atiurin.ultron.extensions.*\nimport org.hamcrest.Matcher\nimport org.hamcrest.Matchers.allOf\n\nobject ChatPage : Page<ChatPage>() {\n    fun assertPageDisplayed() = apply {\n        step(\"Assert friends list page displayed\") {\n            messagesList.isDisplayed()\n        }\n    }\n\n    val messagesList = withRecyclerView(R.id.messages_list).withName(\"Messages list\")\n    val clearHistoryBtn = withText(\"Clear history\")\n    val inputMessageText = withId(R.id.message_input_text)\n    val sendMessageBtn = withId(R.id.send_button)\n    val toolbarTitle = withId(R.id.toolbar_title)\n\n    fun getMessageListItem(text: String): ChatRecyclerItem {\n        return messagesList.getItem(hasDescendant(\n                allOf(\n                    withId(R.id.message_text),\n                    withText(text)\n                )\n            )\n        )\n    }\n\n    private fun getListItemAtPosition(position: Int): ChatRecyclerItem {\n        return messagesList.getItem(position)\n    }\n\n    fun getTitle(title: String): Matcher<View> {\n        return allOf(withId(R.id.toolbar_title), withText(title))\n    }\n\n    class ChatRecyclerItem : UltronRecyclerViewItem(){\n        val text by lazy { getChild(withId(R.id.message_text)) }\n    }\n\n    fun sendMessage(text: String) = apply {\n        step(\"Send message with text '$text\") {\n            inputMessageText.typeText(text)\n            sendMessageBtn.click()\n            this.getMessageListItem(text).text\n                .isDisplayed()\n                .hasText(text)\n        }\n    }\n\n    fun assertMessageTextAtPosition(position: Int, text: String) = apply {\n        step(\"Assert item at position $position has text '$text'\") {\n            this.getListItemAtPosition(position).text.isDisplayed().hasText(text)\n        }\n    }\n\n    fun assertToolbarTitle(text: String){\n        toolbarTitle.withTimeout(1000).withName(\"Toolbar title\").hasText(text)\n    }\n\n    fun assertToolbarTitleWithSuitableRoot(text: String){\n        toolbarTitle.withSuitableRoot().withName(\"Toolbar title\").hasText(text)\n    }\n\n    fun clearHistory() = apply {\n        step(\"Clear chat history\") {\n            UltronEspresso.openContextualActionModeOverflowMenu()\n            clearHistoryBtn.click()\n            messagesList.assertEmpty()\n        }\n    }\n\n    fun assertMessageDisplayed(text: String) {\n        getMessageListItem(text).text\n            .isDisplayed()\n            .hasText(text)\n    }\n}"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/ComposeElementsPage.kt",
    "content": "package com.atiurin.sampleapp.pages\n\nimport androidx.compose.ui.test.hasTestTag\nimport com.atiurin.sampleapp.activity.ComposeElementsActivity\nimport com.atiurin.sampleapp.activity.ComposeElementsActivity.Constants.clickListenerButton\nimport com.atiurin.sampleapp.activity.ComposeElementsActivity.Constants.likesCounterButton\nimport com.atiurin.sampleapp.activity.ComposeElementsActivity.Constants.radioButtonFemaleTestTag\nimport com.atiurin.sampleapp.activity.ComposeElementsActivity.Constants.radioButtonMaleTestTag\nimport com.atiurin.sampleapp.compose.RegionsClickListenerTestTags\nimport com.atiurin.sampleapp.framework.ultronext.progressBar\nimport com.atiurin.ultron.page.Page\n\nobject ComposeElementsPage : Page<ComposeElementsPage>() {\n    val status = hasTestTag(ComposeElementsActivity.Constants.statusText)\n    val likesCounter = hasTestTag(likesCounterButton)\n    val longAndDoubleClickButton = hasTestTag(clickListenerButton)\n    val regionsNode = hasTestTag(RegionsClickListenerTestTags.regionsNode)\n    val clickedRegion = hasTestTag(RegionsClickListenerTestTags.regionsClickedText)\n    val editableText = hasTestTag(ComposeElementsActivity.editableText)\n    val swipeableNode = hasTestTag(ComposeElementsActivity.swipeableNode)\n    val disabledButton = hasTestTag(ComposeElementsActivity.disabledButton)\n    val simpleCheckbox = hasTestTag(ComposeElementsActivity.simpleCheckbox)\n    val progressBar by progressBar { hasTestTag(ComposeElementsActivity.progressBar) }\n    val maleRadioButton = hasTestTag(radioButtonMaleTestTag)\n    val femaleRadioButton = hasTestTag(radioButtonFemaleTestTag)\n    val notExistedElement = hasTestTag(\"NotExistedTestTag\")\n}\n"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/ComposeListPage.kt",
    "content": "package com.atiurin.sampleapp.pages\n\nimport androidx.compose.ui.test.hasAnyDescendant\nimport androidx.compose.ui.test.hasContentDescription\nimport androidx.compose.ui.test.hasTestTag\nimport androidx.compose.ui.test.hasText\nimport com.atiurin.sampleapp.compose.ListItemPositionPropertyKey\nimport com.atiurin.sampleapp.compose.contactNameTestTag\nimport com.atiurin.sampleapp.compose.contactStatusTestTag\nimport com.atiurin.sampleapp.compose.contactsListContentDesc\nimport com.atiurin.sampleapp.compose.getContactItemTestTagById\nimport com.atiurin.sampleapp.data.entities.Contact\nimport com.atiurin.ultron.core.compose.list.UltronComposeListItem\nimport com.atiurin.ultron.core.compose.list.composeList\nimport com.atiurin.ultron.extensions.withDescription\nimport com.atiurin.ultron.page.Page\n\nobject ComposeListPage : Page<ComposeListPage>() {\n    val lazyList = composeList(\n        listMatcher = hasContentDescription(contactsListContentDesc),\n        positionPropertyKey = ListItemPositionPropertyKey\n    ).withDescription(\"Contacts list\")\n\n    fun assertContactStatus(contact: Contact) = apply {\n        getContactItemByTestTag(contact).status.assertTextEquals(contact.status)\n    }\n    fun getItemByPosition(position: Int): ComposeFriendListItem {\n        return lazyList.getItem(position)\n    }\n\n    fun getFirstVisibleItem(): ComposeFriendListItem = lazyList.getFirstVisibleItem()\n    fun getItemByIndex(index: Int): ComposeFriendListItem = lazyList.getVisibleItem(index)\n    fun getContactItemByTestTag(contact: Contact): ComposeFriendListItem = lazyList.getItem(hasTestTag(getContactItemTestTagById(contact)))\n    fun getContactItemByName(contact: Contact): ComposeFriendListItem = lazyList\n        .getItem(hasAnyDescendant(hasText(contact.name) and hasTestTag(contactNameTestTag)).withDescription(\"Contact '${contact.name}'\"))\n\n    class ComposeFriendListItem : UltronComposeListItem() {\n        val name by child { hasTestTag(contactNameTestTag).withDescription(\"Contact name\") }\n        val status by lazy { getChild(hasTestTag(contactStatusTestTag).withDescription(\"Contact status\")) }\n        val notExisted by child { hasTestTag(\"NotExistedChild\").withDescription(\"Not existed child\") }\n    }\n}\n\n\n"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/ComposeSecondPage.kt",
    "content": "package com.atiurin.sampleapp.pages\n\nimport androidx.compose.ui.test.hasTestTag\nimport com.atiurin.ultron.page.Page\n\nobject ComposeSecondPage : Page<ComposeSecondPage>() {\n    val name = hasTestTag(\"name\")\n    val status = hasTestTag(\"status\")\n}"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/FriendsListPage.kt",
    "content": "package com.atiurin.sampleapp.pages\n\nimport androidx.test.espresso.matcher.ViewMatchers.hasDescendant\nimport androidx.test.espresso.matcher.ViewMatchers.withId\nimport androidx.test.espresso.matcher.ViewMatchers.withText\nimport com.atiurin.sampleapp.R\nimport com.atiurin.sampleapp.data.entities.Contact\nimport com.atiurin.ultron.allure.step.step\nimport com.atiurin.ultron.core.espresso.recyclerview.UltronRecyclerViewImpl\nimport com.atiurin.ultron.core.espresso.recyclerview.UltronRecyclerViewItem\nimport com.atiurin.ultron.core.espresso.recyclerview.withRecyclerView\nimport com.atiurin.ultron.custom.espresso.matcher.withSuitableRoot\nimport com.atiurin.ultron.extensions.isDisplayed\nimport com.atiurin.ultron.extensions.withName\nimport com.atiurin.ultron.page.Page\nimport org.hamcrest.Matchers.allOf\nimport org.hamcrest.Matchers.containsString\nimport org.junit.Assert\n\nobject FriendsListPage : Page<FriendsListPage>() {\n    val recycler = withRecyclerView(R.id.recycler_friends,\n        implementation = UltronRecyclerViewImpl.PERFORMANCE).withName(\"Friends list\")\n\n    fun assertPageDisplayed() = apply {\n        step(\"Assert friends list page displayed\") {\n            recycler.recyclerViewMatcher.isDisplayed()\n        }\n    }\n\n    fun assertPageDisplayedWithSuitableRoot() = apply {\n        step(\"Assert friends list page displayed with suitable root\") {\n            recycler.withSuitableRoot().isDisplayed()\n        }\n    }\n\n    class FriendRecyclerItem : UltronRecyclerViewItem() {\n        val name by child(withId(R.id.tv_name).withName(\"Friend name\"))\n        val status by lazy { getChild(withId(R.id.tv_status)).withName(\"Status\") }\n        val avatar by lazy { getChild(withId(R.id.avatar)).withName(\"Avatar\") }\n    }\n\n    fun getListItem(contactName: String): FriendRecyclerItem {\n        return recycler.getItem(\n            hasDescendant(allOf(withId(R.id.tv_name), withText(contactName))).withName(\"Friend '$contactName\")\n        )\n    }\n\n    fun getListItem(positions: Int): FriendRecyclerItem {\n        return recycler.getItem(positions)\n    }\n\n    fun openChat(name: String) = apply {\n        step(\"Open chat with friend '$name'\") {\n            this.getListItem(name).click()\n            ChatPage { assertPageDisplayed() }\n        }\n    }\n\n    fun assertStatus(name: String, status: String) = apply {\n        step(\"Assert friend with name '$name' has status '$status'\") {\n            getListItem(name).status.hasText(status).isDisplayed()\n        }\n    }\n\n    fun assertName(nameText: String) = apply {\n        step(\"Assert friend name '$nameText' in the right place\") {\n            getListItem(nameText).name.hasText(nameText).isDisplayed()\n        }\n    }\n\n    fun assertFriendsListSize(size: Int) {\n        Assert.assertEquals(size, recycler.getSize())\n    }\n\n    fun getItemMatcher(contact: Contact) = hasDescendant(allOf(withId(R.id.tv_name), withText(containsString(contact.name))))\n}\n"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/UiElementsPage.kt",
    "content": "package com.atiurin.sampleapp.pages\n\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.matcher.RootMatchers.isDialog\nimport androidx.test.espresso.matcher.RootMatchers.isPlatformPopup\nimport androidx.test.espresso.matcher.ViewMatchers.withId\nimport androidx.test.espresso.matcher.ViewMatchers.withText\nimport com.atiurin.sampleapp.R\nimport com.atiurin.ultron.page.Page\n\nobject UiElementsPage : Page<UiElementsPage>() {\n    val notExistElement = withText(\"Some not existed text element\")\n    val button = withId(R.id.button1)\n    val eventStatus = withId(R.id.last_event_status)\n    val radioVisibleButton = withId(R.id.radio_visible)\n    val radioInvisibleButton = withId(R.id.radio_invisible)\n    val radioGoneButton = withId(R.id.radio_gone)\n    val checkBoxClickable = withId(R.id.checkbox_clickable)\n    val checkBoxEnabled = withId(R.id.checkbox_enable)\n    val checkBoxSelected = withId(R.id.checkbox_selected)\n    val checkBoxFocusable = withId(R.id.checkbox_focusable)\n    val checkBoxJsEnabled = withId(R.id.checkbox_js_enabled)\n    val editTextContentDesc = withId(R.id.et_contentDesc)\n    val webView = withId(R.id.webview)\n    val appCompatTextView = withId(R.id.app_compat_text)\n    val imageView = withId(R.id.swipe_image_view)\n    val imageView2 = withId(R.id.image_view2)\n    val emptyNotClickableImageView = withId(R.id.empty_image_view)\n    val dialogButtonOk = onView(withText(\"OK\")).inRoot(isDialog())\n    val popupButtonCancel = onView(withText(\"Cancel\")).inRoot(isPlatformPopup())\n    val hiddenButton = withId(R.id.exist_hidden_button)\n}\n"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/UiObject2ElementsPage.kt",
    "content": "package com.atiurin.sampleapp.pages\n\nimport androidx.test.uiautomator.By\nimport com.atiurin.sampleapp.R\nimport com.atiurin.ultron.core.uiautomator.uiobject2.UltronUiObject2.Companion.by\nimport com.atiurin.ultron.core.uiautomator.uiobject2.UltronUiObject2.Companion.byResId\n\nclass UiObject2ElementsPage {\n    val notExistedObject = by(By.res(\"com.atiurin.sampleapp\",\"123123123123\"))\n    val button = byResId(R.id.button1)\n    val eventStatus = byResId(R.id.last_event_status)\n    val radioGroup = byResId(R.id.radio_group_visibility)\n    val radioVisibleButton = byResId(R.id.radio_visible)\n    val radioInvisibleButton = byResId(R.id.radio_invisible)\n    val radioGoneButton = byResId(R.id.radio_gone)\n    val checkBoxClickable = byResId(R.id.checkbox_clickable)\n    val checkBoxEnabled = byResId(R.id.checkbox_enable)\n    val checkBoxSelected = byResId(R.id.checkbox_selected)\n    val checkBoxFocusable = byResId(R.id.checkbox_focusable)\n    val checkBoxJsEnabled = byResId(R.id.checkbox_js_enabled)\n    val editTextContentDesc = byResId(R.id.et_contentDesc)\n    val textElement = by(By.text(\"some text\"))\n    val contentDescElement = by(By.desc(\"Content desc\"))\n    val webView = byResId(R.id.webview)\n    val appCompatTextView = byResId(R.id.app_compat_text)\n    val swipableImageView = byResId(R.id.swipe_image_view)\n    val emptyImageView = byResId(R.id.empty_image_view)\n}\n"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/UiObject2FriendsListPage.kt",
    "content": "package com.atiurin.sampleapp.pages\n\nimport androidx.test.uiautomator.By\nimport com.atiurin.sampleapp.R\nimport com.atiurin.sampleapp.data.repositories.ContactRepositoty\nimport com.atiurin.ultron.core.uiautomator.uiobject2.UltronUiObject2.Companion.by\nimport com.atiurin.ultron.core.uiautomator.uiobject2.UltronUiObject2.Companion.byResId\nimport com.atiurin.ultron.page.Page\n\nobject UiObject2FriendsListPage : Page<UiObject2FriendsListPage>() {\n    val list = byResId(R.id.recycler_friends)\n    val topElement = by(By.text(ContactRepositoty.getFirst().name))\n    val bottomElement = by(By.text(ContactRepositoty.getLast().name))\n}"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/UiObjectElementsPage.kt",
    "content": "package com.atiurin.sampleapp.pages\n\nimport com.atiurin.sampleapp.R\nimport com.atiurin.ultron.core.uiautomator.uiobject.UltronUiObject.Companion.uiResId\n\nclass UiObjectElementsPage {\n    val notExistedObject = uiResId(R.id.send_button)\n    val button = uiResId(R.id.button1)\n    val eventStatus = uiResId(R.id.last_event_status)\n    val radioGroup = uiResId(R.id.radio_group_visibility)\n    val radioVisibleButton = uiResId(R.id.radio_visible)\n    val radioInvisibleButton = uiResId(R.id.radio_invisible)\n    val radioGoneButton = uiResId(R.id.radio_gone)\n    val checkBoxClickable = uiResId(R.id.checkbox_clickable)\n    val checkBoxEnabled = uiResId(R.id.checkbox_enable)\n    val checkBoxSelected = uiResId(R.id.checkbox_selected)\n    val checkBoxFocusable = uiResId(R.id.checkbox_focusable)\n    val checkBoxJsEnabled = uiResId(R.id.checkbox_js_enabled)\n    val editTextContentDesc = uiResId(R.id.et_contentDesc)\n    val webView = uiResId(R.id.webview)\n    val appCompatTextView = uiResId(R.id.app_compat_text)\n    val swipableImageView = uiResId(R.id.swipe_image_view)\n    val emptyImageView = uiResId(R.id.empty_image_view)\n}"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/WebViewPage.kt",
    "content": "package com.atiurin.sampleapp.pages\n\nimport com.atiurin.ultron.core.espressoweb.webelement.UltronWebElements.Companion.classNames\nimport com.atiurin.ultron.core.espressoweb.webelement.UltronWebElement.Companion.className\nimport com.atiurin.ultron.core.espressoweb.webelement.UltronWebElement.Companion.id\nimport com.atiurin.ultron.page.Page\n\nclass WebViewPage : Page<WebViewPage>() {\n    companion object{\n        const val BUTTON2_TITLE = \"button2 clicked\"\n        const val APPLE_LINK_TEXT = \"Apple\"\n        const val APPLE_LINK_HREF = \"fake_link.html\"\n\n    }\n    val textInput = id(\"text_input\")\n    val buttonUpdTitle = id(\"button1\")\n    val buttonSetTitle2 = id(\"button2\")\n    val buttonSetTitleActive = id(\"button3\")\n    val title = id(\"title\")\n    val titleWithCss = className(\"css_title\")\n    val appleLink = id(\"apple_link\")\n    val buttons = classNames(\"button\")\n    val notExistedElement = id(\"Not existed element\")\n}\n"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/uiblock/ComposeUiBlockScreen.kt",
    "content": "package com.atiurin.sampleapp.pages.uiblock\n\nimport androidx.compose.ui.test.SemanticsMatcher\nimport androidx.compose.ui.test.hasTestTag\nimport com.atiurin.sampleapp.activity.ComposeElementsActivity.Constants.contactBlock1Tag\nimport com.atiurin.sampleapp.activity.ComposeElementsActivity.Constants.contactBlock2Tag\nimport com.atiurin.sampleapp.activity.ComposeElementsActivity.Constants.contactNameTag\nimport com.atiurin.sampleapp.activity.ComposeElementsActivity.Constants.contactStatusTag\nimport com.atiurin.sampleapp.activity.ComposeElementsActivity.Constants.contactsListTag\nimport com.atiurin.sampleapp.pages.uiblock.ComposeListUiBlock.Companion.listBlockDesc\nimport com.atiurin.ultron.core.compose.child\nimport com.atiurin.ultron.core.compose.page.UltronComposeUiBlock\nimport com.atiurin.ultron.extensions.withName\nimport com.atiurin.ultron.page.Screen\n\nobject ComposeUiBlockScreen : Screen<ComposeUiBlockScreen>(){\n    val contactBlock1 = ContactUiBlockWithoutDesc(hasTestTag(contactBlock1Tag))\n    val contactBlock2 = ContactUiBlockWithDesc(hasTestTag(contactBlock2Tag), \"Block parent\")\n    val contactListBlock = ComposeListUiBlock(hasTestTag(contactsListTag), listBlockDesc)\n}\nclass ContactUiBlockWithoutDesc(blockMatcher: SemanticsMatcher) : UltronComposeUiBlock(blockMatcher) {\n    val nameWithoutDeepSearch = child(hasTestTag(contactNameTag), descendantSearch = false).withName(\"No deep search element\")\n    val statusDeepSearchText = child(hasTestTag(contactStatusTag))\n}\n\nclass ContactUiBlockWithDesc(blockMatcher: SemanticsMatcher, blockDescription: String) : UltronComposeUiBlock(blockMatcher, blockDescription) {\n    val name = child(hasTestTag(contactNameTag)).withName(\"$сhildNameDesc $blockDescription\")\n    val status = child(hasTestTag(contactStatusTag))\n\n    companion object {\n        const val сhildNameDesc = \"NamE\"\n    }\n}\n\nclass ComposeListUiBlock(parent: SemanticsMatcher, blockDescription: String) : UltronComposeUiBlock(parent) {\n    val itemWithoutDesc = child(uiBlock = ContactUiBlockWithoutDesc(hasTestTag(contactBlock1Tag)))\n    val item1BlockWithDesc = child(ContactUiBlockWithDesc(hasTestTag(contactBlock1Tag), \"1 $descriptionPrefix $blockDescription\"))\n    val item2BlockFactory = child(\n        childMatcher = hasTestTag(contactBlock2Tag),\n        uiBlockFactory = { updatedMatcher ->\n            ContactUiBlockWithDesc(updatedMatcher, blockDescription = \"2 $descriptionPrefix $blockDescription\")\n        }\n    )\n\n    companion object {\n        const val descriptionPrefix = \"Item with parent\"\n        val listBlockDesc = \"ListBlock\"\n    }\n}\n\n"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/uiblock/EspressoUiBlockScreen.kt",
    "content": "package com.atiurin.sampleapp.pages.uiblock\n\nimport android.view.View\nimport androidx.test.espresso.matcher.ViewMatchers.withId\nimport com.atiurin.sampleapp.R\nimport com.atiurin.ultron.core.espresso.UltronEspressoUiBlock\nimport com.atiurin.ultron.extensions.withName\nimport com.atiurin.ultron.page.Screen\nimport org.hamcrest.Matcher\n\nobject EspressoUiBlockScreen : Screen<EspressoUiBlockScreen>() {\n    val contactItem1 = ContactItemUiBlock(withId(R.id.contact_item_1), \"Item 1\")\n    val blockWithoutDeepSearch = ContactItemUiBlockWithoutDeepSearch(withId(R.id.contact_item_2))\n    val contactsListBlock = ContactsListUiBlock(withId(R.id.contact_items), \"Items list\")\n}\n\nclass ContactItemUiBlock(blockMatcher: Matcher<View>, blockDescription: String) : UltronEspressoUiBlock(blockMatcher) {\n    val name = child(withId(R.id.name)).withName(\"Contact name with parent $blockDescription\")\n    val status = child(withId(R.id.status)).withName(\"Contact item status\")\n    val deepSearchChild = child(withId(R.id.deep_search_child))\n}\n\nclass ContactItemUiBlockWithoutDeepSearch(parent: Matcher<View>) : UltronEspressoUiBlock(parent) {\n    val deepSearchFalse = child(withId(R.id.deep_search_child), descendantSearch = false)\n}\n\nclass ContactsListUiBlock(blockMatcher: Matcher<View>, blockDescription: String) : UltronEspressoUiBlock(blockMatcher, blockDescription) {\n    val item1 = child(ContactItemUiBlock(withId(R.id.contact_item_1), \"Item 1\"))\n    val item2 = child(\n        childMatcher = withId(R.id.contact_item_2),\n        uiBlockFactory = { updatedMatcher ->\n            ContactItemUiBlock(updatedMatcher, blockDescription = \"Contact Item 2 with parent $blockDescription\")\n        }\n    )\n}\n\n"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/uiblock/UiObject2UiBlockScreen.kt",
    "content": "package com.atiurin.sampleapp.pages.uiblock\n\nimport androidx.test.uiautomator.BySelector\nimport com.atiurin.sampleapp.R\nimport com.atiurin.ultron.core.uiautomator.uiobject2.UltronUiObject2.Companion.bySelector\nimport com.atiurin.ultron.core.uiautomator.uiobject2.UltronUiObject2UiBlock\nimport com.atiurin.ultron.page.Screen\n\nobject UiObject2UiBlockScreen : Screen<UiObject2UiBlockScreen>() {\n    val block1 = ContactItemUiObject2Block { bySelector(R.id.contact_item_1) }\n    val block2 = ContactItemUiObject2Block(\"Block 2\") { bySelector(R.id.contact_item_2) }\n    val blocks = UiObject2ListUiBlock(\"Item blocks\") { bySelector(R.id.contact_items) }\n}\n\n\nclass ContactItemUiObject2Block(blockDesc: String = \"\", blockSelector: () -> BySelector) : UltronUiObject2UiBlock(blockDesc, blockSelector) {\n    val name = child(bySelector(R.id.name)).withName(\"Name in block $blockDesc\")\n    val status = child(bySelector(R.id.status))\n    val deepSearchChild = child(bySelector(R.id.deep_search_child))\n    val notExisted = child(bySelector(R.id.recycler_friends)).withTimeout(100)\n}\n\nclass UiObject2ListUiBlock(desc: String = \"\", parent: () -> BySelector) : UltronUiObject2UiBlock(desc, parent) {\n    val item1 = child(ContactItemUiObject2Block { bySelector(R.id.contact_item_1) })\n    val item2 = child(\n        selector = bySelector(R.id.contact_item_2),\n        description = \"Item 2 in block $desc\",\n        uiBlockFactory = { desc, selector ->\n            ContactItemUiObject2Block(desc, selector)\n        }\n    )\n}"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/uiblock/WebElementUiBlockScreen.kt",
    "content": "package com.atiurin.sampleapp.pages.uiblock\n\nimport com.atiurin.ultron.core.espressoweb.webelement.UltronWebElement\nimport com.atiurin.ultron.core.espressoweb.webelement.UltronWebElement.Companion.className\nimport com.atiurin.ultron.core.espressoweb.webelement.UltronWebElement.Companion.id\nimport com.atiurin.ultron.core.espressoweb.webelement.UltronWebElementUiBlock\nimport com.atiurin.ultron.page.Screen\n\nobject WebElementUiBlockScreen : Screen<WebElementUiBlockScreen>() {\n    val teacherBlock = WebBlock(id(\"teacher\"), \"Teacher block\")\n    val studentWithoutDesc = WebBlockWithoutDesc(id(\"student\"))\n    val persons = WebPersonsBlock(id(\"persons\"), \"persons block\")\n}\n\nclass WebBlock(blockElement: UltronWebElement, blockDescription: String): UltronWebElementUiBlock(blockElement, blockDescription){\n    val name = child(className(\"person_name\")).withName(\"Name of $blockDescription\")\n}\n\nclass WebBlockWithoutDesc(blockElement: UltronWebElement): UltronWebElementUiBlock(blockElement){\n    val name = child(className(\"person_name\"))\n}\n\nclass WebPersonsBlock(blockElement: UltronWebElement, blockDescription: String): UltronWebElementUiBlock(blockElement, blockDescription){\n    val teacher = child(WebBlock(id(\"teacher\"), \"Teacher child of $blockDescription\"))\n    val studentWithoutDesc = child(WebBlockWithoutDesc(id(\"student\")))\n    val student = child(id(\"student\")){ modifiedElement ->\n        WebBlock(modifiedElement, \"student child of $blockDescription\")\n    }\n}"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/BaseTest.kt",
    "content": "package com.atiurin.sampleapp.tests\n\nimport android.os.Environment\nimport androidx.test.platform.app.InstrumentationRegistry\nimport com.atiurin.sampleapp.data.repositories.CURRENT_USER\nimport com.atiurin.sampleapp.managers.AccountManager\nimport com.atiurin.ultron.allure.config.UltronAllureConfig\nimport com.atiurin.ultron.core.compose.config.UltronComposeConfig\nimport com.atiurin.ultron.core.compose.listeners.ComposDebugListener\nimport com.atiurin.ultron.core.config.UltronCommonConfig\nimport com.atiurin.ultron.core.config.UltronConfig\nimport com.atiurin.ultron.core.espresso.recyclerview.UltronRecyclerViewImpl\nimport com.atiurin.ultron.core.test.UltronTest\nimport com.atiurin.ultron.testlifecycle.rulesequence.RuleSequence\nimport com.atiurin.ultron.testlifecycle.setupteardown.SetUpRule\nimport org.junit.BeforeClass\nimport org.junit.Rule\n\nabstract class BaseTest : UltronTest(){\n    val setupRule = SetUpRule(\"Login user rule\")\n        .add(name = \"Login valid user $CURRENT_USER\") {\n            AccountManager(InstrumentationRegistry.getInstrumentation().targetContext).login(\n                CURRENT_USER.login, CURRENT_USER.password\n            )\n        }\n\n    @get:Rule\n    open val ruleSequence = RuleSequence(setupRule)\n\n    companion object {\n        @BeforeClass\n        @JvmStatic\n        fun config() {\n            UltronConfig.Espresso.RECYCLER_VIEW_IMPLEMENTATION = UltronRecyclerViewImpl.PERFORMANCE\n            UltronConfig.Espresso.ViewActionConfig.autoScroll = true\n            UltronConfig.applyRecommended()\n            UltronComposeConfig.applyRecommended()\n            UltronCommonConfig.addListener(ComposDebugListener())\n            UltronAllureConfig.applyRecommended()\n            UltronAllureConfig.setAllureResultsDirectory(Environment.DIRECTORY_DOWNLOADS)\n        }\n    }\n}\n"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/UiElementsTest.kt",
    "content": "package com.atiurin.sampleapp.tests\n\nimport com.atiurin.sampleapp.activity.UiElementsActivity\nimport com.atiurin.ultron.testlifecycle.activity.UltronActivityRule\n\nabstract class UiElementsTest : BaseTest() {\n    val activityRule = UltronActivityRule(UiElementsActivity::class.java)\n\n    init {\n        ruleSequence.add(activityRule)\n    }\n}"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/CheckboxTest.kt",
    "content": "package com.atiurin.sampleapp.tests.compose\n\nimport androidx.compose.material.TriStateCheckbox\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.semantics.contentDescription\nimport androidx.compose.ui.semantics.semantics\nimport androidx.compose.ui.state.ToggleableState\nimport androidx.compose.ui.test.hasContentDescription\nimport com.atiurin.ultron.core.compose.createDefaultUltronComposeRule\nimport com.atiurin.ultron.core.compose.nodeinteraction.click\nimport com.atiurin.ultron.extensions.assertIsIndeterminate\nimport org.junit.Rule\nimport org.junit.Test\n\nclass CheckboxTest {\n    @get:Rule\n    val composeRule = createDefaultUltronComposeRule()\n\n    @Test\n    fun checkboxStates() {\n        val testTag = \"checkBox\"\n        composeRule.setContent {\n            val checkedState = remember { mutableStateOf(ToggleableState.Indeterminate) }\n            TriStateCheckbox(\n                state = checkedState.value,\n                onClick = {\n                    if (checkedState.value == ToggleableState.Indeterminate || checkedState.value == ToggleableState.Off)\n                        checkedState.value = ToggleableState.On\n                    else checkedState.value = ToggleableState.Off\n                },\n                modifier = Modifier.semantics { contentDescription = testTag }\n            )\n        }\n\n        hasContentDescription(testTag).assertIsIndeterminate()\n            .click().assertIsOn()\n            .click().assertIsOff()\n    }\n}\n"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/CollectionInteractionTest.kt",
    "content": "package com.atiurin.sampleapp.tests.compose\n\nimport androidx.compose.ui.test.hasTestTag\nimport com.atiurin.sampleapp.activity.ComposeListActivity\nimport com.atiurin.sampleapp.compose.contactNameTestTag\nimport com.atiurin.sampleapp.compose.contactsListTestTag\nimport com.atiurin.sampleapp.data.repositories.CONTACTS\nimport com.atiurin.sampleapp.tests.BaseTest\nimport com.atiurin.ultron.core.compose.createSimpleUltronComposeRule\nimport com.atiurin.ultron.core.compose.operation.UltronComposeCollectionInteraction.Companion.allNodes\nimport org.junit.Rule\nimport org.junit.Test\n\nclass CollectionInteractionTest: BaseTest()  {\n    @get:Rule\n    val composeRule = createSimpleUltronComposeRule<ComposeListActivity>()\n    val list = hasTestTag(contactsListTestTag)\n    @Test\n    fun allNodes_getByIndex(){\n        val index = 4\n        val contact = CONTACTS[index]\n        allNodes(hasTestTag(contactNameTestTag), true).get(index).assertTextContains(contact.name)\n    }\n}"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/ComposeConfigTest.kt",
    "content": "package com.atiurin.sampleapp.tests.compose\n\nimport com.atiurin.sampleapp.activity.ComposeElementsActivity\nimport com.atiurin.sampleapp.framework.DummyMetaObject\nimport com.atiurin.sampleapp.framework.utils.AssertUtils\nimport com.atiurin.sampleapp.pages.ComposeElementsPage\nimport com.atiurin.ultron.core.compose.config.UltronComposeConfig\nimport com.atiurin.ultron.core.compose.createSimpleUltronComposeRule\nimport com.atiurin.ultron.extensions.assertExists\nimport com.atiurin.ultron.extensions.isSuccess\nimport com.atiurin.ultron.extensions.withResultHandler\nimport com.atiurin.ultron.extensions.withTimeout\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationResult\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType\nimport com.atiurin.ultron.core.compose.operation.UltronComposeOperation\nimport com.atiurin.ultron.extensions.*\nimport com.atiurin.ultron.testlifecycle.rulesequence.RuleSequence\nimport com.atiurin.ultron.testlifecycle.setupteardown.SetUp\nimport com.atiurin.ultron.testlifecycle.setupteardown.SetUpRule\nimport com.atiurin.ultron.testlifecycle.setupteardown.TearDown\nimport com.atiurin.ultron.testlifecycle.setupteardown.TearDownRule\nimport org.junit.Assert\nimport org.junit.Rule\nimport org.junit.Test\n\nclass ComposeConfigTest {\n    companion object {\n        const val setCustomTimeout = \"Set custom timeout\"\n        const val dropCustomTimeout = \"Drop custom timeout\"\n        const val customTimeout = 1100L\n    }\n\n    val page = ComposeElementsPage\n    val composeRule = createSimpleUltronComposeRule<ComposeElementsActivity>()\n    val setUpRule = SetUpRule().add(setCustomTimeout) { UltronComposeConfig.params.operationTimeoutMs = customTimeout }\n    val tearDownRule = TearDownRule().add(dropCustomTimeout) { UltronComposeConfig.params.operationTimeoutMs = UltronComposeConfig.DEFAULT_OPERATION_TIMEOUT }\n\n    @get:Rule\n    val ruleSequence = RuleSequence().add(composeRule, setUpRule, tearDownRule)\n\n\n    @Test\n    fun resultHandler_successOperation() {\n        lateinit var result: ComposeOperationResult<UltronComposeOperation>\n        page.editableText.withResultHandler { result = it }.assertTextContains(\"\")\n        Assert.assertTrue(result.success)\n        Assert.assertTrue(result.operation.type == ComposeOperationType.CONTAINS_TEXT)\n    }\n\n    @Test\n    fun resultHandler_failedOperation() {\n        lateinit var result: ComposeOperationResult<UltronComposeOperation>\n        page.editableText.withResultHandler { result = it }.withTimeout(100).assertTextContains(\"invalid\")\n        Assert.assertFalse(result.success)\n        Assert.assertTrue(result.operation.type == ComposeOperationType.CONTAINS_TEXT)\n        Assert.assertTrue(result.exceptions.isNotEmpty())\n        Assert.assertTrue(result.description.isNotEmpty())\n    }\n\n    @Test\n    @SetUp(setCustomTimeout)\n    @TearDown(dropCustomTimeout)\n    fun operationTimeout() {\n        page.likesCounter.assertIsDisplayed()\n        AssertUtils.assertExecTimeBetween(customTimeout, 4900) {\n            page.likesCounter.assertTextContains(\"asdqw3213\")\n        }\n    }\n\n    @Test\n    fun isSuccess_false() {\n        Assert.assertFalse(page.editableText.isSuccess { withTimeout(100).assertDoesNotExist() })\n    }\n\n    @Test\n    fun isSuccess_true() {\n        Assert.assertTrue(page.editableText.isSuccess { assertExists() })\n    }\n\n\n    @Test\n    fun withName_inOperationProps_ultronInteraction() {\n        val name = \"ElementName\"\n        page.notExistedElement.withTimeout(100).withName(name).withResultHandler { result ->\n            Assert.assertEquals(name, result.operation.elementInfo.name)\n        }.assertIsDisplayed()\n    }\n\n    @Test\n    fun withName_inOperationProps_matcherExt() {\n        val name = \"ElementName\"\n        page.notExistedElement.withName(name).withTimeout(100).withResultHandler { result ->\n            Assert.assertEquals(name, result.operation.elementInfo.name)\n        }.assertIsDisplayed()\n    }\n\n    @Test\n    fun withName_inExceptionMessage() {\n        val name = \"ElementNameToBeInException\"\n        runCatching {\n            page.notExistedElement.withTimeout(100).withName(name).assertIsDisplayed()\n        }.onFailure { exception ->\n            Assert.assertTrue(exception.message!!.contains(name))\n        }\n    }\n\n    @Test\n    fun withMeta() {\n        val meta = DummyMetaObject(\"ElementMetaInfo\")\n        page.notExistedElement.withTimeout(100).withMetaInfo(meta).withResultHandler { result ->\n            Assert.assertEquals(meta, result.operation.elementInfo.meta)\n        }.assertIsDisplayed()\n    }\n}"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/ComposeCustomAssertionTest.kt",
    "content": "package com.atiurin.sampleapp.tests.compose\n\nimport com.atiurin.sampleapp.activity.ComposeElementsActivity\nimport com.atiurin.sampleapp.framework.utils.AssertUtils\nimport com.atiurin.sampleapp.pages.ComposeElementsPage\nimport com.atiurin.sampleapp.tests.BaseTest\nimport com.atiurin.ultron.core.compose.createSimpleUltronComposeRule\nimport com.atiurin.ultron.extensions.withAssertion\nimport com.atiurin.ultron.extensions.withTimeout\nimport com.atiurin.ultron.core.compose.nodeinteraction.click\nimport org.junit.Rule\nimport org.junit.Test\n\nclass ComposeCustomAssertionTest : BaseTest() {\n    val page = ComposeElementsPage\n\n    @get:Rule\n    val composeRule = createSimpleUltronComposeRule<ComposeElementsActivity>()\n\n    @Test\n    fun validAssertion(){\n        page.likesCounter.withAssertion {\n            page.likesCounter.withTimeout(100).assertTextEquals(\"Like count = 3\")\n        }.click()\n    }\n\n    @Test\n    fun invalidAssertion(){\n        AssertUtils.assertException {\n            page.likesCounter.withTimeout(1000).withAssertion {\n                page.likesCounter.withTimeout(500).assertTextEquals(\"some invalid text\")\n            }.click()\n        }\n    }\n}"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/ComposeEmptyListTest.kt",
    "content": "package com.atiurin.sampleapp.tests.compose\n\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.semantics.semantics\nimport androidx.compose.ui.semantics.testTag\nimport androidx.compose.ui.test.hasTestTag\nimport com.atiurin.sampleapp.framework.utils.AssertUtils\nimport com.atiurin.sampleapp.tests.BaseTest\nimport com.atiurin.ultron.core.compose.createDefaultUltronComposeRule\nimport com.atiurin.ultron.core.compose.list.composeList\nimport org.junit.Rule\nimport org.junit.Test\n\nclass ComposeEmptyListTest : BaseTest() {\n    @get:Rule\n    val composeRule = createDefaultUltronComposeRule()\n\n    private val emptyListTestTag = \"emptyList\"\n\n    @Test\n    fun assertNotEmpty_emptyList() {\n        setEmptyListContent()\n        AssertUtils.assertException {\n            composeList(hasTestTag(emptyListTestTag)).withTimeout(100).assertNotEmpty()\n        }\n    }\n\n    @Test\n    fun assertEmpty_emptyList() {\n        setEmptyListContent()\n        composeList(hasTestTag(emptyListTestTag)).assertEmpty()\n    }\n\n    private fun setEmptyListContent() {\n        composeRule.setContent {\n            LazyColumn(\n                modifier = Modifier.semantics { testTag = emptyListTestTag }\n            ) {}\n        }\n    }\n}"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/ComposeListTest.kt",
    "content": "package com.atiurin.sampleapp.tests.compose\n\nimport androidx.compose.ui.test.hasAnyDescendant\nimport androidx.compose.ui.test.hasTestTag\nimport androidx.compose.ui.test.hasText\nimport androidx.compose.ui.test.performScrollToIndex\nimport com.atiurin.sampleapp.activity.ComposeListActivity\nimport com.atiurin.sampleapp.compose.contactStatusTestTag\nimport com.atiurin.sampleapp.compose.contactsListContentDesc\nimport com.atiurin.sampleapp.compose.contactsListHeaderTag\nimport com.atiurin.sampleapp.compose.contactsListTestTag\nimport com.atiurin.sampleapp.compose.getContactItemTestTagById\nimport com.atiurin.sampleapp.data.repositories.CONTACTS\nimport com.atiurin.sampleapp.data.repositories.ContactRepositoty\nimport com.atiurin.sampleapp.framework.utils.AssertUtils\nimport com.atiurin.sampleapp.pages.ComposeListPage\nimport com.atiurin.sampleapp.pages.ComposeSecondPage\nimport com.atiurin.sampleapp.tests.BaseTest\nimport com.atiurin.ultron.core.common.options.ContentDescriptionContainsOption\nimport com.atiurin.ultron.core.common.options.TextContainsOption\nimport com.atiurin.ultron.core.compose.createSimpleUltronComposeRule\nimport com.atiurin.ultron.core.compose.list.composeList\nimport com.atiurin.ultron.extensions.assertIsDisplayed\nimport com.atiurin.ultron.extensions.assertTextEquals\nimport com.atiurin.ultron.extensions.click\nimport com.atiurin.ultron.extensions.findNodeInTree\nimport com.atiurin.ultron.extensions.withDescription\nimport org.junit.Assert\nimport org.junit.Rule\nimport org.junit.Test\n\nclass ComposeListTest : BaseTest() {\n    @get:Rule\n    val composeRule = createSimpleUltronComposeRule<ComposeListActivity>()\n\n    private val listWithMergedTree = composeList(hasTestTag(contactsListTestTag), false).withDescription(\"Contacts list\")\n    private val listPage = ComposeListPage\n    private val notExistedList = composeList(hasTestTag(\"askjhsalk jdhas dlqk \"))\n    private val emptyListTestTag = \"emptyList\"\n\n    @Test\n    fun item_existItem() {\n        val index = 20\n        val contact = CONTACTS[index]\n        listWithMergedTree.assertIsDisplayed()\n        listWithMergedTree.item(hasAnyDescendant(hasText(contact.name)).withDescription(\"Contact '${contact.name}'\"))\n            .assertIsDisplayed()\n            .assertMatches(hasAnyDescendant(hasText(contact.name)).withDescription(\"Contact '${contact.name}'\"))\n    }\n\n    @Test\n    fun item_notExistItem() {\n        AssertUtils.assertException {\n            listWithMergedTree.item(hasText(\"123gakshgdasl kgas\")).assertIsDisplayed()\n        }\n    }\n\n    @Test\n    fun visibleItem_indexInScope() {\n        val index = 2\n        val contact = CONTACTS[index]\n        listWithMergedTree.visibleItem(index).printToLog(\"ULTRON\")\n            .assertMatches(hasAnyDescendant(hasText(contact.name)))\n    }\n\n    @Test\n    fun visibleItem_indexOutOfScope() {\n        AssertUtils.assertException {\n            listWithMergedTree.visibleItem(20).assertIsDisplayed()\n        }\n    }\n\n    @Test\n    fun firstVisibleItem() {\n        val contact = CONTACTS[0]\n        listWithMergedTree.firstVisibleItem()\n            .assertIsDisplayed()\n            .assertMatches(hasAnyDescendant(hasText(contact.name)))\n            .assertMatches(hasAnyDescendant(hasText(contact.status)))\n    }\n\n    @Test\n    fun getVisibleItemByIndex() {\n        val index = 3\n        val contact = CONTACTS[index]\n        listPage.getItemByIndex(index).apply {\n            name.assertTextEquals(contact.name)\n            status.assertTextEquals(contact.status)\n        }\n    }\n\n    @Test\n    fun getFirstVisibleItem() {\n        val contact = CONTACTS[0]\n        listPage.getFirstVisibleItem().apply {\n            name.assertTextEquals(contact.name)\n            status.assertTextEquals(contact.status)\n        }\n    }\n\n    @Test\n    fun scrollToIndex() {\n        val index = 20\n        val contact = CONTACTS[index]\n        listWithMergedTree.scrollToIndex(index)\n        hasText(contact.name).assertIsDisplayed()\n    }\n\n    @Test\n    fun scrollToKey() {\n        val index = 20\n        val contact = CONTACTS[index]\n        listWithMergedTree.scrollToKey(contact.name)\n        hasText(contact.name).assertIsDisplayed()\n    }\n\n    @Test\n    fun customPerformOnLazyList() {\n        val index = 20\n        val contact = CONTACTS[index]\n        val children = listWithMergedTree.performOnList { node, interactoion ->\n            interactoion.performScrollToIndex(index)\n            node.children\n        }\n        hasText(contact.name).assertIsDisplayed()\n        Assert.assertTrue(children.size > 10)\n        val child = children.findNodeInTree(hasText(contact.name))\n        Assert.assertNotNull(child)\n    }\n\n    @Test\n    fun moveToAnotherComposeActivityPageTest() {\n        val contact = CONTACTS.first()\n        hasText(contact.name).click()\n        ComposeSecondPage.name.assertTextEquals(contact.name)\n        ComposeSecondPage.status.assertTextEquals(contact.status)\n    }\n\n    @Test\n    fun getItem_ByTestTag_assertNameAndStatusOfContact() {\n        val index = 20\n        val contact = CONTACTS[index]\n        listPage.getContactItemByTestTag(contact).apply {\n            name.assertTextEquals(contact.name)\n            status.assertTextContains(contact.status)\n        }\n    }\n\n    @Test\n    fun getItem_ByMatcher_assertNameAndStatusOfContact() {\n        val index = 20\n        val contact = CONTACTS[index]\n        listPage.getContactItemByName(contact).apply {\n            name.assertTextEquals(contact.name)\n            status.assertTextContains(contact.status)\n        }\n    }\n\n    @Test\n    fun listHeader_asChild_TextContains() {\n        listPage.lazyList.visibleChild(hasTestTag(contactsListHeaderTag))\n            .assertTextContains(\"header\", option = TextContainsOption(substring = true))\n    }\n\n    @Test\n    fun listHeader_asChild_TextEquals() {\n        listPage.lazyList.visibleChild(hasTestTag(contactsListHeaderTag).withDescription(\"header\"))\n            .assertTextEquals(\"Lazy column header\")\n    }\n\n    @Test\n    fun listHeader_asChild_TextEquals_mergedTree() {\n        listWithMergedTree.visibleChild(hasTestTag(contactsListHeaderTag).withDescription(\"header\"))\n            .assertTextEquals(\"Lazy column header\")\n    }\n\n    @Test\n    fun visibleItemChild() {\n        val index = 3\n        val contact = CONTACTS[index]\n        listPage.lazyList.onVisibleItemChild(index, hasTestTag(contactStatusTestTag)).assertTextEquals(contact.status)\n    }\n\n    @Test\n    fun assertIsDisplayed_visibleList() {\n        listWithMergedTree.assertIsDisplayed()\n    }\n\n    @Test\n    fun assertIsDisplayed_invisibleList() {\n        AssertUtils.assertException {\n            notExistedList.withTimeout(1000).assertIsDisplayed()\n        }\n    }\n\n    @Test\n    fun assertIsNotDisplayed_visibleList() {\n        AssertUtils.assertException { listWithMergedTree.withTimeout(1000).assertIsNotDisplayed() }\n    }\n\n    @Test\n    fun assertExists_existedList() {\n        listWithMergedTree.assertExists()\n    }\n\n    @Test\n    fun assertExists_notExistedList() {\n        AssertUtils.assertException { notExistedList.withTimeout(1000).assertExists() }\n    }\n\n    @Test\n    fun assertDoesNotExist_notExistedList() {\n        notExistedList.assertDoesNotExist()\n    }\n\n    @Test\n    fun assertDoesNotExist_existedList() {\n        AssertUtils.assertException { listWithMergedTree.withTimeout(1000).assertDoesNotExist() }\n    }\n\n    @Test\n    fun assertContentDescriptionEquals_properContentDescription() {\n        listWithMergedTree.assertContentDescriptionEquals(contactsListContentDesc)\n    }\n\n    @Test\n    fun assertContentDescriptionEquals_invalidContentDescription() {\n        AssertUtils.assertException { listWithMergedTree.withTimeout(1000).assertContentDescriptionEquals(\"some invalid desc\") }\n    }\n\n    @Test\n    fun assertContentDescriptionContains_properContentDescription() {\n        listWithMergedTree.assertContentDescriptionContains(contactsListContentDesc.substring(0, 5), ContentDescriptionContainsOption(substring = true))\n    }\n\n    @Test\n    fun assertContentDescriptionContains_invalidContentDescription() {\n        AssertUtils.assertException { listWithMergedTree.withTimeout(1000).assertContentDescriptionContains(\"some invalid\") }\n    }\n\n    @Test\n    fun assertVisibleItemsCount_properCountProvided() {\n        val count = listWithMergedTree.getVisibleItemsCount()\n        listWithMergedTree.assertVisibleItemsCount(count)\n    }\n\n    @Test\n    fun assertVisibleItemsCount_invalidCountProvided() {\n        AssertUtils.assertException { listWithMergedTree.withTimeout(1000).assertVisibleItemsCount(100) }\n    }\n\n    @Test\n    fun itemByPosition_propertyConfiguredTest() {\n        val index = 20\n        val contact = CONTACTS[index]\n        val item = listPage.lazyList.item(20).assertIsDisplayed()\n        item.assertMatches(hasTestTag(getContactItemTestTagById(contact)))\n    }\n\n    @Test\n    fun getItemByPosition_propertyConfiguredTest() {\n        val index = 20\n        val contact = CONTACTS[index]\n        listPage.getItemByPosition(index).apply {\n            name.assertTextEquals(contact.name)\n            status.assertTextEquals(contact.status)\n            assertIsDisplayed()\n        }\n    }\n\n    @Test\n    fun assertItemDoesNotExistWithSearch_NotExistedItem() {\n        listWithMergedTree.assertItemDoesNotExist(hasText(\"NOT EXISTED TeXT\"))\n    }\n\n    @Test\n    fun assertItemDoesNotExistWithSearch_ExistedItem() {\n        val contact = ContactRepositoty.getLast()\n        AssertUtils.assertException {\n            listWithMergedTree.withTimeout(2000).assertItemDoesNotExist(hasText(contact.name))\n        }\n    }\n\n    @Test\n    fun getItem_NotExistedItemChild() {\n        val index = 20\n        val contact = CONTACTS[index]\n        listPage.getContactItemByName(contact).apply {\n            AssertUtils.assertException { notExisted.withTimeout(1000).assertIsDisplayed() }\n        }\n    }\n\n    @Test\n    fun assertNotEmpty_notEmptyList() {\n        listWithMergedTree.assertNotEmpty()\n    }\n\n    @Test\n    fun assertEmpty_notEmptyList() {\n        AssertUtils.assertException { listWithMergedTree.withTimeout(100).assertEmpty() }\n    }\n\n}\n\n\n"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/ComposeListWithPositionTestTagTest.kt",
    "content": "package com.atiurin.sampleapp.tests.compose\n\nimport androidx.compose.ui.test.hasAnyDescendant\nimport androidx.compose.ui.test.hasContentDescription\nimport androidx.compose.ui.test.hasTestTag\nimport androidx.compose.ui.test.hasText\nimport com.atiurin.sampleapp.activity.ComposeListWithPositionTestTagActivity\nimport com.atiurin.sampleapp.compose.ListItemPositionPropertyKey\nimport com.atiurin.sampleapp.compose.contactsListContentDesc\nimport com.atiurin.sampleapp.compose.getContactItemTestTagByPosition\nimport com.atiurin.sampleapp.data.repositories.CONTACTS\nimport com.atiurin.sampleapp.framework.utils.AssertUtils\nimport com.atiurin.sampleapp.pages.ComposeListPage\nimport com.atiurin.ultron.core.compose.createSimpleUltronComposeRule\nimport com.atiurin.ultron.core.compose.list.composeList\nimport org.junit.Rule\nimport org.junit.Test\n\nclass ComposeListWithPositionTestTagTest {\n    @get:Rule\n    val composeRule = createSimpleUltronComposeRule<ComposeListWithPositionTestTagActivity>()\n    val list = composeList(hasContentDescription(contactsListContentDesc), false)\n    val composeListWithProperty = composeList(hasContentDescription(contactsListContentDesc), false, ListItemPositionPropertyKey)\n\n    @Test\n    fun itemOutOfVisibleScope() {\n        val index = 20\n        val contact = CONTACTS[index]\n        list.item(hasTestTag(getContactItemTestTagByPosition(index)))\n            .assertIsDisplayed()\n            .assertMatches(hasAnyDescendant(hasText(contact.name)))\n    }\n\n    @Test\n    fun lastVisibleItem() {\n        val count = list.getVisibleItemsCount() - 1\n        val contact = CONTACTS[count]\n        list.lastVisibleItem()\n            .assertIsDisplayed()\n            .assertMatches(hasAnyDescendant(hasText(contact.name)))\n            .assertMatches(hasAnyDescendant(hasText(contact.status)))\n    }\n\n    @Test\n    fun itemByPosition_propertyNOTConfiguredInTest() {\n        AssertUtils.assertException {\n            list.item(20).assertIsDisplayed()\n        }\n    }\n\n    @Test\n    fun itemByPosition_propertyNOTConfiguredInApplication() {\n        AssertUtils.assertException {\n            composeListWithProperty.withTimeout(1000).item(20).assertIsDisplayed()\n        }\n    }\n\n    @Test\n    fun getItemByPosition_propertyNOTConfiguredInTest() {\n        AssertUtils.assertException { list.getItem<ComposeListPage.ComposeFriendListItem>(20).assertIsDisplayed() }\n    }\n\n    @Test\n    fun getItemByPosition_propertyNOTConfiguredInApplication() {\n        AssertUtils.assertException {\n            composeListWithProperty.withTimeout(1000).getItem<ComposeListPage.ComposeFriendListItem>(20).assertIsDisplayed()\n        }\n    }\n}"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/ComposeUIElementsTest.kt",
    "content": "package com.atiurin.sampleapp.tests.compose\n\nimport androidx.compose.ui.semantics.ProgressBarRangeInfo\nimport androidx.compose.ui.semantics.SemanticsActions\nimport androidx.compose.ui.semantics.SemanticsProperties\nimport androidx.compose.ui.test.ExperimentalTestApi\nimport androidx.compose.ui.test.assertTextContains\nimport androidx.compose.ui.test.hasContentDescription\nimport androidx.compose.ui.test.hasTestTag\nimport androidx.compose.ui.test.hasText\nimport androidx.compose.ui.test.performSemanticsAction\nimport androidx.compose.ui.text.TextRange\nimport androidx.compose.ui.unit.dp\nimport com.atiurin.sampleapp.activity.ActionsStatus\nimport com.atiurin.sampleapp.activity.ComposeElementsActivity\nimport com.atiurin.sampleapp.activity.ComposeElementsActivity.Constants.likesCounterButton\nimport com.atiurin.sampleapp.activity.ComposeElementsActivity.Constants.likesCounterContentDesc\nimport com.atiurin.sampleapp.activity.ComposeElementsActivity.Constants.likesCounterTextContainerContentDesc\nimport com.atiurin.sampleapp.activity.ComposeElementsActivity.Constants.simpleCheckbox\nimport com.atiurin.sampleapp.activity.ComposeElementsActivity.Constants.statusText\nimport com.atiurin.sampleapp.compose.RegionName\nimport com.atiurin.sampleapp.framework.ultronext.ProgressBar\nimport com.atiurin.sampleapp.framework.ultronext.getProgress\nimport com.atiurin.sampleapp.framework.utils.AssertUtils\nimport com.atiurin.sampleapp.pages.ComposeElementsPage\nimport com.atiurin.sampleapp.tests.BaseTest\nimport com.atiurin.ultron.core.common.assertion.softAssertion\nimport com.atiurin.ultron.core.common.assertion.verifySoftAssertions\nimport com.atiurin.ultron.core.common.options.ClickOption\nimport com.atiurin.ultron.core.common.options.ContentDescriptionContainsOption\nimport com.atiurin.ultron.core.common.options.PerformCustomBlockOption\nimport com.atiurin.ultron.core.common.options.TextContainsOption\nimport com.atiurin.ultron.core.common.options.TextEqualsOption\nimport com.atiurin.ultron.core.compose.createSimpleUltronComposeRule\nimport com.atiurin.ultron.core.compose.nodeinteraction.UltronComposeSemanticsNodeInteraction\nimport com.atiurin.ultron.core.compose.nodeinteraction.click\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType\nimport com.atiurin.ultron.core.compose.operation.UltronComposeCollectionInteraction.Companion.allNodes\nimport com.atiurin.ultron.core.compose.operation.UltronComposeOperationParams\nimport com.atiurin.ultron.core.compose.operation.assertSize\nimport com.atiurin.ultron.core.compose.option.ComposeSwipeOption\nimport com.atiurin.ultron.core.config.UltronCommonConfig\nimport com.atiurin.ultron.extensions.assertContentDescriptionContains\nimport com.atiurin.ultron.extensions.assertContentDescriptionEquals\nimport com.atiurin.ultron.extensions.assertDoesNotExist\nimport com.atiurin.ultron.extensions.assertExists\nimport com.atiurin.ultron.extensions.assertHasClickAction\nimport com.atiurin.ultron.extensions.assertHasNoClickAction\nimport com.atiurin.ultron.extensions.assertHeightIsAtLeast\nimport com.atiurin.ultron.extensions.assertHeightIsEqualTo\nimport com.atiurin.ultron.extensions.assertIsDisplayed\nimport com.atiurin.ultron.extensions.assertIsEnabled\nimport com.atiurin.ultron.extensions.assertIsNotEnabled\nimport com.atiurin.ultron.extensions.assertIsNotFocused\nimport com.atiurin.ultron.extensions.assertIsNotSelected\nimport com.atiurin.ultron.extensions.assertIsOff\nimport com.atiurin.ultron.extensions.assertIsSelectable\nimport com.atiurin.ultron.extensions.assertIsToggleable\nimport com.atiurin.ultron.extensions.assertTextContains\nimport com.atiurin.ultron.extensions.assertTextEquals\nimport com.atiurin.ultron.extensions.assertValueEquals\nimport com.atiurin.ultron.extensions.assertWidthIsAtLeast\nimport com.atiurin.ultron.extensions.assertWidthIsEqualTo\nimport com.atiurin.ultron.extensions.captureToImage\nimport com.atiurin.ultron.extensions.clearText\nimport com.atiurin.ultron.extensions.click\nimport com.atiurin.ultron.extensions.clickBottomCenter\nimport com.atiurin.ultron.extensions.clickBottomLeft\nimport com.atiurin.ultron.extensions.clickBottomRight\nimport com.atiurin.ultron.extensions.clickCenterLeft\nimport com.atiurin.ultron.extensions.clickCenterRight\nimport com.atiurin.ultron.extensions.clickTopCenter\nimport com.atiurin.ultron.extensions.clickTopLeft\nimport com.atiurin.ultron.extensions.clickTopRight\nimport com.atiurin.ultron.extensions.copyText\nimport com.atiurin.ultron.extensions.doubleClick\nimport com.atiurin.ultron.extensions.execute\nimport com.atiurin.ultron.extensions.getNode\nimport com.atiurin.ultron.extensions.getNodeConfigProperty\nimport com.atiurin.ultron.extensions.getText\nimport com.atiurin.ultron.extensions.inputText\nimport com.atiurin.ultron.extensions.longClick\nimport com.atiurin.ultron.extensions.pasteText\nimport com.atiurin.ultron.extensions.perform\nimport com.atiurin.ultron.extensions.performMouseInput\nimport com.atiurin.ultron.extensions.replaceText\nimport com.atiurin.ultron.extensions.selectText\nimport com.atiurin.ultron.extensions.swipe\nimport com.atiurin.ultron.extensions.swipeDown\nimport com.atiurin.ultron.extensions.swipeLeft\nimport com.atiurin.ultron.extensions.swipeRight\nimport com.atiurin.ultron.extensions.swipeUp\nimport com.atiurin.ultron.extensions.typeText\nimport com.atiurin.ultron.extensions.withTimeout\nimport org.junit.Assert\nimport org.junit.Ignore\nimport org.junit.Rule\nimport org.junit.Test\n\n@OptIn(ExperimentalTestApi::class)\nclass ComposeUIElementsTest : BaseTest() {\n    val page = ComposeElementsPage\n\n    @get:Rule\n    val composeRule = createSimpleUltronComposeRule<ComposeElementsActivity>()\n    val initialText = \"Like count = 0\"\n    val expectedText = \"Like count = 1\"\n\n    @Test\n    fun simpleClick() {\n        hasText(initialText).assertIsDisplayed().click()\n        hasText(expectedText).assertIsDisplayed()\n    }\n\n    @Test\n    fun contentDescTest() {\n        hasContentDescription(likesCounterContentDesc).click()\n        hasText(expectedText).assertIsDisplayed()\n    }\n\n    @Test\n    fun testTagTest() {\n        hasText(initialText).click()\n        page.likesCounter.assertTextEquals(expectedText).assertIsDisplayed()\n    }\n\n    @Test\n    fun getTextTest() {\n        hasTestTag(likesCounterButton).click()\n        val text = hasTestTag(likesCounterButton).getText()\n        Assert.assertEquals(expectedText, text)\n    }\n\n    @Test\n    fun clickCheckBox() {\n        hasTestTag(simpleCheckbox).assertIsOff().click().assertIsOn()\n    }\n\n    @Test\n    fun longClick_longClickable() {\n        page.longAndDoubleClickButton.longClick()\n        page.status.assertTextEquals(ActionsStatus.LongClicked.name)\n    }\n\n    @Test\n    fun doubleClick_doubleClickable() {\n        page.longAndDoubleClickButton.doubleClick()\n        page.status.assertTextEquals(ActionsStatus.DoubleClicked.name)\n    }\n\n    @Test\n    fun regionsClickTopLeft() {\n        page.regionsNode.clickTopLeft(ClickOption(xOffset = 20, yOffset = 20))\n        page.status.assertTextEquals(RegionName.TopLeft.name)\n    }\n\n    @Test\n    fun regionsClickTopCenter() {\n        page.regionsNode.clickTopCenter(ClickOption(yOffset = 20))\n        page.status.assertTextEquals(RegionName.TopCenter.name)\n    }\n\n    @Test\n    fun regionsClickTopRight() {\n        page.regionsNode.clickTopRight(ClickOption(yOffset = 20))\n        page.status.assertTextEquals(RegionName.TopRight.name)\n    }\n\n    @Test\n    fun regionsClickCenterLeft() {\n        page.regionsNode.clickCenterLeft()\n        page.status.assertTextEquals(RegionName.CenterLeft.name)\n    }\n\n    @Test\n    fun regionsClickCenterRight() {\n        page.regionsNode.clickCenterRight()\n        page.status.assertTextEquals(RegionName.CenterRight.name)\n    }\n\n    @Test\n    fun regionsClickBottomLeft() {\n        page.regionsNode.clickBottomLeft(ClickOption(xOffset = 16))\n        page.status.assertTextEquals(RegionName.BottomLeft.name)\n    }\n\n    @Test\n    fun regionsClickBottomCenter() {\n        page.regionsNode.clickBottomCenter()\n        page.status.assertTextEquals(RegionName.BottomCenter.name)\n    }\n\n    @Test\n    fun regionsClickBottomRight() {\n        page.regionsNode.clickBottomRight()\n        page.status.assertTextEquals(RegionName.BottomRight.name)\n    }\n\n    //flaky on emulator\n    @Test\n    @Ignore\n    fun copyText() {\n        val startText = \"begin\"\n        page.editableText.apply {\n            replaceText(startText)\n            selectText(TextRange(0, 2))\n            assertIsDisplayed()\n            copyText()\n            assertIsDisplayed()\n            clearText()\n            click()\n            pasteText()\n            assertTextContains(startText)\n        }\n    }\n\n    @Test\n    fun swipeDown() {\n        page.swipeableNode.swipeDown()\n        page.status.assertTextEquals(ActionsStatus.SwipeDown.name)\n    }\n\n    @Test\n    fun swipeUp() {\n        page.swipeableNode.swipeUp()\n        page.status.assertTextEquals(ActionsStatus.SwipeUp.name)\n    }\n\n    @Test\n    fun swipeRight() {\n        page.swipeableNode.swipeRight()\n        page.status.assertTextEquals(ActionsStatus.SwipeRight.name)\n    }\n\n    @Test\n    fun swipeLeft() {\n        page.swipeableNode.swipeLeft()\n        page.status.assertTextEquals(ActionsStatus.SwipeLeft.name)\n    }\n\n    @Test\n    fun swipe_option() {\n        page.swipeableNode.swipeLeft(ComposeSwipeOption(durationMs = 1000L))\n        page.status.assertTextEquals(ActionsStatus.SwipeLeft.name)\n    }\n\n    @Test\n    fun swipe_general() {\n        page.swipeableNode.swipe(ComposeSwipeOption(\n            startXOffset = 0.1f,\n            startYOffset = 0.1f,\n            endXOffset = 0.9f,\n            endYOffset = 0.1f,\n            durationMs = 1000L\n        ))\n        page.status.assertTextEquals(ActionsStatus.SwipeRight.name)\n    }\n\n    @Test\n    fun inputText() {\n        val text = \"some text\"\n        page.editableText.inputText(text).assertTextContains(text)\n    }\n\n    @Test\n    fun typeText() {\n        val text = \"some text\"\n        page.editableText.typeText(text).assertTextContains(text)\n    }\n\n    @Test\n    fun setSelection() {\n        page.editableText.replaceText(\"qwerty\").setSelection(0, 3, true).assertIsDisplayed().cutText().assertTextContains(\"rty\")\n    }\n\n    @Test\n    fun captureToImage() {\n        val image = page.longAndDoubleClickButton.captureToImage()\n        Assert.assertNotNull(image)\n    }\n\n    @Test\n    fun setProgress() {\n        page.progressBar.setProgress(0.7f).assertIsDisplayed()\n        page.status.assertTextEquals(\"set progress 0.7\")\n    }\n\n    @Test\n    fun performCustomSemanticsAction() {\n        val progress = 0.7f\n        val progressBar = ProgressBar(hasTestTag(ComposeElementsActivity.progressBar))\n        progressBar.setProgress(progress)\n        val current = progressBar.getProgress()\n        Assert.assertEquals(current, progress )\n\n    }\n\n\n    @Test\n    fun performCustomSemanticsAssertion() {\n        val progress = 0.7f\n        val progressBar = ProgressBar(hasTestTag(ComposeElementsActivity.progressBar))\n        progressBar.setProgress(progress)\n        progressBar.assertProgress(progress)\n    }\n\n    @Test\n    fun performExtendedAssertion() {\n        val progress = 0.7f\n        page.progressBar.apply {\n            setProgress(progress)\n            assertProgress(progress)\n        }\n    }\n\n    @Test\n    fun performWithLambda() {\n        val progress = 0.7f\n        val result = page.progressBar.perform {\n            it.performSemanticsAction(SemanticsActions.SetProgress) {\n                it.invoke(progress)\n            }\n        }\n        Assert.assertTrue(result is UltronComposeSemanticsNodeInteraction)\n        page.progressBar.assertProgress(progress)\n    }\n\n    @Test\n    fun semanticsMatcher_performDeprecated() {\n        val text = page.status.perform<String>({\n            it.fetchSemanticsNode().config[SemanticsProperties.Text].first().text\n        }, option = PerformCustomBlockOption(ComposeOperationType.CUSTOM, \"\"))\n        Assert.assertTrue(text.isNotBlank())\n    }\n\n    @Test\n    fun ultronComposeSemanticsNodeInteraction_performDeprecated() {\n        val text = page.status.assertExists().perform(option = PerformCustomBlockOption(ComposeOperationType.CUSTOM, \"\")) {\n            it.fetchSemanticsNode().config[SemanticsProperties.Text].first().text\n        }\n        Assert.assertTrue(text.isNotBlank())\n    }\n\n    @Test\n    fun ultronComposeSemanticsNodeInteraction_execute() {\n        val text = page.status.assertExists().execute {\n            it.fetchSemanticsNode().config[SemanticsProperties.Text].first().text\n        }\n        Assert.assertTrue(text.isNotBlank())\n    }\n\n    @Test\n    fun semanticsMatcher_execute() {\n        val text = page.status.execute {\n            it.fetchSemanticsNode().config[SemanticsProperties.Text].first().text\n        }\n        Assert.assertTrue(text.isNotBlank())\n    }\n\n    @Test\n    fun performMouseInput() {\n        page.swipeableNode.performMouseInput { swipeUp() }\n        page.status.assertTextEquals(ActionsStatus.SwipeUp.name)\n    }\n\n    @Test\n    fun getNode_exits() {\n        val node = page.status.getNode()\n        Assert.assertEquals(ComposeElementsActivity.Constants.statusText, node.config[SemanticsProperties.TestTag])\n    }\n\n    @Test\n    fun getNodeConfigProperty_exist() {\n        val testTag = page.status.getNodeConfigProperty(SemanticsProperties.TestTag)\n        Assert.assertEquals(ComposeElementsActivity.Constants.statusText, testTag)\n    }\n\n    @Test\n    fun assertIsDisplayed() {\n        page.status.assertIsDisplayed()\n    }\n\n    @Test\n    fun assertExists() {\n        page.status.assertExists()\n    }\n\n    @Test\n    fun assertExists_notExisted() {\n        AssertUtils.assertException { hasText(\"some not existed node\").withTimeout(100).assertExists() }\n    }\n\n    @Test\n    fun assertDoesNotExist() {\n        hasText(\"some not existed node\").assertDoesNotExist()\n    }\n\n    @Test\n    fun assertDoesNotExist_existed() {\n        AssertUtils.assertException { page.editableText.withTimeout(100).assertDoesNotExist() }\n    }\n\n    @Test\n    fun assertIsEnabled() {\n        page.editableText.assertIsEnabled()\n    }\n\n    @Test\n    fun assertIsEnabled_disabledButton() {\n        AssertUtils.assertException { page.disabledButton.withTimeout(100).assertIsEnabled() }\n    }\n\n    @Test\n    fun assertIsNotEnabled() {\n        page.disabledButton.assertIsNotEnabled()\n    }\n\n    @Test\n    fun assertIsNotEnabled_enabledButton() {\n        AssertUtils.assertException { page.longAndDoubleClickButton.withTimeout(100).assertIsNotEnabled() }\n    }\n\n    @Test\n    fun assertIsFocused() {\n        page.editableText.click().assertIsFocused()\n    }\n\n    @Test\n    fun assertIsFocused_notFocused() {\n        AssertUtils.assertException { page.editableText.withTimeout(100).assertIsFocused() }\n    }\n\n    @Test\n    fun assertIsNotFocused() {\n        page.editableText.assertIsNotFocused()\n    }\n\n    @Test\n    fun assertIsNotFocused_focused() {\n        AssertUtils.assertException { page.editableText.click().withTimeout(100).assertIsNotFocused() }\n    }\n\n\n    @Test\n    fun assertIsSelected() {\n        page.maleRadioButton.click().assertIsSelected()\n    }\n\n    @Test\n    fun assertIsSelected_notSelected() {\n        page.maleRadioButton.click()\n        AssertUtils.assertException { page.femaleRadioButton.withTimeout(100).assertIsSelected() }\n    }\n\n    @Test\n    fun assertIsNotSelected_notSelected() {\n        page.maleRadioButton.click()\n        page.femaleRadioButton.assertIsNotSelected()\n    }\n\n    @Test\n    fun assertIsNotSelected_selected() {\n        AssertUtils.assertException { page.maleRadioButton.click().withTimeout(100).assertIsNotSelected() }\n    }\n\n    @Test\n    fun assertIsSelectable() {\n        page.femaleRadioButton.assertIsSelectable()\n    }\n\n    @Test\n    fun assertIsSelectable_notSelectable() {\n        AssertUtils.assertException { page.status.withTimeout(100).assertIsSelectable() }\n    }\n\n    @Test\n    fun assertIsToggleable() {\n        page.simpleCheckbox.assertIsToggleable()\n    }\n\n    @Test\n    fun assertIsToggleable_notToggleable() {\n        AssertUtils.assertException { page.editableText.withTimeout(100).assertIsToggleable() }\n    }\n\n    @Test\n    fun assertIsOn() {\n        page.simpleCheckbox.click().assertIsOn()\n    }\n\n    @Test\n    fun assertIsOn_checkboxIsOff() {\n        AssertUtils.assertException { page.simpleCheckbox.withTimeout(100).assertIsOn() }\n    }\n\n    @Test\n    fun assertIsOff() {\n        page.simpleCheckbox.assertIsOff()\n    }\n\n    @Test\n    fun assertIsOff_checkboxIsOn() {\n        AssertUtils.assertException { page.simpleCheckbox.click().withTimeout(100).assertIsOff() }\n    }\n\n    @Test\n    fun assertHasClickAction() {\n        page.longAndDoubleClickButton.assertHasClickAction()\n    }\n\n    @Test\n    fun assertHasClickAction_noClickAction() {\n        AssertUtils.assertException { page.status.withTimeout(100).assertHasClickAction() }\n    }\n\n    @Test\n    fun assertHasNoClickAction() {\n        page.status.assertHasNoClickAction()\n    }\n\n    @Test\n    fun assertHasNoClickAction_hasClickAction() {\n        AssertUtils.assertException { page.longAndDoubleClickButton.withTimeout(100).assertHasNoClickAction() }\n    }\n\n    @Test\n    fun assertTextEquals() {\n        page.editableText.assertTextEquals(\"Label\", \"\")\n    }\n\n    @Test\n    fun assertTextEquals_includeEditableFalse() {\n        page.editableText.assertTextEquals(\"Label\", option = TextEqualsOption(false))\n    }\n\n    @Test\n    fun assertTextEquals_includeEditableFalse_editableProvided() {\n        AssertUtils.assertException {\n            page.editableText.withTimeout(100).assertTextEquals(\"Label\", \"\", option = TextEqualsOption(false))\n        }\n    }\n\n    @Test\n    fun assertTextEquals_wrongTextProvided() {\n        AssertUtils.assertException {\n            page.editableText.withTimeout(100).assertTextEquals(\"some invalid text\", \"\")\n        }\n    }\n\n    @Test\n    fun assertTextEquals_editableNotEmpty_ValidText() {\n        val text = \"editable text\"\n        page.editableText.replaceText(text).assertTextEquals(\"Label\", text)\n    }\n\n    @Test\n    fun assertTextEquals_editableNotEmpty_ValidText_mixedOrder() {\n        val text = \"editable text\"\n        page.editableText.replaceText(text).assertTextEquals(text, \"Label\")\n    }\n\n    @Test\n    fun assertTextEquals_editableNotEmpty_includeEditableFalse() {\n        val text = \"editable text\"\n        AssertUtils.assertException {\n            page.editableText.withTimeout(100).replaceText(text).assertTextEquals(\"Label\", text, option = TextEqualsOption(false))\n        }\n    }\n\n    @Test\n    fun assertTextContains_label() {\n        page.editableText.assertTextContains(\"Label\")\n    }\n\n    @Test\n    fun assertTextContains_editable() {\n        val text = \"some text\"\n        page.editableText.replaceText(text).assertTextContains(text)\n    }\n\n    @Test\n    fun assertTextContains_wrongText() {\n        val text = \"some text\"\n        AssertUtils.assertException { page.editableText.withTimeout(100).assertTextContains(text) }\n    }\n\n    @Test\n    fun assertTextContains_emptyText() {\n        page.editableText.assertTextContains(\"\")\n    }\n\n    @Test\n    fun assertTextContains_substringTrue_validSubstringProvided() {\n        val text = \"some text\"\n        page.editableText.replaceText(text).assertTextContains(text.substring(0, 4), TextContainsOption(substring = true))\n    }\n\n    @Test\n    fun assertTextContains_substringTrue_wrongSubstringProvided() {\n        AssertUtils.assertException {\n            page.editableText.replaceText(\"valid text\").withTimeout(100).assertTextContains(\"wrong text\", TextContainsOption(substring = true))\n        }\n    }\n\n    @Test\n    fun assertTextContains_substringFalse_validSubstringProvided() {\n        val text = \"some text\"\n        AssertUtils.assertException {\n            page.editableText.replaceText(text).withTimeout(100).assertTextContains(text.substring(0, 4), TextContainsOption(substring = false))\n        }\n    }\n\n    @Test\n    fun assertTextContains_ignoreCase_lowercase() {\n        val text = \"SoMe TexT\"\n        page.editableText.replaceText(text).assertTextContains(text.lowercase(), TextContainsOption(ignoreCase = true))\n    }\n\n    @Test\n    fun assertTextContains_ignoreCase_uppercase() {\n        val text = \"SoMe TexT\"\n        page.editableText.replaceText(text).assertTextContains(text.uppercase(), TextContainsOption(ignoreCase = true))\n    }\n\n    @Test\n    fun assertTextContains_ignoreCase_and_substring() {\n        val text = \"SoMe TexT\"\n        page.editableText.replaceText(text).assertTextContains(text.substring(0, 4).lowercase(), TextContainsOption(substring = true, ignoreCase = true))\n    }\n\n    @Test\n    fun assertTextContains_ignoreCaseFalse() {\n        val text = \"SoMe TexT\"\n        AssertUtils.assertException {\n            page.editableText.replaceText(text).withTimeout(100).assertTextContains(text.lowercase(), TextContainsOption(ignoreCase = false))\n        }\n    }\n\n    @Test\n    fun assertContentDescriptionEquals() {\n        page.likesCounter.assertContentDescriptionEquals(likesCounterContentDesc, likesCounterTextContainerContentDesc)\n    }\n\n    @Test\n    fun assertContentDescriptionEquals_notEnoughElements() {\n        AssertUtils.assertException {\n            page.likesCounter.withTimeout(100).assertContentDescriptionEquals(likesCounterContentDesc)\n        }\n    }\n\n    @Test\n    fun assertContentDescriptionContains() {\n        page.likesCounter.assertContentDescriptionContains(likesCounterContentDesc)\n    }\n\n    @Test\n    fun assertContentDescriptionContains_substringTrue_validSubstringProvided() {\n        page.likesCounter.assertContentDescriptionContains(likesCounterContentDesc.substring(1, 5), ContentDescriptionContainsOption(substring = true))\n    }\n\n    @Test\n    fun assertContentDescriptionContains_substringTrue_wrongSubstringProvided() {\n        AssertUtils.assertException {\n            page.likesCounter.withTimeout(100).assertContentDescriptionContains(\"wrong substring\", ContentDescriptionContainsOption(substring = true))\n        }\n    }\n\n    @Test\n    fun assertContentDescriptionContains_ignoreCaseTrue_lowercase() {\n        page.likesCounter.assertContentDescriptionContains(likesCounterContentDesc.lowercase(), ContentDescriptionContainsOption(ignoreCase = true))\n    }\n\n    @Test\n    fun assertContentDescriptionContains_ignoreCaseTrue_uppercase() {\n        page.likesCounter.assertContentDescriptionContains(likesCounterContentDesc.uppercase(), ContentDescriptionContainsOption(ignoreCase = true))\n    }\n\n    @Test\n    fun assertContentDescriptionContains_ignoreCaseFalse() {\n        AssertUtils.assertException {\n            page.likesCounter.withTimeout(100).assertContentDescriptionContains(likesCounterContentDesc.lowercase(), ContentDescriptionContainsOption(ignoreCase = false))\n        }\n    }\n\n    @Test\n    fun assertValueEquals() {\n        page.simpleCheckbox.assertValueEquals(\"default\")\n    }\n\n    @Test\n    fun assertValueEquals_invalidValue() {\n        AssertUtils.assertException { page.simpleCheckbox.withTimeout(100).assertValueEquals(\"invalid\") }\n    }\n\n\n    @Test\n    fun assertRangeInfoEquals() {\n        page.progressBar.setProgress(0.7f).assertRangeInfoEquals(ProgressBarRangeInfo(0.7f, range = 0f..0.7f, 100))\n    }\n\n    @Test\n    fun assertRangeInfoEquals_invalidInfo() {\n        AssertUtils.assertException {\n            page.progressBar.setProgress(0.7f).withTimeout(100).assertRangeInfoEquals(ProgressBarRangeInfo(0.0f, range = 0f..0.0f, 100))\n        }\n    }\n\n    @Test\n    fun assertHeightIsEqualTo() {\n        page.swipeableNode.assertHeightIsEqualTo(100.dp)\n    }\n\n    @Test\n    fun assertHeightIsEqualTo_invalidValue() {\n        AssertUtils.assertException { page.swipeableNode.withTimeout(100).assertHeightIsEqualTo(50.dp) }\n    }\n\n    @Test\n    fun assertWidthIsEqualTo() {\n        page.swipeableNode.assertWidthIsEqualTo(100.dp)\n    }\n\n    @Test\n    fun assertWidthIsEqualTo_invalidValue() {\n        AssertUtils.assertException { page.swipeableNode.withTimeout(100).assertWidthIsEqualTo(50.dp) }\n    }\n\n    @Test\n    fun assertHeightIsAtLeast() {\n        page.swipeableNode.assertHeightIsAtLeast(10.dp)\n    }\n\n    @Test\n    fun assertHeightIsAtLeast_invalidValue() {\n        AssertUtils.assertException { page.swipeableNode.withTimeout(100).assertHeightIsAtLeast(500.dp) }\n    }\n\n    @Test\n    fun assertWidthIsAtLeast() {\n        page.swipeableNode.assertWidthIsAtLeast(10.dp)\n    }\n\n    @Test\n    fun assertWidthIsAtLeast_invalidValue() {\n        AssertUtils.assertException { page.swipeableNode.withTimeout(100).assertWidthIsAtLeast(500.dp) }\n    }\n\n    @Test\n    fun assertMatches() {\n        val text = \"some text\"\n        page.editableText.replaceText(text).assertMatches(hasText(text))\n    }\n\n    @Test\n    fun assertMatches_invalid() {\n        AssertUtils.assertException {\n            page.editableText.replaceText(\"some text\").withTimeout(100)\n                .assertMatches(hasText(\"invalid text\"))\n        }\n    }\n\n    @Test\n    fun customPerformParamsMapping() {\n        val params = UltronComposeOperationParams(\n            operationName = \"operationName\",\n            operationDescription = \"operationDescription\",\n            operationType = ComposeOperationType.ASSERT_MATCHES\n        )\n        page.status.withTimeout(100).withResultHandler {\n            val op = it.operation\n            Assert.assertEquals(params.operationName, op.name)\n            Assert.assertEquals(params.operationDescription, op.description)\n            Assert.assertEquals(params.operationType, op.type)\n        }.perform(params) {\n            it.assertTextContains(\"Some invalid text\")\n        }\n    }\n\n    @Test\n    fun softAssertionTest() {\n        UltronCommonConfig.testContext.softAnalyzer.clear()\n        softAssertion(false) {\n            hasText(\"NotExistText\").withTimeout(100).assertIsDisplayed()\n            hasTestTag(\"NotExistTestTag\").withTimeout(100).assertHasClickAction()\n        }\n        runCatching {\n            verifySoftAssertions()\n        }.onFailure { exception ->\n            val message = exception.message ?: throw RuntimeException(\"Empty exception message: $exception\")\n            Assert.assertTrue(message.contains(\"NotExistText\"))\n            Assert.assertTrue(message.contains(\"NotExistTestTag\"))\n        }\n\n    }\n\n    @Test\n    fun allNodesTest_invalidExpectedValue(){\n        AssertUtils.assertException { allNodes(hasTestTag(statusText)).assertSize(2, operationTimeoutMs = 1000) }\n    }\n\n    @Test\n    fun allNodesTest_correctExpectedValue(){\n        allNodes(hasTestTag(statusText)).assertSize(1)\n    }\n}"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/DefaultComponentActivityTest.kt",
    "content": "package com.atiurin.sampleapp.tests.compose\n\nimport androidx.compose.material.Text\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.semantics.semantics\nimport androidx.compose.ui.semantics.testTag\nimport androidx.compose.ui.test.hasTestTag\nimport com.atiurin.ultron.core.compose.createDefaultUltronComposeRule\nimport com.atiurin.ultron.extensions.assertIsDisplayed\nimport org.junit.Rule\nimport org.junit.Test\n\nclass DefaultComponentActivityTest {\n    @get:Rule\n    val composeRule = createDefaultUltronComposeRule()\n\n    @Test\n    fun setContent() {\n        val testTagValue = \"testTag\"\n        composeRule.setContent {\n            Text(text = \"Hello, world!\", modifier = Modifier.semantics { testTag = testTagValue })\n        }\n        hasTestTag(testTagValue)\n            .assertIsDisplayed()\n            .assertTextEquals(\"Hello, world!\")\n    }\n}"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/RunUltronUiTest.kt",
    "content": "package com.atiurin.sampleapp.tests.compose\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.material.Button\nimport androidx.compose.material.Text\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.testTag\nimport androidx.compose.ui.test.ExperimentalTestApi\nimport androidx.compose.ui.test.hasTestTag\nimport com.atiurin.ultron.core.compose.runUltronUiTest\nimport com.atiurin.ultron.extensions.assertTextContains\nimport com.atiurin.ultron.extensions.isSuccess\nimport org.junit.Test\nimport kotlin.test.assertTrue\n\n@OptIn(ExperimentalTestApi::class)\nclass RunUltronUiTest {\n\n    @Test\n    fun useUnmergedTreeConfigTest() = runUltronUiTest {\n        val testTag = \"element\"\n        setContent {\n            Column {\n                Button(onClick = {}, modifier = Modifier.testTag(testTag)) {\n                    Text(\"Text1\")\n                    Text(\"Text2\")\n                }\n            }\n        }\n        assertTrue (\"Ultron operation success should be true\") {\n            hasTestTag(testTag).isSuccess { assertTextContains(\"Text1\") }\n        }\n    }\n}"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/SampleClassTest.kt",
    "content": "package com.atiurin.sampleapp.tests.compose\n\nimport androidx.compose.ui.test.hasTestTag\nimport com.atiurin.sampleapp.activity.ComposeElementsActivity\nimport com.atiurin.sampleapp.tests.BaseTest\nimport com.atiurin.ultron.core.compose.createSimpleUltronComposeRule\nimport com.atiurin.ultron.extensions.assertIsDisplayed\nimport com.atiurin.ultron.extensions.withName\nimport com.atiurin.ultron.extensions.withTimeout\nimport com.atiurin.ultron.page.Page\nimport org.junit.Rule\nimport org.junit.Test\n\nclass SampleClassTest : BaseTest() {\n    @get:Rule\n    val composeRuleBase = createSimpleUltronComposeRule<ComposeElementsActivity>()\n\n    @Test\n    fun test() {\n        SomePage{\n            elementWithName.assertIsDisplayed()\n            elementWithTimeout.assertIsDisplayed()\n            elementMatcher.assertIsDisplayed()\n        }\n    }\n\n    @Test\n    fun test2() {\n        SomePage{\n            elementWithName.assertIsDisplayed()\n            elementWithTimeout.assertIsDisplayed()\n            elementMatcher.assertIsDisplayed()\n        }\n    }\n}\n\nobject SomePage : Page<SomePage>() {\n    val elementWithName = hasTestTag(\"statusText\").withName(\"sample element name\")\n    val elementWithTimeout = hasTestTag(\"statusText\").withTimeout(4000)\n    val elementMatcher = hasTestTag(\"statusText\")\n}"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/SemNodeInteractionObjectTest.kt",
    "content": "package com.atiurin.sampleapp.tests.compose\n\nimport com.atiurin.sampleapp.activity.ActionsStatus\nimport com.atiurin.sampleapp.activity.ComposeElementsActivity\nimport com.atiurin.sampleapp.pages.ComposeElementsPage\nimport com.atiurin.sampleapp.tests.BaseTest\nimport com.atiurin.ultron.core.common.options.TextContainsOption\nimport com.atiurin.ultron.core.compose.createSimpleUltronComposeRule\nimport com.atiurin.ultron.core.compose.nodeinteraction.click\nimport com.atiurin.ultron.core.compose.nodeinteraction.doubleClick\nimport com.atiurin.ultron.core.compose.nodeinteraction.longClick\nimport com.atiurin.ultron.core.compose.option.ComposeSwipeOption\nimport com.atiurin.ultron.extensions.assertTextEquals\nimport com.atiurin.ultron.extensions.withMetaInfo\nimport com.atiurin.ultron.extensions.withName\nimport org.junit.Rule\nimport org.junit.Test\n\nclass SemNodeInteractionObjectTest : BaseTest() {\n    @get:Rule\n    val composeRule = createSimpleUltronComposeRule<ComposeElementsActivity>()\n    val page = ComposeElementsPage\n\n    @Test\n    fun clickTest(){\n        page.likesCounter.withMetaInfo(\"likesCounter\")\n            .click()\n            .assertTextContains(option = TextContainsOption(substring = true), expected = \"= 1\")\n    }\n\n    @Test\n    fun longClickTest(){\n        page.longAndDoubleClickButton.withName(\"longAndDoubleClickButton\").longClick()\n        page.status.assertTextEquals(ActionsStatus.LongClicked.name)\n    }\n\n    @Test\n    fun doubleClick_doubleClickable() {\n        page.longAndDoubleClickButton.withName(\"longAndDoubleClickButton\").doubleClick()\n        page.status.assertTextEquals(ActionsStatus.DoubleClicked.name)\n    }\n\n    @Test\n    fun swipeDownTest(){\n        page.swipeableNode.withName(\"swipeableNode\").swipeDown()\n        page.status.assertTextEquals(ActionsStatus.SwipeDown.name)\n    }\n\n    @Test\n    fun swipeUp() {\n        page.swipeableNode.withName(\"swipeableNode\").swipeUp()\n        page.status.assertTextEquals(ActionsStatus.SwipeUp.name)\n    }\n\n    @Test\n    fun swipeRight() {\n        page.swipeableNode.withName(\"swipeableNode\").swipeRight()\n        page.status.assertTextEquals(ActionsStatus.SwipeRight.name)\n    }\n\n    @Test\n    fun swipeLeft() {\n        page.swipeableNode.withName(\"swipeableNode\").swipeLeft()\n        page.status.assertTextEquals(ActionsStatus.SwipeLeft.name)\n    }\n\n    @Test\n    fun swipe_option() {\n        page.swipeableNode.withName(\"swipeableNode\").swipeLeft(ComposeSwipeOption(durationMs = 1000L))\n        page.status.assertTextEquals(ActionsStatus.SwipeLeft.name)\n    }\n\n    @Test\n    fun swipe_general() {\n        page.swipeableNode.withName(\"Swipeable Node\").swipe(ComposeSwipeOption(\n            startXOffset = 0.1f,\n            startYOffset = 0.1f,\n            endXOffset = 0.9f,\n            endYOffset = 0.1f,\n            durationMs = 1000L\n        ))\n        page.status.assertTextEquals(ActionsStatus.SwipeRight.name)\n    }\n}"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/TreeTest.kt",
    "content": "package com.atiurin.sampleapp.tests.compose\n\nimport androidx.compose.ui.test.onRoot\nimport androidx.compose.ui.test.printToString\nimport com.atiurin.sampleapp.activity.ComposeElementsActivity\nimport com.atiurin.sampleapp.pages.ComposeElementsPage\nimport com.atiurin.sampleapp.tests.BaseTest\nimport com.atiurin.ultron.allure.attachment.AttachUtil\nimport com.atiurin.ultron.core.compose.createSimpleUltronComposeRule\nimport com.atiurin.ultron.file.MimeType\nimport com.atiurin.ultron.log.UltronLog\nimport com.atiurin.ultron.utils.createCacheFile\nimport org.junit.Test\n\nclass TreeTest : BaseTest() {\n    val page = ComposeElementsPage\n    val composeRule = createSimpleUltronComposeRule<ComposeElementsActivity>()\n    init {\n        ruleSequence.add(composeRule)\n    }\n    @Test\n    fun generateSemanticsTreeTest(){\n        val node = composeRule.onRoot(useUnmergedTree = true).printToString()\n        val file = createCacheFile(\"tree_\", \".log\")\n        file.writeText(node)\n        val fileName = AttachUtil.attachFile(file, MimeType.PLAIN_TEXT)\n        UltronLog.error(node)\n    }\n}"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/UltronComposeUiBlockTest.kt",
    "content": "package com.atiurin.sampleapp.tests.compose\n\nimport androidx.compose.ui.test.hasTestTag\nimport com.atiurin.sampleapp.activity.ComposeElementsActivity\nimport com.atiurin.sampleapp.activity.ComposeElementsActivity.Constants.contactBlock1Tag\nimport com.atiurin.sampleapp.data.repositories.CONTACTS\nimport com.atiurin.sampleapp.framework.utils.AssertUtils\nimport com.atiurin.sampleapp.pages.uiblock.ComposeListUiBlock.Companion.listBlockDesc\nimport com.atiurin.sampleapp.pages.uiblock.ComposeUiBlockScreen\nimport com.atiurin.sampleapp.pages.uiblock.ContactUiBlockWithDesc\nimport com.atiurin.sampleapp.tests.BaseTest\nimport com.atiurin.sampleapp.tests.espresso.descriptionPrefix\nimport com.atiurin.sampleapp.tests.espresso.сhildNameDesc\nimport com.atiurin.ultron.core.common.assertion.softAssertion\nimport com.atiurin.ultron.core.compose.createSimpleUltronComposeRule\nimport com.atiurin.ultron.extensions.assertIsDisplayed\nimport com.atiurin.ultron.extensions.assertTextContains\nimport com.atiurin.ultron.extensions.withUseUnmergedTree\nimport org.junit.Assert\nimport org.junit.Rule\nimport org.junit.Test\n\nclass UltronComposeUiBlockTest : BaseTest() {\n    @get:Rule\n    val composeRule = createSimpleUltronComposeRule<ComposeElementsActivity>()\n\n    @Test\n    fun noUniqueElementTest() {\n        ComposeUiBlockScreen {\n            contactBlock1.blockMatcher.withUseUnmergedTree(true).printToLog(\"tree\")\n            contactBlock1.statusDeepSearchText.assertIsDisplayed()\n            AssertUtils.assertException { contactBlock1.nameWithoutDeepSearch.withTimeout(100).assertIsDisplayed() }\n            contactBlock2.name.assertIsDisplayed()\n        }\n    }\n\n    @Test\n    fun uiBlockInBlock() {\n        ComposeUiBlockScreen {\n            contactListBlock.blockMatcher.withUseUnmergedTree(true).printToLog(\"tree\")\n            contactListBlock.itemWithoutDesc.statusDeepSearchText.assertIsDisplayed()\n            contactListBlock.itemWithoutDesc.uiBlock.assertIsDisplayed()\n            contactListBlock.item1BlockWithDesc.name.assertIsDisplayed()\n            AssertUtils.assertException { contactListBlock.itemWithoutDesc.nameWithoutDeepSearch.withTimeout(100).assertIsDisplayed() }\n        }\n    }\n\n    @Test\n    fun childElementDescription() {\n        val blockDesc = \"Parent_Name\"\n        val expectedChildName = \"${ContactUiBlockWithDesc.сhildNameDesc} $blockDesc\"\n        ContactUiBlockWithDesc(hasTestTag(contactBlock1Tag), blockDesc).name.assertIsDisplayed().withResultHandler {\n            Assert.assertEquals(expectedChildName, it.operation.elementInfo.name)\n        }.withTimeout(100).assertTextContains(\"Invalid text\")\n    }\n\n    @Test\n    fun childBlockDescriptionTest() {\n        val expectedItem1Description = \"1 $descriptionPrefix $listBlockDesc\"\n        val expectedItem2Description = \"2 $descriptionPrefix $listBlockDesc\"\n        val expectedChildNameDescInBlock1 = \"$сhildNameDesc $expectedItem1Description\"\n        val expectedChildNameDescInBlock2 = \"$сhildNameDesc $expectedItem2Description\"\n\n        ComposeUiBlockScreen {\n            softAssertion {\n                contactListBlock.item1BlockWithDesc.uiBlock.withTimeout(100).withResultHandler {\n                    Assert.assertEquals(expectedItem1Description, it.operation.elementInfo.name)\n                }.assertTextEquals(\"Invalid\")\n                contactListBlock.item1BlockWithDesc.name.withTimeout(100).withResultHandler {\n                    Assert.assertEquals(expectedChildNameDescInBlock1, it.operation.elementInfo.name)\n                }.assertTextEquals(\"Invalid\")\n                contactListBlock.item2BlockFactory.name.withTimeout(100).withResultHandler {\n                    Assert.assertEquals(expectedChildNameDescInBlock2, it.operation.elementInfo.name)\n                }.assertTextEquals(\"Invalid\")\n            }\n        }\n    }\n\n    @Test\n    fun properSearchOfElementsTest(){\n        ComposeUiBlockScreen {\n            softAssertion {\n                contactBlock1.statusDeepSearchText.assertTextContains(CONTACTS[0].status)\n                contactListBlock.item1BlockWithDesc.name.assertTextContains(CONTACTS[0].name)\n                contactListBlock.item1BlockWithDesc.status.assertTextContains(CONTACTS[0].status)\n                contactListBlock.item2BlockFactory.name.assertTextContains(CONTACTS[1].name)\n                contactListBlock.item2BlockFactory.status.assertTextContains(CONTACTS[1].status)\n            }\n        }\n    }\n}\n\n"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/elements/DataPickerTest.kt",
    "content": "package com.atiurin.sampleapp.tests.compose.elements\n\nimport androidx.compose.ui.test.ExperimentalTestApi\nimport androidx.compose.ui.test.SemanticsMatcher\nimport androidx.compose.ui.test.hasTestTag\nimport androidx.compose.ui.test.performCustomAccessibilityActionWithLabel\nimport com.atiurin.sampleapp.activity.ComposeRouterActivity\nimport com.atiurin.sampleapp.compose.DatePickerTestData\nimport com.atiurin.sampleapp.compose.DatePickerTestTags\nimport com.atiurin.sampleapp.compose.DatePickerTestTags.SetDatePickerTimeCustomActionLabel\nimport com.atiurin.sampleapp.compose.screen.NavigationTestTags\nimport com.atiurin.sampleapp.framework.utils.TimeUtils\nimport com.atiurin.sampleapp.tests.BaseTest\nimport com.atiurin.sampleapp.utils.convertMillisToDate\nimport com.atiurin.ultron.core.compose.createSimpleUltronComposeRule\nimport com.atiurin.ultron.core.compose.nodeinteraction.UltronComposeSemanticsNodeInteraction\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType\nimport com.atiurin.ultron.core.compose.operation.UltronComposeOperationParams\nimport com.atiurin.ultron.extensions.assertTextContains\nimport com.atiurin.ultron.extensions.click\nimport com.atiurin.ultron.testlifecycle.setupteardown.SetUpRule\nimport org.junit.Test\nimport java.util.concurrent.TimeUnit\n\nclass DataPickerTest : BaseTest() {\n    private val composeRule = createSimpleUltronComposeRule<ComposeRouterActivity>()\n    private val navigateRule = SetUpRule().add { hasTestTag(NavigationTestTags.DatePicker).click() }\n    init {\n        ruleSequence.add(composeRule, navigateRule)\n    }\n\n    /**\n     * [convertMillisToDate] is defined in app to show the date, see [com.atiurin.sampleapp.compose.DatePickerKt.DatePickerDocked]\n     * [TimeUtils.getTimestampStartOfDay] is used cause DatePicker return a start of the selected date timestamp\n     */\n    @Test\n    fun selectDateTest(){\n        hasTestTag(DatePickerTestTags.DockedIconButton).click()\n        val time = TimeUtils.getTimestampStartOfDay() + TimeUnit.DAYS.toMillis(120)\n        hasTestTag(DatePickerTestTags.DataPicker).setDatePickerTime(time)\n        hasTestTag(DatePickerTestTags.SelectedDateValue).assertTextContains(convertMillisToDate(time))\n    }\n}\n\n// Make this action native for Ultron\n@OptIn(ExperimentalTestApi::class)\nfun UltronComposeSemanticsNodeInteraction.setDatePickerTime(timeMs: Long) = perform(\n    UltronComposeOperationParams(\n        operationName = \"SetDatePickerTime '${TimeUtils.formatTimestamp(timeMs)}' for '${elementInfo.name}'\",\n        operationDescription = \"Compose SetDatePickerTime '${TimeUtils.formatTimestamp(timeMs)}' for '${elementInfo.name}' during $timeoutMs ms\",\n        operationType = ComposeOperationType.CUSTOM\n    )\n) {\n    DatePickerTestData.time = timeMs\n    semanticsNodeInteraction.performCustomAccessibilityActionWithLabel(SetDatePickerTimeCustomActionLabel)\n}\n\nfun SemanticsMatcher.setDatePickerTime(timeMs: Long) = UltronComposeSemanticsNodeInteraction(this).setDatePickerTime(timeMs)\n\n\n\n\n"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso/CustomClicksTest.kt",
    "content": "package com.atiurin.sampleapp.tests.espresso\n\nimport androidx.test.core.app.ActivityScenario\nimport androidx.test.espresso.matcher.ViewMatchers.withId\nimport com.atiurin.sampleapp.R\nimport com.atiurin.sampleapp.activity.CustomClicksActivity\nimport com.atiurin.sampleapp.tests.BaseTest\nimport com.atiurin.ultron.extensions.*\nimport com.atiurin.ultron.testlifecycle.setupteardown.SetUpRule\nimport org.junit.Test\n\nclass CustomClicksTest : BaseTest() {\n\n    private val startActivity = SetUpRule().add {\n        ActivityScenario.launch(CustomClicksActivity::class.java)\n    }\n\n    init {\n        ruleSequence.addLast(startActivity)\n    }\n\n    @Test\n    fun clickTopLeft() {\n        withId(R.id.imageView).clickTopLeft(offsetX = 30, offsetY = 30)\n        withId(R.id.rB_top_left).isChecked()\n    }\n\n    @Test\n    fun clickTopCenter() {\n        withId(R.id.imageView).clickTopCenter(offsetY = 30)\n        withId(R.id.rB_top_center).isChecked()\n    }\n\n    @Test\n    fun clickTopRight() {\n        withId(R.id.imageView).clickTopRight(offsetX = -30, offsetY = 30)\n        withId(R.id.rB_top_right).isChecked()\n    }\n\n    @Test\n    fun clickCenterRight() {\n        withId(R.id.imageView).clickCenterRight(offsetX = -30)\n        withId(R.id.rB_center_right).isChecked()\n    }\n\n    @Test\n    fun clickBottomRight() {\n        withId(R.id.imageView).clickBottomRight(offsetX = -30, offsetY = -30)\n        withId(R.id.rB_bottom_right).isChecked()\n    }\n\n    @Test\n    fun clickBottomCenter() {\n        withId(R.id.imageView).clickBottomCenter(offsetY = -30)\n        withId(R.id.rB_bottom_center).isChecked()\n    }\n\n    @Test\n    fun clickBottomLeft() {\n        withId(R.id.imageView).clickBottomLeft(offsetX = 30, offsetY = -30)\n        withId(R.id.rB_bottom_left).isChecked()\n    }\n\n    @Test\n    fun clickCenterLeft() {\n        withId(R.id.imageView).clickCenterLeft(offsetX = 30)\n        withId(R.id.rB_center_left).isChecked()\n    }\n}"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso/CustomMatchersTest.kt",
    "content": "package com.atiurin.sampleapp.tests.espresso\n\nimport androidx.test.espresso.matcher.ViewMatchers.withId\nimport com.atiurin.sampleapp.R\nimport com.atiurin.sampleapp.activity.MainActivity\nimport com.atiurin.sampleapp.data.repositories.CONTACTS\nimport com.atiurin.sampleapp.data.repositories.ContactRepositoty\nimport com.atiurin.sampleapp.pages.ChatPage\nimport com.atiurin.sampleapp.tests.BaseTest\nimport com.atiurin.ultron.custom.espresso.matcher.first\nimport com.atiurin.ultron.custom.espresso.matcher.hierarchyNumber\nimport com.atiurin.ultron.extensions.click\nimport com.atiurin.ultron.extensions.hasText\nimport com.atiurin.ultron.testlifecycle.activity.UltronActivityRule\nimport org.junit.Test\n\nclass CustomMatchersTest : BaseTest() {\n\n    private val activityTestRule = UltronActivityRule(MainActivity::class.java)\n\n    init {\n        ruleSequence.addLast(activityTestRule)\n    }\n\n    @Test\n    fun actionOnFirstMatchedView(){\n        withId(R.id.tv_name).first().click()\n        ChatPage.assertToolbarTitle(ContactRepositoty.getFirst().name)\n    }\n\n    @Test\n    fun assertionOnFirstMatchedView(){\n        withId(R.id.tv_name).first().hasText(ContactRepositoty.getFirst().name)\n    }\n\n    @Test\n    fun actionOnHierarchyNumberedItem(){\n        withId(R.id.tv_name).hierarchyNumber(1).click()\n        ChatPage.assertToolbarTitle(CONTACTS[1].name)\n    }\n\n    @Test\n    fun assertionOnHierarchyNumberedItem(){\n        withId(R.id.tv_name).hierarchyNumber(1).hasText(CONTACTS[1].name)\n    }\n}"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso/DemoEspressoTest.kt",
    "content": "package com.atiurin.sampleapp.tests.espresso\n\nimport com.atiurin.sampleapp.activity.MainActivity\nimport com.atiurin.sampleapp.pages.ChatPage\nimport com.atiurin.sampleapp.pages.FriendsListPage\nimport com.atiurin.sampleapp.tests.BaseTest\nimport com.atiurin.ultron.core.espresso.UltronEspresso\nimport com.atiurin.ultron.extensions.doesNotExist\nimport com.atiurin.ultron.extensions.isDisplayed\nimport com.atiurin.ultron.testlifecycle.activity.UltronActivityRule\nimport org.junit.Test\n\nclass DemoEspressoTest : BaseTest() {\n\n    private val activityTestRule = UltronActivityRule(MainActivity::class.java)\n\n    init {\n        ruleSequence.addLast(activityTestRule)\n    }\n\n\n    @Test\n    fun friendsItemCheck() {\n        FriendsListPage {\n            assertName(\"Janice\")\n            assertStatus(\"Janice\", \"Oh. My. God\")\n        }\n    }\n\n    @Test\n    fun sendMessage() {\n        FriendsListPage.openChat(\"Chandler Bing\")\n        ChatPage\n            .clearHistory()\n            .sendMessage(\"test message\")\n    }\n\n    @Test\n    fun checkMessagesPositionsInChat() {\n        val firstMessage = \"first message\"\n        val secondMessage = \"second message\"\n        FriendsListPage.openChat(\"Janice\")\n        ChatPage {\n            clearHistory()\n            sendMessage(firstMessage)\n            sendMessage(secondMessage)\n            assertMessageTextAtPosition(0, firstMessage)\n        }\n    }\n\n    @Test\n    fun pressBackTest(){\n        FriendsListPage.openChat(\"Chandler Bing\")\n        ChatPage.assertPageDisplayed()\n        UltronEspresso.pressBack()\n        FriendsListPage.assertPageDisplayed()\n    }\n\n    @Test\n    fun openContextualActionModeOverflowMenuTest(){\n        FriendsListPage.openChat(\"Chandler Bing\")\n        ChatPage.clearHistoryBtn.doesNotExist()\n        UltronEspresso.openContextualActionModeOverflowMenu()\n        ChatPage.clearHistoryBtn.isDisplayed()\n    }\n\n    @Test\n    fun openActionBarOverflowOrOptionsMenuTest(){\n        FriendsListPage.openChat(\"Chandler Bing\")\n        ChatPage.clearHistoryBtn.doesNotExist()\n        UltronEspresso.openActionBarOverflowOrOptionsMenu()\n        ChatPage.clearHistoryBtn.isDisplayed()\n    }\n}"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso/RecyclerPerfTest.kt",
    "content": "package com.atiurin.sampleapp.tests.espresso\n\nimport androidx.recyclerview.widget.RecyclerView\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.contrib.RecyclerViewActions\nimport androidx.test.espresso.matcher.ViewMatchers.hasDescendant\nimport androidx.test.espresso.matcher.ViewMatchers.withId\nimport androidx.test.espresso.matcher.ViewMatchers.withText\nimport com.atiurin.sampleapp.MyApplication\nimport com.atiurin.sampleapp.R\nimport com.atiurin.sampleapp.activity.MainActivity\nimport com.atiurin.sampleapp.data.repositories.ContactRepositoty\nimport com.atiurin.sampleapp.framework.Log\nimport com.atiurin.sampleapp.pages.FriendsListPage\nimport com.atiurin.sampleapp.tests.BaseTest\nimport com.atiurin.ultron.extensions.isDisplayed\nimport com.atiurin.ultron.testlifecycle.activity.UltronActivityRule\nimport com.atiurin.ultron.testlifecycle.setupteardown.SetUpRule\nimport org.junit.Test\n\nclass RecyclerPerfTest : BaseTest() {\n\n    private val activityTestRule = UltronActivityRule(MainActivity::class.java)\n    private val timeoutRule = SetUpRule().add {\n        MyApplication.CONTACTS_LOADING_TIMEOUT_MS = 0L\n    }\n\n    init {\n        ruleSequence.addLast(timeoutRule, activityTestRule)\n    }\n\n    @Test\n    fun test2() {\n        Log.time(\"Scroll+Click\") {\n            (0..100).forEach { _ ->\n                onView(withId(R.id.recycler_friends))\n                    .perform(\n                        RecyclerViewActions\n                            .scrollTo<RecyclerView.ViewHolder>(\n                                hasDescendant(withText(ContactRepositoty.getLast().name)),\n                            )\n                    )\n                withText(ContactRepositoty.getLast().name).isDisplayed()\n                onView(withId(R.id.recycler_friends))\n                    .perform(\n                        RecyclerViewActions\n                            .scrollTo<RecyclerView.ViewHolder>(\n                                hasDescendant(withText(ContactRepositoty.getFirst().name)),\n                            )\n                    )\n                withText(ContactRepositoty.getFirst().name).isDisplayed()\n            }\n        }\n    }\n\n    @Test\n    fun recyclerViewV1PerfTest() {\n        Log.time(\"FriendsPageClick\") {\n            (0..100).forEach { _ ->\n                FriendsListPage.getListItem(ContactRepositoty.getLast().name).name.isDisplayed()\n                FriendsListPage.getListItem(ContactRepositoty.getFirst().name).name.isDisplayed()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso/RecyclerViewTest.kt",
    "content": "package com.atiurin.sampleapp.tests.espresso\n\nimport androidx.test.espresso.matcher.ViewMatchers.Visibility\nimport androidx.test.espresso.matcher.ViewMatchers.hasDescendant\nimport androidx.test.espresso.matcher.ViewMatchers.withId\nimport androidx.test.espresso.matcher.ViewMatchers.withText\nimport com.atiurin.sampleapp.MyApplication\nimport com.atiurin.sampleapp.R\nimport com.atiurin.sampleapp.activity.MainActivity\nimport com.atiurin.sampleapp.data.repositories.CONTACTS\nimport com.atiurin.sampleapp.data.repositories.ContactRepositoty\nimport com.atiurin.sampleapp.framework.utils.AssertUtils\nimport com.atiurin.sampleapp.pages.ChatPage\nimport com.atiurin.sampleapp.pages.FriendsListPage\nimport com.atiurin.sampleapp.tests.BaseTest\nimport com.atiurin.ultron.core.espresso.recyclerview.withRecyclerView\nimport com.atiurin.ultron.extensions.withAssertion\nimport com.atiurin.ultron.extensions.withName\nimport com.atiurin.ultron.extensions.withTimeout\nimport com.atiurin.ultron.testlifecycle.activity.UltronActivityRule\nimport com.atiurin.ultron.testlifecycle.setupteardown.SetUp\nimport com.atiurin.ultron.testlifecycle.setupteardown.SetUpRule\nimport com.atiurin.ultron.testlifecycle.setupteardown.TearDown\nimport com.atiurin.ultron.testlifecycle.setupteardown.TearDownRule\nimport org.hamcrest.Matchers.allOf\nimport org.hamcrest.Matchers.containsString\nimport org.junit.Assert\nimport org.junit.Test\n\nclass RecyclerViewTest : BaseTest() {\n    companion object {\n        const val CUSTOM_TIMEOUT = \"CUSTOM_TIMEOUT\"\n        val notExistItemMatcher = hasDescendant(withText(\"zxcbmzxmbc\"))\n    }\n\n    private val setUpRule = SetUpRule().add(CUSTOM_TIMEOUT) {\n        MyApplication.CONTACTS_LOADING_TIMEOUT_MS = 6_000L\n    }\n    private val tearDownRule = TearDownRule().add(CUSTOM_TIMEOUT) {\n        MyApplication.CONTACTS_LOADING_TIMEOUT_MS = 500L\n    }\n\n    init {\n        ruleSequence.add(setUpRule, tearDownRule).addLast(UltronActivityRule(MainActivity::class.java))\n    }\n\n    val page = FriendsListPage\n\n    @Test\n    fun childTest() {\n        withRecyclerView(R.id.recycler_friends).withName(\"Friends list\").item(0).getChild(withId(R.id.tv_status).withName(\"Status\")).hasText(ContactRepositoty.getFirst().status)\n    }\n\n    @Test\n    fun testDisplayedItemPositions() {\n        for (index in 0..3) {\n            page.recycler.item(index).assertMatches(hasDescendant(withText(CONTACTS[index].name)))\n                .assertMatches(hasDescendant(withText(CONTACTS[index].status))).isDisplayed()\n        }\n    }\n\n    @Test\n    fun getRecyclerViewTest() {\n        val view = page.recycler.getRecyclerViewList()\n        Assert.assertNotNull(view)\n        Assert.assertEquals(Visibility.VISIBLE.value, view.visibility)\n    }\n\n    @Test\n    fun scrollToItemTest() {\n        val contact = CONTACTS[CONTACTS.size - 1]\n        val item = page.getListItem(contact.name)\n        item.isDisplayed()\n        item.name.hasText(contact.name)\n        item.status.hasText(contact.status)\n    }\n\n    @Test\n    fun wrongChild() {\n        val contact = CONTACTS[CONTACTS.size - 1]\n        val wrongContact = CONTACTS[CONTACTS.size - 2]\n        val item = page.getListItem(contact.name)\n        item.isDisplayed()\n        AssertUtils.assertException { item.name.withTimeout(100).hasText(wrongContact.name) }\n    }\n\n    @Test\n    fun recyclerViewItemClassTest() {\n        val contact = CONTACTS[1]\n        with(page.getListItem(contact.name)) {\n            this.isDisplayed().isClickable()\n            this.name.isDisplayed().isEnabled().hasText(contact.name)\n            this.status.isDisplayed().isEnabled().hasText(contact.status)\n        }\n    }\n\n    @Test\n    fun scrollToLastItem() {\n        withRecyclerView(R.id.recycler_friends).item(CONTACTS.size - 1).isDisplayed()\n    }\n\n    @Test\n    fun scrollToLastWithMatcher() {\n        withRecyclerView(R.id.recycler_friends).item(hasDescendant(withText(\"Friend14\"))).isDisplayed()\n    }\n\n    @Test\n    fun getNotExistedRecyclerItemWithPosition() {\n        AssertUtils.assertException {\n            page.recycler.item(100).withTimeout(100).isDisplayed()\n        }\n    }\n\n    @Test\n    fun assertListSize() {\n        page.recycler.assertSize(CONTACTS.size)\n    }\n\n    @Test\n    fun recyclerView_notExist() {\n        AssertUtils.assertException {\n            withRecyclerView(withText(\"Not existed recycler\")).withTimeout(100).isDisplayed()\n        }\n    }\n\n    @Test\n    fun item_notExist() {\n        AssertUtils.assertException {\n            page.recycler.item(withText(\"Not existed item\"), false).withTimeout(100).isDisplayed()\n        }\n    }\n\n    @Test\n    fun item_notExist_executionTime() {\n        val timeout = 5_000L\n        AssertUtils.assertExecTimeMoreThen(timeout){\n            AssertUtils.assertException {\n                runCatching { page.recycler.withTimeout(timeout).item(withText(\"Not existed item\")).isDisplayed() }\n            }\n        }\n    }\n\n    @Test\n    @SetUp(CUSTOM_TIMEOUT)\n    @TearDown(CUSTOM_TIMEOUT)\n    fun defaultTimeoutOnItemWaiting() {\n        AssertUtils.assertException { page.recycler.item(10).isDisplayed() }\n    }\n\n    @Test\n    @SetUp(CUSTOM_TIMEOUT)\n    @TearDown(CUSTOM_TIMEOUT)\n    fun customTimeoutOnItemWaiting() {\n        withRecyclerView(R.id.recycler_friends, 8000).item(10).isDisplayed()\n    }\n\n    @Test\n    @SetUp(CUSTOM_TIMEOUT)\n    @TearDown(CUSTOM_TIMEOUT)\n    fun item_autoScroll_False_item_NotLoaded() {\n        AssertUtils.assertException { page.recycler.item(10, false).isDisplayed() }\n    }\n\n    @Test\n    @SetUp(CUSTOM_TIMEOUT)\n    @TearDown(CUSTOM_TIMEOUT)\n    fun item_autoScroll_False_scroll_Force() {\n        withRecyclerView(R.id.recycler_friends, 8_000).item(10, false).scrollToItem().isDisplayed()\n    }\n\n    @Test\n    fun itemMatcher_autoScroll_false_itemNotDisplayed() {\n        AssertUtils.assertException {\n            page.recycler.item(\n                hasDescendant(withText(\"Friend14\")), false\n            ).isDisplayed()\n        }\n    }\n\n    @Test\n    fun itemMatcher_autoScroll_false_scroll_force() {\n        page.recycler.item(hasDescendant(withText(\"Friend14\")), false).scrollToItem().isDisplayed()\n    }\n\n    @Test\n    @SetUp(CUSTOM_TIMEOUT)\n    @TearDown(CUSTOM_TIMEOUT)\n    fun itemMatcher_autoScroll_false() {\n        AssertUtils.assertException {\n            page.recycler.item(\n                hasDescendant(withText(\"Friend14\")), false\n            ).scrollToItem().isDisplayed()\n        }\n    }\n\n    @Test\n    @SetUp(CUSTOM_TIMEOUT)\n    @TearDown(CUSTOM_TIMEOUT)\n    fun itemMatcher_autoScroll_true_custom_timeout() {\n        withRecyclerView(R.id.recycler_friends, 10_000).item(hasDescendant(withText(\"Friend14\"))).isDisplayed()\n    }\n\n    @Test\n    fun getViewHolder() {\n        val position = CONTACTS.size - 1\n        Assert.assertEquals(\n            position,\n            page.recycler.item(hasDescendant(withText(CONTACTS[position].name)))\n                .getViewHolder()\n                ?.layoutPosition\n        )\n    }\n\n    @Test\n    fun transferFromGenericToSubclass() {\n        val position = 5\n        page.recycler.getItem<FriendsListPage.FriendRecyclerItem>(position)\n            .status.hasText(CONTACTS[position].status).isDisplayed()\n    }\n\n    @Test\n    fun getViewHolderList() {\n        page.recycler.waitItemsLoaded()\n        Assert.assertTrue(page.recycler.getViewHolderList(hasDescendant(withId(R.id.tv_name))).isNotEmpty())\n    }\n\n    @Test\n    fun waitLoaded_ofAlreadyLoadedList() {\n        page.recycler.waitItemsLoaded()\n        page.recycler.waitItemsLoaded()\n    }\n\n    @Test\n    fun waitLoaded_allItemsLoaded() {\n        val count = page.recycler.waitItemsLoaded().getSize()\n        Assert.assertEquals(CONTACTS.size, count)\n    }\n\n    @Test\n    fun getLastItem() {\n        page.recycler.lastItem().isDisplayed().click()\n    }\n\n    @Test\n    fun getLastItemWithCustomType() {\n        page.recycler.getLastItem<FriendsListPage.FriendRecyclerItem>().name.hasText(ContactRepositoty.getLast().name)\n    }\n\n    @Test\n    fun perfScroll() {\n        page.recycler.apply {\n            for (i in 0..10) {\n                lastItem().isDisplayed()\n                firstItem().isDisplayed()\n            }\n        }\n    }\n\n    @Test\n    fun getViewHolderAtPosition_outOfVisibleList() {\n        Assert.assertNull(page.recycler.waitItemsLoaded().getViewHolderAtPosition(15))\n    }\n\n    @Test\n    fun getViewHolderAtPosition_inVisibleList() {\n        Assert.assertNotNull(page.recycler.waitItemsLoaded().getViewHolderAtPosition(2))\n    }\n\n    @Test\n    fun getItemsAdapterPositionList() {\n        page.recycler.waitItemsLoaded()\n        val matcher = hasDescendant(allOf(withId(R.id.tv_name), withText(containsString(\"Friend\"))))\n        Assert.assertEquals(0, page.recycler.getViewHolderList(matcher).size)\n        Assert.assertTrue(page.recycler.getItemsAdapterPositionList(matcher).isNotEmpty())\n    }\n\n    @Test\n    fun firstItemMatched_existItem() {\n        val pattern = \"Friend\"\n        val expectedContacts = CONTACTS.filter { it.name.contains(pattern) }\n        val matcher = hasDescendant(allOf(withId(R.id.tv_name), withText(containsString(pattern))))\n\n        page.recycler.firstItemMatched(matcher).isDisplayed().click()\n        ChatPage.assertToolbarTitle(expectedContacts.first().name)\n    }\n\n    @Test\n    fun itemMatched_existItem() {\n        val pattern = \"Friend\"\n        val expectedContacts = CONTACTS.filter { it.name.contains(pattern) }\n        val matcher = hasDescendant(allOf(withId(R.id.tv_name), withText(containsString(pattern))))\n\n        page.recycler.itemMatched(matcher, 1).isDisplayed().click()\n        ChatPage.assertToolbarTitle(expectedContacts[1].name)\n    }\n\n    @Test\n    fun itemMatched_notExistItem() {\n        val matcher = hasDescendant(allOf(withId(R.id.tv_name), withText(containsString(\"Friend\"))))\n        AssertUtils.assertException { page.recycler.withTimeout(1000).itemMatched(matcher, 99) }\n    }\n\n    @Test\n    fun lastItemMatched_existItem() {\n        val pattern = \"Friend\"\n        val expectedContacts = CONTACTS.filter { it.name.contains(pattern) }\n        val matcher = hasDescendant(allOf(withId(R.id.tv_name), withText(containsString(pattern))))\n\n        page.recycler.itemMatched(matcher, expectedContacts.lastIndex).isDisplayed().click()\n        ChatPage.assertToolbarTitle(expectedContacts.last().name)\n    }\n\n    @Test\n    fun getFirstItemMatched_existItem() {\n        val pattern = \"Friend\"\n        val expectedContact = CONTACTS.filter { it.name.contains(pattern) }.first()\n        val matcher = hasDescendant(allOf(withId(R.id.tv_name), withText(containsString(pattern))))\n\n        page.recycler.getFirstItemMatched<FriendsListPage.FriendRecyclerItem>(matcher).apply {\n            name.isDisplayed().hasText(expectedContact.name)\n            status.hasText(expectedContact.status)\n            click()\n        }\n        ChatPage.assertToolbarTitle(expectedContact.name)\n    }\n\n    @Test\n    fun getItemMatched_existItem() {\n        val pattern = \"Friend\"\n        val expectedContact = CONTACTS.filter { it.name.contains(pattern) }[1]\n        val matcher = hasDescendant(allOf(withId(R.id.tv_name), withText(containsString(pattern))))\n\n        page.recycler.getItemMatched<FriendsListPage.FriendRecyclerItem>(matcher, 1).apply {\n            name.isDisplayed().hasText(expectedContact.name)\n            status.hasText(expectedContact.status)\n            click()\n        }\n        ChatPage.assertToolbarTitle(expectedContact.name)\n    }\n\n    @Test\n    fun getItemMatched_notExistItem() {\n        val matcher = hasDescendant(allOf(withId(R.id.tv_name), withText(containsString(\"Friend\"))))\n        AssertUtils.assertException { page.recycler.withTimeout(1000).getItemMatched<FriendsListPage.FriendRecyclerItem>(matcher, 99) }\n    }\n\n    @Test\n    fun getLastItemMatched_existItem() {\n        val pattern = \"Friend\"\n        val expectedContacts = CONTACTS.filter { it.name.contains(pattern) }\n        val matcher = hasDescendant(allOf(withId(R.id.tv_name), withText(containsString(pattern))))\n\n        page.recycler.getItemMatched<FriendsListPage.FriendRecyclerItem>(matcher, expectedContacts.lastIndex).apply {\n            name.isDisplayed().hasText(expectedContacts.last().name)\n            status.hasText(expectedContacts.last().status)\n            click()\n        }\n        ChatPage.assertToolbarTitle(expectedContacts.last().name)\n    }\n\n    @Test\n    fun assertItemNotExist_notExistItem() {\n        page.recycler.assertItemNotExist(notExistItemMatcher, 2000)\n    }\n\n    @Test\n    fun assertItemNotExist_existItem() {\n        val matcher = hasDescendant(allOf(withId(R.id.tv_name), withText(containsString(CONTACTS.first().name))))\n        AssertUtils.assertException { page.recycler.assertItemNotExist(matcher, 2000) }\n    }\n\n    @Test\n    fun assertItemNotExistImmediately_notExistItem() {\n        page.recycler.assertItemNotExistImmediately(notExistItemMatcher, 2000)\n    }\n\n    @Test\n    fun assertItemNotExistImmediately_existItem() {\n        page.recycler.waitItemsLoaded()\n        val matcher = hasDescendant(allOf(withId(R.id.tv_name), withText(containsString(CONTACTS.first().name))))\n        AssertUtils.assertException { page.recycler.assertItemNotExistImmediately(matcher, 2000) }\n    }\n\n    @Test\n    fun assertItemOutOfLimitNotFound() {\n        val rv = withRecyclerView(R.id.recycler_friends, itemSearchLimit = 2)\n        AssertUtils.assertException {\n            rv.withTimeout(2000L)\n                .item(hasDescendant(allOf(withId(R.id.tv_name), withText(containsString(CONTACTS[10].name)))))\n                .click()\n        }\n    }\n\n    @Test\n    fun assertItemInLimitFound() {\n        val rv = withRecyclerView(R.id.recycler_friends, itemSearchLimit = 10)\n        rv.withTimeout(2000L)\n            .item(hasDescendant(allOf(withId(R.id.tv_name), withText(containsString(CONTACTS[2].name)))))\n            .isDisplayed()\n    }\n\n    @Test\n    fun createHandlerFromUiTest() {\n        page.recycler.getItemAdapterPositionAtIndex(hasDescendant(allOf(withId(R.id.tv_name), withText(containsString(CONTACTS.last().name)))), 0)\n    }\n\n    //item+offset\n    @Test\n    fun item_scrollOffsetInItemCountRange_MatcherItem() {\n        val target = 5\n        val offset = 10\n        val targetContact = CONTACTS[target]\n        val offsetContact = CONTACTS[target + offset]\n        page.recycler.item(page.getItemMatcher(targetContact), scrollOffset = offset)\n        page.recycler.item(page.getItemMatcher(offsetContact), autoScroll = false).isDisplayed().click()\n    }\n\n    @Test\n    fun item_scrollOffsetInItemCountRangeBothAreVisible_MatcherItem() {\n        val target = 8\n        val offset = 2\n        val targetContact = CONTACTS[target]\n        val offsetContact = CONTACTS[target + offset]\n        page.recycler.item(page.getItemMatcher(targetContact), scrollOffset = offset).isDisplayed()\n        page.recycler.item(page.getItemMatcher(offsetContact), autoScroll = false).isDisplayed()\n    }\n\n    @Test\n    fun item_scrollOffsetOutOfItemCountRange_MatcherItem() {\n        val target = 5\n        val offset = CONTACTS.size\n        val targetContact = CONTACTS[target]\n        val offsetContact = CONTACTS.last()\n        page.recycler.item(page.getItemMatcher(targetContact), scrollOffset = offset)\n        page.recycler.item(page.getItemMatcher(offsetContact), autoScroll = false).isDisplayed().click()\n    }\n\n    @Test\n    fun item_scrollOffsetLessThenZero_MatcherItem() {\n        val target = 8\n        val offset = -18\n        val targetContact = CONTACTS[target]\n        val offsetContact = CONTACTS.first()\n        page.recycler.item(page.getItemMatcher(targetContact), scrollOffset = offset)\n        page.recycler.item(page.getItemMatcher(offsetContact), autoScroll = false).isDisplayed().click()\n    }\n\n    @Test\n    fun item_scrollOffsetInItemCountRange_positionItem() {\n        val target = 2\n        val offset = 12\n        val offsetContact = CONTACTS[target + offset]\n        page.recycler.item(target, scrollOffset = offset)\n        page.recycler.item(target + offset, autoScroll = false).isDisplayed()\n        page.recycler.getItem<FriendsListPage.FriendRecyclerItem>(target + offset, autoScroll = false).name.hasText(offsetContact.name).isDisplayed()\n    }\n\n    @Test\n    fun item_scrollOffsetInItemCountRangeBothAreVisible_positionItem() {\n        val target = 8\n        val offset = 2\n        val targetContact = CONTACTS[target]\n        val offsetContact = CONTACTS[target + offset]\n        page.recycler.item(target, scrollOffset = offset).isDisplayed()\n        page.recycler.item(target + offset, autoScroll = false).isDisplayed()\n        page.recycler.getItem<FriendsListPage.FriendRecyclerItem>(target, autoScroll = false).name.hasText(targetContact.name).isDisplayed()\n        page.recycler.getItem<FriendsListPage.FriendRecyclerItem>(target + offset, autoScroll = false).name.hasText(offsetContact.name).isDisplayed()\n    }\n\n    @Test\n    fun item_scrollOffsetOutOfItemCountRange_positionItem() {\n        val target = 5\n        val offset = CONTACTS.size\n        val offsetContact = CONTACTS.last()\n        page.recycler.item(target, scrollOffset = offset)\n        page.recycler.lastItem(autoScroll = false).isDisplayed()\n        page.recycler.getLastItem<FriendsListPage.FriendRecyclerItem>(autoScroll = false).name.hasText(offsetContact.name).isDisplayed()\n    }\n\n    @Test\n    fun item_scrollOffsetLessThenZero_positionItem() {\n        val target = 10\n        val offset = -18\n        val offsetContact = CONTACTS.first()\n        page.recycler.item(target, scrollOffset = offset)\n        page.recycler.getFirstItem<FriendsListPage.FriendRecyclerItem>(autoScroll = false).name.hasText(offsetContact.name).isDisplayed()\n    }\n\n    //itemMatched+offset\n    @Test\n    fun itemMatched_scrollOffsetInItemCountRange_MatcherItem() {\n        val target = 5\n        val offset = 10\n        val targetContact = CONTACTS[target]\n        val offsetContact = CONTACTS[target + offset]\n        page.recycler.itemMatched(page.getItemMatcher(targetContact), 0, scrollOffset = offset)\n        page.recycler.item(page.getItemMatcher(offsetContact), autoScroll = false).isDisplayed().click()\n    }\n\n    @Test\n    fun itemMatched_scrollOffsetInItemCountRangeBothAreVisible_MatcherItem() {\n        val target = 8\n        val offset = 2\n        val targetContact = CONTACTS[target]\n        val offsetContact = CONTACTS[target + offset]\n        page.recycler.itemMatched(page.getItemMatcher(targetContact), 0, scrollOffset = offset).isDisplayed()\n        page.recycler.item(page.getItemMatcher(offsetContact), autoScroll = false).isDisplayed()\n    }\n\n    @Test\n    fun itemMatched_scrollOffsetOutOfItemCountRange_MatcherItem() {\n        val target = 5\n        val offset = CONTACTS.size\n        val targetContact = CONTACTS[target]\n        val offsetContact = CONTACTS.last()\n        page.recycler.itemMatched(page.getItemMatcher(targetContact), 0, scrollOffset = offset)\n        page.recycler.item(page.getItemMatcher(offsetContact), autoScroll = false).isDisplayed().click()\n    }\n\n    //firstItemMatched+offset\n    @Test\n    fun firstItemMatched_scrollOffsetInItemCountRange_MatcherItem() {\n        val target = 5\n        val offset = 10\n        val targetContact = CONTACTS[target]\n        val offsetContact = CONTACTS[target + offset]\n        page.recycler.firstItemMatched(page.getItemMatcher(targetContact), scrollOffset = offset)\n        page.recycler.item(page.getItemMatcher(offsetContact), autoScroll = false).isDisplayed().click()\n    }\n\n    //lastItemMatched+offset\n    @Test\n    fun lastItemMatched_scrollOffsetInItemCountRange_MatcherItem() {\n        val target = 5\n        val offset = 10\n        val targetContact = CONTACTS[target]\n        val offsetContact = CONTACTS[target + offset]\n        page.recycler.lastItemMatched(page.getItemMatcher(targetContact), scrollOffset = offset)\n        page.recycler.item(page.getItemMatcher(offsetContact), autoScroll = false).isDisplayed().click()\n    }\n\n    //getItem+offset\n    @Test\n    fun getItem_scrollOffsetInItemCountRange_MatcherItem() {\n        val target = 5\n        val offset = 10\n        val targetContact = CONTACTS[target]\n        val offsetContact = CONTACTS[target + offset]\n        page.recycler.getItem<FriendsListPage.FriendRecyclerItem>(page.getItemMatcher(targetContact), scrollOffset = offset)\n        page.recycler.getItem<FriendsListPage.FriendRecyclerItem>(page.getItemMatcher(offsetContact), autoScroll = false).name.hasText(offsetContact.name).isDisplayed()\n    }\n\n    @Test\n    fun getItem_scrollOffsetInItemCountRangeBothAreVisible_MatcherItem() {\n        val target = 8\n        val offset = 2\n        val targetContact = CONTACTS[target]\n        val offsetContact = CONTACTS[target + offset]\n        page.recycler.getItem<FriendsListPage.FriendRecyclerItem>(page.getItemMatcher(targetContact), scrollOffset = offset)\n        page.recycler.getItem<FriendsListPage.FriendRecyclerItem>(page.getItemMatcher(offsetContact), autoScroll = false).name.hasText(offsetContact.name).isDisplayed()\n    }\n\n    @Test\n    fun getItem_scrollOffsetOutOfItemCountRange_MatcherItem() {\n        val target = 5\n        val offset = CONTACTS.size\n        val targetContact = CONTACTS[target]\n        val offsetContact = CONTACTS.last()\n        page.recycler.getItem<FriendsListPage.FriendRecyclerItem>(page.getItemMatcher(targetContact), scrollOffset = offset)\n        page.recycler.getItem<FriendsListPage.FriendRecyclerItem>(page.getItemMatcher(offsetContact), autoScroll = false).name.hasText(offsetContact.name).isDisplayed()\n    }\n\n    @Test\n    fun getItem_scrollOffsetLessThenZero_MatcherItem() {\n        val target = 8\n        val offset = -18\n        val targetContact = CONTACTS[target]\n        val offsetContact = CONTACTS.first()\n        page.recycler.getItem<FriendsListPage.FriendRecyclerItem>(page.getItemMatcher(targetContact), scrollOffset = offset)\n        page.recycler.getItem<FriendsListPage.FriendRecyclerItem>(page.getItemMatcher(offsetContact), autoScroll = false).name.hasText(offsetContact.name).isDisplayed()\n    }\n\n    @Test\n    fun getItem_scrollOffsetInItemCountRange_positionItem() {\n        val target = 2\n        val offset = 12\n        val offsetContact = CONTACTS[target + offset]\n        page.recycler.getItem<FriendsListPage.FriendRecyclerItem>(target, scrollOffset = offset)\n        page.recycler.getItem<FriendsListPage.FriendRecyclerItem>(target + offset, autoScroll = false).name.hasText(offsetContact.name).isDisplayed()\n    }\n\n    @Test\n    fun getItem_scrollOffsetInItemCountRangeBothAreVisible_positionItem() {\n        val target = 8\n        val offset = 2\n        val targetContact = CONTACTS[target]\n        val offsetContact = CONTACTS[target + offset]\n        page.recycler.getItem<FriendsListPage.FriendRecyclerItem>(target, scrollOffset = offset).name.hasText(targetContact.name).isDisplayed()\n        page.recycler.getItem<FriendsListPage.FriendRecyclerItem>(target + offset, autoScroll = false).name.hasText(offsetContact.name).isDisplayed()\n    }\n\n    @Test\n    fun getItem_scrollOffsetOutOfItemCountRange_positionItem() {\n        val target = 5\n        val offset = CONTACTS.size\n        val offsetContact = CONTACTS.last()\n        page.recycler.getItem<FriendsListPage.FriendRecyclerItem>(target, scrollOffset = offset)\n        page.recycler.getLastItem<FriendsListPage.FriendRecyclerItem>(autoScroll = false).name.hasText(offsetContact.name).isDisplayed()\n    }\n\n    @Test\n    fun getItem_scrollOffsetLessThenZero_positionItem() {\n        val target = 10\n        val offset = -18\n        val offsetContact = CONTACTS.first()\n        page.recycler.getItem<FriendsListPage.FriendRecyclerItem>(target, scrollOffset = offset)\n        page.recycler.getFirstItem<FriendsListPage.FriendRecyclerItem>(autoScroll = false).name.hasText(offsetContact.name).isDisplayed()\n    }\n\n    //getItemMatched+offset\n    @Test\n    fun getItemMatched_scrollOffsetInItemCountRange_MatcherItem() {\n        val target = 5\n        val offset = 10\n        val targetContact = CONTACTS[target]\n        val offsetContact = CONTACTS[target + offset]\n        page.recycler.getItemMatched<FriendsListPage.FriendRecyclerItem>(page.getItemMatcher(targetContact), 0, scrollOffset = offset)\n        page.recycler.getItem<FriendsListPage.FriendRecyclerItem>(page.getItemMatcher(offsetContact), autoScroll = false).name.hasText(offsetContact.name).isDisplayed()\n    }\n\n    @Test\n    fun getItemMatched_scrollOffsetInItemCountRangeBothAreVisible_MatcherItem() {\n        val target = 8\n        val offset = 2\n        val targetContact = CONTACTS[target]\n        val offsetContact = CONTACTS[target + offset]\n        page.recycler.getItemMatched<FriendsListPage.FriendRecyclerItem>(page.getItemMatcher(targetContact), 0, scrollOffset = offset)\n        page.recycler.getItem<FriendsListPage.FriendRecyclerItem>(page.getItemMatcher(offsetContact), autoScroll = false).name.hasText(offsetContact.name).isDisplayed()\n    }\n\n    @Test\n    fun getItemMatched_scrollOffsetOutOfItemCountRange_MatcherItem() {\n        val target = 5\n        val offset = CONTACTS.size\n        val targetContact = CONTACTS[target]\n        val offsetContact = CONTACTS.last()\n        page.recycler.getItemMatched<FriendsListPage.FriendRecyclerItem>(page.getItemMatcher(targetContact), 0, scrollOffset = offset)\n        page.recycler.getItem<FriendsListPage.FriendRecyclerItem>(page.getItemMatcher(offsetContact), autoScroll = false).name.hasText(offsetContact.name).isDisplayed()\n    }\n\n    //getFirstItemMatched+offset\n    @Test\n    fun getFirstItemMatched_scrollOffsetInItemCountRange_MatcherItem() {\n        val target = 5\n        val offset = 10\n        val targetContact = CONTACTS[target]\n        val offsetContact = CONTACTS[target + offset]\n        page.recycler.getFirstItemMatched<FriendsListPage.FriendRecyclerItem>(page.getItemMatcher(targetContact), scrollOffset = offset)\n        page.recycler.getItem<FriendsListPage.FriendRecyclerItem>(page.getItemMatcher(offsetContact), autoScroll = false).name.hasText(offsetContact.name).isDisplayed()\n    }\n\n    //getLastItemMatched+offset\n    @Test\n    fun getLastItemMatched_scrollOffsetInItemCountRange_MatcherItem() {\n        val target = 5\n        val offset = 10\n        val targetContact = CONTACTS[target]\n        val offsetContact = CONTACTS[target + offset]\n        page.recycler.getLastItemMatched<FriendsListPage.FriendRecyclerItem>(page.getItemMatcher(targetContact), scrollOffset = offset)\n        page.recycler.getItem<FriendsListPage.FriendRecyclerItem>(page.getItemMatcher(offsetContact), autoScroll = false).name.hasText(offsetContact.name).isDisplayed()\n    }\n\n    @Test\n    fun validItemCustomAssertion() {\n        val contact = CONTACTS.first()\n        page.recycler.firstItem().withAssertion(\"Toolbar title = ${contact.name}\") {\n            ChatPage.assertToolbarTitle(contact.name)\n        }.click()\n    }\n\n    @Test\n    fun invalidItemCustomAssertion() {\n        AssertUtils.assertException {\n            val invalidExpectedName = \"InvalidTitle\"\n            page.recycler.firstItem().withTimeout(3000).withAssertion(\"Toolbar title = $invalidExpectedName\") {\n                ChatPage.assertToolbarTitle(invalidExpectedName)\n            }.click()\n        }\n    }\n\n    @Test\n    fun swipeUntil() {\n        withId(R.id.recycler_friends).withAssertion(isListened = true) {\n            withText(CONTACTS.last().name).withTimeout(200).isDisplayed()\n        }.swipeUp()\n    }\n}\n\n"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso/UltronActivityRuleTest.kt",
    "content": "package com.atiurin.sampleapp.tests.espresso\n\nimport android.content.Intent\nimport androidx.test.ext.junit.rules.ActivityScenarioRule\nimport androidx.test.platform.app.InstrumentationRegistry\nimport com.atiurin.sampleapp.activity.BusyActivity\nimport com.atiurin.sampleapp.activity.MainActivity\nimport com.atiurin.sampleapp.tests.BaseTest\nimport com.atiurin.ultron.core.compose.createSimpleUltronComposeRule\nimport com.atiurin.ultron.core.compose.createUltronComposeRule\nimport com.atiurin.ultron.testlifecycle.activity.UltronActivityRule\nimport org.junit.Test\nimport org.junit.rules.Timeout\nimport java.util.concurrent.TimeUnit\n\nclass UltronActivityRuleTest: BaseTest() {\n\n    //private val activityTestRule = ActivityScenarioRule(MainActivity::class.java)\n    //private val activityTestRule = UltronActivityRule(MainActivity::class.java)\n    //private val activityTestRule = createUltronComposeRule<MainActivity>()\n    private val activityTestRule = createSimpleUltronComposeRule<MainActivity>()\n    private val timeoutRule: Timeout = Timeout\n        .builder()\n        .withTimeout(100, TimeUnit.SECONDS)\n        .withLookingForStuckThread(true)\n        .build()\n\n    init {\n        ruleSequence.add(timeoutRule, activityTestRule)\n    }\n\n    @Test\n    fun appNotIdle() {\n        val intent = Intent(\n            InstrumentationRegistry.getInstrumentation().targetContext,\n            BusyActivity::class.java\n        )\n        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n        InstrumentationRegistry.getInstrumentation().targetContext.startActivity(intent)\n        assert(true)\n    }\n}"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso/UltronEspressoConfigTest.kt",
    "content": "package com.atiurin.sampleapp.tests.espresso\n\nimport androidx.test.espresso.matcher.ViewMatchers\nimport com.atiurin.sampleapp.framework.DummyMetaObject\nimport com.atiurin.sampleapp.framework.Log\nimport com.atiurin.sampleapp.framework.utils.AssertUtils\nimport com.atiurin.sampleapp.pages.UiElementsPage\nimport com.atiurin.sampleapp.tests.UiElementsTest\nimport com.atiurin.ultron.core.common.resultanalyzer.UltronDefaultOperationResultAnalyzer\nimport com.atiurin.ultron.core.config.UltronCommonConfig\nimport com.atiurin.ultron.core.config.UltronConfig\nimport com.atiurin.ultron.core.espresso.EspressoOperationResult\nimport com.atiurin.ultron.core.espresso.UltronEspressoOperation\nimport com.atiurin.ultron.exceptions.UltronException\nimport com.atiurin.ultron.extensions.click\nimport com.atiurin.ultron.extensions.hasText\nimport com.atiurin.ultron.extensions.isDisplayed\nimport com.atiurin.ultron.extensions.isSuccess\nimport com.atiurin.ultron.extensions.withAssertion\nimport com.atiurin.ultron.extensions.withName\nimport com.atiurin.ultron.extensions.withResultHandler\nimport com.atiurin.ultron.extensions.withTimeout\nimport com.atiurin.ultron.testlifecycle.setupteardown.SetUp\nimport com.atiurin.ultron.testlifecycle.setupteardown.SetUpRule\nimport com.atiurin.ultron.testlifecycle.setupteardown.TearDown\nimport com.atiurin.ultron.testlifecycle.setupteardown.TearDownRule\nimport org.junit.Assert\nimport org.junit.Test\nimport kotlin.system.measureTimeMillis\n\nclass UltronEspressoConfigTest : UiElementsTest() {\n    val page = UiElementsPage\n\n    companion object {\n        const val SET_CUSTOM_RESULT_ANALYZER = \"SET_CUSTOM_RESULT_ANALYZER\"\n        const val SET_DEFAULT_CONFIG = \"SET_DEFAULT_CONFIG\"\n\n        const val SET_CUSTOM_ASSERTIONS_TIMEOUT = \"SET_ASSERTIONS_TIMEOUT\"\n        const val SET_CUSTOM_ACTIONS_TIMEOUT = \"SET_ACTIONS_TIMEOUT\"\n        const val SET_DEFAULT_TIMEOUT = \"DROP_DEFAULT_TIMEOUT\"\n        const val MODIFIED_OPERATIONS_TIMEOUT = 7_000L\n    }\n\n    val setUpRule = SetUpRule()\n        .add(SET_CUSTOM_RESULT_ANALYZER) {\n            UltronConfig.Espresso.setResultAnalyzer {\n                Log.debug(\"SET_CUSTOM_RESULT_ANALYZER ${it.success}\")\n                if (it.success) throw UltronException(\"Special reversed analyzer exception on ${it.description}\")\n                it.success\n            }\n        }.add(SET_CUSTOM_ASSERTIONS_TIMEOUT) {\n            UltronConfig.Espresso.ASSERTION_TIMEOUT =\n                MODIFIED_OPERATIONS_TIMEOUT\n        }\n        .add(SET_CUSTOM_ACTIONS_TIMEOUT) {\n            UltronConfig.Espresso.ACTION_TIMEOUT =\n                MODIFIED_OPERATIONS_TIMEOUT\n        }\n    private val tearDownRule = TearDownRule()\n        .add(SET_DEFAULT_CONFIG) {\n            UltronConfig.Espresso.resultAnalyzer = UltronDefaultOperationResultAnalyzer()\n        }\n        .add(SET_DEFAULT_TIMEOUT) {\n            UltronConfig.Espresso.ACTION_TIMEOUT = UltronCommonConfig.Defaults.OPERATION_TIMEOUT_MS\n            UltronConfig.Espresso.ASSERTION_TIMEOUT = UltronCommonConfig.Defaults.OPERATION_TIMEOUT_MS\n        }\n\n    init {\n        ruleSequence.add(setUpRule, tearDownRule)\n    }\n\n    @Test\n    @SetUp(SET_CUSTOM_RESULT_ANALYZER)\n    @TearDown(SET_DEFAULT_CONFIG)\n    fun resultAnalyzer_reversed_should_throw_on_success_action() {\n        AssertUtils.assertException { page.button.click() }\n    }\n\n    @Test\n    @SetUp(SET_CUSTOM_RESULT_ANALYZER)\n    @TearDown(SET_DEFAULT_CONFIG)\n    fun resultAnalyzer_reversed_should_NOT_throw_on_failed_action() {\n        page.notExistElement.withTimeout(100).click()\n    }\n\n    @Test\n    @SetUp(SET_CUSTOM_RESULT_ANALYZER)\n    @TearDown(SET_DEFAULT_CONFIG)\n    fun resultAnalyzer_reversed_should_throw_on_success_assertion() {\n        AssertUtils.assertException { page.button.isDisplayed() }\n    }\n\n    @Test\n    @SetUp(SET_CUSTOM_RESULT_ANALYZER)\n    @TearDown(SET_DEFAULT_CONFIG)\n    fun resultAnalyzer_reversed_should_NOT_throw_on_failed_assertion() {\n        page.notExistElement.withTimeout(100).isDisplayed()\n    }\n\n    //timeouts\n    @Test\n    fun withTimeout_action_default() {\n        val default = UltronCommonConfig.Defaults.OPERATION_TIMEOUT_MS\n        AssertUtils.assertExecTimeBetween(default, default + 5_000) { page.notExistElement.click() }\n    }\n\n    @Test\n    fun withTimeou_actiont_customValue() {\n        AssertUtils.assertExecTimeBetween(2_000, 4_500) {\n            page.notExistElement.withTimeout(2000).click()\n        }\n    }\n\n    @Test\n    @SetUp(SET_CUSTOM_ACTIONS_TIMEOUT)\n    @TearDown(\n        SET_DEFAULT_TIMEOUT\n    )\n    fun withTimeout_action_modifiedDefaultValue() {\n        AssertUtils.assertExecTimeBetween(\n            MODIFIED_OPERATIONS_TIMEOUT, MODIFIED_OPERATIONS_TIMEOUT + 2_000L\n        ) { page.notExistElement.click() }\n    }\n\n    @Test\n    fun withTimeout_assertion_default() {\n        val default = UltronCommonConfig.Defaults.OPERATION_TIMEOUT_MS\n        AssertUtils.assertExecTimeBetween(\n            default,\n            default + 2_000\n        ) { page.notExistElement.isDisplayed() }\n    }\n\n    @Test\n    fun withTimeout_assertion_customValue() {\n        AssertUtils.assertExecTimeBetween(2_000, 4_500) {\n            page.notExistElement.withTimeout(2000).isDisplayed()\n        }\n    }\n\n    @Test\n    @SetUp(SET_CUSTOM_ASSERTIONS_TIMEOUT)\n    @TearDown(\n        SET_DEFAULT_TIMEOUT\n    )\n    fun withTimeout_assertion_modifiedDefaultValue() {\n        AssertUtils.assertExecTimeBetween(\n            MODIFIED_OPERATIONS_TIMEOUT,\n            MODIFIED_OPERATIONS_TIMEOUT + 2_000L\n        ) { page.notExistElement.isDisplayed() }\n    }\n\n    //resultHandler\n    @Test\n    fun withResultHandler_action_default_true() {\n        var result: EspressoOperationResult<UltronEspressoOperation>? = null\n        page.button.withResultHandler {\n            result = it\n        }.click()\n        Assert.assertNotNull(result)\n        Assert.assertTrue(result!!.success)\n//        Assert.assertTrue(result!!.exceptions.isEmpty())\n        Assert.assertFalse(result!!.operation.name.isNullOrEmpty())\n        Assert.assertFalse(result!!.operation.description.isNullOrEmpty())\n        Assert.assertEquals(UltronConfig.Espresso.ACTION_TIMEOUT, result!!.operation.timeoutMs)\n    }\n\n    @Test\n    fun withResultHandler_action_default_false() {\n        var result: EspressoOperationResult<UltronEspressoOperation>? = null\n        page.notExistElement.withTimeout(100).withResultHandler {\n            result = it\n        }.click()\n        Assert.assertNotNull(result)\n        Assert.assertFalse(result!!.success)\n        Assert.assertTrue(result!!.exceptions.isNotEmpty())\n        Assert.assertFalse(result!!.operation.name.isNullOrEmpty())\n        Assert.assertFalse(result!!.operation.description.isNullOrEmpty())\n        Assert.assertEquals(100, result!!.operation.timeoutMs)\n    }\n\n\n    @Test\n    fun customAssertionTest() {\n        val text = \"some text\"\n        val execTime = measureTimeMillis {\n            page.editTextContentDesc.withAssertion(\"demo name\") {\n                page.editTextContentDesc.hasText(text)\n            }.replaceText(text)\n        }\n        Assert.assertTrue(execTime < UltronConfig.Espresso.ACTION_TIMEOUT)\n    }\n\n    @Test\n    fun withAssertion_failedAssertion() {\n        AssertUtils.assertException {\n            page.editTextContentDesc.withTimeout(1000).withAssertion {\n                ViewMatchers.withText(\"asd23213 12312\").withTimeout(500).isDisplayed()\n            }.typeText(\"1\")\n        }\n    }\n\n    @Test\n    fun withAssertion_failedAssertion_timeout() {\n        val operationTime = 1000L\n        val execTime = measureTimeMillis {\n            page.editTextContentDesc.isSuccess {\n                withTimeout(operationTime).withAssertion {\n                    ViewMatchers.withText(\"asd23213 12312\").withTimeout(100).isDisplayed()\n                }.typeText(\"1\")\n            }\n        }\n        Assert.assertTrue(execTime > operationTime)\n    }\n\n    @Test\n    fun withName_inOperationProps_ultronInteraction() {\n        val name = \"ElementName\"\n        page.notExistElement.withTimeout(100).withName(name).withResultHandler { result ->\n            Assert.assertEquals(name, result.operation.elementInfo.name)\n        }.isDisplayed()\n    }\n\n    @Test\n    fun withName_inOperationProps_matcherExt() {\n        val name = \"ElementName\"\n        page.notExistElement.withName(name).withTimeout(100).withResultHandler { result ->\n            Assert.assertEquals(name, result.operation.elementInfo.name)\n        }.isDisplayed()\n    }\n\n    @Test\n    fun withName_inExceptionMessage() {\n        val name = \"ElementNameToBeInException\"\n        runCatching {\n            page.notExistElement.withTimeout(100).withName(name).isDisplayed()\n        }.onFailure { exception ->\n            Assert.assertTrue(exception.message!!.contains(name))\n        }\n    }\n\n    @Test\n    fun withMeta() {\n        val meta = DummyMetaObject(\"ElementMetaInfo\")\n        page.notExistElement.withTimeout(100).withMetaInfo(meta).withResultHandler { result ->\n            Assert.assertEquals(meta, result.operation.elementInfo.meta)\n        }.isDisplayed()\n    }\n}"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso/UltronEspressoUiBlockTest.kt",
    "content": "package com.atiurin.sampleapp.tests.espresso\n\nimport android.view.View\nimport androidx.test.espresso.matcher.ViewMatchers.withId\nimport com.atiurin.sampleapp.R\nimport com.atiurin.sampleapp.activity.UiBlockActivity\nimport com.atiurin.sampleapp.data.repositories.CONTACTS\nimport com.atiurin.sampleapp.framework.utils.AssertUtils\nimport com.atiurin.sampleapp.pages.uiblock.EspressoUiBlockScreen\nimport com.atiurin.sampleapp.tests.BaseTest\nimport com.atiurin.ultron.core.common.assertion.softAssertion\nimport com.atiurin.ultron.core.espresso.UltronEspressoUiBlock\nimport com.atiurin.ultron.extensions.withName\nimport com.atiurin.ultron.extensions.withTimeout\nimport com.atiurin.ultron.testlifecycle.activity.UltronActivityRule\nimport org.hamcrest.Matcher\nimport org.junit.Assert\nimport org.junit.Rule\nimport org.junit.Test\n\nclass UltronEspressoUiBlockTest : BaseTest() {\n    @get:Rule\n    val activityRule = UltronActivityRule(UiBlockActivity::class.java)\n\n    @Test\n    fun notUniqueUiElement_WithDeepSearch() {\n        EspressoUiBlockScreen {\n            contactItem1.name.isDisplayed()\n            contactItem1.deepSearchChild.withTimeout(100).isDisplayed()\n        }\n    }\n\n    @Test\n    fun notUniqueUiElement_WithoutDeepSearch() {\n        EspressoUiBlockScreen {\n            AssertUtils.assertException {\n                blockWithoutDeepSearch.deepSearchFalse.withTimeout(100).isDisplayed()\n            }\n        }\n    }\n\n    @Test\n    fun uiBlockInBlock() {\n        EspressoUiBlockScreen {\n            contactsListBlock.item1.name.isDisplayed().hasText(CONTACTS[0].name)\n            contactsListBlock.item1.status.isDisplayed().hasText(CONTACTS[0].status)\n            contactsListBlock.item2.name.isDisplayed().hasText(CONTACTS[1].name)\n            contactsListBlock.item2.status.isDisplayed().hasText(CONTACTS[1].status)\n        }\n    }\n\n    @Test\n    fun childElementDescription() {\n        val descriptionPrefix = \"Item with parent\"\n        val blockDesc = \"Parent_Name\"\n        val expectedChildName = \"$descriptionPrefix $blockDesc\"\n\n        class BlockDesc(blockMatcher: Matcher<View>, blockDescription: String) : UltronEspressoUiBlock(blockMatcher, blockDescription) {\n            val name = child(withId(R.id.name)).withName(\"$descriptionPrefix $blockDescription\")\n        }\n        BlockDesc(withId(R.id.contact_item_1), blockDesc).name.isDisplayed().withResultHandler {\n            Assert.assertEquals(expectedChildName, it.operation.elementInfo.name)\n        }.withTimeout(100).hasText(\"Invalid text\")\n    }\n\n    @Test\n    fun childBlockDescriptionTest() {\n        val listBlockDesc = \"ListBlock\"\n        val expectedItem1Description = \"1 $descriptionPrefix $listBlockDesc\"\n        val expectedItem2Description = \"2 $descriptionPrefix $listBlockDesc\"\n        val expectedChildNameDescInBlock1 = \"$сhildNameDesc $expectedItem1Description\"\n        val expectedChildNameDescInBlock2 = \"$сhildNameDesc $expectedItem2Description\"\n\n        val listBlock = ListUiBlock(withId(R.id.contact_items), listBlockDesc)\n        softAssertion {\n            listBlock.item1.uiBlock.withTimeout(100).withResultHandler {\n                Assert.assertEquals(expectedItem1Description, it.operation.elementInfo.name)\n            }.hasText(\"Invalid\")\n            listBlock.item1.name.withTimeout(100).withResultHandler {\n                Assert.assertEquals(expectedChildNameDescInBlock1, it.operation.elementInfo.name)\n            }.hasText(\"Invalid\")\n            listBlock.item2.name.withTimeout(100).withResultHandler {\n                Assert.assertEquals(expectedChildNameDescInBlock2, it.operation.elementInfo.name)\n            }.hasText(\"Invalid\")\n        }\n    }\n}\n\nclass BlockDesc(blockMatcher: Matcher<View>, blockDescription: String) : UltronEspressoUiBlock(blockMatcher, blockDescription) {\n    val name = child(withId(R.id.name)).withName(\"$сhildNameDesc $blockDescription\")\n}\n\nclass ListUiBlock(blockMatcher: Matcher<View>, blockDescription: String) : UltronEspressoUiBlock(blockMatcher, blockDescription) {\n    val item1 = child(BlockDesc(withId(R.id.contact_item_1), \"1 $descriptionPrefix $blockDescription\"))\n    val item2 = child(\n        childMatcher = withId(R.id.contact_item_2),\n        uiBlockFactory = { updatedMatcher ->\n            BlockDesc(updatedMatcher, blockDescription = \"2 $descriptionPrefix $blockDescription\")\n        }\n    )\n}\n\nconst val descriptionPrefix = \"Item with parent\"\nconst val сhildNameDesc = \"NamE\"\n\n"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso/ViewInteractionActionsTest.kt",
    "content": "package com.atiurin.sampleapp.tests.espresso\n\n\nimport android.os.SystemClock\nimport android.view.KeyEvent\nimport androidx.test.espresso.action.EspressoKey\nimport androidx.test.espresso.action.ViewActions.click\nimport androidx.test.espresso.matcher.ViewMatchers.withText\nimport com.atiurin.sampleapp.R\nimport com.atiurin.sampleapp.framework.ultronext.appendText\nimport com.atiurin.sampleapp.framework.utils.AssertUtils\nimport com.atiurin.sampleapp.framework.utils.TestDataUtils.getResourceString\nimport com.atiurin.sampleapp.pages.UiElementsPage\nimport com.atiurin.sampleapp.tests.UiElementsTest\nimport com.atiurin.ultron.core.common.assertion.softAssertion\nimport com.atiurin.ultron.core.common.assertion.verifySoftAssertions\nimport com.atiurin.ultron.core.config.UltronCommonConfig\nimport com.atiurin.ultron.core.config.UltronConfig\nimport com.atiurin.ultron.core.espresso.UltronEspresso\nimport com.atiurin.ultron.custom.espresso.action.getContentDescription\nimport com.atiurin.ultron.custom.espresso.action.getDrawable\nimport com.atiurin.ultron.custom.espresso.action.getText\nimport com.atiurin.ultron.custom.espresso.assertion.hasAnyDrawable\nimport com.atiurin.ultron.custom.espresso.assertion.hasDrawable\nimport com.atiurin.ultron.extensions.clearText\nimport com.atiurin.ultron.extensions.click\nimport com.atiurin.ultron.extensions.closeSoftKeyboard\nimport com.atiurin.ultron.extensions.doubleClick\nimport com.atiurin.ultron.extensions.isDisplayed\nimport com.atiurin.ultron.extensions.isSameAs\nimport com.atiurin.ultron.extensions.isSuccess\nimport com.atiurin.ultron.extensions.longClick\nimport com.atiurin.ultron.extensions.perform\nimport com.atiurin.ultron.extensions.replaceText\nimport com.atiurin.ultron.extensions.textContains\nimport com.atiurin.ultron.extensions.withTimeout\nimport com.atiurin.ultron.utils.getTargetString\nimport org.junit.Assert\nimport org.junit.Test\n\nclass ViewInteractionActionsTest : UiElementsTest() {\n    val page = UiElementsPage\n\n    @Test\n    fun isSuccess_notExistedElement_return_false() {\n        val startTime = SystemClock.elapsedRealtime()\n        val result = page.notExistElement.isSuccess { isDisplayed() }\n        val endTime = SystemClock.elapsedRealtime()\n        Assert.assertTrue(endTime - startTime >= UltronConfig.Espresso.ASSERTION_TIMEOUT)\n        Assert.assertFalse(result)\n    }\n\n    @Test\n    fun isSuccess_existedElement_return_true() {\n        Assert.assertTrue(page.button.isSuccess { isDisplayed() })\n    }\n\n    @Test\n    fun click_onClickable() {\n        page.button.click()\n        page.eventStatus.textContains(getTargetString(R.string.button_event_click))\n    }\n\n    @Test\n    fun click_notExisted() {\n        AssertUtils.assertException { page.notExistElement.withTimeout(100).click() }\n    }\n\n    @Test\n    fun longClick_onLongClickable() {\n        page.button.longClick()\n        page.eventStatus.textContains(getTargetString(R.string.button_event_long_click))\n    }\n\n    @Test\n    fun longClick_notExisted() {\n        AssertUtils.assertException { page.notExistElement.withTimeout(100).longClick() }\n    }\n\n    @Test\n    fun doubleClick_onClickable() {\n        page.button.doubleClick()\n        page.button.withTimeout(1000).isDisplayed()\n        var success = false\n        with(page.eventStatus) {\n            textContains(getResourceString(R.string.button_event_click))\n            success = isSuccess { withTimeout(3000).textContains(\"1\") } || isSuccess {\n                withTimeout(2000).textContains(\"2\")\n            }\n        }\n        Assert.assertTrue(success)\n    }\n\n    @Test\n    fun doubleClick_notExisted() {\n        AssertUtils.assertException { page.notExistElement.withTimeout(100).doubleClick() }\n    }\n\n    @Test\n    fun typeText_onEditable() {\n        val text1 = \"begin\"\n        val text2 = \"simple text\"\n        page.editTextContentDesc\n            .replaceText(text1)\n            .typeText(text2)\n            .hasText(\"$text1$text2\")\n    }\n\n    @Test\n    fun typeText_onNotEditable() {\n        AssertUtils.assertException { page.eventStatus.withTimeout(100).typeText(\"simple text\") }\n    }\n\n    @Test\n    fun typeText_notExisted() {\n        AssertUtils.assertException { page.notExistElement.withTimeout(100).typeText(\"asd\") }\n    }\n\n    @Test\n    fun replaceText_onEditable() {\n        val text = \"simple text\"\n        page.editTextContentDesc.replaceText(text).hasText(text)\n    }\n\n    @Test\n    fun replaceText_notExisted() {\n        AssertUtils.assertException { page.notExistElement.withTimeout(100).replaceText(\"asd\") }\n    }\n\n    @Test\n    fun clearText_onEditable() {\n        page.editTextContentDesc.clearText().hasText(\"\")\n    }\n\n    @Test\n    fun clearText_notExisted() {\n        AssertUtils.assertException { page.notExistElement.withTimeout(100).clearText() }\n    }\n\n    @Test\n    fun pressKey_onEditable() {\n        val text = \"simple text\"\n        val expectedText = text.substring(0, text.length - 1)\n        page.editTextContentDesc\n            .replaceText(text)\n            .click()\n            .pressKey(KeyEvent.KEYCODE_DEL)\n            .hasText(expectedText)\n    }\n\n    @Test\n    fun pressKey_notExisted() {\n        AssertUtils.assertException {\n            page.notExistElement.withTimeout(100).pressKey(KeyEvent.KEYCODE_DEL)\n        }\n    }\n\n\n    @Test\n    fun pressEspressoKey_onEditable() {\n        val text = \"simple text\"\n        val expectedText = text.substring(0, text.length - 1)\n        page.editTextContentDesc\n            .replaceText(text)\n            .click()\n            .pressKey(EspressoKey.Builder().withKeyCode(KeyEvent.KEYCODE_DEL).build())\n            .hasText(expectedText)\n    }\n\n    @Test\n    fun pressEspressoKey_notExisted() {\n        AssertUtils.assertException {\n            page.notExistElement.withTimeout(100)\n                .pressKey(EspressoKey.Builder().withKeyCode(KeyEvent.KEYCODE_DEL).build())\n        }\n    }\n\n    @Test\n    fun closeSoftKeyboard_whenItOpened() {\n        page.editTextContentDesc.click()\n        SystemClock.sleep(500)\n        page.editTextContentDesc.closeSoftKeyboard()\n    }\n\n    @Test\n    fun preformCustomClick_onClickable() {\n        page.button.perform(click())\n        page.eventStatus.textContains(getResourceString(R.string.button_event_click))\n    }\n\n    @Test\n    fun performCustom_notExisted() {\n        AssertUtils.assertException { page.notExistElement.withTimeout(100).perform(click()) }\n    }\n\n    @Test\n    fun closeSoftKeyboardTest() {\n        page.editTextContentDesc.click()\n        UltronEspresso.closeSoftKeyboard()\n        page.imageView.isDisplayed()\n    }\n\n    @Test\n    fun customVisibilityAction() {\n        val text = \"appended\"\n        page.editTextContentDesc.appendText(text)\n            .hasText(getTargetString(R.string.button_default_content_desc) + text)\n        page.button.appendText(text)\n    }\n\n    @Test\n    fun notExist_customAction() {\n        AssertUtils.assertException { withText(\"not existed\").withTimeout(19).appendText(\"asd\") }\n    }\n\n    @Test\n    fun getTextActionTest_textExist() {\n        val text = page.appCompatTextView.getText()\n        Assert.assertEquals(getTargetString(R.string.app_compat_text), text)\n    }\n\n    @Test\n    fun getTextActionTest_noTextInView() {\n        AssertUtils.assertException { page.imageView.withTimeout(100).getText() }\n    }\n\n    @Test\n    fun getDrawable_drawableExist() {\n        Assert.assertNotNull(page.imageView.getDrawable())\n    }\n\n    @Test\n    fun hasDrawableTest() {\n        page.imageView.hasDrawable(R.drawable.ic_account)\n    }\n\n    @Test\n    fun hasDrawable_wrongResourceId() {\n        AssertUtils.assertException {\n            page.imageView.withTimeout(1000).hasDrawable(R.drawable.chandler)\n        }\n    }\n\n    @Test\n    fun drawableCompare() {\n        val actDr = page.imageView.getDrawable()\n        val actDr2 = page.imageView2.getDrawable()\n        Assert.assertTrue(actDr!!.isSameAs(actDr2!!))\n    }\n\n    @Test\n    fun hasAnyDrawable_noDrawable() {\n        AssertUtils.assertException {\n            page.emptyNotClickableImageView.withTimeout(1000).hasAnyDrawable()\n        }\n    }\n\n    @Test\n    fun hasAnyDrawable_imageHasDrawable() {\n        page.imageView.hasAnyDrawable()\n    }\n\n    @Test\n    fun getContentDesc_descIsNull() {\n        Assert.assertEquals(null, page.imageView.getContentDescription())\n    }\n\n    @Test\n    fun getContentDesc_descNotNull() {\n        Assert.assertEquals(\n            getTargetString(R.string.button_default_content_desc),\n            page.button.getContentDescription()\n        )\n    }\n\n    @Test\n    fun verifySoftAssertionsTest() {\n        UltronCommonConfig.testContext.softAnalyzer.clear()\n        softAssertion(false) {\n            withText(\"NotExistText\").withTimeout(100).click()\n            withText(\"NotExistTestTag\").withTimeout(100).click()\n        }\n        runCatching {\n            verifySoftAssertions()\n        }.onFailure { exception ->\n            val message = exception.message ?: throw RuntimeException(\"Empty exception message: $exception\")\n            Assert.assertTrue(message.contains(\"NotExistText\"))\n            Assert.assertTrue(message.contains(\"NotExistTestTag\"))\n        }\n    }\n\n    @Test\n    fun softAssertionTest() {\n        UltronCommonConfig.testContext.softAnalyzer.clear()\n        AssertUtils.assertException {\n            softAssertion {\n                withText(\"NotExistText\").withTimeout(100).click()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso/ViewInteractionAssertionsTest.kt",
    "content": "package com.atiurin.sampleapp.tests.espresso\n\nimport androidx.test.espresso.matcher.ViewMatchers.isDisplayed\nimport androidx.test.espresso.matcher.ViewMatchers.isEnabled\nimport androidx.test.espresso.matcher.ViewMatchers.withText\nimport com.atiurin.sampleapp.R\nimport com.atiurin.sampleapp.framework.ultronext.assertChecked\nimport com.atiurin.sampleapp.framework.utils.AssertUtils\nimport com.atiurin.sampleapp.framework.utils.TestDataUtils.getResourceString\nimport com.atiurin.sampleapp.pages.UiElementsPage\nimport com.atiurin.sampleapp.tests.UiElementsTest\nimport com.atiurin.ultron.core.common.assertion.softAssertion\nimport com.atiurin.ultron.core.common.assertion.verifySoftAssertions\nimport com.atiurin.ultron.core.config.UltronCommonConfig\nimport com.atiurin.ultron.custom.espresso.assertion.doesNotExistInAnyVisibleRoot\nimport com.atiurin.ultron.custom.espresso.assertion.hasCurrentHintTextColor\nimport com.atiurin.ultron.custom.espresso.assertion.hasCurrentTextColor\nimport com.atiurin.ultron.custom.espresso.assertion.hasHighlightColor\nimport com.atiurin.ultron.custom.espresso.assertion.hasShadowColor\nimport com.atiurin.ultron.custom.espresso.matcher.hasAnyDrawable\nimport com.atiurin.ultron.custom.espresso.matcher.withDrawable\nimport com.atiurin.ultron.extensions.assertMatches\nimport com.atiurin.ultron.extensions.click\nimport com.atiurin.ultron.extensions.doesNotExist\nimport com.atiurin.ultron.extensions.exists\nimport com.atiurin.ultron.extensions.hasContentDescription\nimport com.atiurin.ultron.extensions.hasFocus\nimport com.atiurin.ultron.extensions.hasText\nimport com.atiurin.ultron.extensions.isChecked\nimport com.atiurin.ultron.extensions.isClickable\nimport com.atiurin.ultron.extensions.isDisplayed\nimport com.atiurin.ultron.extensions.isEnabled\nimport com.atiurin.ultron.extensions.isFocusable\nimport com.atiurin.ultron.extensions.isJavascriptEnabled\nimport com.atiurin.ultron.extensions.isNotChecked\nimport com.atiurin.ultron.extensions.isNotClickable\nimport com.atiurin.ultron.extensions.isNotDisplayed\nimport com.atiurin.ultron.extensions.isNotEnabled\nimport com.atiurin.ultron.extensions.isNotFocusable\nimport com.atiurin.ultron.extensions.isNotSelected\nimport com.atiurin.ultron.extensions.isSelected\nimport com.atiurin.ultron.extensions.isSuccess\nimport com.atiurin.ultron.extensions.textContains\nimport com.atiurin.ultron.extensions.withTimeout\nimport org.hamcrest.Matchers.allOf\nimport org.hamcrest.Matchers.containsString\nimport org.junit.Assert\nimport org.junit.Test\n\nclass ViewInteractionAssertionsTest : UiElementsTest() {\n    val page = UiElementsPage\n\n    //displayed\n    @Test\n    fun isDisplayed_ofDisplayedObject() {\n        page.button.isDisplayed()\n    }\n\n    @Test\n    fun isDisplayed_ofNotDisplayedObject() {\n        page.radioInvisibleButton.click()\n        AssertUtils.assertException { page.button.withTimeout(100).isDisplayed() }\n    }\n\n    @Test\n    fun isNotDisplayed_ofDisplayedObject() {\n        page.radioVisibleButton.click()\n        AssertUtils.assertException { page.radioVisibleButton.withTimeout(100).isNotDisplayed() }\n    }\n\n    @Test\n    fun isNotDisplayed_ofNotDisplayedObject() {\n        page.radioInvisibleButton.click()\n        page.button.isNotDisplayed()\n    }\n\n    //doesNotExist\n    @Test\n    fun doesNotExist_notExisted() {\n        page.notExistElement.doesNotExist()\n    }\n\n    @Test\n    fun doesNotExist_existed() {\n        AssertUtils.assertException { page.button.withTimeout(100).doesNotExist() }\n    }\n\n    //doesNotExistAnyRoot\n    @Test\n    fun doesNotExistAnyRoot_notExisted() {\n        page.notExistElement.doesNotExistInAnyVisibleRoot()\n    }\n\n    @Test\n    fun doesNotExistAnyRoot_existed() {\n        AssertUtils.assertException { page.button.withTimeout(100).doesNotExistInAnyVisibleRoot() }\n    }\n\n    //exists\n    @Test\n    fun exists_ExistedHiddenView() {\n        page.hiddenButton.exists()\n    }\n\n    @Test\n    fun exists_NotExisted() {\n        AssertUtils.assertException { page.notExistElement.withTimeout(100).exists() }\n    }\n\n    //checked\n    @Test\n    fun isChecked_ofChecked() {\n        page.checkBoxEnabled.isChecked()\n    }\n\n    @Test\n    fun isChecked_ofNotChecked() {\n        AssertUtils.assertException { page.checkBoxSelected.withTimeout(100).isChecked() }\n    }\n\n    @Test\n    fun isNotChecked_ofChecked() {\n        AssertUtils.assertException { page.checkBoxClickable.withTimeout(100).isNotChecked() }\n    }\n\n    @Test\n    fun isNotChecked_ofNotChecked() {\n        page.checkBoxSelected.isNotChecked()\n    }\n\n    // selected\n    @Test\n    fun isSelected_ofSelected() {\n        page.checkBoxSelected.click()\n        page.button.isSelected()\n    }\n\n    @Test\n    fun isSelected_ofNotSelected() {\n        AssertUtils.assertException { page.button.withTimeout(100).isSelected() }\n    }\n\n    @Test\n    fun isNotSelected_ofSelected() {\n        page.checkBoxSelected.click()\n        AssertUtils.assertException { page.button.withTimeout(100).isNotSelected() }\n    }\n\n    @Test\n    fun isNotSelected_ofNotSelected() {\n        page.button.isNotSelected()\n    }\n\n    // enabled\n    @Test\n    fun isEnabled_ofEnabled() {\n        page.button.isEnabled()\n    }\n\n    @Test\n    fun isEnabled_ofNotEnabled() {\n        page.checkBoxEnabled.click()\n        AssertUtils.assertException { page.button.withTimeout(100).isEnabled() }\n    }\n\n    @Test\n    fun isNotEnabled_ofEnabled() {\n        AssertUtils.assertException { page.button.withTimeout(100).isNotEnabled() }\n    }\n\n    @Test\n    fun isNotEnabled_ofNotEnabled() {\n        page.checkBoxEnabled.click()\n        page.button.isNotEnabled()\n    }\n\n    //clickable\n    @Test\n    fun isClickable_ofClickable() {\n        page.button.isClickable()\n    }\n\n    @Test\n    fun isClickable_ofNotClickable() {\n        page.checkBoxClickable.click()\n        AssertUtils.assertException { page.button.withTimeout(100).isClickable() }\n    }\n\n    @Test\n    fun isNotClickable_ofClickable() {\n        AssertUtils.assertException { page.button.withTimeout(100).isNotClickable() }\n    }\n\n    @Test\n    fun isNotClickable_ofNotClickable() {\n        page.checkBoxClickable.click()\n        page.button.isNotClickable()\n    }\n\n    //focusable\n    @Test\n    fun isFocusable_ofFocusable() {\n        page.button.isFocusable()\n    }\n\n    @Test\n    fun isFocusable_ofNotFocusable() {\n        page.checkBoxFocusable.click()\n        AssertUtils.assertException { page.button.withTimeout(100).isFocusable() }\n    }\n\n    @Test\n    fun isNotFocusable_ofFocusable() {\n        AssertUtils.assertException { page.button.withTimeout(100).isNotFocusable() }\n    }\n\n    @Test\n    fun isNotFocusable_ofNotFocusable() {\n        page.checkBoxFocusable.click()\n        page.button.isNotFocusable()\n    }\n\n    //hasFocus\n    @Test\n    fun hasFocus_ofFocused() {\n        page.editTextContentDesc.click()\n        page.editTextContentDesc.hasFocus()\n    }\n\n    @Test\n    fun hasFocus_ofNotFocused() {\n        AssertUtils.assertException { page.editTextContentDesc.withTimeout(100).hasFocus() }\n    }\n\n    //hasText\n    @Test\n    fun hasText_CorrectText_withResourceId() {\n        page.editTextContentDesc.hasText(R.string.button_default_content_desc)\n    }\n\n    @Test\n    fun hasText_InvalidSubstringText() {\n        val text = getResourceString(R.string.button_default_content_desc)\n        AssertUtils.assertException { page.editTextContentDesc.withTimeout(100).hasText(text.substring(3)) }\n    }\n\n    @Test\n    fun hasText_InvalidText_withResourceId() {\n        AssertUtils.assertException {\n            page.editTextContentDesc.withTimeout(100).hasText(\n                R.string.action_clear_history\n            )\n        }\n    }\n\n    @Test\n    fun hasText_CorrectText_withString() {\n        val text = getResourceString(R.string.button_default_content_desc)\n        page.editTextContentDesc.hasText(text)\n    }\n\n    @Test\n    fun hasText_InvalidText_withString() {\n        val text = getResourceString(R.string.button_default_content_desc)\n        AssertUtils.assertException {\n            page.editTextContentDesc.withTimeout(100).hasText(\n                \"$text to be invalid\"\n            )\n        }\n    }\n\n    @Test\n    fun hasText_CorrectText_withStringMatcher() {\n        val text = getResourceString(R.string.button_default_content_desc)\n        page.editTextContentDesc.hasText(containsString(text.substring(2)))\n    }\n\n    @Test\n    fun hasText_InvalidText_withStringMatcher() {\n        val text = getResourceString(R.string.button_default_content_desc)\n        AssertUtils.assertException {\n            page.editTextContentDesc.withTimeout(100).hasText(\n                containsString(\"$text to be invalid\")\n            )\n        }\n    }\n\n    //containsText\n    @Test\n    fun containsText_CorrectText_withResourceId() {\n        val text = getResourceString(R.string.button_default_content_desc)\n        page.editTextContentDesc.textContains(text.substring(3))\n    }\n\n    @Test\n    fun containsText_InvalidSubstringText() {\n        val text = getResourceString(R.string.button_default_content_desc)\n        AssertUtils.assertException {\n            page.editTextContentDesc.withTimeout(100).textContains(\n                \"${text.substring(3)} to be invalid\"\n            )\n        }\n    }\n\n    //hasContentDescription\n    @Test\n    fun hasContentDescription_CorrectText_withResourceId() {\n        page.button.hasContentDescription(R.string.button_default_content_desc)\n    }\n\n    @Test\n    fun hasContentDescription_InvalidSubstringText() {\n        val text = getResourceString(R.string.button_default_content_desc)\n        AssertUtils.assertException { page.button.withTimeout(100).hasContentDescription(text.substring(3)) }\n    }\n\n    @Test\n    fun hasContentDescription_InvalidText_withResourceId() {\n        AssertUtils.assertException {\n            page.button.withTimeout(100).hasContentDescription(\n                R.string.action_clear_history\n            )\n        }\n    }\n\n    @Test\n    fun hasContentDescription_CorrectText_withString() {\n        val text = getResourceString(R.string.button_default_content_desc)\n        page.button.hasContentDescription(text)\n    }\n\n    @Test\n    fun hasContentDescription_InvalidText_withString() {\n        val text = getResourceString(R.string.button_default_content_desc)\n        AssertUtils.assertException {\n            page.button.withTimeout(100).hasContentDescription(\n                \"$text to be invalid\"\n            )\n        }\n    }\n\n    //contentDescriptionContains\n    @Test\n    fun contentDescriptionContains_CorrectText_withString() {\n        val text = getResourceString(R.string.button_default_content_desc)\n        page.button.withTimeout(100).contentDescriptionContains(text.substring(2))\n    }\n\n    @Test\n    fun contentDescriptionContains_InvalidText_withString() {\n        val text = getResourceString(R.string.button_default_content_desc)\n        AssertUtils.assertException {\n            page.button.withTimeout(100).contentDescriptionContains(\n                \"${text.substring(2)} to be invalid\"\n            )\n        }\n    }\n\n    //assertMatches\n    @Test\n    fun assertMatches_ofMatched() {\n        page.button.assertMatches(allOf(isDisplayed(), isEnabled(), withText(R.string.button_text)))\n    }\n\n    @Test\n    fun assertMatches_ofNotMatched() {\n        page.checkBoxEnabled.click()\n        AssertUtils.assertException {\n            page.button.withTimeout(100).assertMatches(\n                allOf(\n                    isDisplayed(),\n                    isEnabled(),\n                    withText(R.string.button_text)\n                ),\n            )\n        }\n    }\n\n    //javascriptEnabled\n    @Test\n    fun jsEnabled_ofEnabled() {\n        page.webView.isJavascriptEnabled()\n    }\n\n    @Test\n    fun jsEnabled_ofNotEnabled() {\n        page.checkBoxJsEnabled.click()\n        AssertUtils.assertException { page.webView.withTimeout(100).isJavascriptEnabled() }\n    }\n    //isSuccess\n\n    @Test\n    fun isSuccess_FalseTest() {\n        val success = page.radioVisibleButton.isSuccess { withTimeout(100).isNotDisplayed() }\n        Assert.assertFalse(success)\n    }\n\n    @Test\n    fun isSuccess_TrueTest() {\n        val success = page.radioVisibleButton.isSuccess { withTimeout(100).isDisplayed() }\n        Assert.assertTrue(success)\n    }\n\n    @Test\n    fun isSuccess_NotExist_FalseTest() {\n        val success = page.notExistElement.isSuccess { withTimeout(100).isDisplayed() }\n        Assert.assertFalse(success)\n    }\n\n    // withAppCompatTextView\n\n    @Test\n    fun appCompatTextView_assertText() {\n        page.appCompatTextView.hasText(getResourceString(R.string.app_compat_text))\n    }\n\n    // hasDrawable\n\n    @Test\n    fun hasDrawable_viewHasDrawable() {\n        page.imageView.assertMatches(hasAnyDrawable())\n    }\n\n    @Test\n    fun hasDrawable_viewHasNoDrawable() {\n        AssertUtils.assertException { page.emptyNotClickableImageView.withTimeout(100).assertMatches(hasAnyDrawable()) }\n    }\n\n    @Test\n    fun withDrawable_correctDrawable() {\n        page.imageView.assertMatches(withDrawable(R.drawable.ic_account))\n    }\n\n    @Test\n    fun withDrawable_invalidDrawable() {\n        AssertUtils.assertException {\n            page.imageView.withTimeout(100).assertMatches(\n                withDrawable(R.drawable.ic_attach_file)\n            )\n        }\n    }\n\n    @Test\n    fun hasCurrentTextColor() {\n        page.eventStatus.hasCurrentTextColor(R.color.colorPrimary)\n    }\n\n    @Test\n    fun hasCurrentTextColor_invalidColor() {\n        AssertUtils.assertException { page.eventStatus.withTimeout(100).hasCurrentTextColor(R.color.invalid) }\n    }\n\n    @Test\n    fun hasCurrentHintTextColor() {\n        page.eventStatus.hasCurrentHintTextColor(R.color.colorHint)\n    }\n\n    @Test\n    fun hasCurrentHintTextColor_invalidColor() {\n        AssertUtils.assertException { page.eventStatus.withTimeout(100).hasCurrentHintTextColor(R.color.invalid) }\n    }\n\n    @Test\n    fun hasShadowColor() {\n        page.eventStatus.hasShadowColor(R.color.colorShadow)\n    }\n\n    @Test\n    fun hasShadowColor_invalidColor() {\n        AssertUtils.assertException { page.eventStatus.withTimeout(100).hasShadowColor(R.color.invalid) }\n    }\n\n    @Test\n    fun hasHighlightColor() {\n        page.eventStatus.hasHighlightColor(R.color.colorHighlight)\n    }\n\n    @Test\n    fun hasHighlightColor_invalidColor() {\n        AssertUtils.assertException { page.eventStatus.withTimeout(100).hasHighlightColor(R.color.invalid) }\n    }\n\n    @Test\n    fun textViewColors() {\n        page.eventStatus\n            .hasCurrentTextColor(R.color.colorPrimary)\n            .hasCurrentHintTextColor(R.color.colorHint)\n            .hasShadowColor(R.color.colorShadow)\n            .hasHighlightColor(R.color.colorHighlight)\n    }\n\n    @Test\n    fun appCompatTextViewTextColor() {\n        page.appCompatTextView.hasCurrentTextColor(R.color.colorPrimary)\n    }\n\n    @Test\n    fun appCompatTextViewTextColor_invalidColor() {\n        AssertUtils.assertException { page.appCompatTextView.withTimeout(100).hasCurrentTextColor(R.color.invalid) }\n    }\n\n    @Test\n    fun customAssertTest() {\n        page.checkBoxEnabled.assertChecked(true)\n    }\n\n    @Test\n    fun customAssertTest_invalidValue() {\n        AssertUtils.assertException { page.checkBoxEnabled.withTimeout(100).assertChecked(false) }\n    }\n\n    @Test\n    fun notExist_customAssertion() {\n        AssertUtils.assertException { withText(\"not exist\").withTimeout(100).assertChecked(true) }\n    }\n\n    @Test\n    fun verifySoftAssertionsTest() {\n        UltronCommonConfig.testContext.softAnalyzer.clear()\n        softAssertion(false) {\n            withText(\"NotExistText\").withTimeout(100).isDisplayed()\n            withText(\"NotExistTestTag\").withTimeout(100).isDisplayed()\n        }\n        runCatching {\n            verifySoftAssertions()\n        }.onFailure { exception ->\n            val message = exception.message ?: throw RuntimeException(\"Empty exception message: $exception\")\n            Assert.assertTrue(message.contains(\"NotExistText\"))\n            Assert.assertTrue(message.contains(\"NotExistTestTag\"))\n        }\n    }\n\n    @Test\n    fun softAssertionTest() {\n        UltronCommonConfig.testContext.softAnalyzer.clear()\n        AssertUtils.assertException {\n            softAssertion {\n                withText(\"NotExistText\").withTimeout(100).isDisplayed()\n            }\n        }\n    }\n}\n\n"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso/ViewTest.kt",
    "content": "package com.atiurin.sampleapp.tests.espresso\n\nimport android.content.Intent\nimport android.widget.Button\nimport android.widget.RadioButton\nimport androidx.test.core.app.ActivityScenario\nimport androidx.test.espresso.matcher.ViewMatchers.withId\nimport androidx.test.espresso.matcher.ViewMatchers.withText\nimport androidx.test.platform.app.InstrumentationRegistry\nimport com.atiurin.sampleapp.R\nimport com.atiurin.sampleapp.activity.CustomClicksActivity\nimport com.atiurin.sampleapp.async.task.CompatAsyncTask.Companion.ASYNC\nimport com.atiurin.sampleapp.async.task.CompatAsyncTask.Companion.COMPAT_ASYNC_TASK_TIME_EXECUTION\nimport com.atiurin.sampleapp.framework.ultronext.getViewSimple\nimport com.atiurin.sampleapp.tests.BaseTest\nimport com.atiurin.ultron.custom.espresso.action.getView\nimport com.atiurin.ultron.custom.espresso.base.getViewForcibly\nimport com.atiurin.ultron.extensions.isChecked\nimport com.atiurin.ultron.extensions.perform\nimport com.atiurin.ultron.extensions.performOnView\nimport com.atiurin.ultron.extensions.performOnViewForcibly\nimport com.atiurin.ultron.testlifecycle.setupteardown.SetUp\nimport com.atiurin.ultron.testlifecycle.setupteardown.SetUpRule\nimport com.atiurin.ultron.testlifecycle.setupteardown.TearDown\nimport com.atiurin.ultron.testlifecycle.setupteardown.TearDownRule\nimport org.junit.Assert\nimport org.junit.Test\n\nclass ViewTest : BaseTest() {\n\n    private lateinit var customClicksActivity: CustomClicksActivity\n\n    private val startActivity = SetUpRule()\n        .add(START_ACTIVITY) {\n            ActivityScenario.launch(CustomClicksActivity::class.java).onActivity { activity ->\n                customClicksActivity = activity\n            }\n        }\n        .add(START_ACTIVITY_WITH_ASYNC_TASK) {\n            val intent = Intent(InstrumentationRegistry.getInstrumentation().targetContext, CustomClicksActivity::class.java)\n            intent.putExtra(ASYNC, true)\n            ActivityScenario.launch<CustomClicksActivity>(intent).onActivity { activity ->\n                customClicksActivity = activity\n            }\n        }\n\n    private val tearDownRule = TearDownRule().add(STOP_ASYNC_TASK) {\n        customClicksActivity.stopCompatAsyncTask()\n    }\n\n    init {\n        ruleSequence.add(startActivity).add(tearDownRule)\n    }\n\n    @Test\n    @SetUp(START_ACTIVITY)\n    fun actionGetView() {\n        val view = withId(R.id.button).getView()\n        Assert.assertNotNull(view)\n        Assert.assertTrue((view as Button).text.isNotBlank())\n    }\n\n    @Test\n    @SetUp(START_ACTIVITY)\n    fun actionGetViewSimple() {\n        val view = withId(R.id.button).getViewSimple()\n        Assert.assertNotNull(view)\n        Assert.assertTrue((view as Button).text.isNotBlank())\n    }\n\n    @Test\n    @SetUp(START_ACTIVITY_WITH_ASYNC_TASK)\n    @TearDown(STOP_ASYNC_TASK)\n    fun actionGetViewForcibly() {\n        val startTime = System.currentTimeMillis()\n        val view = withId(R.id.rB_top_right).getViewForcibly()\n        Assert.assertNotNull(view)\n        Assert.assertTrue(System.currentTimeMillis() < COMPAT_ASYNC_TASK_TIME_EXECUTION + startTime)\n    }\n\n    @Test\n    @SetUp(START_ACTIVITY)\n    fun matcherActionPerformOnView_Deprecated() {\n        withId(R.id.rB_top_left).apply {\n            performOnView {\n                performClick()\n                Assert.assertTrue((this as RadioButton).isChecked)\n            }\n            isChecked()\n        }\n    }\n\n    @Test\n    @SetUp(START_ACTIVITY)\n    fun matcherActionPerform() {\n        withId(R.id.rB_top_left).apply {\n            perform { _, view ->\n                view.performClick()\n                Assert.assertTrue((view as RadioButton).isChecked)\n            }\n            isChecked()\n        }\n    }\n\n\n    @Test\n    @SetUp(START_ACTIVITY_WITH_ASYNC_TASK)\n    @TearDown(STOP_ASYNC_TASK)\n    fun matcherActionPerformOnViewForcibly() {\n        val startTime = System.currentTimeMillis()\n        withId(R.id.rB_top_left).apply {\n            performOnViewForcibly {\n                performClick()\n                Assert.assertTrue((this as RadioButton).isChecked)\n            }\n        }\n        Assert.assertTrue(System.currentTimeMillis() < COMPAT_ASYNC_TASK_TIME_EXECUTION + startTime)\n    }\n\n    companion object {\n        const val STOP_ASYNC_TASK = \"STOP_ASYNC_TASK\"\n        const val START_ACTIVITY = \"START_ACTIVITY\"\n        const val START_ACTIVITY_WITH_ASYNC_TASK = \"START_ACTIVITY_WITH_ASYNC_TASK\"\n    }\n}"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso/WithSuitableRootTest.kt",
    "content": "package com.atiurin.sampleapp.tests.espresso\n\nimport com.atiurin.sampleapp.activity.MainActivity\nimport com.atiurin.sampleapp.data.repositories.ContactRepositoty\nimport com.atiurin.sampleapp.pages.ChatPage\nimport com.atiurin.sampleapp.pages.FriendsListPage\nimport com.atiurin.sampleapp.tests.BaseTest\nimport com.atiurin.ultron.testlifecycle.activity.UltronActivityRule\nimport org.junit.Test\n\nclass WithSuitableRootTest : BaseTest() {\n\n    private val activityTestRule = UltronActivityRule(MainActivity::class.java)\n\n    init {\n        ruleSequence.addLast(activityTestRule)\n    }\n\n    @Test\n    fun withSuitableRootForViewMatcher() {\n        FriendsListPage\n            .getListItem(0).click()\n        ChatPage\n            .assertToolbarTitleWithSuitableRoot(ContactRepositoty.getFirst().name)\n    }\n\n    @Test\n    fun withSuitableRootForRecyclerMatcher() {\n        FriendsListPage\n            .assertPageDisplayedWithSuitableRoot()\n    }\n\n    @Test\n    fun withSuitableRootForRecyclerItemMatcher() {\n        FriendsListPage\n            .assertPageDisplayedWithSuitableRoot()\n            .getListItem(0).withSuitableRoot().isDisplayed().click()\n        ChatPage\n            .assertToolbarTitle(ContactRepositoty.getFirst().name)\n    }\n}"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso_web/BaseWebViewTest.kt",
    "content": "package com.atiurin.sampleapp.tests.espresso_web\n\nimport androidx.test.core.app.ActivityScenario\nimport com.atiurin.sampleapp.activity.WebViewActivity\nimport com.atiurin.sampleapp.pages.WebViewPage\nimport com.atiurin.sampleapp.tests.BaseTest\nimport com.atiurin.ultron.testlifecycle.setupteardown.SetUpRule\n\nabstract class BaseWebViewTest : BaseTest() {\n    val page = WebViewPage()\n\n    private val startActivity = SetUpRule().add {\n        ActivityScenario.launch(WebViewActivity::class.java)\n//        UltronWebDocument.forceJavascriptEnabled()\n    }\n\n    init {\n        ruleSequence.addLast(startActivity)\n    }\n}"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso_web/EspressoWebUiElementsTest.kt",
    "content": "package com.atiurin.sampleapp.tests.espresso_web\n\nimport androidx.test.espresso.matcher.ViewMatchers.withId\nimport androidx.test.espresso.web.assertion.WebViewAssertions.webContent\nimport androidx.test.espresso.web.assertion.WebViewAssertions.webMatches\nimport androidx.test.espresso.web.matcher.DomMatchers.elementById\nimport androidx.test.espresso.web.matcher.DomMatchers.withTextContent\nimport androidx.test.espresso.web.sugar.Web.onWebView\nimport androidx.test.espresso.web.webdriver.DriverAtoms.*\nimport androidx.test.espresso.web.webdriver.Locator\nimport com.atiurin.sampleapp.R\nimport com.atiurin.sampleapp.framework.utils.AssertUtils\nimport com.atiurin.ultron.core.config.UltronConfig\nimport com.atiurin.ultron.core.espressoweb.webelement.UltronWebDocument\nimport com.atiurin.ultron.core.espressoweb.webelement.UltronWebDocument.Companion.evalJS\nimport com.atiurin.ultron.core.espressoweb.webelement.UltronWebElement.Companion.className\nimport com.atiurin.ultron.core.espressoweb.webelement.UltronWebElement.Companion.id\nimport com.atiurin.ultron.core.espressoweb.webelement.UltronWebElement.Companion.linkText\nimport com.atiurin.ultron.core.espressoweb.webelement.UltronWebElement.Companion.xpath\nimport com.atiurin.ultron.core.espressoweb.webelement.UltronWebElements\nimport com.atiurin.ultron.core.espressoweb.webelement.UltronWebElements.Companion.classNames\nimport org.hamcrest.CoreMatchers.containsString\nimport org.hamcrest.Matchers.`is`\nimport org.junit.Assert\nimport org.junit.Test\n\nclass EspressoWebUiElementsTest : BaseWebViewTest() {\n\n    @Test\n    fun simpleWebViewTest() {\n        val newTitle = \"New title\"\n        onWebView()\n            .forceJavascriptEnabled()\n            .withElement(findElement(Locator.ID, \"text_input\"))\n            .perform(webKeys(newTitle))\n            .withElement(findElement(Locator.ID, \"button1\"))\n            .perform(webClick())\n            .withElement(findElement(Locator.ID, \"title\"))\n            .check(webMatches(getText(), containsString(\"New title\")))\n        //findMultipleElements\n//        onWebView().perform(findMultipleElements(Locator.CLASS_NAME, \"button\"))\n//            .get()\n//            .forEach {\n//                onWebView().withElement(it).perform(webClick())\n//            }\n    }\n\n    @Test\n    fun multipleElementsWebViewTest() {\n        UltronWebElements(Locator.CLASS_NAME, \"button\").getElements().forEach {\n            it.webClick()\n        }\n        UltronWebElements(Locator.CLASS_NAME, \"link\").getElements()\n            .filter {\n                it.isSuccess {\n                    withTimeout(100).hasText(\"Apple\")\n                }\n            }\n            .forEach { it.webClick() }\n        classNames(\"link\").getElements()\n            .filter {\n                it.isSuccess {\n                    withTimeout(100).hasText(\"Apple\")\n                }\n            }\n            .forEach { it.webClick() }\n        page.title.containsText(\"apple\")\n    }\n\n    @Test\n    fun pageMultipleElements() {\n        page.buttons.getElements().forEach {\n            it.webClick()\n        }\n    }\n\n    @Test\n    fun extVar2WebViewTest() {\n        val newTitle = \"New title\"\n        id(\"text_input\").webKeys(newTitle).webKeys(\"and more\").replaceText(newTitle)\n        id(\"button1\").webClick()\n        id(\"title\").hasText(newTitle)\n        className(\"css_title\").containsText(newTitle)\n        linkText(\"Apple\").containsText(\"Apple\")\n    }\n\n    @Test\n    fun jsEvaluationTest() {\n        val jsTitle = \"JS_TITLE\"\n        val jsTitleNew = \"JS_TITLE_NEW\"\n        evalJS(\"document.getElementById(\\\"title\\\").innerHTML = '$jsTitle';\")\n        className(\"css_title\", withId(R.id.webview)).containsText(jsTitle)\n//        onWebView().script(\"document.getElementById(\\\"title\\\").innerHTML = '$jsTitleNew';\")\n//        WebElement(Locator.CLASS_NAME,\"css_title\", withId(R.id.webview)).containsText(jsTitleNew)\n    }\n\n    @Test\n    fun webViewFinderTest() {\n        val jsTitleNew = \"JS_TITLE_NEW\"\n        UltronConfig.Espresso.webViewMatcher = withId(R.id.webview)\n        evalJS(\"document.getElementById(\\\"title\\\").innerHTML = '$jsTitleNew';\")\n        className(\"css_title\").containsText(jsTitleNew)\n    }\n\n    @Test\n    fun pageVar3WebViewTest() {\n        val newTitle = \"New title\"\n        page.textInput.webKeys(newTitle)\n        page.buttonUpdTitle.webClick()\n        page.title.containsText(newTitle)\n        page.titleWithCss.containsText(newTitle)\n        AssertUtils.assertException { page.appleLink.withTimeout(100).containsText(\"Apple12312\") }\n    }\n\n    @Test\n    fun getText2Test() {\n        val newTitle = \"New title\"\n        page.textInput.webKeys(newTitle)\n        page.buttonUpdTitle.webClick()\n        page.title.hasText(newTitle)\n        val titleText = page.title.getText()\n        Assert.assertEquals(newTitle, titleText)\n    }\n\n    @Test\n    fun checkButtonTextTest() {\n        xpath(\".//*[@id='button3']\").apply {\n            exists()\n            hasAttribute(\"value\", \"Set title active\")\n        }\n    }\n\n    @Test\n    fun webInteractionLyambda() {\n        Assert.assertTrue(id(\"button2\").getText().isBlank())\n        Assert.assertEquals(\"Apple\", id(\"apple_link\").getText())\n    }\n\n    @Test\n    fun elementNotPresentDefaultTimeout() {\n        AssertUtils.assertExecTimeBetween(5_000, 7_000) { id(\"asdasdasd\").getText() }\n    }\n\n    @Test\n    fun elementNotPresentCustomTimeout() {\n        AssertUtils.assertExecTimeBetween(1_000, 3_000) { id(\"asdasdasd\").withTimeout(2000).getText() }\n    }\n\n    @Test\n    fun customWebViewAssertionTest() {\n        UltronWebDocument.assertThat(webContent(elementById(\"apple_link\", withTextContent(\"Apple\"))))\n        id(\"apple_link\").hasAttribute(\"href\", `is`(\"fake_link.html\"))\n    }\n\n    @Test\n    fun customWebAssertionTest() {\n        id(\"apple_link\").assertThat(webMatches(getText(), `is`(\"Apple\")))\n    }\n\n    @Test\n    fun contextualElement() {\n        id(\"student\")\n            .withContextual(className(\"person_name\"))\n            .hasText(\"Plato\")\n    }\n}"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso_web/UltronWebDocumentTest.kt",
    "content": "package com.atiurin.sampleapp.tests.espresso_web\n\nimport androidx.test.espresso.web.assertion.WebViewAssertions.webContent\nimport androidx.test.espresso.web.matcher.DomMatchers.elementById\nimport androidx.test.espresso.web.matcher.DomMatchers.withTextContent\nimport com.atiurin.sampleapp.framework.utils.AssertUtils\nimport com.atiurin.ultron.core.espressoweb.webelement.UltronWebDocument\nimport com.atiurin.ultron.core.espressoweb.webelement.UltronWebDocument.Companion.evalJS\nimport org.junit.Assert\nimport org.junit.Test\n\nclass UltronWebDocumentTest : BaseWebViewTest() {\n    @Test\n    fun forceJS_Test() {\n        UltronWebDocument.forceJavascriptEnabled()\n    }\n\n    @Test\n    fun evalJS_Test() {\n        val title = \"SOME NEW TITLE\"\n        evalJS(\"document.getElementById(\\\"title\\\").innerHTML = '$title';\")\n        page.title.hasText(title)\n    }\n\n    @Test\n    fun assertThat_validValueTest() {\n        UltronWebDocument.assertThat(\n            webContent(\n                elementById(\n                    \"apple_link\",\n                    withTextContent(\"Apple\")\n                )\n            )\n        )\n    }\n\n    @Test\n    fun assertThat_invalidValueTest() {\n        AssertUtils.assertException {\n            UltronWebDocument.assertThat(\n                webContent(\n                    elementById(\n                        \"apple_link\",\n                        withTextContent(\"Apple1123123\")\n                    )\n                ),\n                timeoutMs = 100\n            )\n        }\n    }\n\n    @Test\n    fun setActiveElement(){\n        page.buttonSetTitleActive.webClick()\n        val elementRef = UltronWebDocument.selectActiveElement()\n        Assert.assertNotNull(elementRef)\n    }\n\n    @Test\n    fun selectFrame_validElement(){\n        val frame = UltronWebDocument.selectFrameByIdOrName(\"iframe\")\n        Assert.assertNotNull(frame)\n    }\n\n    @Test\n    fun selectFrame_invalidNameOrId(){\n        AssertUtils.assertException { UltronWebDocument.selectFrameByIdOrName(\"asdhgasdlkjasdasd\", timeoutMs = 100) }\n    }\n}"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso_web/UltronWebElementTest.kt",
    "content": "package com.atiurin.sampleapp.tests.espresso_web\n\nimport androidx.test.espresso.web.assertion.WebViewAssertions.webMatches\nimport androidx.test.espresso.web.webdriver.DriverAtoms.getText\nimport com.atiurin.sampleapp.framework.DummyMetaObject\nimport com.atiurin.sampleapp.framework.ultronext.appendText\nimport com.atiurin.sampleapp.framework.utils.AssertUtils\nimport com.atiurin.sampleapp.pages.WebViewPage\nimport com.atiurin.ultron.core.common.assertion.softAssertion\nimport com.atiurin.ultron.core.common.assertion.verifySoftAssertions\nimport com.atiurin.ultron.core.config.UltronCommonConfig\nimport com.atiurin.ultron.core.espressoweb.webelement.UltronWebElement.Companion.className\nimport com.atiurin.ultron.core.espressoweb.webelement.UltronWebElement.Companion.id\nimport com.atiurin.ultron.core.espressoweb.webelement.UltronWebElement.Companion.xpath\nimport org.hamcrest.Matchers.`is`\nimport org.junit.Assert\nimport org.junit.Test\n\nclass UltronWebElementTest : BaseWebViewTest() {\n    @Test\n    fun webClick_onExistElement() {\n        page.buttonSetTitle2.webClick()\n        page.title.hasText(WebViewPage.BUTTON2_TITLE)\n    }\n\n    @Test\n    fun webClick_onNotExistedElement() {\n        AssertUtils.assertException { xpath(\"notExistId\").withTimeout(100).withName(\"Custome name\").webClick() }\n    }\n\n    @Test\n    fun webClick_isSuccessTrueValue() {\n        val success = page.buttonSetTitle2.isSuccess { webClick() }\n        Assert.assertTrue(success)\n    }\n\n    @Test\n    fun webClick_isSuccessFalseValue() {\n        val success = id(\"notExistId\").isSuccess { withTimeout(100).webClick() }\n        Assert.assertFalse(success)\n    }\n\n    @Test\n    fun exists_onExistElement() {\n        page.buttonUpdTitle.exists()\n    }\n\n    @Test\n    fun exists_onNotExistedElement() {\n        AssertUtils.assertException { id(\"notExistId\").withTimeout(100).exists() }\n    }\n\n    @Test\n    fun exists_isSuccessTrueValue() {\n        val success = page.buttonSetTitle2.isSuccess { exists() }\n        Assert.assertTrue(success)\n    }\n\n    @Test\n    fun exists_isSuccessFalseValue() {\n        val success = id(\"notExistId\").isSuccess { withTimeout(100).exists() }\n        Assert.assertFalse(success)\n    }\n\n    @Test\n    fun clearElement_onExistedEditableElement() {\n        page.textInput.webKeys(\"initial text\").clearElement().hasText(\"\")\n    }\n\n    @Test\n    fun clearElement_onExistedNotEditableElement() {\n        AssertUtils.assertException { page.buttonUpdTitle.withTimeout(100).clearElement() }\n    }\n\n    @Test\n    fun clearElement_onNotExistedElement() {\n        AssertUtils.assertException { id(\"notExistId\").withTimeout(100).clearElement() }\n    }\n\n    @Test\n    fun getText_onExistedTextContainerElement() {\n        val text = \"some text 2\"\n        page.textInput.replaceText(text)\n        page.buttonUpdTitle.webClick()\n        val receivedText = page.title.getText()\n        Assert.assertEquals(text, receivedText)\n    }\n\n    @Test\n    fun getText_onNotExistedElement() {\n        AssertUtils.assertException { id(\"notExistId\").withTimeout(100).getText() }\n    }\n\n    @Test\n    fun webKeys_onExistedTextContainerElement() {\n        val text = \"some text 3\"\n        page.textInput.clearElement().webKeys(text)\n        page.buttonUpdTitle.webClick()\n        page.title.hasText(text)\n    }\n\n    @Test\n    fun webKeys_onNotExistedElement() {\n        AssertUtils.assertException { id(\"notExistId\").withTimeout(100).webKeys(\"asd\") }\n    }\n\n    @Test\n    fun replaceText_and_hasText_onExistedElement() {\n        val text = \"some text 3\"\n        page.textInput.replaceText(text)\n        page.buttonUpdTitle.webClick()\n        page.title.hasText(text)\n    }\n\n    @Test\n    fun replaceText_onNotExistedElement() {\n        AssertUtils.assertException { id(\"notExistId\").withTimeout(100).replaceText(\"asd\") }\n    }\n\n    @Test\n    fun hasText_onNotExistedElement() {\n        AssertUtils.assertException { id(\"notExistId\").withTimeout(100).hasText(\"asd\") }\n    }\n\n    @Test\n    fun containsText_onExistedElement() {\n        val text = \"some text 3\"\n        page.textInput.replaceText(text + \"additional\")\n        page.buttonUpdTitle.webClick()\n        page.title.containsText(text)\n    }\n\n    @Test\n    fun containsText_onNotExistedElement() {\n        AssertUtils.assertException { id(\"notExistId\").withTimeout(100).containsText(\"asd\") }\n    }\n\n    @Test\n    fun hasAttribute_withMatcher_onExistedElement() {\n        page.appleLink.hasAttribute(\"href\", `is`(WebViewPage.APPLE_LINK_HREF))\n    }\n\n    @Test\n    fun hasAttribute_withString_onExistedElement() {\n        page.appleLink.hasAttribute(\"href\", WebViewPage.APPLE_LINK_HREF)\n    }\n\n    @Test\n    fun hasAttribute_invalidMatcher_onExistedElement() {\n        AssertUtils.assertException {\n            page.appleLink.withTimeout(100).hasAttribute(\"href\", `is`(\"SomeInvalidValue\"))\n        }\n    }\n\n    @Test\n    fun hasAttribute_onNotExistedElement() {\n        AssertUtils.assertException { id(\"notExistId\").withTimeout(100).hasAttribute(\"asd\", `is`(\"asdasd\")) }\n    }\n\n    @Test\n    fun assertThat_onExistedElement() {\n        page.appleLink.assertThat(webMatches(getText(), `is`(\"Apple\")))\n    }\n\n    @Test\n    fun assertThat_onNotExistedElement() {\n        AssertUtils.assertException {\n            id(\"notExistId\").withTimeout(100).assertThat(\n                webMatches(\n                    getText(), `is`(\"Apple\")\n                )\n            )\n        }\n    }\n\n    @Test\n    fun withContextual_hasText() {\n        id(\"teacher\").containsText(\"Teachers\").withContextual(className(\"person_name\")).hasText(\"Socrates\")\n    }\n\n    @Test\n    fun withContextual_containsText() {\n        id(\"student\").withContextual(className(\"person_name\")).hasText(\"Plato\")\n    }\n\n    @Test\n    fun scrollToWebElement() {\n        id(\"list_element_12\").webScrollIntoView().hasText(\"list_element_12\").webClick()\n    }\n\n    @Test\n    fun webScrollIntoViewBoolean() {\n        val result = id(\"list_element_12\").webScrollIntoViewBoolean()\n        Assert.assertTrue(result)\n    }\n\n    @Test\n    fun customActionTest() {\n        val initText = \"start\"\n        val finishText = \"finish\"\n        page.textInput\n            .replaceText(initText)\n            .appendText(finishText)\n            .exists()\n        page.buttonUpdTitle.webClick()\n        page.title.hasText(initText+finishText)\n    }\n\n\n    @Test\n    fun validAssertion(){\n        val text = \"start\"\n        page.textInput.replaceText(text)\n        page.buttonUpdTitle.withAssertion {\n            page.title.hasText(text)\n        }.webClick()\n    }\n\n    @Test\n    fun invalidAssertion(){\n        val text = \"start\"\n        AssertUtils.assertException {\n            page.textInput.replaceText(text)\n            page.buttonUpdTitle.withTimeout(3000).withAssertion {\n                page.title.withTimeout(500).hasText(text + \"adas\")\n            }.webClick()\n        }\n    }\n\n\n    @Test\n    fun withName_inOperationProps_ultronInteraction() {\n        val name = \"ElementName\"\n        page.notExistedElement.withTimeout(100).withName(name).withResultHandler { result ->\n            Assert.assertEquals(name, result.operation.elementInfo.name)\n        }.exists()\n    }\n\n    @Test\n    fun withName_inOperationProps_matcherExt() {\n        val name = \"ElementName\"\n        page.notExistedElement.withName(name).withTimeout(100).withResultHandler { result ->\n            Assert.assertEquals(name, result.operation.elementInfo.name)\n        }.exists()\n    }\n\n    @Test\n    fun withName_inExceptionMessage() {\n        val name = \"ElementNameToBeInException\"\n        runCatching {\n            page.notExistedElement.withTimeout(100).withName(name).exists()\n        }.onFailure { exception ->\n            Assert.assertTrue(exception.message!!.contains(name))\n        }\n    }\n\n    @Test\n    fun withMeta() {\n        val meta = DummyMetaObject(\"ElementMetaInfo\")\n        page.notExistedElement.withTimeout(100).withMetaInfo(meta).withResultHandler { result ->\n            Assert.assertEquals(meta, result.operation.elementInfo.meta)\n        }.exists()\n    }\n\n    @Test\n    fun verifySoftAssertionsTest() {\n        UltronCommonConfig.testContext.softAnalyzer.clear()\n        softAssertion(false) {\n            id(\"NotExistText\").withTimeout(100).webClick()\n            id(\"NotExistTestTag\").withTimeout(100).webClick()\n        }\n        runCatching {\n            verifySoftAssertions()\n        }.onFailure { exception ->\n            val message = exception.message ?: throw RuntimeException(\"Empty exception message: $exception\")\n            Assert.assertTrue(message.contains(\"NotExistText\"))\n            Assert.assertTrue(message.contains(\"NotExistTestTag\"))\n        }\n    }\n\n    @Test\n    fun softAssertionTest() {\n        UltronCommonConfig.testContext.softAnalyzer.clear()\n        AssertUtils.assertException {\n            softAssertion {\n                page.notExistedElement.withTimeout(100).webClick()\n            }\n        }\n    }\n}"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso_web/UltronWebElementsTest.kt",
    "content": "package com.atiurin.sampleapp.tests.espresso_web\n\nimport com.atiurin.sampleapp.framework.Log\nimport com.atiurin.ultron.core.espressoweb.webelement.UltronWebElements.Companion.classNames\nimport org.junit.Assert\nimport org.junit.Test\n\nclass UltronWebElementsTest : BaseWebViewTest() {\n    @Test\n    fun getSizeTest() {\n        val buttonsAmount = classNames(\"button\").getSize()\n        Assert.assertTrue(buttonsAmount == 3)\n    }\n\n    @Test\n    fun getSize_notExistedElement() {\n        Log.debug(\">>>\" + classNames(\"not_existed_classname\").getSize())\n//        AssertUtils.assertException {  }\n    }\n\n    @Test\n    fun getListElementTest(){\n        classNames(\"link\").getElements()\n            .find { ultronWebElement ->\n                ultronWebElement.isSuccess {\n                    withTimeout(100).hasText(\"Apple\")\n                }\n            }?.webClick()\n        page.title.containsText(\"apple\")\n    }\n}"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso_web/UltronWebUiBlockTest.kt",
    "content": "package com.atiurin.sampleapp.tests.espresso_web\n\nimport com.atiurin.sampleapp.pages.uiblock.WebElementUiBlockScreen\nimport org.junit.Test\n\nclass UltronWebUiBlockTest : BaseWebViewTest() {\n    @Test\n    fun webUiBlock(){\n        WebElementUiBlockScreen {\n            teacherBlock.name.exists().hasText(\"Socrates\")\n            teacherBlock.uiBlock.exists()\n            studentWithoutDesc.name.exists().hasText(\"Plato\")\n        }\n    }\n\n    @Test\n    fun uiBlockFactoryTest(){\n        WebElementUiBlockScreen {\n            persons.student.name.hasText(\"Plato\")\n        }\n    }\n\n    @Test\n    fun childUiBlockCreation(){\n        WebElementUiBlockScreen {\n            persons.teacher.name.hasText(\"Socrates\")\n            persons.studentWithoutDesc.name.hasText(\"Plato\")\n        }\n    }\n}"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/testlifecycle/ExceptionsProcessingTest.kt",
    "content": "package com.atiurin.sampleapp.tests.testlifecycle\n\nimport com.atiurin.sampleapp.framework.Log\nimport com.atiurin.ultron.testlifecycle.rulesequence.RuleSequence\nimport com.atiurin.ultron.testlifecycle.setupteardown.SetUp\nimport com.atiurin.ultron.testlifecycle.setupteardown.SetUpRule\nimport com.atiurin.ultron.testlifecycle.setupteardown.TearDown\nimport com.atiurin.ultron.testlifecycle.setupteardown.TearDownRule\nimport org.junit.*\nimport org.junit.rules.ExpectedException\nimport java.lang.RuntimeException\nimport java.util.concurrent.atomic.AtomicInteger\n\nclass ExceptionsProcessingTest {\n    val counter = AtomicInteger(0)\n    val conditionsOrderMap = mutableMapOf<Int, String>()\n\n    companion object {\n        const val setUp1Key = \"setUp1\"\n        const val setUp2Key = \"setUp2\"\n        const val testKey = \"testKey\"\n        const val tearDown1Key = \"tearDown1\"\n        const val tearDown2Key = \"tearDown2\"\n\n        const val NO_EXCEPTION_FLOW = \"NO_EXCEPTION_FLOW\"\n        const val SET_UP_EXCEPTION_FLOW = \"SET_UP_EXCEPTION_FLOW\"\n        const val TEST_EXCEPTION_FLOW = \"TEST_EXCEPTION_FLOW\"\n        const val TEAR_DOWN_EXCEPTION_FLOW = \"TEAR_DOWN_EXCEPTION_FLOW\"\n\n        const val SET_UP_WITH_EXCEPTION = \"SET_UP_WITH_EXCEPTION\"\n        const val TEAR_DOWN_WITH_EXCEPTION = \"TEAR_DOWN_WITH_EXCEPTION\"\n    }\n\n    val setUp1 = SetUpRule(\"setUpRule1\").add(name = \"setUp1\") {\n        conditionsOrderMap[counter.incrementAndGet()] = setUp1Key\n    }.add(key = SET_UP_WITH_EXCEPTION) {\n        throw SetUpException(SET_UP_WITH_EXCEPTION)\n    }\n    val tearDown1 = TearDownRule(\"tearDown1\").add(name = tearDown1Key) {\n        conditionsOrderMap[counter.incrementAndGet()] = tearDown1Key\n    }.add(key = TEAR_DOWN_WITH_EXCEPTION) {\n        throw TearDownException(TEAR_DOWN_WITH_EXCEPTION)\n    }\n\n    val setUp2 = SetUpRule(\"setUp2\").add(name = setUp2Key) {\n        conditionsOrderMap[counter.incrementAndGet()] = setUp2Key\n    }\n    val tearDown2 = TearDownRule(\"tearDown2\").add(name = tearDown2Key) {\n        conditionsOrderMap[counter.incrementAndGet()] = tearDown2Key\n    }\n\n    val controlTearDown = TearDownRule().add(key = NO_EXCEPTION_FLOW) {\n        var index = 1\n        Log.info(conditionsOrderMap.toString())\n        Assert.assertEquals(setUp1Key, conditionsOrderMap[index++])\n        Assert.assertEquals(setUp2Key, conditionsOrderMap[index++])\n        Assert.assertEquals(testKey, conditionsOrderMap[index++])\n        Assert.assertEquals(tearDown1Key, conditionsOrderMap[index++])\n        Assert.assertEquals(tearDown2Key, conditionsOrderMap[index])\n    }.add(key = SET_UP_EXCEPTION_FLOW) {\n        var index = 1\n        Log.info(conditionsOrderMap.toString())\n        Assert.assertEquals(setUp1Key, conditionsOrderMap[index++])\n        Assert.assertEquals(tearDown1Key, conditionsOrderMap[index++])\n        Assert.assertEquals(tearDown2Key, conditionsOrderMap[index])\n    }.add(key = NO_EXCEPTION_FLOW) {\n        var index = 1\n        Log.info(conditionsOrderMap.toString())\n        Assert.assertEquals(setUp1Key, conditionsOrderMap[index++])\n        Assert.assertEquals(setUp2Key, conditionsOrderMap[index++])\n        Assert.assertEquals(testKey, conditionsOrderMap[index++])\n        Assert.assertEquals(tearDown1Key, conditionsOrderMap[index++])\n        Assert.assertEquals(tearDown2Key, conditionsOrderMap[index])\n    }.add(key = TEST_EXCEPTION_FLOW) {\n        var index = 1\n        Log.info(conditionsOrderMap.toString())\n        Assert.assertEquals(setUp1Key, conditionsOrderMap[index++])\n        Assert.assertEquals(setUp2Key, conditionsOrderMap[index++])\n        Assert.assertEquals(tearDown1Key, conditionsOrderMap[index++])\n        Assert.assertEquals(tearDown2Key, conditionsOrderMap[index])\n    }.add {\n        counter.set(0)\n        conditionsOrderMap.clear()\n\n    }\n\n    val expectedException = ExpectedException.none()\n\n    @get:Rule\n    val ruleSequence = RuleSequence(setUp1, tearDown1).add(tearDown2, setUp2).addLast(controlTearDown)\n\n    @Test\n    @TearDown(NO_EXCEPTION_FLOW)\n    fun noExceptionFlow() {\n        conditionsOrderMap[counter.incrementAndGet()] = testKey\n    }\n\n    @Ignore(\"There is a bug in expected part of JUnit. If an expected exception occurs out of the @Test scope the test fails\")\n    @SetUp(SET_UP_WITH_EXCEPTION)\n    @TearDown(SET_UP_EXCEPTION_FLOW)\n    @Test(expected = SetUpException::class)\n    fun setUpExceptionFlow() {\n        conditionsOrderMap[counter.incrementAndGet()] = testKey\n    }\n\n    @Suppress(\"UNREACHABLE_CODE\")\n    @Test(expected = TestException::class)\n    @TearDown(TEST_EXCEPTION_FLOW)\n    fun testExceptionFlow() {\n        throw TestException(TEST_EXCEPTION_FLOW)\n        conditionsOrderMap[counter.incrementAndGet()] = testKey\n    }\n\n    @Ignore(\"There is a bug in expected part of JUnit. If an expected exception occurs out of the @Test scope(e.g. @Before or @After) the test fails\")\n    @TearDown(TEAR_DOWN_WITH_EXCEPTION)\n    @SetUp(TEAR_DOWN_EXCEPTION_FLOW)\n    @Test(expected = TearDownException::class)\n    fun tearDownExceptionFlow() {\n        conditionsOrderMap[counter.incrementAndGet()] = testKey\n    }\n\n    private class SetUpException(override val message: String) : RuntimeException()\n    private class TestException(override val message: String) : RuntimeException()\n    private class TearDownException(override val message: String) : RuntimeException()\n}\n\n"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/testlifecycle/ParametrizedTest.kt",
    "content": "package com.atiurin.sampleapp.tests.testlifecycle\n\nimport com.atiurin.sampleapp.activity.ComposeElementsActivity\nimport com.atiurin.ultron.core.compose.createSimpleUltronComposeRule\nimport com.atiurin.ultron.log.UltronLog\nimport com.atiurin.ultron.testlifecycle.rulesequence.RuleSequence\nimport com.atiurin.ultron.testlifecycle.setupteardown.SetUp\nimport com.atiurin.ultron.testlifecycle.setupteardown.SetUpRule\nimport com.atiurin.ultron.testlifecycle.setupteardown.TearDown\nimport com.atiurin.ultron.testlifecycle.setupteardown.TearDownRule\nimport org.junit.Assert\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.junit.runners.Parameterized\n\n@RunWith(Parameterized::class)\nclass ParametrizedTest(private val testValue1: String, private val testValue2: String) {\n    private var commonSetUpDone = false\n    private var singleTestSetUpDone = false\n    private var commonTearDownDone = false\n    private var singleTestTearDownDone = false\n    private val activityRule = createSimpleUltronComposeRule<ComposeElementsActivity>()\n\n    private val beforeEachRule = SetUpRule(\"Precondition before each test\")\n        .add {\n            commonSetUpDone = true\n        }\n        .add(key = \"singleTestSetUpDone\") {\n            singleTestSetUpDone = true\n        }\n    private val afterEachRule = TearDownRule(\"Post condition after each test\")\n        .add {\n            commonTearDownDone = true\n        }\n        .add(key = \"singleTestTearDownDone\") {\n            singleTestTearDownDone = true\n        }\n    private val controlTearDown = TearDownRule()\n        .add {\n            Assert.assertTrue(commonSetUpDone)\n            Assert.assertTrue(singleTestSetUpDone)\n            Assert.assertTrue(commonTearDownDone)\n            Assert.assertTrue(singleTestTearDownDone)\n        }\n\n    @get:Rule\n    val ruleSequence = RuleSequence(activityRule, beforeEachRule, afterEachRule, controlTearDown)\n\n    companion object {\n        @JvmStatic\n        @Parameterized.Parameters(name = \"[{0}-{1}]\")\n        fun testData() = listOf(\n            arrayOf(\"param1_1\", \"param1_2\"),\n            arrayOf(\"param2_1\", \"param2_2\"),\n        )\n    }\n\n    @Test\n    @SetUp(\"singleTestSetUpDone\")\n    @TearDown(\"singleTestTearDownDone\")\n    fun myAwesomeTest() {\n        Assert.assertTrue(singleTestSetUpDone)\n        Assert.assertTrue(commonSetUpDone)\n        UltronLog.debug(\"testValue1 = $testValue1\")\n        UltronLog.debug(\"testValue2 = $testValue2\")\n    }\n}\n"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/testlifecycle/SetUpTearDownRuleTest.kt",
    "content": "package com.atiurin.sampleapp.tests.testlifecycle\n\nimport com.atiurin.sampleapp.framework.Log\nimport com.atiurin.ultron.allure.step.step\nimport com.atiurin.ultron.core.config.UltronConfig\nimport com.atiurin.ultron.testlifecycle.rulesequence.RuleSequence\nimport com.atiurin.ultron.testlifecycle.setupteardown.*\nimport org.junit.Assert\nimport org.junit.Rule\nimport org.junit.Test\nimport java.util.concurrent.atomic.AtomicInteger\nimport kotlin.reflect.KClass\n\nclass SetUpTearDownRuleTest {\n    val counter = AtomicInteger(0)\n    val conditionsOrderMap = mutableMapOf<Int, String>()\n\n    init {\n        UltronConfig.Conditions.conditionExecutorWrapper = CustomConditionExecutorWrapper()\n        UltronConfig.Conditions.conditionsExecutor = CustomConditionsExecutor()\n    }\n\n    class CustomConditionExecutorWrapper : ConditionExecutorWrapper {\n        override fun execute(condition: Condition) {\n            step(condition.name) { condition.actions() }\n        }\n    }\n    class CustomConditionsExecutor : DefaultConditionsExecutor(){\n        var lastRuleName: String = \"\"\n        override val conditionExecutor: ConditionExecutorWrapper = CustomConditionExecutorWrapper()\n        override fun before(name: String, ruleClass: KClass<*>) {\n            this.lastRuleName = name\n            super.before(name, ruleClass)\n        }\n        override fun execute(conditions: List<Condition>, keys: List<String>, description: String) {\n            step(lastRuleName){ super.execute(conditions, keys, description) }\n        }\n    }\n\n    companion object {\n        const val firstSetUpKey = \"firstSetUp\"\n        const val firstTearDownKey = \"firstTearDown\"\n        const val setUp1Key = \"setUp1\"\n        const val setUp2Key = \"setUp2\"\n        const val tearDown1Key = \"tearDown1\"\n        const val tearDown2Key = \"tearDown2\"\n        const val lastSetUpKey = \"lastSetUp\"\n        const val lastTearDownKey = \"lastTearDown\"\n    }\n\n    val firstSetUp = SetUpRule(\"firstSetUp\").add {\n        conditionsOrderMap.put(counter.incrementAndGet(), firstSetUpKey)\n    }\n    val firstTearDown = TearDownRule(\"firstTearDown\").add {\n        conditionsOrderMap.put(counter.incrementAndGet(), firstTearDownKey)\n    }\n\n    val setUp1 = SetUpRule(\"setUpRule1\")\n        .add(name = \"setUp1\") {\n            conditionsOrderMap.put(counter.incrementAndGet(), setUp1Key)\n        }\n        .add(name = \"setUp1_2\") {\n        }\n    val tearDown1 = TearDownRule(\"tearDown1\").add {\n        conditionsOrderMap.put(counter.incrementAndGet(), tearDown1Key)\n    }\n\n    val setUp2 = SetUpRule(\"setUp2\")\n        .add {\n            conditionsOrderMap.put(counter.incrementAndGet(), setUp2Key)\n        }\n    val tearDown2 = TearDownRule(\"tearDown2\").add {\n        conditionsOrderMap.put(counter.incrementAndGet(), tearDown2Key)\n    }\n\n    val lastSetUp = SetUpRule()\n        .add {\n            conditionsOrderMap.put(counter.incrementAndGet(), lastSetUpKey)\n//            throw Exception(\"asd\")\n        }\n    val lastTearDown = TearDownRule()\n        .add {\n            conditionsOrderMap.put(counter.incrementAndGet(), lastTearDownKey)\n        }\n    val controlTearDown = TearDownRule()\n        .add {\n            Log.info(conditionsOrderMap.toString())\n            Assert.assertEquals(firstSetUpKey, conditionsOrderMap[1])\n            Assert.assertEquals(setUp1Key, conditionsOrderMap[2])\n            Assert.assertEquals(setUp2Key, conditionsOrderMap[3])\n            Assert.assertEquals(lastSetUpKey, conditionsOrderMap[4])\n            Assert.assertEquals(firstTearDownKey, conditionsOrderMap[5])\n            Assert.assertEquals(tearDown1Key, conditionsOrderMap[6])\n            Assert.assertEquals(tearDown2Key, conditionsOrderMap[7])\n            Assert.assertEquals(lastTearDownKey, conditionsOrderMap[8])\n\n        }\n\n    @get:Rule\n    val ruleSequence =\n        RuleSequence(setUp1, tearDown1)\n            .add(tearDown2, setUp2)\n            .addFirst(firstSetUp, firstTearDown)\n            .addLast(lastTearDown, lastSetUp, controlTearDown)\n\n    @Test\n    fun mockTestConditions() {\n        Log.info(\">>>> test\")\n//        throw Exception(\"asd\")\n    }\n}"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/testlifecycle/UltronTestFlowTest.kt",
    "content": "package com.atiurin.sampleapp.tests.testlifecycle\n\nimport com.atiurin.sampleapp.tests.BaseTest\nimport com.atiurin.ultron.annotations.ExperimentalUltronApi\nimport com.atiurin.ultron.log.UltronLog\nimport com.atiurin.ultron.testlifecycle.setupteardown.SetUpRule\nimport com.atiurin.ultron.testlifecycle.setupteardown.TearDownRule\nimport org.junit.Assert\nimport org.junit.Test\n\nclass UltronTestFlowTest : BaseTest() {\n    companion object {\n        var order = 0\n        var isBeforeFirstTestCounter = 0\n        var commonBeforeOrder = -1\n        var commonAfterOrder = -1\n        var afterOrder = -1\n    }\n    val ruleSetUp = SetUpRule().add { UltronLog.info(\"SetUpRule\") }\n    val tearDownRule = TearDownRule().add { UltronLog.info(\"TearDownRule\") }\n\n    init {\n        ruleSequence.add(ruleSetUp, tearDownRule)\n    }\n\n    @OptIn(ExperimentalUltronApi::class)\n    override val beforeFirstTest = {\n        isBeforeFirstTestCounter++\n        UltronLog.info(\"beforeFirstTest\")\n    }\n\n    override val beforeTest = {\n        commonBeforeOrder = order\n        order++\n        UltronLog.info(\"Before test common\")\n    }\n    override val afterTest = {\n        commonAfterOrder = order\n        order++\n        assert(afterOrder < commonAfterOrder, lazyMessage = { \"CommonAfter block should run after 'after' test block\" })\n        UltronLog.info(\"After test common\")\n    }\n\n    @Test\n    fun someTest1() = test {\n        var beforeOrder = -1\n        var goOrder = -1\n        order++\n        before {\n            assert(isBeforeFirstTestCounter == 1, lazyMessage = { \"beforeFirstTest block should run before commonBefore block\" })\n            beforeOrder = order\n            order++\n            UltronLog.info(\"Before TestMethod 1\")\n        }.go {\n            goOrder = order\n            order++\n            UltronLog.info(\"Run TestMethod 1\")\n        }.after {\n            afterOrder = order\n            order++\n            assert(commonBeforeOrder < beforeOrder, lazyMessage = { \"beforeOrder block should run after commonBefore block\" })\n            assert(beforeOrder < goOrder, lazyMessage = { \"Before block should run before 'go'\" })\n            assert(goOrder < afterOrder, lazyMessage = { \"After block should run after 'go'\" })\n        }\n    }\n\n    @Test\n    fun someTest2() = test(suppressCommonBefore = true) {\n        before {\n            UltronLog.info(\"Before TestMethod 2\")\n        }\n        after {\n            UltronLog.info(\"After TestMethod 2\")\n        }\n        go {\n            assert(isBeforeFirstTestCounter == 1, lazyMessage = { \"beforeFirstTest block should run only once\" })\n            UltronLog.info(\"Run TestMethod 2\")\n        }\n    }\n\n    @Test\n    fun simpleTest() = test {\n        assert(isBeforeFirstTestCounter == 1, lazyMessage = { \"beforeAllTests block should run only once\" })\n        UltronLog.info(\"UltronTest simpleTest\")\n    }\n\n    @Test\n    fun afterBlockExecutedOnFailedTest() {\n        var isAfterExecuted = false\n        runCatching {\n            test {\n                go{\n                    throw RuntimeException(\"test exception\")\n                }\n                after {\n                    isAfterExecuted = true\n                }\n            }\n        }\n        Assert.assertTrue(isAfterExecuted)\n    }\n}"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/testlifecycle/UltronTestFlowTest2.kt",
    "content": "package com.atiurin.sampleapp.tests.testlifecycle\n\nimport com.atiurin.sampleapp.tests.BaseTest\nimport com.atiurin.ultron.annotations.ExperimentalUltronApi\nimport com.atiurin.ultron.log.UltronLog\nimport org.junit.Test\n\nclass UltronTestFlowTest2 : BaseTest() {\n    var order = 0\n    var beforeAllTestCounter = 0\n    @OptIn(ExperimentalUltronApi::class)\n    override val beforeFirstTest = {\n        beforeAllTestCounter = order\n        order++\n        UltronLog.info(\"Before Class\")\n    }\n    @Test\n    fun someTest1() = test {\n        var beforeOrder = -1\n        var afterOrder = -1\n        var goOrder = -1\n        order++\n        before {\n            beforeOrder = order\n            order++\n            UltronLog.info(\"Before TestMethod 1\")\n        }.go {\n            goOrder = order\n            order++\n            UltronLog.info(\"Run TestMethod 1\")\n        }.after {\n            afterOrder = order\n            assert(beforeAllTestCounter == 0, lazyMessage = { \"beforeAllTests block should run before all test\" })\n            assert(beforeOrder > beforeAllTestCounter, lazyMessage = { \"Before block should run after 'Before All'\" })\n            assert(beforeOrder < goOrder, lazyMessage = { \"Before block should run before 'go'\" })\n            assert(goOrder < afterOrder, lazyMessage = { \"After block should run after 'go'\" })\n        }\n    }\n\n    @Test\n    fun someTest2() = test(suppressCommonBefore = true) {\n        before {\n            UltronLog.info(\"Before TestMethod 2\")\n        }.after {\n            UltronLog.info(\"After TestMethod 2\")\n        }.go {\n            assert(beforeAllTestCounter == 0, lazyMessage = { \"beforeAllTests block should run only once\" })\n            UltronLog.info(\"Run TestMethod 2\")\n        }\n    }\n}"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/testlifecycle/UltronTestPlan.kt",
    "content": "package com.atiurin.sampleapp.tests.testlifecycle\n\n//@RunWith(Suite::class)\n//@Suite.SuiteClasses(\n//    UltronTestFlowTest::class,\n//    UltronTestFlowTest2::class,\n//)\n//class UltronTestPlan"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/testlifecycle/UltronTestRuleSequenceMergeTest.kt",
    "content": "package com.atiurin.sampleapp.tests.testlifecycle\n\nimport com.atiurin.sampleapp.tests.BaseTest\nimport com.atiurin.ultron.annotations.ExperimentalUltronApi\nimport com.atiurin.ultron.log.UltronLog\nimport com.atiurin.ultron.testlifecycle.setupteardown.SetUpRule\nimport com.atiurin.ultron.testlifecycle.setupteardown.TearDownRule\nimport org.junit.Test\n\nclass UltronTestRuleSequenceMergeTest : BaseTest() {\n    private val ruleSetUp = SetUpRule().add { UltronLog.info(\"SetUpRule\") }\n    private val tearDownRule = TearDownRule().add { UltronLog.info(\"TearDownRule\") }\n\n    init {\n        ruleSequence.add(ruleSetUp, tearDownRule)\n    }\n\n    @OptIn(ExperimentalUltronApi::class)\n    override val beforeFirstTest = {\n        UltronLog.info(\"beforeFirstTest\")\n    }\n\n    override val beforeTest = {\n        UltronLog.info(\"Before test common\")\n    }\n\n    override val afterTest = {\n        UltronLog.info(\"After test common\")\n    }\n    @Test\n    fun someTest1() = test {\n        before {\n            UltronLog.info(\"Before TestMethod 1\")\n        }.go {\n            UltronLog.info(\"Run TestMethod 1\")\n        }.after {\n            UltronLog.info(\"After TestMethod 1\")\n        }\n    }\n}"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/uiautomator/UiAutomatorCustomAssertionTest.kt",
    "content": "package com.atiurin.sampleapp.tests.uiautomator\n\nimport com.atiurin.sampleapp.R\nimport com.atiurin.sampleapp.framework.utils.AssertUtils\nimport com.atiurin.sampleapp.framework.utils.TestDataUtils\nimport com.atiurin.sampleapp.pages.UiObject2ElementsPage\nimport com.atiurin.sampleapp.pages.UiObjectElementsPage\nimport com.atiurin.sampleapp.tests.UiElementsTest\nimport org.junit.Test\n\nclass UiAutomatorCustomAssertionTest : UiElementsTest() {\n    val uiObjectPage = UiObjectElementsPage()\n    val uiObject2Page = UiObject2ElementsPage()\n\n    @Test\n    fun uiObjectValidAssertionTest() {\n        uiObjectPage.button.withAssertion {\n            uiObjectPage.eventStatus.textContains(TestDataUtils.getResourceString(R.string.button_event_click))\n        }.click()\n    }\n\n    @Test\n    fun uiObjectInvalidAssertionTest() {\n        AssertUtils.assertException {\n            uiObjectPage.button.withTimeout(1000).withAssertion {\n                uiObjectPage.eventStatus.withTimeout(400).textContains(\"some invalid text\")\n            }.click()\n        }\n    }\n\n    @Test\n    fun withAssertionAllureResult(){\n        uiObjectPage.editTextContentDesc.withAssertion(\"some test\", isListened = true) {\n            uiObjectPage.editTextContentDesc.hasText(\"123\")\n        }.replaceText(\"123\")\n    }\n\n    @Test\n    fun uiObject2ValidAssertionTest() {\n        uiObject2Page.button.withAssertion {\n            uiObject2Page.eventStatus.textContains(TestDataUtils.getResourceString(R.string.button_event_click))\n        }.click()\n    }\n\n    @Test\n    fun uiObject2InvalidAssertionTest() {\n        AssertUtils.assertException {\n            uiObject2Page.button.withTimeout(1000).withAssertion {\n                uiObject2Page.eventStatus.withTimeout(400).textContains(\"some invalid text\")\n            }.click()\n        }\n    }\n}"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/uiautomator/UltronUiAutomatorPerfTest.kt",
    "content": "package com.atiurin.sampleapp.tests.uiautomator\n\nimport android.os.SystemClock\nimport com.atiurin.sampleapp.framework.Log\nimport com.atiurin.sampleapp.pages.UiObject2ElementsPage\nimport com.atiurin.sampleapp.tests.UiElementsTest\nimport org.junit.Test\n\nclass UltronUiAutomatorPerfTest: UiElementsTest() {\n    val page = UiObject2ElementsPage()\n\n    @Test\n    fun perfTest(){\n        val startTime = SystemClock.elapsedRealtime()\n        for (i in 1..200){\n            page.button.click()\n            page.eventStatus.textContains(i.toString())\n        }\n        Log.debug(\"Duration ${SystemClock.elapsedRealtime() - startTime} ms\")\n    }\n}"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/uiautomator/UltronUiObject2ActionsTest.kt",
    "content": "package com.atiurin.sampleapp.tests.uiautomator\n\nimport android.view.ViewConfiguration\nimport android.widget.LinearLayout\nimport androidx.compose.ui.test.hasText\nimport androidx.test.filters.FlakyTest\nimport androidx.test.platform.app.InstrumentationRegistry\nimport com.atiurin.sampleapp.R\nimport com.atiurin.sampleapp.activity.UiElementsActivity\nimport com.atiurin.sampleapp.framework.Log\nimport com.atiurin.sampleapp.framework.ultronext.appendText\nimport com.atiurin.sampleapp.framework.utils.AssertUtils\nimport com.atiurin.sampleapp.framework.utils.TestDataUtils\nimport com.atiurin.sampleapp.pages.UiObject2ElementsPage\nimport com.atiurin.sampleapp.tests.UiElementsTest\nimport com.atiurin.ultron.core.uiautomator.uiobject2.UltronUiObject2.Companion.bySelector\nimport com.atiurin.ultron.extensions.withTimeout\nimport com.atiurin.ultron.log.UltronLog\nimport com.atiurin.ultron.utils.getTargetString\nimport org.junit.Assert\nimport org.junit.Test\n\nclass UltronUiObject2ActionsTest : UiElementsTest() {\n    val page = UiObject2ElementsPage()\n\n    //getParent\n    @Test\n    fun getParent_parentExist() {\n        Assert.assertEquals(\n            LinearLayout::class.qualifiedName,\n            page.button.getParent()?.getClassName()\n        )\n    }\n\n    //getChildren\n    @Test\n    fun getChildren_returnsAllChildren() {\n        val children = page.radioGroup.getChildren()\n        Assert.assertEquals(3, children.size)\n        var foundElements = 0\n        children.forEach {\n            val resName = it.getResourceName()!!\n            Log.debug(\">>> $resName\")\n            if (resName.endsWith(\"radio_visible\")) {\n                foundElements++\n                it.click()\n                page.button.isDisplayed()\n            } else if (resName.endsWith(\"radio_invisible\")) {\n                foundElements++\n                it.click()\n                page.button.isNotDisplayed()\n            } else if (resName.endsWith(\"radio_gone\")) foundElements++\n        }\n        Assert.assertEquals(3, foundElements)\n    }\n\n    @Test\n    fun getChildren_noChildExist() {\n        val children = page.button.getChildren()\n        Assert.assertTrue(children.isEmpty())\n    }\n\n    @Test\n    fun getChildrenWithResultHandler() {\n        page.button.getChildren()\n    }\n\n    //getChildCount\n    @Test\n    fun getChildCount_childExist() {\n        Assert.assertEquals(3, page.radioGroup.getChildCount())\n    }\n\n    @Test\n    fun getChildCount_noChildExist() {\n        Assert.assertEquals(0, page.button.getChildCount())\n    }\n\n    //findObject\n    @Test\n    fun findObject_existedChildObject() {\n        val child = page.radioGroup.findObject(bySelector(R.id.radio_invisible))\n        Assert.assertNotNull(child)\n        child!!.click()\n        page.button.isNotDisplayed()\n    }\n\n    @Test\n    fun findObject_notExistedChildObject() {\n        val child = page.radioGroup.findObject(bySelector(R.id.button1))\n        Assert.assertNull(child)\n    }\n\n    @Test\n    fun findObject_notExistedParentObject() {\n        AssertUtils.assertException {\n            page.notExistedObject.withTimeout(100).findObject(bySelector(R.id.button1))\n        }\n    }\n\n    //findObjects\n    @Test\n    fun findObjects_existedChildObject() {\n        val children = page.radioGroup.findObjects(bySelector(R.id.radio_invisible))\n        Assert.assertEquals(1, children.size)\n        children.forEach {\n            it.click()\n            page.button.isNotDisplayed()\n        }\n    }\n\n    @Test\n    fun findObjects_notExistedChildObject() {\n        val children = page.radioGroup.findObjects(bySelector(R.id.button1))\n        Assert.assertTrue(children.isEmpty())\n    }\n\n    //getText\n    @Test\n    fun getText_objectHasText() {\n        Assert.assertEquals(getTargetString(R.string.button_text), page.button.getText())\n    }\n\n    @Test\n    fun getText_objectHasNoText() {\n        Assert.assertEquals(null, page.swipableImageView.getText())\n    }\n\n    @Test\n    fun getText_notExistedObject() {\n        AssertUtils.assertException { page.notExistedObject.withTimeout(100).getText() }\n    }\n\n    //getClassName\n    @Test\n\n    fun getClassName_existObject() {\n        Assert.assertEquals(\"android.widget.Button\", page.button.getClassName())\n    }\n\n    @Test\n    fun getClassName_notExistObject() {\n        AssertUtils.assertException { page.notExistedObject.withTimeout(100).getClassName() }\n    }\n\n    //getApplicationPackage\n    @Test\n    fun getApplicationPackage_existedObject() {\n        val expected =\n            InstrumentationRegistry.getInstrumentation().targetContext.applicationInfo.packageName\n        Assert.assertEquals(expected, page.button.getApplicationPackage())\n    }\n\n    @Test\n    fun getApplicationPackage_notExistedObject() {\n        AssertUtils.assertException {\n            page.notExistedObject.withTimeout(100).getApplicationPackage()\n        }\n    }\n\n    //getVisibleBounds\n    @Test\n    fun getVisibleBounds_existedObject() {\n        Assert.assertNotNull(page.button.getVisibleBounds())\n    }\n\n    @Test\n    fun getVisibleBounds_notExistedObject() {\n        AssertUtils.assertException { page.notExistedObject.withTimeout(100).getVisibleBounds() }\n    }\n\n    //getVisibleCenter\n    @Test\n    fun getVisibleCenter_existedObject() {\n        val bounds = page.button.getVisibleBounds()!!\n        val point = page.button.getVisibleCenter()!!\n        Assert.assertEquals(bounds.exactCenterX().toInt(), point.x)\n        Assert.assertEquals(bounds.exactCenterY().toInt(), point.y)\n    }\n\n    @Test\n    fun getVisibleCenter_notExistedObject() {\n        AssertUtils.assertException { page.notExistedObject.withTimeout(100).getVisibleCenter() }\n    }\n\n    //getResourceName\n    @Test\n    fun getResourceName_existedResourceName() {\n        val resName = page.button.getResourceName()\n        val pkgName =\n            InstrumentationRegistry.getInstrumentation().targetContext.applicationInfo.packageName\n        Assert.assertEquals(\"$pkgName:id/button1\", resName)\n    }\n\n    @Test\n    fun getResourceName_notExistedResourceName() {\n        Assert.assertNull(page.button.getParent()?.getResourceName())\n    }\n\n    @Test\n    fun getResourceName_notExistedObject() {\n        AssertUtils.assertException { page.notExistedObject.withTimeout(100).getResourceName() }\n    }\n\n    //getContentDescription\n    @Test\n    fun getContentDescription_existedContentDescription() {\n        val expectedContDesc = getTargetString(R.string.button_default_content_desc)\n        Assert.assertEquals(expectedContDesc, page.button.getContentDescription())\n    }\n\n    @Test\n    fun getContentDescription_notExistedResourceName() {\n        Assert.assertNull(page.button.getParent()?.getContentDescription())\n    }\n\n    @Test\n    fun getContentDescription_notExistedObject() {\n        AssertUtils.assertException {\n            page.notExistedObject.withTimeout(100).getContentDescription()\n        }\n    }\n\n    //click\n    @Test\n    fun click_onClickable() {\n        page.button.click()\n        page.eventStatus.textContains(TestDataUtils.getResourceString(R.string.button_event_click))\n    }\n\n    @Test\n    fun click_withDuration_onClickable() {\n        page.button.click(duration = 50)\n        page.eventStatus.textContains(TestDataUtils.getResourceString(R.string.button_event_click))\n    }\n\n    @Test\n    fun click_onNotExistedObject() {\n        AssertUtils.assertException { page.notExistedObject.withTimeout(100).click() }\n    }\n\n    //longClick\n    @Test\n    fun longClick_onLongClickable() {\n        val duration = ViewConfiguration.getLongPressTimeout().toLong()\n        page.button.click(duration * 2)\n        page.eventStatus.textContains(getTargetString(R.string.button_event_long_click))\n    }\n\n    @Test\n    fun longClick_onNotExistedObject() {\n        AssertUtils.assertException { page.notExistedObject.withTimeout(100).longClick() }\n    }\n\n    //clear\n    @Test\n    fun clear_editableObject() {\n        page.editTextContentDesc.clear().textIsNullOrEmpty()\n    }\n\n    @Test\n    fun clear_UneditableObject() {\n        val btnText = getTargetString(R.string.button_text)\n        page.button.hasText(btnText).clear().hasText(btnText)\n    }\n\n    @Test\n    fun clear_notExistedObject() {\n        AssertUtils.assertException { page.notExistedObject.withTimeout(100).clear() }\n    }\n\n    //addText\n    @Test\n    fun addText_toEditableObject() {\n        val startText = \"start \"\n        val textToAdd = \"added new Text\"\n        page.editTextContentDesc\n            .replaceText(startText)\n            .addText(textToAdd)\n            .hasText(startText + textToAdd)\n    }\n\n    @Test\n    fun addText_toUneditableObject() {\n        val btnText = getTargetString(R.string.button_text)\n        page.button.addText(\"some new text\").hasText(btnText)\n    }\n\n    @Test\n    fun addText_toNotExistedObject() {\n        AssertUtils.assertException { page.notExistedObject.withTimeout(100).addText(\"asdasd\") }\n    }\n\n    @Test\n    fun legacySetText_toEditable() {\n        val text = \"text to replace \"\n        page.editTextContentDesc.legacySetText(text).withAssertion {\n            hasText(text).withTimeout(1000)\n        }\n    }\n\n    @Test\n    fun legacySetText_toUneditableObject() {\n        AssertUtils.assertException { page.button.withTimeout(100).legacySetText(\"some new text\") }\n    }\n\n    @Test\n    fun legacySetText_toNotExistedObject() {\n        AssertUtils.assertException {\n            page.notExistedObject.withTimeout(100).legacySetText(\"some new text\")\n        }\n    }\n\n    //replaceText\n    @Test\n    fun replaceText_toEditable() {\n        val text = \"replaceText to new\"\n        page.editTextContentDesc\n            .replaceText(text)\n            .hasText(text)\n    }\n\n    @Test\n    fun replaceText_toUneditableObject() {\n        val btnText = getTargetString(R.string.button_text)\n        page.button.replaceText(\"some new text\").hasText(btnText)\n    }\n\n    @Test\n    fun replaceText_toNotExistedObject() {\n        AssertUtils.assertException {\n            page.notExistedObject.withTimeout(100).replaceText(\"some new text\")\n        }\n    }\n\n    //perform\n    @Test\n    fun perform_existedObject() {\n        page.button.perform({ this.click() }, \"Click on button\")\n        page.eventStatus.textContains(TestDataUtils.getResourceString(R.string.button_event_click))\n    }\n\n    @Test\n    fun perform_notExistedObject() {\n        AssertUtils.assertException {\n            page.notExistedObject.withTimeout(100).perform({ this.click() }, \"Click on button\")\n        }\n    }\n\n    //swipe\n    @FlakyTest\n    @Test\n    fun swipeUpTest() {\n        page.editTextContentDesc.replaceText(\"some text\")\n        page.swipableImageView.withAssertion {\n            page.eventStatus.withTimeout(300).textContains(UiElementsActivity.Event.SWIPE_UP.name)\n        }.swipeUp(speed = 2000)\n\n    }\n\n    @FlakyTest\n    @Test\n    fun swipeDownTest() {\n        page.editTextContentDesc.replaceText(\"some text\")\n        page.swipableImageView.withAssertion {\n            page.eventStatus.withTimeout(300).textContains(UiElementsActivity.Event.SWIPE_DOWN.name)\n        }.swipeDown(speed = 2000)\n    }\n\n    @Test\n    @FlakyTest\n    fun swipeRightTest() {\n        page.editTextContentDesc.replaceText(\"some text\")\n        UltronLog.info(\"In assertion, text = '${page.eventStatus.getText()}'\")\n        page.swipableImageView.withAssertion {\n            UltronLog.info(\"In assertion, text = '${page.eventStatus.getText()}'\")\n            page.eventStatus.withTimeout(500).textContains(UiElementsActivity.Event.SWIPE_RIGHT.name)\n        }.swipeRight(speed = 2000)\n    }\n\n    @Test\n    @FlakyTest\n    fun swipeLeftTest() {\n        page.editTextContentDesc.replaceText(\"some text\")\n        page.swipableImageView.withAssertion {\n            page.eventStatus.withTimeout(300).textContains(UiElementsActivity.Event.SWIPE_LEFT.name)\n        }.swipeLeft(speed = 2000)\n    }\n\n    @Test\n    fun customAppendText_toEditableObject() {\n        val startText = \"start \"\n        val textToAdd = \"added new Text\"\n        page.editTextContentDesc\n            .replaceText(startText)\n            .appendText(textToAdd)\n            .hasText(startText + textToAdd)\n    }\n}"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/uiautomator/UltronUiObject2AssertionsTest.kt",
    "content": "package com.atiurin.sampleapp.tests.uiautomator\n\nimport com.atiurin.sampleapp.R\nimport com.atiurin.sampleapp.framework.Log\nimport com.atiurin.sampleapp.framework.utils.AssertUtils\nimport com.atiurin.sampleapp.pages.UiObject2ElementsPage\nimport com.atiurin.sampleapp.tests.UiElementsTest\nimport com.atiurin.ultron.utils.getTargetString\nimport org.hamcrest.Matchers.containsString\nimport org.hamcrest.Matchers.equalToIgnoringCase\nimport org.junit.Assert\nimport org.junit.Test\n\nclass UltronUiObject2AssertionsTest: UiElementsTest() {\n    val page = UiObject2ElementsPage()\n\n    //hasText\n    @Test\n    fun hasText_CorrectText_withResourceId() {\n        page.editTextContentDesc.hasText(getTargetString(R.string.button_default_content_desc))\n    }\n\n    @Test\n    fun hasText_InvalidText_withResourceId() {\n\n        AssertUtils.assertException { page.editTextContentDesc.withTimeout(100).withTimeout(100).hasText(\"invalid text\") }\n    }\n\n    @Test\n    fun hasText_notExisted(){\n        AssertUtils.assertException { page.notExistedObject.withTimeout(100).withTimeout(100).hasText(\"asd\") }\n    }\n\n    //hasText matcher\n    @Test\n    fun hasText_matcher_CorrectText() {\n        page.editTextContentDesc.hasText(containsString(\"content description\"))\n    }\n\n    @Test\n    fun hasText_matcher_InvalidText() {\n        AssertUtils.assertException { page.editTextContentDesc.withTimeout(100).hasText(equalToIgnoringCase(\"invalid text\") ) }\n    }\n\n    @Test\n    fun hasText_matcher_notExisted(){\n        AssertUtils.assertException { page.notExistedObject.withTimeout(100).hasText(containsString(\"asd\") ) }\n    }\n\n    //textContains\n    @Test\n    fun textContains_validText(){\n        val substring = getTargetString(R.string.button_text).substring(0, 5)\n        page.button.textContains(substring)\n    }\n\n    @Test\n    fun textContains_invalidText(){\n        AssertUtils.assertException { page.button.withTimeout(100).textContains(\"invalid substring\" ) }\n    }\n\n    @Test\n    fun textContains_notExisted(){\n        AssertUtils.assertException { page.notExistedObject.withTimeout(100).textContains(\"invalid substring\" ) }\n    }\n\n    //textIsNullOrEmpty\n    @Test\n    fun textIsNullOrEmpty_nullText(){\n        page.editTextContentDesc.clear().withTimeout(100).textIsNullOrEmpty()\n    }\n\n    @Test\n    fun textIsNullOrEmpty_notEmptyText(){\n        AssertUtils.assertException { page.editTextContentDesc.withTimeout(100).textIsNullOrEmpty() }\n    }\n\n    @Test\n    fun textIsNullOrEmpty_notExisted(){\n        AssertUtils.assertException { page.notExistedObject.withTimeout(100).textIsNullOrEmpty() }\n    }\n\n    //textIsNotNullOrEmpty\n    @Test\n    fun textIsNotNullOrEmpty_notEmptyText(){\n        page.editTextContentDesc.textIsNotNullOrEmpty()\n    }\n\n    @Test\n    fun textIsNotNullOrEmpty_nullText(){\n        AssertUtils.assertException { page.editTextContentDesc.clear().withTimeout(100).textIsNotNullOrEmpty() }\n    }\n\n    //contentDescription\n\n    //hasContentDescription\n    @Test\n    fun hasContentDescription_CorrectText_withResourceId() {\n        page.button.hasContentDescription(getTargetString(R.string.button_default_content_desc))\n    }\n\n    @Test\n    fun hasContentDescription_InvalidText_withResourceId() {\n        AssertUtils.assertException { page.button.withTimeout(100).hasContentDescription(\"invalid text\" ) }\n    }\n\n    @Test\n    fun hasContentDescription_notExisted(){\n        AssertUtils.assertException { page.notExistedObject.withTimeout(100).hasContentDescription(\"asd\" ) }\n    }\n\n    //contentDescription matcher\n    @Test\n    fun hasContentDescription_matcher_CorrectText() {\n        page.button.hasContentDescription(containsString(getTargetString(R.string.button_default_content_desc).substring(0, 5)))\n    }\n\n    @Test\n    fun hasContentDescription_matcher_InvalidText() {\n        AssertUtils.assertException { page.button.withTimeout(100).hasContentDescription(equalToIgnoringCase(\"invalid text\") ) }\n    }\n\n    @Test\n    fun hasContentDescription_matcher_notExisted(){\n        AssertUtils.assertException { page.notExistedObject.withTimeout(100).hasContentDescription(containsString(\"asd\") ) }\n    }\n\n    //contentDescriptionContains\n    @Test\n    fun contentDescriptionContains_validText(){\n        val substring = getTargetString(R.string.button_default_content_desc).substring(0, 5)\n        page.button.contentDescriptionContains(substring)\n    }\n\n    @Test\n    fun contentDescriptionContains_invalidText(){\n        AssertUtils.assertException { page.button.withTimeout(100).contentDescriptionContains(\"invalid substring\" ) }\n    }\n\n    @Test\n    fun contentDescriptionContains_notExisted(){\n        AssertUtils.assertException { page.notExistedObject.withTimeout(100).contentDescriptionContains(\"invalid substring\" ) }\n    }\n\n    //contentDescriptionIsNullOrEmpty\n    @Test\n    fun contentDescriptionIsNullOrEmpty_nullText(){\n        page.editTextContentDesc.clear()\n        page.button.contentDescriptionIsNullOrEmpty()\n    }\n\n    @Test\n    fun contentDescriptionIsNullOrEmpty_notEmptyText(){\n        AssertUtils.assertException { page.button.withTimeout(100).contentDescriptionIsNullOrEmpty() }\n    }\n\n    @Test\n    fun contentDescriptionIsNullOrEmpty_notExisted(){\n        AssertUtils.assertException { page.notExistedObject.withTimeout(100).contentDescriptionIsNullOrEmpty() }\n    }\n\n    //contentDescriptionIsNotNullOrEmpty\n    @Test\n    fun contentDescriptionIsNotNullOrEmpty_notEmptyText(){\n        page.button.contentDescriptionIsNotNullOrEmpty()\n    }\n\n    @Test\n    fun contentDescriptionIsNotNullOrEmpty_nullText(){\n        page.editTextContentDesc.clear()\n        AssertUtils.assertException { page.button.withTimeout(100).contentDescriptionIsNotNullOrEmpty() }\n    }\n    //isDisplayed\n    @Test\n    fun isDisplayed_ofDisplayedObject() {\n        page.button.isDisplayed()\n    }\n\n    @Test\n    fun isDisplayed_ofInvisibleObject() {\n        page.radioInvisibleButton.click()\n        AssertUtils.assertException { page.button.withTimeout(100).isDisplayed() }\n    }\n\n    @Test\n    fun isDisplayed_BooleanTrue_ofInvisibleObject() {\n        val result = page.radioVisibleButton.isSuccess { withTimeout(100).isDisplayed() }\n        Assert.assertTrue(result)\n    }\n\n    @Test\n    fun isDisplayed_BooleanFalse_ofInvisibleObject() {\n        page.radioInvisibleButton.click()\n        val result = page.button.isSuccess { withTimeout(100).isDisplayed() }\n        Assert.assertFalse(result)\n    }\n    //isNotDisplayed\n    @Test\n    fun isNotDisplayed_ofDisplayedObject() {\n        AssertUtils.assertException { page.button.isDisplayed().withTimeout(100).isNotDisplayed() }\n    }\n\n    @Test\n    fun isNotDisplayed_ofInvisibleObject() {\n        page.radioInvisibleButton.click()\n        page.button.isNotDisplayed()\n    }\n\n    //isCheckable\n    @Test\n    fun isCheckable_ofCheckable(){\n        page.checkBoxClickable.isCheckable()\n    }\n    @Test\n    fun isCheckable_ofNotCheckable(){\n        AssertUtils.assertException { page.emptyImageView.withTimeout(100).isCheckable() }\n    }\n\n    //isNotCheckable\n    @Test\n    fun isNotCheckable_ofNotCheckable(){\n        page.emptyImageView.isNotCheckable()\n    }\n\n    @Test\n    fun isNotCheckable_ofCheckable(){\n        AssertUtils.assertException { page.checkBoxClickable.withTimeout(100).isNotCheckable() }\n    }\n\n    //isChecked\n    @Test\n    fun isChecked_ofChecked(){\n        page.checkBoxClickable.isChecked()\n    }\n\n    @Test\n    fun isChecked_ofNotChecked(){\n        AssertUtils.assertException { page.checkBoxSelected.withTimeout(100).isChecked() }\n    }\n    //isNotChecked\n    @Test\n    fun isNotChecked_ofNotChecked(){\n        page.checkBoxSelected.isNotChecked()\n    }\n\n    @Test\n    fun isNotChecked_ofChecked(){\n        AssertUtils.assertException { page.checkBoxClickable.withTimeout(100).isNotChecked() }\n    }\n\n    //isClickable\n    @Test\n    fun isClickable_ofClickable(){\n        page.button.isClickable()\n    }\n\n    @Test\n    fun isClickable_ofNotClickable(){\n        page.checkBoxClickable.click()\n        AssertUtils.assertException { page.button.withTimeout(100).isClickable() }\n    }\n\n    //isNotClickable\n    @Test\n    fun isNotClickable_ofNotClickable(){\n        page.checkBoxClickable.click()\n        page.button.isNotClickable()\n    }\n\n    @Test\n    fun isNotClickable_ofClickable(){\n        AssertUtils.assertException { page.button.withTimeout(100).isNotClickable() }\n    }\n\n    //isEnabled\n    @Test\n    fun isEnabled_ofEnable(){\n        page.button.isEnabled()\n    }\n\n    @Test\n    fun isEnabled_ofNotEnable(){\n        page.checkBoxEnabled.click()\n        AssertUtils.assertException { page.button.withTimeout(100).isEnabled() }\n    }\n\n    //isNotEnabled\n    @Test\n    fun isNotEnabled_ofNotEnable(){\n        page.checkBoxEnabled.click()\n        page.button.isNotEnabled()\n    }\n\n    @Test\n    fun isNotEnabled_ofEnable(){\n        AssertUtils.assertException { page.button.withTimeout(100).isNotEnabled() }\n    }\n\n    //isFocusable\n    @Test\n    fun isFocusable_ofFocusable(){\n        page.button.isFocusable()\n    }\n\n    @Test\n    fun isFocusable_ofNotFocusable(){\n        page.checkBoxFocusable.click()\n        AssertUtils.assertException { page.button.withTimeout(100).isFocusable() }\n    }\n\n    //isNotFocusable\n    @Test\n    fun isNotFocusable_ofNotFocusable(){\n        page.checkBoxFocusable.click()\n        page.button.isNotFocusable()\n    }\n\n    @Test\n    fun isNotFocusable_ofFocusable(){\n        AssertUtils.assertException { page.button.withTimeout(100).isNotFocusable() }\n    }\n\n    //isFocused\n    @Test\n    fun isFocused_ofFocused(){\n        page.editTextContentDesc.click().isFocused()\n    }\n\n    @Test\n    fun isFocused_ofNotFocused(){\n        AssertUtils.assertException { page.editTextContentDesc.withTimeout(100).isFocused() }\n    }\n\n    //isNotFocused\n    @Test\n    fun isNotFocused_ofNotFocused(){\n        page.editTextContentDesc.isNotFocused()\n    }\n\n    @Test\n    fun isNotFocused_ofFocused(){\n        AssertUtils.assertException { page.editTextContentDesc.click().withTimeout(100).isNotFocused() }\n    }\n\n    //isLongClickable\n    @Test\n    fun isLongClickable_ofLongClickable(){\n        page.button.isLongClickable()\n    }\n\n    @Test\n    fun isLongClickable_ofNotLongClickable(){\n        AssertUtils.assertException { page.emptyImageView.withTimeout(100).isLongClickable() }\n    }\n\n    //isNotLongClickable\n    @Test\n    fun isNotLongClickable_ofNotLongClickable(){\n        page.emptyImageView.isNotLongClickable()\n    }\n\n    @Test\n    fun isNotLongClickable_ofLongClickable(){\n        AssertUtils.assertException { page.button.withTimeout(100).isNotLongClickable() }\n    }\n\n    //isSelected\n    @Test\n    fun isSelected_ofSelected(){\n        page.checkBoxSelected.click()\n        page.button.isSelected()\n    }\n\n    @Test\n    fun isSelected_ofNotSelected(){\n        AssertUtils.assertException { page.button.withTimeout(100).isSelected() }\n    }\n\n    //isNotSelected\n    @Test\n    fun isNotSelected_ofNotSelected(){\n        page.button.isNotSelected()\n    }\n\n    @Test\n    fun isNotSelected_ofSelected(){\n        page.checkBoxSelected.click()\n        AssertUtils.assertException { page.button.withTimeout(100).isNotSelected() }\n    }\n\n    //isNotScrollable\n    @Test\n    fun isNotScrollable_ofNotScrollable(){\n        page.button.isNotScrollable()\n    }\n\n    @Test\n    fun isScrollable_ofNotScrollable(){\n        AssertUtils.assertException { page.button.withTimeout(100).isScrollable() }\n    }\n\n    //assertThat\n    @Test\n    fun assertThat_validAssertion_existedObject(){\n        page.button.assertThat({ this.isClickable }, \"Object is clickable\")\n    }\n\n    @Test\n    fun assertThat_invalidAssertion_existedObject(){\n        AssertUtils.assertException { page.button.withTimeout(100).assertThat({ !this.isClickable }, \"Object isn't clickable\") }\n    }\n\n    @Test\n    fun assertThat_notExistedObject(){\n        AssertUtils.assertException { page.notExistedObject.withTimeout(100).assertThat({ !this.isClickable }, \"Should fail\") }\n    }\n\n    @Test\n    fun resultHandlerTest(){\n        page.editTextContentDesc.withResultHandler {\n            Assert.assertFalse(it.success)\n            Assert.assertTrue(it.description.isNotEmpty())\n        }.withTimeout(100).hasText(\"invalid text\")\n        AssertUtils.assertException { page.editTextContentDesc.withTimeout(100).isNotDisplayed() }\n    }\n}"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/uiautomator/UltronUiObject2ScrollTest.kt",
    "content": "package com.atiurin.sampleapp.tests.uiautomator\n\nimport android.os.Build\nimport com.atiurin.sampleapp.activity.MainActivity\nimport com.atiurin.sampleapp.pages.UiObject2FriendsListPage\nimport com.atiurin.sampleapp.tests.BaseTest\nimport com.atiurin.ultron.core.config.UltronConfig\nimport com.atiurin.ultron.testlifecycle.activity.UltronActivityRule\nimport org.junit.BeforeClass\nimport org.junit.Test\n\nclass UltronUiObject2ScrollTest : BaseTest() {\n    init {\n        ruleSequence.addLast(UltronActivityRule(MainActivity::class.java))\n    }\n\n    companion object {\n        @BeforeClass\n        @JvmStatic\n        fun speedUpAutomator() {\n            UltronConfig.UiAutomator.speedUp()\n        }\n    }\n\n    val page = UiObject2FriendsListPage\n\n    @Test\n    fun scrollToBottom() {\n        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {\n            for (i in 0..10) {\n                if (page.bottomElement.isSuccess { withTimeout(100).isDisplayed() }) break\n                page.list.scrollDown(percent = 0.5f)\n            }\n            page.bottomElement.isDisplayed()\n        }\n    }\n\n    @Test\n    fun scrollToTop() {\n        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {\n            for (i in 0..10) {\n                if (page.bottomElement.isSuccess { withTimeout(100).isDisplayed() }) break\n                page.list.scrollDown(percent = 0.5f)\n            }\n            page.bottomElement.isDisplayed()\n            for (i in 0..10) {\n                if (page.topElement.isSuccess { withTimeout(100).isDisplayed() }) break\n                page.list.scrollUp(percent = 0.5f)\n            }\n        }\n    }\n}"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/uiautomator/UltronUiObject2UiBlockTest.kt",
    "content": "package com.atiurin.sampleapp.tests.uiautomator\n\nimport com.atiurin.sampleapp.activity.UiBlockActivity\nimport com.atiurin.sampleapp.data.repositories.CONTACTS\nimport com.atiurin.sampleapp.framework.utils.AssertUtils\nimport com.atiurin.sampleapp.pages.uiblock.UiObject2UiBlockScreen\nimport com.atiurin.sampleapp.tests.BaseTest\nimport com.atiurin.ultron.testlifecycle.activity.UltronActivityRule\nimport org.junit.Rule\nimport org.junit.Test\n\nclass UltronUiObject2UiBlockTest: BaseTest() {\n    @get:Rule\n    val activityRule = UltronActivityRule(UiBlockActivity::class.java)\n\n    @Test\n    fun notUniqueUiElement_WithDeepSearch(){\n        UiObject2UiBlockScreen {\n            block1.uiBlock.isDisplayed()\n            block2.name.isDisplayed().hasText(CONTACTS[1].name)\n            block1.deepSearchChild.withTimeout(100).isDisplayed()\n        }\n    }\n\n    @Test\n    fun notUniqueUiElement_WithoutDeepSearch(){\n        UiObject2UiBlockScreen {\n            AssertUtils.assertException {\n                block2.notExisted.isDisplayed()\n            }\n        }\n    }\n\n    @Test\n    fun uiBlockInBlock(){\n        UiObject2UiBlockScreen {\n            blocks.uiBlock.isDisplayed()\n            blocks.item1.uiBlock.isDisplayed()\n            blocks.item1.name.isDisplayed().hasText(CONTACTS[0].name)\n            blocks.item1.status.isDisplayed().hasText(CONTACTS[0].status)\n            AssertUtils.assertException {\n                blocks.item2.notExisted.isDisplayed()\n            }\n        }\n    }\n}"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/uiautomator/UltronUiObjectActionsTest.kt",
    "content": "package com.atiurin.sampleapp.tests.uiautomator\n\nimport androidx.test.platform.app.InstrumentationRegistry\nimport com.atiurin.sampleapp.R\nimport com.atiurin.sampleapp.activity.UiElementsActivity\nimport com.atiurin.sampleapp.framework.utils.AssertUtils\nimport com.atiurin.sampleapp.framework.utils.TestDataUtils\nimport com.atiurin.sampleapp.pages.UiObjectElementsPage\nimport com.atiurin.sampleapp.tests.UiElementsTest\nimport com.atiurin.ultron.core.uiautomator.uiobject.UltronUiObject.Companion.uiSelector\nimport com.atiurin.ultron.utils.getTargetString\nimport org.junit.Assert\nimport org.junit.Test\n\nclass UltronUiObjectActionsTest: UiElementsTest() {\n    val page = UiObjectElementsPage()\n    //getChild\n    @Test\n    fun getChild_existedChild(){\n        val child = page.radioGroup.getChild(uiSelector(R.id.radio_gone))\n        Assert.assertNotNull(child)\n        child.click()\n    }\n\n    @Test\n    fun getChild_notExistedChild(){\n        val child = page.radioGroup.getChild(uiSelector(R.id.button1))\n        Assert.assertNotNull(child)\n        AssertUtils.assertException { child.withTimeout(100).click() }\n    }\n\n    //getChildCount\n    @Test\n    fun getChildCount_childExist(){\n        Assert.assertEquals(3, page.radioGroup.getChildCount())\n    }\n\n    @Test\n    fun getChildCount_noChildExist(){\n        Assert.assertEquals(0, page.button.getChildCount())\n    }\n\n    //getFromParent\n    @Test\n    fun findObject_existedChildObject(){\n        val child = page.radioGroup.getFromParent(uiSelector(R.id.radio_invisible))\n        Assert.assertNotNull(child)\n        child.click()\n        page.button.notExists()\n    }\n\n    @Test\n    fun getFromParent_notExistedChildObject(){\n        val child = page.radioGoneButton.getFromParent(uiSelector(R.id.button1))\n        Assert.assertNotNull(child)\n        AssertUtils.assertException { child.withTimeout(100).click() }\n    }\n\n    //getText\n    @Test\n    fun getText_objectHasText(){\n        Assert.assertEquals(getTargetString(R.string.button_text), page.button.getText())\n    }\n\n    @Test\n    fun getText_objectHasNoText(){\n        Assert.assertTrue( page.swipableImageView.getText()!!.isEmpty())\n    }\n\n    @Test\n    fun getText_notExistedObject(){\n        AssertUtils.assertException { page.notExistedObject.withTimeout(100).getText() }\n    }\n\n    //getClassName\n    @Test\n\n    fun getClassName_existObject(){\n        Assert.assertEquals(\"android.widget.Button\", page.button.getClassName())\n    }\n\n    @Test\n    fun getClassName_notExistObject(){\n        AssertUtils.assertException { page.notExistedObject.withTimeout(100).getClassName() }\n    }\n\n    //getApplicationPackage\n    @Test\n    fun getApplicationPackage_existedObject(){\n        val expected = InstrumentationRegistry.getInstrumentation().targetContext.applicationInfo.packageName\n        Assert.assertEquals(expected, page.button.getPackageName())\n    }\n\n    @Test\n    fun getApplicationPackage_notExistedObject(){\n        AssertUtils.assertException {  page.notExistedObject.withTimeout(100).getPackageName() }\n    }\n\n    //getVisibleBounds\n    @Test\n    fun getVisibleBounds_existedObject(){\n        Assert.assertNotNull(page.button.getVisibleBounds())\n    }\n\n    @Test\n    fun getVisibleBounds_notExistedObject(){\n        AssertUtils.assertException {  page.notExistedObject.withTimeout(100).getVisibleBounds() }\n    }\n\n    //getContentDescription\n    @Test\n    fun getContentDescription_existedContentDescription(){\n        val expectedContDesc = getTargetString(R.string.button_default_content_desc)\n        Assert.assertEquals(expectedContDesc, page.button.getContentDescription())\n    }\n\n    @Test\n    fun getContentDescription_notExistedResourceName(){\n        Assert.assertTrue(page.checkBoxClickable.getContentDescription()!!.isEmpty())\n    }\n\n    @Test\n    fun getContentDescription_notExistedObject(){\n        AssertUtils.assertException { page.notExistedObject.withTimeout(100).getContentDescription() }\n    }\n\n    //click\n    @Test\n    fun click_onClickable() {\n        page.button.exists().click()\n        page.eventStatus.textContains(TestDataUtils.getResourceString(R.string.button_event_click))\n    }\n\n    @Test\n    fun click_withDuration_onClickable() {\n        page.button.exists().click()\n        page.eventStatus.textContains(TestDataUtils.getResourceString(R.string.button_event_click))\n    }\n\n    @Test\n    fun click_onNotExistedObject() {\n        AssertUtils.assertException { page.notExistedObject.withTimeout(100).click() }\n    }\n    //longClick\n    @Test\n    fun longClick_onLongClickable() {\n        page.button.exists().withAssertion {\n            page.eventStatus.textContains(getTargetString(R.string.button_event_long_click))\n        }.longClick()\n    }\n\n    @Test\n    fun longClick_onNotExistedObject() {\n        AssertUtils.assertException { page.notExistedObject.withTimeout(100).longClick() }\n    }\n\n    @Test\n    fun legacySetText_toEditable(){\n        val text = \"text to replace \"\n        page.editTextContentDesc\n            .clearTextField()\n            .legacyAddText(text)\n            .hasText(text)\n    }\n\n    @Test\n    fun legacySetText_toNotExistedObject(){\n        AssertUtils.assertException { page.notExistedObject.withTimeout(100).legacyAddText(\"some new text\") }\n    }\n    //replaceText\n    @Test\n    fun replaceText_toEditable(){\n        val text = \"replaceText to new\"\n        page.editTextContentDesc\n            .replaceText(text)\n            .hasText(text)\n    }\n    @Test\n    fun replaceText_toUneditableObject(){\n        AssertUtils.assertException { page.button.withTimeout(100).replaceText(\"some new text\") }\n    }\n\n    @Test\n    fun replaceText_toNotExistedObject(){\n        AssertUtils.assertException { page.notExistedObject.withTimeout(100).replaceText(\"some new text\") }\n    }\n\n    //perform\n    @Test\n    fun perform_existedObject(){\n        page.button.perform({ this.click() }, \"Click on button\")\n        page.eventStatus.textContains(TestDataUtils.getResourceString(R.string.button_event_click))\n    }\n\n    @Test\n    fun perform_notExistedObject(){\n        AssertUtils.assertException { page.notExistedObject.withTimeout(100).perform({this.click()}, \"Click on button\") }\n    }\n\n    //swipe\n    @Test\n    fun swipeUpTest(){\n        page.swipableImageView.withAssertion {\n            page.eventStatus.withTimeout(300).textContains(UiElementsActivity.Event.SWIPE_UP.name)\n        }.swipeUp(40)\n\n    }\n\n    @Test\n    fun swipeDownTest(){\n        page.eventStatus.hasText(getTargetString(R.string.button_text))\n        page.swipableImageView.withAssertion {\n            page.eventStatus.withTimeout(300).textContains(UiElementsActivity.Event.SWIPE_DOWN.name)\n        }.swipeDown(40)\n    }\n\n    @Test\n    fun swipeRightTest(){\n        page.swipableImageView.withAssertion {\n            page.eventStatus.withTimeout(300).textContains(UiElementsActivity.Event.SWIPE_RIGHT.name)\n        }.swipeRight(40)\n    }\n\n    @Test\n    fun swipeLeftTest(){\n        page.swipableImageView.withAssertion {\n            page.eventStatus.withTimeout(300).textContains(UiElementsActivity.Event.SWIPE_LEFT.name)\n        }.swipeLeft(40)\n    }\n}"
  },
  {
    "path": "sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/uiautomator/UltronUiObjectAssertionsTest.kt",
    "content": "package com.atiurin.sampleapp.tests.uiautomator\n\nimport com.atiurin.sampleapp.R\nimport com.atiurin.sampleapp.framework.utils.AssertUtils\nimport com.atiurin.sampleapp.pages.UiObjectElementsPage\nimport com.atiurin.sampleapp.tests.UiElementsTest\nimport com.atiurin.ultron.utils.getTargetString\nimport org.hamcrest.Matchers.*\nimport org.junit.Assert\nimport org.junit.Test\n\nclass UltronUiObjectAssertionsTest: UiElementsTest() {\n    val page = UiObjectElementsPage()\n\n    //hasText\n    @Test\n    fun hasText_CorrectText_withResourceId() {\n        page.editTextContentDesc.hasText(getTargetString(R.string.button_default_content_desc))\n    }\n\n    @Test\n    fun hasText_InvalidText_withResourceId() {\n\n        AssertUtils.assertException { page.editTextContentDesc.withTimeout(100).withTimeout(100).hasText(\"invalid text\") }\n    }\n\n    @Test\n    fun hasText_notExisted(){\n        AssertUtils.assertException { page.notExistedObject.withTimeout(100).withTimeout(100).hasText(\"asd\") }\n    }\n\n    //hasText matcher\n    @Test\n    fun hasText_matcher_CorrectText() {\n        page.editTextContentDesc.hasText(containsString(\"content description\"))\n    }\n\n    @Test\n    fun hasText_matcher_InvalidText() {\n        AssertUtils.assertException { page.editTextContentDesc.withTimeout(100).hasText(equalToIgnoringCase(\"invalid text\") ) }\n    }\n\n    @Test\n    fun hasText_matcher_notExisted(){\n        AssertUtils.assertException { page.notExistedObject.withTimeout(100).hasText(containsString(\"asd\") ) }\n    }\n\n    //textContains\n    @Test\n    fun textContains_validText(){\n        val substring = getTargetString(R.string.button_text).substring(0, 5)\n        page.button.textContains(substring)\n    }\n\n    @Test\n    fun textContains_invalidText(){\n        AssertUtils.assertException { page.button.withTimeout(100).textContains(\"invalid substring\" ) }\n    }\n\n    @Test\n    fun textContains_notExisted(){\n        AssertUtils.assertException { page.notExistedObject.withTimeout(100).textContains(\"invalid substring\" ) }\n    }\n\n    //textIsNullOrEmpty\n    @Test\n    fun textIsNullOrEmpty_nullText(){\n        page.editTextContentDesc.clearTextField().withTimeout(100).textIsNullOrEmpty()\n    }\n\n    @Test\n    fun textIsNullOrEmpty_notEmptyText(){\n        AssertUtils.assertException { page.editTextContentDesc.withTimeout(100).hasText(\"\") }\n    }\n\n    @Test\n    fun textIsNullOrEmpty_notExisted(){\n        AssertUtils.assertException { page.notExistedObject.withTimeout(100).hasText(\"\") }\n    }\n\n    //textIsNotNullOrEmpty\n    @Test\n    fun textIsNotNullOrEmpty_notEmptyText(){\n        page.editTextContentDesc.textIsNotNullOrEmpty()\n    }\n\n    @Test\n    fun textIsNotNullOrEmpty_nullText(){\n        AssertUtils.assertException { page.editTextContentDesc.clearTextField().withTimeout(100).textIsNotNullOrEmpty() }\n    }\n\n    //contentDescription\n\n    //hasContentDescription\n    @Test\n    fun hasContentDescription_CorrectText_withResourceId() {\n        page.button.hasContentDescription(getTargetString(R.string.button_default_content_desc))\n    }\n\n    @Test\n    fun hasContentDescription_InvalidText_withResourceId() {\n        AssertUtils.assertException { page.button.withTimeout(100).hasContentDescription(\"invalid text\" ) }\n    }\n\n    @Test\n    fun hasContentDescription_notExisted(){\n        AssertUtils.assertException { page.notExistedObject.withTimeout(100).hasContentDescription(\"asd\" ) }\n    }\n\n    //contentDescription matcher\n    @Test\n    fun hasContentDescription_matcher_CorrectText() {\n        page.button.hasContentDescription(containsString(getTargetString(R.string.button_default_content_desc).substring(0, 5)))\n    }\n\n    @Test\n    fun hasContentDescription_matcher_InvalidText() {\n        AssertUtils.assertException { page.button.withTimeout(100).hasContentDescription(equalToIgnoringCase(\"invalid text\") ) }\n    }\n\n    @Test\n    fun hasContentDescription_matcher_notExisted(){\n        AssertUtils.assertException { page.notExistedObject.withTimeout(100).hasContentDescription(containsString(\"asd\") ) }\n    }\n\n    //contentDescriptionContains\n    @Test\n    fun contentDescriptionContains_validText(){\n        val substring = getTargetString(R.string.button_default_content_desc).substring(0, 5)\n        page.button.contentDescriptionContains(substring)\n    }\n\n    @Test\n    fun contentDescriptionContains_invalidText(){\n        AssertUtils.assertException { page.button.withTimeout(100).contentDescriptionContains(\"invalid substring\" ) }\n    }\n\n    @Test\n    fun contentDescriptionContains_notExisted(){\n        AssertUtils.assertException { page.notExistedObject.withTimeout(100).contentDescriptionContains(\"invalid substring\" ) }\n    }\n\n    //contentDescriptionIsNullOrEmpty\n    @Test\n    fun contentDescriptionIsNullOrEmpty_nullText(){\n        page.editTextContentDesc.clearTextField()\n        page.button.contentDescriptionIsNullOrEmpty()\n    }\n\n    @Test\n    fun contentDescriptionIsNullOrEmpty_notEmptyText(){\n        AssertUtils.assertException { page.button.withTimeout(100).contentDescriptionIsNullOrEmpty() }\n    }\n\n    @Test\n    fun contentDescriptionIsNullOrEmpty_notExisted(){\n        AssertUtils.assertException { page.notExistedObject.withTimeout(100).contentDescriptionIsNullOrEmpty() }\n    }\n\n    //contentDescriptionIsNotNullOrEmpty\n    @Test\n    fun contentDescriptionIsNotNullOrEmpty_notEmptyText(){\n        page.button.contentDescriptionIsNotNullOrEmpty()\n    }\n\n    @Test\n    fun contentDescriptionIsNotNullOrEmpty_nullText(){\n        page.editTextContentDesc.clearTextField()\n        page.button.contentDescriptionIsNullOrEmpty()\n    }\n    //exists\n    @Test\n    fun isDisplayed_ofDisplayedObject() {\n        page.button.exists()\n    }\n\n    @Test\n    fun isDisplayed_ofInvisibleObject() {\n        page.radioInvisibleButton.click()\n        AssertUtils.assertException { page.button.withTimeout(100).exists() }\n    }\n\n    @Test\n    fun isDisplayed_BooleanTrue_ofInvisibleObject() {\n        val result = page.radioInvisibleButton.isSuccess { withTimeout(100).exists() }\n        Assert.assertTrue(result)\n    }\n\n    @Test\n    fun isDisplayed_BooleanFalse_ofInvisibleObject() {\n        page.radioInvisibleButton.click()\n        val result = page.button.isSuccess { withTimeout(100).exists() }\n        Assert.assertFalse(result)\n    }\n    //notExists\n    @Test\n    fun isNotDisplayed_ofDisplayedObject() {\n        AssertUtils.assertException { page.button.exists().withTimeout(100).notExists() }\n    }\n\n    @Test\n    fun isNotDisplayed_ofInvisibleObject() {\n        page.radioInvisibleButton.click()\n        page.button.notExists()\n    }\n\n    //isCheckable\n    @Test\n    fun isCheckable_ofCheckable(){\n        page.checkBoxClickable.isCheckable()\n    }\n    @Test\n    fun isCheckable_ofNotCheckable(){\n        AssertUtils.assertException { page.emptyImageView.withTimeout(100).isCheckable() }\n    }\n\n    //isNotCheckable\n    @Test\n    fun isNotCheckable_ofNotCheckable(){\n        page.emptyImageView.isNotCheckable()\n    }\n\n    @Test\n    fun isNotCheckable_ofCheckable(){\n        AssertUtils.assertException { page.checkBoxClickable.withTimeout(100).isNotCheckable() }\n    }\n\n    //isChecked\n    @Test\n    fun isChecked_ofChecked(){\n        page.checkBoxClickable.isChecked()\n    }\n\n    @Test\n    fun isChecked_ofNotChecked(){\n        AssertUtils.assertException { page.checkBoxSelected.withTimeout(100).isChecked() }\n    }\n    //isNotChecked\n    @Test\n    fun isNotChecked_ofNotChecked(){\n        page.checkBoxSelected.isNotChecked()\n    }\n\n    @Test\n    fun isNotChecked_ofChecked(){\n        AssertUtils.assertException { page.checkBoxClickable.withTimeout(100).isNotChecked() }\n    }\n\n    //isClickable\n    @Test\n    fun isClickable_ofClickable(){\n        page.button.isClickable()\n    }\n\n    @Test\n    fun isClickable_ofNotClickable(){\n        page.checkBoxClickable.click()\n        AssertUtils.assertException { page.button.withTimeout(100).isClickable() }\n    }\n\n    //isNotClickable\n    @Test\n    fun isNotClickable_ofNotClickable(){\n        page.checkBoxClickable.click()\n        page.button.isNotClickable()\n    }\n\n    @Test\n    fun isNotClickable_ofClickable(){\n        AssertUtils.assertException { page.button.withTimeout(100).isNotClickable() }\n    }\n\n    //isEnabled\n    @Test\n    fun isEnabled_ofEnable(){\n        page.button.isEnabled()\n    }\n\n    @Test\n    fun isEnabled_ofNotEnable(){\n        page.checkBoxEnabled.click()\n        AssertUtils.assertException { page.button.withTimeout(100).isEnabled() }\n    }\n\n    //isNotEnabled\n    @Test\n    fun isNotEnabled_ofNotEnable(){\n        page.checkBoxEnabled.click()\n        page.button.isNotEnabled()\n    }\n\n    @Test\n    fun isNotEnabled_ofEnable(){\n        AssertUtils.assertException { page.button.withTimeout(100).isNotEnabled() }\n    }\n\n    //isFocusable\n    @Test\n    fun isFocusable_ofFocusable(){\n        page.button.isFocusable()\n    }\n\n    @Test\n    fun isFocusable_ofNotFocusable(){\n        page.checkBoxFocusable.click()\n        AssertUtils.assertException { page.button.withTimeout(100).isFocusable() }\n    }\n\n    //isNotFocusable\n    @Test\n    fun isNotFocusable_ofNotFocusable(){\n        page.checkBoxFocusable.withAssertion {\n            page.button.isNotFocusable()\n        }.click()\n    }\n\n    @Test\n    fun isNotFocusable_ofFocusable(){\n        AssertUtils.assertException { page.button.withTimeout(100).isNotFocusable() }\n    }\n\n    //isFocused\n    @Test\n    fun isFocused_ofFocused(){\n        page.editTextContentDesc.click().isFocused()\n    }\n\n    @Test\n    fun isFocused_ofNotFocused(){\n        AssertUtils.assertException { page.editTextContentDesc.withTimeout(100).isFocused() }\n    }\n\n    //isNotFocused\n    @Test\n    fun isNotFocused_ofNotFocused(){\n        page.editTextContentDesc.isNotFocused()\n    }\n\n    @Test\n    fun isNotFocused_ofFocused(){\n        AssertUtils.assertException { page.editTextContentDesc.click().withTimeout(100).isNotFocused() }\n    }\n\n    //isLongClickable\n    @Test\n    fun isLongClickable_ofLongClickable(){\n        page.button.isLongClickable()\n    }\n\n    @Test\n    fun isLongClickable_ofNotLongClickable(){\n        AssertUtils.assertException { page.emptyImageView.withTimeout(100).isLongClickable() }\n    }\n\n    //isNotLongClickable\n    @Test\n    fun isNotLongClickable_ofNotLongClickable(){\n        page.emptyImageView.isNotLongClickable()\n    }\n\n    @Test\n    fun isNotLongClickable_ofLongClickable(){\n        AssertUtils.assertException { page.button.withTimeout(100).isNotLongClickable() }\n    }\n\n    //isSelected\n    @Test\n    fun isSelected_ofSelected(){\n        page.checkBoxSelected.click()\n        page.button.isSelected()\n    }\n\n    @Test\n    fun isSelected_ofNotSelected(){\n        AssertUtils.assertException { page.button.withTimeout(100).isSelected() }\n    }\n\n    //isNotSelected\n    @Test\n    fun isNotSelected_ofNotSelected(){\n        page.button.isNotSelected()\n    }\n\n    @Test\n    fun isNotSelected_ofSelected(){\n        page.checkBoxSelected.click()\n        AssertUtils.assertException { page.button.withTimeout(100).isNotSelected() }\n    }\n\n    //isNotScrollable\n    @Test\n    fun isNotScrollable_ofNotScrollable(){\n        page.button.isNotScrollable()\n    }\n\n    @Test\n    fun isScrollable_ofNotScrollable(){\n        AssertUtils.assertException { page.button.withTimeout(100).isScrollable() }\n    }\n\n    //assertThat\n    @Test\n    fun assertThat_validAssertion_existedObject(){\n        page.button.assertThat({ this.isClickable }, \"Object is clickable\")\n    }\n\n    @Test\n    fun assertThat_invalidAssertion_existedObject(){\n        AssertUtils.assertException { page.button.withTimeout(100).assertThat({ !this.isClickable }, \"Object isn't clickable\") }\n    }\n\n    @Test\n    fun assertThat_notExistedObject(){\n        AssertUtils.assertException { page.notExistedObject.withTimeout(100).assertThat({ !this.isClickable }, \"Should fail\") }\n    }\n\n    @Test\n    fun resultHandlerTest(){\n        page.editTextContentDesc.withResultHandler {\n            Assert.assertFalse(it.success)\n            Assert.assertTrue(it.description.isNotEmpty())\n        }.withTimeout(100).hasText(\"invalid text\")\n        AssertUtils.assertException { page.editTextContentDesc.withTimeout(100).notExists() }\n    }\n}"
  },
  {
    "path": "sample-app/src/debug/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.atiurin.sampleapp\">\n    <application>\n        <activity android:name=\"androidx.activity.ComponentActivity\" />\n    </application>\n\n</manifest>"
  },
  {
    "path": "sample-app/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n    <application\n        android:allowBackup=\"true\"\n        android:icon=\"@mipmap/ic_launcher\"\n        android:label=\"@string/app_name\"\n        android:requestLegacyExternalStorage=\"true\"\n        android:roundIcon=\"@mipmap/ic_launcher_round\"\n        android:supportsRtl=\"true\"\n        android:theme=\"@style/AppTheme\"\n        android:usesCleartextTraffic=\"true\">\n        <activity\n            android:name=\"com.atiurin.sampleapp.activity.SplashActivity\"\n            android:exported=\"true\"\n            android:theme=\"@style/SplashTheme\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\" />\n\n                <category android:name=\"android.intent.category.LAUNCHER\" />\n            </intent-filter>\n        </activity>\n        <activity\n            android:name=\"com.atiurin.sampleapp.activity.MainActivity\"\n            android:label=\"@string/app_name\"\n            android:theme=\"@style/AppTheme.NoActionBar\" />\n        <activity\n            android:name=\"com.atiurin.sampleapp.activity.UiBlockActivity\"\n            android:theme=\"@style/AppTheme.NoActionBar\" />\n        <activity\n            android:name=\"com.atiurin.sampleapp.activity.ChatActivity\"\n            android:theme=\"@style/AppTheme.NoActionBar\" />\n        <activity\n            android:name=\"com.atiurin.sampleapp.activity.LoginActivity\"\n            android:theme=\"@style/AppTheme\" />\n        <activity\n            android:name=\"com.atiurin.sampleapp.activity.ProfileActivity\"\n            android:theme=\"@style/AppTheme.NoActionBar\" />\n        <activity\n            android:name=\"com.atiurin.sampleapp.activity.UiElementsActivity\"\n            android:theme=\"@style/AppTheme.NoActionBar\" />\n\n        <activity\n            android:name=\"com.atiurin.sampleapp.activity.WebViewActivity\"\n            android:theme=\"@style/AppTheme.NoActionBar\" />\n        <activity\n            android:name=\"com.atiurin.sampleapp.activity.CustomClicksActivity\"\n            android:theme=\"@style/AppTheme.NoActionBar\" />\n        <activity\n            android:name=\"com.atiurin.sampleapp.activity.ComposeElementsActivity\"\n            android:theme=\"@style/AppTheme.NoActionBar\" />\n        <activity\n            android:name=\"com.atiurin.sampleapp.activity.ComposeRouterActivity\"\n            android:theme=\"@style/AppTheme.NoActionBar\" />\n        <activity\n            android:name=\"com.atiurin.sampleapp.activity.ComposeListActivity\"\n            android:theme=\"@style/AppTheme.NoActionBar\" />\n        <activity\n            android:name=\"com.atiurin.sampleapp.activity.ComposeListWithPositionTestTagActivity\"\n            android:theme=\"@style/AppTheme.NoActionBar\" />\n        <activity\n            android:name=\"com.atiurin.sampleapp.activity.ComposeSecondActivity\"\n            android:theme=\"@style/AppTheme.NoActionBar\" />\n        <activity\n            android:name=\"com.atiurin.sampleapp.activity.BusyActivity\"\n            android:exported=\"true\"\n            android:theme=\"@style/WhiteBackgroundTheme\">\n            <intent-filter android:priority=\"-100\" >\n                <action android:name=\"android.intent.action.VIEW\" />\n                <category android:name=\"android.intent.category.LAUNCHER\" />\n            </intent-filter>\n        </activity>\n    </application>\n\n</manifest>"
  },
  {
    "path": "sample-app/src/main/assets/webview.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n    <title>Android Web View</title>\n</head>\n<body>\n    <h1 id=\"title\" class=\"css_title\">WebView title</h1>\n    <textarea id=\"title_changer\" name=\"title_changer\"\n              rows=\"5\" cols=\"33\">\nSome defaule title\n</textarea>\n    <ol>\n        <li><a href=\"fake_link.html\" class=\"link\" id=\"apple_link\"\n               onclick=\"setTitle('apple clicked')\">Apple</a>\n        </li>\n        <li><a href=\"fake_link.html\" class=\"link\" id=\"banana\">Banana</a></li>\n    </ol>\n    <p>The button below activates a JavaScript when it is clicked.</p>\n    <form id=\"buttons_from\">\n        <input type=\"button\" class=\"button\" id=\"button1\" value=\"Rename title\" onclick=\"updTitle()\">\n        <input type=\"button\" class=\"button\" id=\"button2\" value=\"Button2\"\n               onclick=\"setTitle('button2 clicked')\">\n        <input type=\"button\" class=\"button\" id=\"button3\" value=\"Set title active\"\n               onclick=\"setTitleActive()\">\n    </form>\n    <input type=\"text\" id=\"text_input\" hint=\"input hint\">\n    <div id=\"persons\">\n        <div id=\"teacher\">\n            <p>Teachers</p>\n            <div class=\"person_name\">\n                <p>Socrates</p>\n            </div>\n        </div>\n        <div id=\"student\">\n            <p>Students</p>\n            <div class=\"person_name\">\n                <p>Plato</p>\n            </div>\n        </div>\n    </div>\n    <iframe src=\"demo.htm\" id=\"iframe\" height=\"200\" width=\"300\" title=\"Iframe Example\"></iframe>\n    <div id=\"list_element_1\" desc=\"some desc 1\"><p>list_element_1</p></div>\n    <div id=\"list_element_1\" desc=\"some desc 2\"><p>list_element_2</p></div>\n    <div id=\"list_element_3\"><p>list_element_3</p></div>\n    <div id=\"list_element_4\"><p>list_element_4</p></div>\n    <div id=\"list_element_5\"><p>list_element_5</p></div>\n    <div id=\"list_element_6\"><p>list_element_6</p></div>\n    <div id=\"list_element_7\"><p>list_element_7</p></div>\n    <div id=\"list_element_8\"><p>list_element_8</p></div>\n    <div id=\"list_element_9\"><p>list_element_9</p></div>\n    <div id=\"list_element_10\"><p>list_element_10</p></div>\n    <div id=\"list_element_11\"><p>list_element_11</p></div>\n    <div id=\"list_element_12\"><p>list_element_12</p></div>\n\n\n    <script>\n\n            function updTitle() {\n                 var value = document.getElementById(\"text_input\").value\n                 console.log(value)\n                 document.getElementById(\"title\").innerHTML = value;\n            }\n            function setTitle(newTitle) {\n                document.getElementById(\"title\").innerHTML = newTitle;\n            }\n            function setTitleActive() {\n                document.getElementById(\"title\").setActive();\n                document.getElementById(\"title\").focus();\n            }\n    </script>\n    </body>\n</html>"
  },
  {
    "path": "sample-app/src/main/assets/webview_small.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n    <title>Android Web View</title>\n</head>\n<body>\n<h1 id=\"title\" class=\"css_title\">WebView title</h1>\n<input type=\"text\" id=\"text_input\" hint=\"input hint\">\n</body>\n</html>"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/MyApplication.kt",
    "content": "package com.atiurin.sampleapp\n\nimport android.app.Application\nimport android.content.Context\n\nobject MyApplication : Application() {\n    var context: Context? = null\n    override fun onCreate() {\n        super.onCreate()\n        context = applicationContext\n    }\n\n    var CONTACTS_LOADING_TIMEOUT_MS = 2000L\n}"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/activity/BusyActivity.kt",
    "content": "package com.atiurin.sampleapp.activity\n\nimport android.app.Activity\nimport android.os.Bundle\nimport android.os.Handler\nimport android.os.Looper\n\nclass BusyActivity : Activity(){\n    private val handler = Handler(Looper.getMainLooper())\n    private val busyRunnable = object : Runnable {\n        override fun run() {\n            // Post a delayed runnable to keep the main thread busy indefinitely\n            handler.postDelayed(this, 0)\n        }\n    }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        // Start the busy loop\n        handler.post(busyRunnable)\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n        handler.removeCallbacks(busyRunnable)\n    }\n}"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/activity/ChatActivity.kt",
    "content": "package com.atiurin.sampleapp.activity\n\nimport android.os.Build\nimport android.os.Bundle\nimport android.util.Log\nimport android.view.Menu\nimport android.view.MenuItem\nimport android.view.View\nimport android.widget.EditText\nimport android.widget.ImageView\nimport android.widget.TextView\nimport android.widget.Toast\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.appcompat.widget.Toolbar\nimport androidx.recyclerview.widget.LinearLayoutManager\nimport androidx.recyclerview.widget.RecyclerView\nimport com.atiurin.sampleapp.R\nimport com.atiurin.sampleapp.adapters.MessageAdapter\nimport com.atiurin.sampleapp.data.entities.Contact\nimport com.atiurin.sampleapp.data.entities.Message\nimport com.atiurin.sampleapp.data.repositories.CURRENT_USER\nimport com.atiurin.sampleapp.data.repositories.ContactRepositoty\nimport com.atiurin.sampleapp.data.repositories.MessageRepository\nimport com.atiurin.sampleapp.view.CircleImageView\n\nconst val INTENT_CONTACT_ID_EXTRA_NAME = \"contactId\"\n\nclass ChatActivity : AppCompatActivity(){\n    private lateinit var recyclerView: RecyclerView\n    private lateinit var viewAdapter: MessageAdapter\n    private lateinit var viewManager: RecyclerView.LayoutManager\n    private lateinit var contact: Contact\n    private val onItemClickListener: View.OnClickListener? = null\n    override fun onCreate(savedInstanceState: Bundle?) {\n//        enableEdgeToEdge()\n        super.onCreate(savedInstanceState)\n        setContentView(R.layout.activity_chat)\n        val context = this\n        //TOOLBAR\n        val toolbar: Toolbar = findViewById(R.id.toolbar)\n        toolbar.title = \"\"\n        setSupportActionBar(toolbar)\n        supportActionBar!!.setDisplayHomeAsUpEnabled(true)\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {\n            window.statusBarColor = getColor(R.color.colorPrimaryDark)\n        }\n        val mIntent = intent\n        val title = findViewById<TextView>(R.id.toolbar_title)\n        val contactId = mIntent.getIntExtra(INTENT_CONTACT_ID_EXTRA_NAME, -1)\n        if (contactId < 0){\n            Log.d(\"EspressoGuide\", \"Something goes wrong!\")\n        }\n        contact = ContactRepositoty.getContact(contactId)\n        title.text = contact.name\n        val avatar = findViewById<CircleImageView>(R.id.toolbar_avatar)\n        avatar.setImageDrawable(getDrawable(contact.avatar))\n        //message input area\n        val messageInput = findViewById<EditText>(R.id.message_input_text)\n        val sendBtn = findViewById<ImageView>(R.id.send_button)\n        val attachBtn = findViewById<ImageView>(R.id.attach_button)\n\n        //recycler view and adapter\n        viewManager = LinearLayoutManager(this)\n        viewAdapter = MessageAdapter(\n            ArrayList<Message>(),\n            object : MessageAdapter.OnItemClickListener {\n                override fun onItemClick(message: Message) {\n                    Log.w(\"EspressoGuid\", \"Clicked message ${message.text}\")\n                }\n            })\n        recyclerView = findViewById<RecyclerView>(R.id.messages_list).apply {\n            setHasFixedSize(true)\n            layoutManager = viewManager\n            adapter = viewAdapter\n        }\n        sendBtn.setOnClickListener(\n            object: View.OnClickListener{\n                override fun onClick(v: View?) {\n                    if (messageInput.text.isEmpty()){\n                        Toast.makeText(context, \"Type message text\", Toast.LENGTH_LONG).show()\n                    }else{\n                        val mes = Message(CURRENT_USER.id, contactId, messageInput.text.toString())\n                        val curMessages = MessageRepository.messages\n                        curMessages.add(mes)\n                        updateAdapter(curMessages)\n                        messageInput.setText(\"\")\n                        recyclerView.smoothScrollToPosition(viewAdapter.itemCount - 1)\n                    }\n                }\n            })\n    }\n\n    override fun onCreateOptionsMenu(menu: Menu): Boolean {\n        menuInflater.inflate(R.menu.main, menu)\n        return true\n    }\n\n    override fun onOptionsItemSelected(item: MenuItem): Boolean {\n        return when (item.itemId) {\n            R.id.action_clear -> {\n                updateAdapter(ArrayList())\n                true\n            }\n            else -> super.onOptionsItemSelected(item)\n        }\n    }\n\n    override fun onSupportNavigateUp(): Boolean {\n        onBackPressed()\n        return true\n    }\n\n    override fun onResume() {\n        super.onResume()\n        viewAdapter.updateData(MessageRepository.getChatMessages(contact.id))\n        viewAdapter.notifyDataSetChanged()\n    }\n\n    private fun updateAdapter(list: ArrayList<Message>){\n        MessageRepository.messages = list\n        viewAdapter.updateData(list)\n        viewAdapter.notifyDataSetChanged()\n    }\n}"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/activity/ComposeElementsActivity.kt",
    "content": "package com.atiurin.sampleapp.activity\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport androidx.activity.enableEdgeToEdge\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.selection.toggleable\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.Button\nimport androidx.compose.material.ButtonDefaults\nimport androidx.compose.material.Checkbox\nimport androidx.compose.material.CheckboxDefaults\nimport androidx.compose.material.ExperimentalMaterialApi\nimport androidx.compose.material.Icon\nimport androidx.compose.material.Text\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Favorite\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.CardDefaults\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.MutableState\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.platform.testTag\nimport androidx.compose.ui.semantics.contentDescription\nimport androidx.compose.ui.semantics.semantics\nimport androidx.compose.ui.semantics.stateDescription\nimport androidx.compose.ui.semantics.testTag\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.ExperimentalUnitApi\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport com.atiurin.sampleapp.activity.ComposeElementsActivity.Constants.clickListenerButton\nimport com.atiurin.sampleapp.activity.ComposeElementsActivity.Constants.contactBlock1Tag\nimport com.atiurin.sampleapp.activity.ComposeElementsActivity.Constants.contactBlock2Tag\nimport com.atiurin.sampleapp.activity.ComposeElementsActivity.Constants.contactNameTag\nimport com.atiurin.sampleapp.activity.ComposeElementsActivity.Constants.contactStatusTag\nimport com.atiurin.sampleapp.activity.ComposeElementsActivity.Constants.disabledButton\nimport com.atiurin.sampleapp.activity.ComposeElementsActivity.Constants.likesCounterButton\nimport com.atiurin.sampleapp.activity.ComposeElementsActivity.Constants.likesCounterContentDesc\nimport com.atiurin.sampleapp.activity.ComposeElementsActivity.Constants.likesCounterTextContainer\nimport com.atiurin.sampleapp.activity.ComposeElementsActivity.Constants.likesCounterTextContainerContentDesc\nimport com.atiurin.sampleapp.compose.CustomButton\nimport com.atiurin.sampleapp.compose.LinearProgressBar\nimport com.atiurin.sampleapp.compose.RadioGroup\nimport com.atiurin.sampleapp.compose.RegionsClickListener\nimport com.atiurin.sampleapp.compose.SimpleOutlinedText\nimport com.atiurin.sampleapp.compose.SwipeableNode\nimport com.atiurin.sampleapp.data.repositories.CONTACTS\n\nclass ComposeElementsActivity : ComponentActivity() {\n    companion object Constants {\n        const val statusText = \"statusText\"\n        const val likesCounterButton = \"LikeS_CounteR\"\n        const val likesCounterTextContainer = \"text_container\"\n        const val clickListenerButton = \"click_listener\"\n        const val simpleCheckbox = \"simpleCheckbox\"\n        const val editableText = \"editableText\"\n        const val swipeableNode = \"swipableNode\"\n        const val progressBar = \"ProgressBar\"\n        const val disabledButton = \"disabledButton\"\n        const val radioButtonMaleTestTag = \"radioButtonMaleTestTag\"\n        const val radioButtonFemaleTestTag = \"radioButtonFemaleTestTag\"\n        const val likesCounterContentDesc = \"LikeS_CounteR_ContentDesc\"\n        const val likesCounterTextContainerContentDesc = \"text_container_content_desc\"\n        const val contactsListTag = \"contactsListTag\"\n        const val contactBlock1Tag = \"contactBlock1\"\n        const val contactBlock2Tag = \"contactBlock2\"\n        const val contactNameTag = \"firstNameTag\"\n        const val contactStatusTag = \"lastNameTag\"\n    }\n\n    @ExperimentalMaterialApi\n    @ExperimentalUnitApi\n    @ExperimentalFoundationApi\n    override fun onCreate(savedInstanceState: Bundle?) {\n        enableEdgeToEdge()\n        super.onCreate(savedInstanceState)\n\n        setContent {\n            Column(\n                modifier = Modifier\n                    .fillMaxSize()\n                    .padding(start = 16.dp),\n                verticalArrangement = Arrangement.Top,\n                horizontalAlignment = Alignment.Start\n            ) {\n                val status = remember { mutableStateOf(\"nothing\") }\n                Text(text = status.value, modifier = Modifier.semantics { testTag = statusText })\n                ButtonWithCount()\n                CheckBox(title = \"Simple checkbox\", testTagValue = simpleCheckbox, status)\n                ClickListener(status)\n                RegionsClickListener(status)\n                SimpleOutlinedText(myTestTag = editableText)\n                SwipeableNode(status)\n                DisabledButton()\n                LinearProgressBar(statusState = status)\n                RadioGroup()\n                ContactsList(modifier = Modifier.testTag(contactsListTag))\n            }\n        }\n    }\n}\n\nenum class ActionsStatus {\n    LongClicked, DoubleClicked, Clicked, SwipeDown, SwipeUp, SwipeRight, SwipeLeft\n}\n\n@Composable\nfun CheckBox(title: String, testTagValue: String, statusState: MutableState<String>) {\n    val state = remember { mutableStateOf(false) }\n    Row(verticalAlignment = Alignment.CenterVertically) {\n        Checkbox(\n            checked = state.value,\n            onCheckedChange = { state.value = it },\n            enabled = true,\n            colors = CheckboxDefaults.colors(Color.Green),\n            modifier = Modifier\n                .semantics {\n                    testTag = testTagValue\n                    stateDescription = \"default\"\n                }\n                .toggleable(state.value, true, onValueChange = { newValue -> statusState.value = \"toggleable state $newValue\" })\n        )\n        Text(text = title, modifier = Modifier.padding(16.dp))\n    }\n\n}\n\n@OptIn(ExperimentalFoundationApi::class)\n@Composable\nfun ButtonWithCount() {\n    var count = remember {\n        mutableStateOf(0)\n    }\n    Button(onClick = { count.value++ }, modifier = Modifier\n        .semantics {\n            testTag = likesCounterButton\n            contentDescription = likesCounterContentDesc\n        }) {\n        Icon(\n            imageVector = Icons.Filled.Favorite,\n            contentDescription = null\n        )\n        Spacer(Modifier.size(ButtonDefaults.IconSpacing))\n        Text(\"Like count = ${count.value}\",\n            modifier = Modifier.semantics {\n                testTag = likesCounterTextContainer\n                contentDescription = likesCounterTextContainerContentDesc\n            }\n        )\n    }\n}\n\n@Preview\n@Composable\nfun ContactInfoBlockPreview() {\n    ContactInfoBlock(name= \"Tonny Kark\", status = \"Wipes off Thanos\" )\n}\n\n@Composable\nfun ClickListener(statusState: MutableState<String>) {\n    CustomButton(\n        onClickAction = { statusState.value = ActionsStatus.Clicked.name },\n        onLongClick = { statusState.value = ActionsStatus.LongClicked.name },\n        onDoubleClick = { statusState.value = ActionsStatus.DoubleClicked.name },\n        modifier = Modifier\n            .semantics {\n                testTag = clickListenerButton\n                contentDescription = clickListenerButton\n            }\n    ) {\n        Text(text = \"Click listener button\")\n    }\n}\n\n@Composable\nfun DisabledButton() {\n    Button(\n        modifier = Modifier.semantics {\n            testTag = disabledButton\n        },\n        onClick = {},\n        content = { Text(text = \"Disabled button\") },\n        enabled = false\n    )\n}\n\n@Composable\nfun ContactsList(modifier: Modifier){\n    Column(modifier = modifier) {\n        ContactInfoBlock(Modifier.testTag(contactBlock1Tag), CONTACTS[0].name, CONTACTS[0].status)\n        ContactInfoBlock(Modifier.testTag(contactBlock2Tag), CONTACTS[1].name, CONTACTS[1].status)\n    }\n}\n\n@Composable\nfun ContactInfoBlock(\n    modifier: Modifier = Modifier,\n    name: String,\n    status: String\n) {\n    Card(\n        modifier = modifier\n            .fillMaxWidth()\n            .padding(16.dp),\n        shape = RoundedCornerShape(12.dp),\n        elevation = CardDefaults.cardElevation(8.dp),\n        colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)\n    ) {\n        Row(\n            modifier = Modifier\n                .padding(16.dp)\n                .fillMaxWidth()\n                .testTag(\"Inner row\"),\n            verticalAlignment = Alignment.CenterVertically\n        ) {\n            Box(\n                modifier = Modifier\n                    .size(48.dp)\n                    .background(\n                        color = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f),\n                        shape = RoundedCornerShape(24.dp)\n                    )\n            ) {\n                Text(\n                    text = name.firstOrNull()?.toString()?.uppercase() ?: \"\",\n                    modifier = Modifier.align(Alignment.Center),\n                    style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.Bold),\n                    color = MaterialTheme.colorScheme.primary\n                )\n            }\n\n            Spacer(modifier = Modifier.width(16.dp))\n\n            Column {\n                Text(\n                    modifier = Modifier.testTag(contactNameTag),\n                    text = name,\n                    style = MaterialTheme.typography.bodyLarge.copy(\n                        fontSize = 18.sp,\n                        fontWeight = FontWeight.Bold\n                    ),\n                    color = MaterialTheme.colorScheme.onSurface\n                )\n                Text(\n                    modifier = Modifier.testTag(contactStatusTag),\n                    text = status,\n                    style = MaterialTheme.typography.bodyMedium,\n                    color = MaterialTheme.colorScheme.onSurfaceVariant\n                )\n            }\n        }\n    }\n}\n\n\n\n\n"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/activity/ComposeListActivity.kt",
    "content": "package com.atiurin.sampleapp.activity\n\nimport android.os.Bundle\nimport android.widget.Toast\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport androidx.activity.enableEdgeToEdge\nimport androidx.activity.viewModels\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.material.ExperimentalMaterialApi\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.ExperimentalUnitApi\nimport androidx.lifecycle.Observer\nimport com.atiurin.sampleapp.async.GetContacts\nimport com.atiurin.sampleapp.async.UseCase\nimport com.atiurin.sampleapp.compose.ContactsList\nimport com.atiurin.sampleapp.compose.LoadingAnimation\nimport com.atiurin.sampleapp.compose.getContactItemTestTagById\nimport com.atiurin.sampleapp.compose.listItemPosition\nimport com.atiurin.sampleapp.data.entities.Contact\nimport com.atiurin.sampleapp.data.repositories.ContactRepositoty\nimport com.atiurin.sampleapp.data.viewmodel.ContactsViewModel\nimport kotlinx.coroutines.GlobalScope\nimport kotlinx.coroutines.async\n\nclass ComposeListActivity : ComponentActivity() {\n    val model: ContactsViewModel by viewModels()\n\n    @ExperimentalMaterialApi\n    @ExperimentalUnitApi\n    @ExperimentalFoundationApi\n    override fun onCreate(savedInstanceState: Bundle?) {\n        enableEdgeToEdge()\n        super.onCreate(savedInstanceState)\n        setContent {\n            Column(\n                modifier = Modifier.fillMaxSize(),\n                verticalArrangement = Arrangement.Center,\n                horizontalAlignment = Alignment.CenterHorizontally\n            ) {\n                LoadingAnimation()\n            }\n        }\n        val contactsObserver = Observer<List<Contact>> {\n            setContent {\n                Column {\n                    ContactsList(\n                        contacts = ContactRepositoty.all(),\n                        context = this@ComposeListActivity,\n                        testTagProvider = { contact, _ -> getContactItemTestTagById(contact) },\n                        modifierProvider = { position -> Modifier.listItemPosition(position) }\n                    )\n                }\n            }\n        }\n        model.contacts.observe(this, contactsObserver)\n        GlobalScope.async {\n            GetContacts(0)(\n                UseCase.None,\n                onSuccess = { model.contacts.value = it },\n                onFailure = { Toast.makeText(this@ComposeListActivity, \"Failed to load contacts\", Toast.LENGTH_LONG).show() }\n            )\n        }\n    }\n}\n\n\n"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/activity/ComposeListWithPositionTestTagActivity.kt",
    "content": "package com.atiurin.sampleapp.activity\n\nimport android.os.Bundle\nimport android.widget.Toast\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport androidx.activity.enableEdgeToEdge\nimport androidx.activity.viewModels\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.material.ExperimentalMaterialApi\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.ExperimentalUnitApi\nimport androidx.lifecycle.Observer\nimport com.atiurin.sampleapp.async.GetContacts\nimport com.atiurin.sampleapp.async.UseCase\nimport com.atiurin.sampleapp.compose.ContactsList\nimport com.atiurin.sampleapp.compose.LoadingAnimation\nimport com.atiurin.sampleapp.compose.getContactItemTestTagById\nimport com.atiurin.sampleapp.compose.getContactItemTestTagByPosition\nimport com.atiurin.sampleapp.compose.listItemPosition\nimport com.atiurin.sampleapp.data.entities.Contact\nimport com.atiurin.sampleapp.data.repositories.ContactRepositoty\nimport com.atiurin.sampleapp.data.viewmodel.ContactsViewModel\nimport kotlinx.coroutines.GlobalScope\nimport kotlinx.coroutines.async\n\nclass ComposeListWithPositionTestTagActivity: ComponentActivity() {\n    val model: ContactsViewModel by viewModels()\n\n    @ExperimentalMaterialApi\n    @ExperimentalUnitApi\n    @ExperimentalFoundationApi\n    override fun onCreate(savedInstanceState: Bundle?) {\n        enableEdgeToEdge()\n        super.onCreate(savedInstanceState)\n        setContent {\n            Column(\n                modifier = Modifier.fillMaxSize(),\n                verticalArrangement = Arrangement.Center,\n                horizontalAlignment = Alignment.CenterHorizontally\n            ) {\n                LoadingAnimation()\n            }\n        }\n        val contactsObserver = Observer<List<Contact>> {\n            setContent {\n                Column {\n                    ContactsList(\n                        contacts = ContactRepositoty.all(),\n                        context = this@ComposeListWithPositionTestTagActivity,\n                        addStickyHeader = false,\n                        testTagProvider = { _, position -> getContactItemTestTagByPosition(position) },\n                        modifierProvider = { _ -> Modifier }\n                    )\n                }\n            }\n        }\n        model.contacts.observe(this, contactsObserver)\n        GlobalScope.async {\n            GetContacts(0)(\n                UseCase.None,\n                onSuccess = { model.contacts.value = it },\n                onFailure = { Toast.makeText(this@ComposeListWithPositionTestTagActivity, \"Failed to load contacts\", Toast.LENGTH_LONG).show() }\n            )\n        }\n    }\n}\n\n\n"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/activity/ComposeRouterActivity.kt",
    "content": "package com.atiurin.sampleapp.activity\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport androidx.activity.enableEdgeToEdge\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.material.ExperimentalMaterialApi\nimport androidx.compose.ui.unit.ExperimentalUnitApi\nimport com.atiurin.sampleapp.compose.app.App\n\nclass ComposeRouterActivity : ComponentActivity() {\n    @ExperimentalMaterialApi\n    @ExperimentalUnitApi\n    @ExperimentalFoundationApi\n    override fun onCreate(savedInstanceState: Bundle?) {\n        enableEdgeToEdge()\n        super.onCreate(savedInstanceState)\n\n        setContent {\n            App()\n        }\n    }\n}\n\n\n"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/activity/ComposeSecondActivity.kt",
    "content": "package com.atiurin.sampleapp.activity\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport androidx.activity.enableEdgeToEdge\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.material.Text\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.semantics.semantics\nimport androidx.compose.ui.semantics.testTag\nimport androidx.compose.ui.unit.ExperimentalUnitApi\nimport androidx.compose.ui.unit.TextUnit\nimport androidx.compose.ui.unit.TextUnitType\nimport androidx.compose.ui.unit.dp\nimport com.atiurin.sampleapp.data.repositories.ContactRepositoty\n\n\nclass ComposeSecondActivity : ComponentActivity() {\n    @ExperimentalUnitApi\n    override fun onCreate(savedInstanceState: Bundle?) {\n        enableEdgeToEdge()\n        super.onCreate(savedInstanceState)\n        val contactId = intent.getIntExtra(INTENT_CONTACT_ID, -1)\n        val contact = ContactRepositoty.getContact(contactId)\n        setContent {\n            Column() {\n                Text(contact.name, Modifier.semantics { testTag = \"name\" }, fontSize = TextUnit(16f, TextUnitType.Sp) )\n                Spacer(modifier = Modifier.height(8.dp))\n                Text(text = contact.status, Modifier.semantics { testTag = \"status\" })\n            }\n        }\n    }\n\n    companion object {\n        const val INTENT_CONTACT_ID = \"contactId\"\n    }\n}"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/activity/CustomClicksActivity.kt",
    "content": "package com.atiurin.sampleapp.activity\n\nimport android.os.Bundle\nimport androidx.activity.enableEdgeToEdge\nimport androidx.appcompat.app.AppCompatActivity\nimport com.atiurin.sampleapp.R\nimport com.atiurin.sampleapp.async.task.CompatAsyncTask\nimport com.atiurin.sampleapp.async.task.CompatAsyncTask.Companion.ASYNC\n\nclass CustomClicksActivity : AppCompatActivity() {\n\n    private val compatAsyncTask = CompatAsyncTask()\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        enableEdgeToEdge()\n        super.onCreate(savedInstanceState)\n        setContentView(R.layout.activity_custom_clicks)\n        if(shouldBeAsyncTaskStart()) {\n            startCompatAsyncTask()\n        }\n    }\n\n    fun shouldBeAsyncTaskStart(): Boolean = intent.getBooleanExtra(ASYNC, false)\n\n    fun startCompatAsyncTask() {\n        compatAsyncTask.start()\n    }\n\n    fun stopCompatAsyncTask() {\n        compatAsyncTask.stop()\n    }\n\n}"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/activity/LoginActivity.kt",
    "content": "package com.atiurin.sampleapp.activity\n\nimport android.content.Intent\nimport android.os.Bundle\nimport android.view.Gravity\nimport android.widget.Button\nimport android.widget.EditText\nimport android.widget.Toast\nimport androidx.activity.enableEdgeToEdge\nimport androidx.appcompat.app.AppCompatActivity\nimport com.atiurin.sampleapp.R\nimport com.atiurin.sampleapp.managers.AccountManager\n\nclass LoginActivity : AppCompatActivity(){\n    lateinit var etUserName : EditText\n    lateinit var etPassword : EditText\n    lateinit var loginBtn : Button\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        enableEdgeToEdge()\n        super.onCreate(savedInstanceState)\n        setContentView(R.layout.activity_login)\n\n        val bar = supportActionBar\n        bar!!.title = \"Login or Sign Up\"\n\n        etUserName = findViewById(R.id.et_username)\n        etPassword = findViewById(R.id.et_password)\n        loginBtn = findViewById(R.id.login_button)\n\n        loginBtn.setOnClickListener{\n            val accountManager = AccountManager(applicationContext)\n            val userName = etUserName.text.toString()\n            val password = etPassword.text.toString()\n\n            if (userName.isEmpty()){\n                with(etUserName){\n                    setHint(\"Enter user name\")\n                    setHintTextColor(resources.getColor(android.R.color.holo_red_dark))\n                }\n            }\n            if (password.isEmpty()){\n                with(etPassword){\n                    setHint(\"Enter password\")\n                    setHintTextColor(resources.getColor(android.R.color.holo_red_dark))\n                }\n            }\n            val result = accountManager.login(userName, password)\n            if (result){\n                var intent = Intent(this, MainActivity::class.java)\n                startActivity(intent)\n            }else{\n                var toast = Toast.makeText(applicationContext, \"Wrong login or password\", Toast.LENGTH_LONG)\n                    toast.setGravity(Gravity.CENTER_VERTICAL, 0, 0)\n                    toast.show()\n            }\n        }\n    }\n\n\n}"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/activity/MainActivity.kt",
    "content": "package com.atiurin.sampleapp.activity\n\nimport android.content.Intent\nimport android.os.Bundle\nimport android.view.MenuItem\nimport android.view.View\nimport android.widget.Toast\nimport androidx.activity.enableEdgeToEdge\nimport androidx.appcompat.app.ActionBarDrawerToggle\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.appcompat.widget.Toolbar\nimport androidx.core.view.GravityCompat\nimport androidx.drawerlayout.widget.DrawerLayout\nimport androidx.recyclerview.widget.LinearLayoutManager\nimport androidx.recyclerview.widget.RecyclerView\nimport com.atiurin.sampleapp.MyApplication\nimport com.atiurin.sampleapp.R\nimport com.atiurin.sampleapp.adapters.ContactAdapter\nimport com.atiurin.sampleapp.async.ContactsPresenter\nimport com.atiurin.sampleapp.async.ContactsProvider\nimport com.atiurin.sampleapp.data.Tags\nimport com.atiurin.sampleapp.data.entities.Contact\nimport com.atiurin.sampleapp.idlingresources.IdlingHelper\nimport com.atiurin.sampleapp.idlingresources.resources.ContactsIdlingResource\nimport com.atiurin.sampleapp.managers.AccountManager\nimport com.atiurin.sampleapp.view.CircleImageView\nimport com.google.android.material.navigation.NavigationView\nimport com.google.android.material.snackbar.Snackbar\n\n\nclass MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener, ContactsProvider {\n\n\n    private lateinit var recyclerView: RecyclerView\n    private lateinit var viewAdapter: ContactAdapter\n    private lateinit var viewManager: RecyclerView.LayoutManager\n    private lateinit var accountManager: AccountManager\n    private val onItemClickListener: View.OnClickListener? = null\n    private val contactsPresenter = ContactsPresenter(this)\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        enableEdgeToEdge()\n        super.onCreate(savedInstanceState)\n        accountManager = AccountManager(applicationContext)\n        if (!accountManager.isLogedIn()) {\n            val intent = Intent(applicationContext, LoginActivity::class.java)\n            startActivity(intent)\n        }\n        setContentView(R.layout.activity_main)\n        MyApplication.context = applicationContext\n        val toolbar: Toolbar = findViewById(R.id.toolbar)\n        toolbar.setTitle(R.string.title_friends_list)\n        setSupportActionBar(toolbar)\n        val drawerLayout: DrawerLayout = findViewById(R.id.drawer_layout)\n        val navView: NavigationView = findViewById(R.id.nav_view)\n        val navigationAvatar = navView.getHeaderView(0).findViewById<CircleImageView>(R.id.navigation_user_avatar)\n        navigationAvatar.setOnClickListener {\n            startActivity(Intent(applicationContext, ProfileActivity::class.java))\n        }\n\n        val toggle = ActionBarDrawerToggle(\n            this, drawerLayout, toolbar,\n            R.string.navigation_drawer_open,\n            R.string.navigation_drawer_close\n        )\n        drawerLayout.addDrawerListener(toggle)\n        toggle.syncState()\n\n        navView.setNavigationItemSelectedListener(this)\n\n\n        viewManager = LinearLayoutManager(this)\n        viewAdapter = ContactAdapter(this, ArrayList<Contact>(),\n            object : ContactAdapter.OnItemClickListener {\n                override fun onItemClick(contact: Contact) {\n                    val intent = Intent(applicationContext, ChatActivity::class.java)\n                    intent.putExtra(INTENT_CONTACT_ID_EXTRA_NAME, contact.id)\n                    startActivity(intent)\n                }\n            })\n        recyclerView = findViewById<RecyclerView>(R.id.recycler_friends).apply {\n            setHasFixedSize(true)\n            layoutManager = viewManager\n            adapter = viewAdapter\n        }\n        recyclerView.tag = Tags.CONTACTS_LIST\n        contactsPresenter.getAllContacts()\n    }\n\n    override fun onBackPressed() {\n        val drawerLayout: DrawerLayout = findViewById(com.atiurin.sampleapp.R.id.drawer_layout)\n        if (drawerLayout.isDrawerOpen(GravityCompat.START)) {\n            drawerLayout.closeDrawer(GravityCompat.START)\n        } else {\n            super.onBackPressed()\n        }\n    }\n\n    override fun onContactsLoaded(contacts: ArrayList<Contact>) {\n        viewAdapter.updateData(contacts)\n        viewAdapter.notifyDataSetChanged()\n        IdlingHelper.ifAllowed { ContactsIdlingResource.getInstanceFromApp()?.setIdleState(true) }\n    }\n\n    override fun onFailedToLoadContacts(message: String?) {\n        Toast.makeText(this, message, Toast.LENGTH_LONG).show()\n    }\n\n    override fun onNavigationItemSelected(item: MenuItem): Boolean {\n        // Handle navigation view item clicks here.\n        when (item.itemId) {\n            R.id.nav_settings -> {\n                Snackbar.make(recyclerView, \"Settings not implemented\", Snackbar.LENGTH_LONG)\n                    .setAction(\"Action\", null).show()\n            }\n            R.id.nav_saved_messages -> {\n                Snackbar.make(recyclerView, \"Saved messages not implemented\", Snackbar.LENGTH_LONG)\n                    .setAction(\"Action\", null).show()\n            }\n            R.id.nav_profile -> {\n                Toast.makeText(this, \"Profile not implemented\", Toast.LENGTH_LONG).show()\n            }\n            R.id.nav_logout -> {\n                accountManager.logout()\n                val intent = Intent(applicationContext, LoginActivity::class.java)\n                startActivity(intent)\n            }\n            R.id.ui_elements -> {\n                val intent = Intent(applicationContext, UiElementsActivity::class.java)\n                startActivity(intent)\n            }\n            R.id.web_view_nav_item -> {\n                val intent = Intent(applicationContext, WebViewActivity::class.java)\n                startActivity(intent)\n            }\n            R.id.compose_elements -> {\n                startActivity(Intent(applicationContext, ComposeElementsActivity::class.java))\n            }\n            R.id.compose_router -> {\n                startActivity(Intent(applicationContext, ComposeRouterActivity::class.java))\n            }\n            R.id.compose_list -> {\n                startActivity(Intent(applicationContext, ComposeListActivity::class.java))\n            }\n            R.id.custom_clicks_nav_item -> {\n                val intent = Intent(applicationContext, CustomClicksActivity::class.java)\n                startActivity(intent)\n            }\n        }\n        val drawerLayout: DrawerLayout = findViewById(R.id.drawer_layout)\n        drawerLayout.closeDrawer(GravityCompat.START)\n        return true\n    }\n}\n"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/activity/ProfileActivity.kt",
    "content": "package com.atiurin.sampleapp.activity\n\nimport android.os.Bundle\nimport android.widget.EditText\nimport androidx.activity.enableEdgeToEdge\nimport androidx.appcompat.app.AppCompatActivity\nimport com.atiurin.sampleapp.R\nimport com.atiurin.sampleapp.data.repositories.CURRENT_USER\nimport com.atiurin.sampleapp.view.CircleImageView\n\nclass ProfileActivity : AppCompatActivity(){\n     override fun onCreate(savedInstanceState: Bundle?) {\n         enableEdgeToEdge()\n         super.onCreate(savedInstanceState)\n         setContentView(R.layout.activity_profile)\n         val avatar = findViewById<CircleImageView>(R.id.avatar)\n         avatar.setImageDrawable(getDrawable(CURRENT_USER.avatar))\n         val name = findViewById<EditText>(R.id.et_username)\n         name.hint = CURRENT_USER.name\n     }\n}"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/activity/SplashActivity.kt",
    "content": "package com.atiurin.sampleapp.activity\n\nimport android.content.Intent\nimport android.os.Bundle\nimport androidx.activity.enableEdgeToEdge\nimport androidx.appcompat.app.AppCompatActivity\nimport com.atiurin.sampleapp.managers.AccountManager\n\nclass SplashActivity : AppCompatActivity() {\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        enableEdgeToEdge()\n        super.onCreate(savedInstanceState)\n\n        val accountManager = AccountManager(applicationContext)\n        if (accountManager.isLogedIn()){\n            val intent = Intent(applicationContext, MainActivity::class.java)\n            startActivity(intent)\n        }else{\n            val intent = Intent(applicationContext, LoginActivity::class.java)\n            startActivity(intent)\n        }\n        finish()\n    }\n\n}"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/activity/UiBlockActivity.kt",
    "content": "package com.atiurin.sampleapp.activity\n\nimport android.os.Bundle\nimport android.widget.LinearLayout\nimport android.widget.TextView\nimport androidx.appcompat.app.AppCompatActivity\nimport com.atiurin.sampleapp.R\nimport com.atiurin.sampleapp.data.repositories.CONTACTS\n\nclass UiBlockActivity : AppCompatActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContentView(R.layout.activity_uiblock)\n        val contactItem1: LinearLayout = this.findViewById(R.id.contact_item_1)\n        val contactItem2: LinearLayout = this.findViewById(R.id.contact_item_2)\n        contactItem1.findViewById<TextView>(R.id.name).text = CONTACTS[0].name\n        contactItem1.findViewById<TextView>(R.id.status).text = CONTACTS[0].status\n        contactItem2.findViewById<TextView>(R.id.name).text = CONTACTS[1].name\n        contactItem2.findViewById<TextView>(R.id.status).text = CONTACTS[1].status\n    }\n\n    override fun onSupportNavigateUp(): Boolean {\n        onBackPressed()\n        return true\n    }\n\n}"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/activity/UiElementsActivity.kt",
    "content": "package com.atiurin.sampleapp.activity\n\nimport android.annotation.SuppressLint\nimport android.os.Build\nimport android.os.Bundle\nimport android.text.Editable\nimport android.text.TextWatcher\nimport android.util.Log\nimport android.view.View\nimport android.view.View.*\nimport android.webkit.WebView\nimport android.widget.*\nimport androidx.activity.enableEdgeToEdge\nimport androidx.activity.viewModels\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.lifecycle.Observer\nimport com.atiurin.sampleapp.R\nimport com.atiurin.sampleapp.async.AsyncDataLoading\nimport com.atiurin.sampleapp.async.UseCase\nimport com.atiurin.sampleapp.data.viewmodel.DataViewModel\nimport com.atiurin.sampleapp.view.listeners.OnSwipeTouchListener\nimport kotlinx.coroutines.GlobalScope\nimport kotlinx.coroutines.async\n\nclass UiElementsActivity : AppCompatActivity() {\n    var lastEventDescription: TextView? = null\n    var clickedInRow = 0\n    var lastEvent = Event.NO_EVENT\n    val model: DataViewModel by viewModels()\n    @SuppressLint(\"ClickableViewAccessibility\")\n    override fun onCreate(savedInstanceState: Bundle?) {\n        enableEdgeToEdge()\n        super.onCreate(savedInstanceState)\n        setContentView(R.layout.activity_uielements)\n        val simpleButton: Button = findViewById(R.id.button1)\n        simpleButton.visibility = GONE\n        lastEventDescription = findViewById(R.id.last_event_status)\n        val enableCheckBox: CheckBox = findViewById(R.id.checkbox_enable)\n        val clickableCheckBox: CheckBox = findViewById(R.id.checkbox_clickable)\n        val selectedCheckBox: CheckBox = findViewById(R.id.checkbox_selected)\n        val focusableCheckBox: CheckBox = findViewById(R.id.checkbox_focusable)\n        val radioGroupVisibility: RadioGroup = findViewById(R.id.radio_group_visibility)\n        val etContentDescription: EditText = findViewById(R.id.et_contentDesc)\n        val webView: WebView = findViewById(R.id.webview)\n        val jsCheckBox: CheckBox = findViewById(R.id.checkbox_js_enabled)\n        val imageView : ImageView = findViewById(R.id.swipe_image_view)\n        webView.settings.javaScriptEnabled = true\n        val customHtml = applicationContext.assets.open(\"webview_small.html\").reader().readText()\n        webView.loadData(customHtml, \"text/html\", \"UTF-8\")\n\n        simpleButton.setOnClickListener {\n            setLastEvent(Event.CLICK, getString(R.string.button_event_click))\n        }\n        simpleButton.setOnLongClickListener { view ->\n            setLastEvent(Event.LONG_CLICK, getString(R.string.button_event_long_click))\n            return@setOnLongClickListener true\n        }\n        enableCheckBox.setOnClickListener { view ->\n            val checked = (view as CheckBox).isChecked\n            simpleButton.isEnabled = checked\n            setLastEvent(Event.ENABLED, checked.toString())\n        }\n        clickableCheckBox.setOnClickListener { view ->\n            val checked = (view as CheckBox).isChecked\n            simpleButton.isClickable = checked\n            setLastEvent(Event.CLICKABLE, checked.toString())\n        }\n        selectedCheckBox.setOnClickListener { view ->\n            val checked = (view as CheckBox).isChecked\n            simpleButton.isSelected = checked\n            setLastEvent(Event.SELECTED, checked.toString())\n        }\n        jsCheckBox.setOnClickListener { view ->\n            val checked = (view as CheckBox).isChecked\n            webView.settings.javaScriptEnabled = checked\n            setLastEvent(Event.JS_ENABLED, checked.toString())\n        }\n        radioGroupVisibility.setOnCheckedChangeListener { group, checkedId ->\n            when (checkedId) {\n                R.id.radio_visible -> simpleButton.visibility = View.VISIBLE\n                R.id.radio_invisible -> simpleButton.visibility = View.INVISIBLE\n                R.id.radio_gone -> simpleButton.visibility = View.GONE\n            }\n            setLastEvent(Event.DISPLAYED, simpleButton.visibility.toString())\n        }\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n            focusableCheckBox.visibility = VISIBLE\n            focusableCheckBox.setOnClickListener { view ->\n                val checked = (view as CheckBox).isChecked\n                if (checked) {\n                    simpleButton.focusable = FOCUSABLE\n                } else {\n                    simpleButton.focusable = NOT_FOCUSABLE\n                }\n                setLastEvent(Event.FOCUSABLE, checked.toString())\n            }\n        }\n        val addTextChangedListener =\n            etContentDescription.addTextChangedListener(object : TextWatcher {\n                override fun afterTextChanged(text: Editable?) {\n                    simpleButton.contentDescription = text\n                    setLastEvent(Event.CONTENT_DESC, text.toString())\n                }\n\n                override fun beforeTextChanged(\n                    s: CharSequence?,\n                    start: Int,\n                    count: Int,\n                    after: Int\n                ) {\n                }\n\n                override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}\n            })\n        val context = this\n        imageView.setOnTouchListener(object : OnSwipeTouchListener(context) {\n            override fun onSwipeUp() {\n                setLastEvent(Event.SWIPE_UP)\n                Log.d(\"Ultron\", \"onSwipeTop\")\n            }\n\n            override fun onSwipeRight() {\n                setLastEvent(Event.SWIPE_RIGHT)\n                Log.d(\"Ultron\", \"onSwipeRight\")\n            }\n\n            override fun onSwipeLeft() {\n                setLastEvent(Event.SWIPE_LEFT)\n                Log.d(\"Ultron\", \"onSwipeLeft\")\n            }\n\n            override fun onSwipeDown() {\n                setLastEvent(Event.SWIPE_DOWN)\n                Log.d(\"Ultron\", \"onSwipeBottom\")\n            }\n\n        })\n        val observer = Observer<String> {\n            simpleButton.visibility = VISIBLE\n            setLastEvent(Event.DATA_LOADED)\n        }\n        model.data.observe(this, observer)\n        GlobalScope.async {\n            AsyncDataLoading(1600)(\n                UseCase.None,\n                onSuccess = { model.data.value = it },\n                onFailure = { Toast.makeText(this@UiElementsActivity, \"Failed to load data\", Toast.LENGTH_LONG).show() }\n            )\n        }\n    }\n\n    override fun onSupportNavigateUp(): Boolean {\n        onBackPressed()\n        return true\n    }\n\n    override fun onResume() {\n        super.onResume()\n    }\n\n    @SuppressLint(\"SetTextI18n\")\n    fun setLastEvent(event: Event, desc: String? = null) {\n        var status = desc\n        lastEvent = event\n        if (lastEvent == Event.CLICK) {\n            clickedInRow++\n            status += \" $clickedInRow\"\n        } else clickedInRow = 0\n        lastEventDescription?.text = \"${event.name}${if (desc != null) \": $status\" else \"\"}\"\n    }\n\n    enum class Event {\n        NO_EVENT, CLICK, LONG_CLICK, CLICKABLE, ENABLED, SELECTED, FOCUSABLE, DISPLAYED, JS_ENABLED, CONTENT_DESC,\n        SWIPE_LEFT, SWIPE_RIGHT, SWIPE_UP, SWIPE_DOWN, DATA_LOADED\n    }\n}"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/activity/WebViewActivity.kt",
    "content": "package com.atiurin.sampleapp.activity\n\nimport android.os.Build\nimport android.os.Bundle\nimport android.text.Editable\nimport android.text.TextWatcher\nimport android.view.View\nimport android.view.View.*\nimport android.webkit.WebView\nimport android.widget.*\nimport androidx.activity.enableEdgeToEdge\nimport androidx.appcompat.app.AppCompatActivity\nimport com.atiurin.sampleapp.R\n\nclass WebViewActivity : AppCompatActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        enableEdgeToEdge()\n        super.onCreate(savedInstanceState)\n        setContentView(R.layout.activity_webview)\n        val webView: WebView = findViewById(R.id.webview)\n        webView.settings.javaScriptEnabled = true\n        val customHtml = applicationContext.assets.open(\"webview.html\").reader().readText()\n        webView.loadData(customHtml, \"text/html\", \"UTF-8\")\n    }\n\n    override fun onSupportNavigateUp(): Boolean {\n        onBackPressed()\n        return true\n    }\n\n    override fun onResume() {\n        super.onResume()\n    }\n}"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/adapters/ContactAdapter.kt",
    "content": "package com.atiurin.sampleapp.adapters\n\nimport android.content.Context\nimport android.view.GestureDetector\nimport android.view.LayoutInflater\nimport android.view.MotionEvent\nimport android.view.ViewGroup\nimport android.widget.LinearLayout\nimport android.widget.TextView\nimport androidx.core.view.GestureDetectorCompat\nimport androidx.recyclerview.widget.RecyclerView\nimport com.atiurin.sampleapp.R\nimport com.atiurin.sampleapp.data.entities.Contact\nimport com.atiurin.sampleapp.view.CircleImageView\n\nclass ContactAdapter(\n    val context: Context,\n    private var mDataset: ArrayList<Contact>,\n    val listener: OnItemClickListener\n) :\n    RecyclerView.Adapter<ContactAdapter.MyViewHolder>(), GestureDetector.OnGestureListener {\n\n    interface OnItemClickListener {\n        fun onItemClick(item: Contact)\n    }\n\n    class MyViewHolder(val view: LinearLayout) : RecyclerView.ViewHolder(view)\n\n    open fun updateData(data: ArrayList<Contact>) {\n        mDataset = data\n    }\n\n    override fun onCreateViewHolder(\n        parent: ViewGroup,\n        viewType: Int\n    ): MyViewHolder {\n        val view = LayoutInflater.from(parent.context)\n            .inflate(R.layout.list_item, parent, false) as LinearLayout\n        return MyViewHolder(view)\n    }\n\n    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {\n        holder.view.setOnClickListener { listener.onItemClick(mDataset.get(position)) }\n        val tvTitle = holder.view.findViewById(R.id.tv_name) as TextView\n        val avatar = holder.view.findViewById(R.id.avatar) as CircleImageView\n        val status = holder.view.findViewById(R.id.tv_status) as TextView\n        tvTitle.text = mDataset[position].name\n        status.text = mDataset[position].status\n        avatar.setImageDrawable(\n            holder.view.context.getResources().getDrawable(mDataset[position].avatar)\n        )\n        GestureDetectorCompat(context, this)\n    }\n\n    override fun getItemCount() = mDataset.size\n\n    //    GestureDetector.OnGestureListener\n    override fun onDown(p0: MotionEvent): Boolean = true\n    override fun onShowPress(p0: MotionEvent) = Unit\n    override fun onSingleTapUp(p0: MotionEvent): Boolean = true\n    override fun onScroll(p0: MotionEvent?, p1: MotionEvent, p2: Float, p3: Float): Boolean = true\n    override fun onLongPress(p0: MotionEvent) = Unit\n    override fun onFling(p0: MotionEvent?, p1: MotionEvent, p2: Float, p3: Float): Boolean = true\n}"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/adapters/MessageAdapter.kt",
    "content": "package com.atiurin.sampleapp.adapters\n\nimport android.view.Gravity\nimport android.view.LayoutInflater\nimport android.view.ViewGroup\nimport android.widget.LinearLayout\nimport android.widget.TextView\nimport androidx.cardview.widget.CardView\nimport androidx.core.view.get\nimport androidx.recyclerview.widget.RecyclerView\nimport com.atiurin.sampleapp.R\nimport com.atiurin.sampleapp.data.entities.Message\nimport com.atiurin.sampleapp.data.repositories.CURRENT_USER\n\n\nclass MessageAdapter(private var messages: ArrayList<Message>, val listener: OnItemClickListener) :\n    RecyclerView.Adapter<MessageAdapter.MessageViewHolder>() {\n\n    interface OnItemClickListener {\n        fun onItemClick(item: Message)\n    }\n    class MessageViewHolder(val view: LinearLayout) : RecyclerView.ViewHolder(view)\n\n    open fun updateData(data: ArrayList<Message>) {\n        messages = data\n    }\n\n    override fun onCreateViewHolder(\n        parent: ViewGroup,\n        viewType: Int\n    ): MessageViewHolder {\n        val view = LayoutInflater.from(parent.context)\n            .inflate(R.layout.message_item, parent, false) as LinearLayout\n        return MessageViewHolder(view)\n    }\n\n    override fun onBindViewHolder(holder: MessageViewHolder, position: Int) {\n        holder.view.setOnClickListener {\n            listener.onItemClick(messages.get(position))\n        }\n        val messageText = holder.view.findViewById(R.id.message_text) as TextView\n        val authorName = holder.view.findViewById(R.id.author) as TextView\n        val message = messages[position]\n        messageText.text = message.text\n        if (message.authorId == CURRENT_USER.id){\n            val view = holder.view.get(0)\n            val cardView = view.findViewById<CardView>(R.id.card_view)\n            cardView.setCardBackgroundColor(view.context.resources.getColor(R.color.colorLight))\n            val layoutParams = view.layoutParams\n            if (layoutParams is LinearLayout.LayoutParams){\n                layoutParams.gravity = Gravity.RIGHT\n            }\n            view.layoutParams = layoutParams\n        }\n\n    }\n\n    override fun getItemCount() = messages.size\n}"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/async/AsyncDataLoading.kt",
    "content": "package com.atiurin.sampleapp.async\n\nimport com.atiurin.sampleapp.MyApplication.CONTACTS_LOADING_TIMEOUT_MS\nimport kotlinx.coroutines.delay\n\nclass AsyncDataLoading(val delayMs: Long = CONTACTS_LOADING_TIMEOUT_MS) : UseCase<String, UseCase.None>() {\n\n    override suspend fun run(params: None): Either<Exception, String> {\n        return try {\n            delay(delayMs)\n            Success( \"Loaded\")\n        } catch (e: Exception) {\n            Failure(e)\n        }\n    }\n}"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/async/ContactsPresenter.kt",
    "content": "package com.atiurin.sampleapp.async\n\nimport com.atiurin.sampleapp.data.entities.Contact\nimport com.atiurin.sampleapp.idlingresources.IdlingHelper\nimport com.atiurin.sampleapp.idlingresources.resources.ContactsIdlingResource\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.launch\nimport kotlin.coroutines.CoroutineContext\n\nclass ContactsPresenter <T : ContactsProvider>(\n    private val executor: T,\n    private val coroutineContext: CoroutineContext = Dispatchers.Default) {\n\n    protected lateinit var scope: PresenterCoroutineScope\n\n    fun getAllContacts() {\n        IdlingHelper.ifAllowed { ContactsIdlingResource.getInstanceFromApp()?.setIdleState(false) }\n        scope = PresenterCoroutineScope(coroutineContext)\n        scope.launch {\n            GetContacts()(\n                UseCase.None,\n                onSuccess = { executor.onContactsLoaded(it) },\n                onFailure = { executor.onFailedToLoadContacts(it.message) }\n            )\n        }\n    }\n}\n\nclass PresenterCoroutineScope(context: CoroutineContext) : CoroutineScope {\n    override val coroutineContext: CoroutineContext = context + Job()\n}\n\ninterface ContactsProvider {\n    fun onContactsLoaded(contacts: ArrayList<Contact>)\n    fun onFailedToLoadContacts(message: String?)\n}"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/async/Either.kt",
    "content": "package com.atiurin.sampleapp.async\n\n/**\n * An algebraic data type to provide either a [Failure][F] or a [Success][S] result.\n */\nsealed class Either<out F, out S> {\n\n    /**\n     * Calls [failed] with the [failure][Failure.failure] value if result is a [Failure]\n     * or [succeeded] with the [success][Success.success] value if result is a [Success]\n     */\n    inline fun <T> fold(failed: (F) -> T, succeeded: (S) -> T): T =\n        when (this) {\n            is Failure -> failed(failure)\n            is Success -> succeeded(success)\n        }\n}\n\ndata class Failure<out F>(val failure: F) : Either<F, Nothing>()\n\ndata class Success<out S>(val success: S) : Either<Nothing, S>()\n\n/**\n * Allows chaining of multiple calls taking as argument the [success][Success.success] value of the previous call and\n * returning an [Either].\n *\n * 1. Unwrap the result of the first call from the [Either] wrapper.\n * 2. Check if it is a [Success].\n * 3. If yes, call the next function (passed as [ifSucceeded]) with the value of the [success][Success.success]\n * property as an input parameter (chain the calls).\n * 4. If no, just pass the [Failure] through as the end result of the whole call chain.\n *\n * In case any of the calls in the chain returns a [Failure], none of the subsequent flatmapped functions is called\n * and the whole chain returns this failure.\n *\n * @param ifSucceeded next function which should be called if this is a [Success]. The [success][Success.success]\n * value will be then passed as the input parameter.\n */\ninline fun <F, S1, S2> Either<F, S1>.flatMap(succeeded: (S1) -> Either<F, S2>): Either<F, S2> =\n    fold({ this as Failure }, succeeded)\n\n/**\n * Map the [Success] value of the [Either] to another value.\n *\n * You can for example map an `Success<String>` to an `Success<Int>` by\n * using the following code:\n * ```\n * val fiveString: Either<Nothing, String> = Success(\"5\")\n * val fiveInt : Either<Nothing, Int> = fiveString.map { it.toInt() }\n * ```\n */\ninline fun <F, S1, S2> Either<F, S1>.map(f: (S1) -> S2): Either<F, S2> =\n    flatMap { Success(f(it)) }"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/async/GetContacts.kt",
    "content": "package com.atiurin.sampleapp.async\n\nimport com.atiurin.sampleapp.MyApplication.CONTACTS_LOADING_TIMEOUT_MS\nimport com.atiurin.sampleapp.data.entities.Contact\nimport com.atiurin.sampleapp.data.repositories.CONTACTS\nimport kotlinx.coroutines.delay\n\nclass GetContacts(val delayMs: Long = CONTACTS_LOADING_TIMEOUT_MS) : UseCase<ArrayList<Contact>, UseCase.None>() {\n\n    override suspend fun run(params: None): Either<Exception, ArrayList<Contact>> {\n        return try {\n            delay(delayMs)\n            val contacts = CONTACTS\n            Success(contacts)\n        } catch (e: Exception) {\n            Failure(e)\n        }\n    }\n}"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/async/UseCase.kt",
    "content": "package com.atiurin.sampleapp.async\n\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.coroutineScope\nimport kotlinx.coroutines.launch\n\n\n/**\n * Base class for a `coroutine` use case.\n */\nabstract class UseCase<out Type, in Params> where Type : Any {\n\n    /**\n     * Runs the actual logic of the use case.\n     */\n    abstract suspend fun run(params: Params): Either<Exception, Type>\n\n    suspend operator fun invoke(params: Params, onSuccess: (Type) -> Unit, onFailure: (Exception) -> Unit) {\n        val result = run(params)\n        coroutineScope {\n            launch(Dispatchers.Main) {\n                result.fold(\n                    failed = { onFailure(it) },\n                    succeeded = { onSuccess(it) }\n                )\n            }\n        }\n    }\n\n    /**\n     * Placeholder for a use case that doesn't need any input parameters.\n     */\n    object None\n}"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/async/task/CompatAsyncTask.kt",
    "content": "package com.atiurin.sampleapp.async.task\n\nimport android.os.AsyncTask\n\n@Suppress(\"DEPRECATION\")\nclass CompatAsyncTask : AsyncTask<Void, Void, Void>() {\n\n    companion object {\n        const val COMPAT_ASYNC_TASK_TIME_EXECUTION = 5000\n        const val ASYNC = \"ASYNC\"\n    }\n\n    @Deprecated(\"Suppress\")\n    override fun doInBackground(vararg params: Void?): Void? {\n        val startTime = System.currentTimeMillis()\n        while (!isCancelled && System.currentTimeMillis() - startTime < COMPAT_ASYNC_TASK_TIME_EXECUTION) {\n            Thread.sleep(1000)\n        }\n        return null\n    }\n\n    @Deprecated(\"Suppress\")\n    override fun onPostExecute(result: Void?) {}\n\n    fun start() {\n        executeOnExecutor(THREAD_POOL_EXECUTOR)\n    }\n\n    fun stop() {\n        cancel(true)\n    }\n}\n\n"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/compose/ContacsList.kt",
    "content": "package com.atiurin.sampleapp.compose\n\nimport android.content.Context\nimport android.content.Intent\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.material.Divider\nimport androidx.compose.material.ExperimentalMaterialApi\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.semantics.SemanticsPropertyKey\nimport androidx.compose.ui.semantics.SemanticsPropertyReceiver\nimport androidx.compose.ui.semantics.contentDescription\nimport androidx.compose.ui.semantics.semantics\nimport androidx.compose.ui.semantics.testTag\nimport androidx.compose.ui.unit.ExperimentalUnitApi\nimport androidx.compose.ui.unit.TextUnit\nimport androidx.compose.ui.unit.TextUnitType\nimport androidx.compose.ui.unit.dp\nimport androidx.core.content.ContextCompat\nimport com.atiurin.sampleapp.activity.ComposeSecondActivity\nimport com.atiurin.sampleapp.data.entities.Contact\n\nconst val contactsListHeaderTag = \"headerTestTag\"\nconst val contactNameTestTag = \"nameTestTag\"\nconst val contactStatusTestTag = \"statusTestTag\"\nconst val contactsListContentDesc = \"contacts list\"\nconst val contactsListTestTag = \"contactsListTestTag\"\n\n@ExperimentalMaterialApi\n@ExperimentalUnitApi\n@OptIn(ExperimentalFoundationApi::class)\n@Composable\nfun ContactsList(\n    contacts: List<Contact>,\n    context: Context,\n    addStickyHeader: Boolean = true,\n    testTagProvider: (Contact, Int) -> String,\n    modifierProvider: (Int) -> Modifier,\n) {\n    val selectedItem = remember { mutableStateOf(\"\") }\n    Text(text = \"Selected item = ${selectedItem.value}\")\n    LazyColumn(\n        contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),\n        verticalArrangement = Arrangement.spacedBy(4.dp),\n        modifier = Modifier.semantics {\n            contentDescription = contactsListContentDesc\n            testTag = contactsListTestTag\n        }\n    ) {\n        if (addStickyHeader) {\n            stickyHeader(key = \"header\") {\n                Text(text = \"Lazy column header\", modifier = Modifier.semantics { testTag = contactsListHeaderTag })\n            }\n        }\n        itemsIndexed(contacts, key = { _, c -> c.name }) { index, contact ->\n            Box(modifier = modifierProvider\n                .invoke(index)\n                .semantics {\n                    testTag = testTagProvider.invoke(contact, index)\n                }\n            ) {\n                Column(\n                    modifier = Modifier\n                        .then(Modifier.clickable {\n                            selectedItem.value = contact.name\n                            val intent = Intent(context, ComposeSecondActivity::class.java)\n                            intent.putExtra(ComposeSecondActivity.INTENT_CONTACT_ID, contact.id)\n                            ContextCompat.startActivity(context, intent, null)\n                        })\n                ) {\n                    Row {\n                        Image(\n                            painter = painterResource(contact.avatar),\n                            contentDescription = \"avatar\",\n                            contentScale = ContentScale.Crop,            // crop the image if it's not a square\n                            modifier = Modifier\n                                .size(80.dp)\n                                .clip(CircleShape)                       // clip to the circle shape\n                                .border(2.dp, Color.Transparent, CircleShape)   // add a border (optional)\n                        )\n                        Spacer(modifier = Modifier.width(16.dp))\n                        Column {\n                            Text(contact.name, Modifier.semantics { testTag = contactNameTestTag }, fontSize = TextUnit(20f, TextUnitType.Sp))\n                            Spacer(modifier = Modifier.height(8.dp))\n                            Text(text = contact.status, Modifier.semantics { testTag = contactStatusTestTag }, fontSize = TextUnit(16f, TextUnitType.Sp))\n                            Spacer(modifier = Modifier.height(8.dp))\n                        }\n\n                    }\n                    Spacer(modifier = Modifier.height(8.dp))\n                    Divider(color = Color.Black)\n                }\n            }\n        }\n    }\n}\n\nfun getContactItemTestTagById(contact: Contact) = \"contactId=${contact.id}\"\nfun getContactItemTestTagByPosition(position: Int) = \"position=$position\"\n\n// configure position matching for lazy list\nval ListItemPositionPropertyKey = SemanticsPropertyKey<Int>(\"ListItemPosition\")\nvar SemanticsPropertyReceiver.listItemPosition by ListItemPositionPropertyKey\nfun Modifier.listItemPosition(position: Int): Modifier {\n    return semantics { listItemPosition = position }\n}"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/compose/CustomButton.kt",
    "content": "package com.atiurin.sampleapp.compose\n\nimport androidx.compose.foundation.BorderStroke\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.combinedClickable\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.material.*\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Shape\nimport androidx.compose.ui.semantics.Role\n\n@Composable\n@OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class)\nfun CustomButton(\n    onClickAction: () -> Unit,\n    modifier: Modifier = Modifier,\n    onLongClick: (() -> Unit)? = null,\n    onDoubleClick: (() -> Unit)? = null,\n    enabled: Boolean = true,\n    elevation: ButtonElevation? = ButtonDefaults.elevation(),\n    shape: Shape = MaterialTheme.shapes.small,\n    border: BorderStroke? = null,\n    colors: ButtonColors = ButtonDefaults.buttonColors(),\n    contentPadding: PaddingValues = ButtonDefaults.ContentPadding,\n    content: @Composable RowScope.() -> Unit\n) {\n    val contentColor by colors.contentColor(enabled)\n    Surface(\n        shape = shape,\n        color = colors.backgroundColor(enabled).value,\n        contentColor = contentColor.copy(alpha = 1f),\n        border = border,\n        modifier = modifier\n            .combinedClickable(\n                onClick = onClickAction,\n                onDoubleClick = onDoubleClick,\n                onLongClick = onLongClick,\n                enabled = enabled,\n                role = Role.Button\n            )\n    ) {\n        CompositionLocalProvider(LocalContentAlpha provides contentColor.alpha) {\n            ProvideTextStyle(\n                value = MaterialTheme.typography.button\n            ) {\n                Row(\n                    Modifier\n                        .defaultMinSize(\n                            minWidth = ButtonDefaults.MinWidth,\n                            minHeight = ButtonDefaults.MinHeight\n                        )\n                        .padding(contentPadding),\n                    horizontalArrangement = Arrangement.Center,\n                    verticalAlignment = Alignment.CenterVertically,\n                    content = content\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/compose/DatePicker.kt",
    "content": "package com.atiurin.sampleapp.compose\n\nimport android.os.SystemClock\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.gestures.awaitEachGesture\nimport androidx.compose.foundation.gestures.awaitFirstDown\nimport androidx.compose.foundation.gestures.waitForUpOrCancellation\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.offset\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.DateRange\nimport androidx.compose.material3.DatePicker\nimport androidx.compose.material3.DatePickerDialog\nimport androidx.compose.material3.DatePickerState\nimport androidx.compose.material3.DisplayMode\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.OutlinedTextField\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.material3.rememberDatePickerState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.shadow\nimport androidx.compose.ui.input.pointer.PointerEventPass\nimport androidx.compose.ui.input.pointer.pointerInput\nimport androidx.compose.ui.platform.testTag\nimport androidx.compose.ui.semantics.CustomAccessibilityAction\nimport androidx.compose.ui.semantics.customActions\nimport androidx.compose.ui.semantics.semantics\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.Popup\nimport com.atiurin.sampleapp.BuildConfig\nimport com.atiurin.sampleapp.compose.DatePickerTestTags.SelectedDateValue\nimport com.atiurin.sampleapp.compose.DatePickerTestTags.SetDatePickerTimeCustomActionLabel\nimport com.atiurin.sampleapp.utils.convertMillisToDate\n\nobject DatePickerTestTags {\n    const val DockedIconButton = \"DockerIconButton\"\n    const val SelectedDateValue = \"SelectedDateValue\"\n    const val DataPicker = \"DataPicker\"\n    const val SetDatePickerTimeCustomActionLabel = \"SetDatePickerTimeCustomAction\"\n}\n\nobject DatePickerTestData {\n    var time: Long = SystemClock.currentThreadTimeMillis()\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\nfun Modifier.setDatePickerTimeCustomAction(state: DatePickerState): Modifier {\n    return this.semantics {\n        customActions = listOf(\n            CustomAccessibilityAction(SetDatePickerTimeCustomActionLabel) {\n                state.selectedDateMillis = DatePickerTestData.time\n                true\n            }\n        )\n    }\n}\n\n@Composable\n@OptIn(ExperimentalMaterial3Api::class)\nfun TestableDatePicker(\n    state: DatePickerState,\n    modifier: Modifier = Modifier,\n    showModeToggle: Boolean = true,\n) {\n    val testableModifier = if (BuildConfig.DEBUG) {\n        modifier\n            .testTag(DatePickerTestTags.DataPicker)\n            .setDatePickerTimeCustomAction(state)\n    } else modifier\n    DatePicker(\n        state = state,\n        modifier = testableModifier,\n        showModeToggle = showModeToggle\n    )\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun DatePickerDocked() {\n    var showDatePicker by remember { mutableStateOf(false) }\n    val datePickerState = rememberDatePickerState()\n    val selectedDate = datePickerState.selectedDateMillis?.let {\n        convertMillisToDate(it)\n    } ?: \"\"\n\n    Box(\n        modifier = Modifier.fillMaxWidth()\n    ) {\n        OutlinedTextField(\n            value = selectedDate,\n            onValueChange = { },\n            label = { Text(\"No value selected\") },\n            readOnly = true,\n            trailingIcon = {\n                IconButton(\n                    modifier = Modifier.testTag(DatePickerTestTags.DockedIconButton),\n                    onClick = { showDatePicker = !showDatePicker }\n                ) {\n                    Icon(\n                        imageVector = Icons.Default.DateRange,\n                        contentDescription = \"Select date\"\n                    )\n                }\n            },\n            modifier = Modifier.testTag(SelectedDateValue)\n                .fillMaxWidth()\n                .height(64.dp)\n\n        )\n\n        if (showDatePicker) {\n            Popup(\n                onDismissRequest = { showDatePicker = false },\n                alignment = Alignment.TopStart\n            ) {\n                Box(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .offset(y = 64.dp)\n                        .shadow(elevation = 4.dp)\n                        .background(MaterialTheme.colorScheme.surface)\n                        .padding(16.dp)\n                ) {\n                    TestableDatePicker(\n                        modifier = Modifier,\n                        state = datePickerState,\n                        showModeToggle = false,\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nfun DatePickerFieldToModal(modifier: Modifier = Modifier) {\n    var selectedDate by remember { mutableStateOf<Long?>(null) }\n    var showModal by remember { mutableStateOf(false) }\n\n    OutlinedTextField(\n        value = selectedDate?.let { convertMillisToDate(it) } ?: \"\",\n        onValueChange = { },\n        label = { Text(\"DOB\") },\n        placeholder = { Text(\"MM/DD/YYYY\") },\n        trailingIcon = {\n            Icon(Icons.Default.DateRange, contentDescription = \"Select date\")\n        },\n        modifier = modifier\n            .fillMaxWidth()\n            .pointerInput(selectedDate) {\n                awaitEachGesture {\n                    // Modifier.clickable doesn't work for text fields, so we use Modifier.pointerInput\n                    // in the Initial pass to observe events before the text field consumes them\n                    // in the Main pass.\n                    awaitFirstDown(pass = PointerEventPass.Initial)\n                    val upEvent = waitForUpOrCancellation(pass = PointerEventPass.Initial)\n                    if (upEvent != null) {\n                        showModal = true\n                    }\n                }\n            }\n    )\n\n    if (showModal) {\n        DatePickerModal(\n            onDateSelected = { selectedDate = it },\n            onDismiss = { showModal = false }\n        )\n    }\n}\n\n\n@OptIn(ExperimentalMaterial3Api::class)\n// [START android_compose_components_datepicker_modal]\n@Composable\nfun DatePickerModal(\n    onDateSelected: (Long?) -> Unit,\n    onDismiss: () -> Unit\n) {\n    val datePickerState = rememberDatePickerState()\n\n    DatePickerDialog(\n        onDismissRequest = onDismiss,\n        confirmButton = {\n            TextButton(onClick = {\n                onDateSelected(datePickerState.selectedDateMillis)\n                onDismiss()\n            }) {\n                Text(\"OK\")\n            }\n        },\n        dismissButton = {\n            TextButton(onClick = onDismiss) {\n                Text(\"Cancel\")\n            }\n        }\n    ) {\n        TestableDatePicker(state = datePickerState)\n    }\n}\n// [END android_compose_components_datepicker_modal]\n\n@OptIn(ExperimentalMaterial3Api::class)\n// [START android_compose_components_datepicker_inputmodal]\n@Composable\nfun DatePickerModalInput(\n    onDateSelected: (Long?) -> Unit,\n    onDismiss: () -> Unit\n) {\n    val datePickerState = rememberDatePickerState(initialDisplayMode = DisplayMode.Input)\n\n    DatePickerDialog(\n        onDismissRequest = onDismiss,\n        confirmButton = {\n            TextButton(onClick = {\n                onDateSelected(datePickerState.selectedDateMillis)\n                onDismiss()\n            }) {\n                Text(\"OK\")\n            }\n        },\n        dismissButton = {\n            TextButton(onClick = onDismiss) {\n                Text(\"Cancel\")\n            }\n        }\n    ) {\n        TestableDatePicker(state = datePickerState)\n    }\n}\n\n"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/compose/LinearProgressBar.kt",
    "content": "package com.atiurin.sampleapp.compose\n\nimport androidx.compose.foundation.progressSemantics\nimport androidx.compose.material.LinearProgressIndicator\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.MutableState\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.semantics.*\nimport com.atiurin.sampleapp.activity.ComposeElementsActivity\n\n@Composable\nfun LinearProgressBar(statusState: MutableState<String>){\n    val progressState = remember {\n        mutableStateOf(0f)\n    }\n    LinearProgressIndicator(progress = progressState.value, modifier =\n    Modifier\n        .semantics {\n            testTag = ComposeElementsActivity.progressBar\n            setProgress { value ->\n                progressState.value = value\n                statusState.value = \"set progress $value\"\n                true\n            }\n            progressBarRangeInfo = ProgressBarRangeInfo(progressState.value, 0f..progressState.value, 100)\n        }\n        .getProgress(progressState.value)\n        .progressSemantics()\n    )\n}\n\nval GetProgress = SemanticsPropertyKey<Float>(\"ProgressValue\")\nvar SemanticsPropertyReceiver.getProgress by GetProgress\n\nfun Modifier.getProgress(progress: Float): Modifier {\n    return semantics { getProgress = progress }\n}"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/compose/LoadingAnimation.kt",
    "content": "package com.atiurin.sampleapp.compose\n\nimport androidx.compose.animation.core.*\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.material.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport kotlinx.coroutines.delay\n\n@Composable\nfun LoadingAnimation(\n    modifier: Modifier = Modifier,\n    circleSize: Dp = 25.dp,\n    circleColor: Color = MaterialTheme.colors.primary,\n    spaceBetween: Dp = 10.dp,\n    travelDistance: Dp = 20.dp\n) {\n    val circles = listOf(\n        remember { Animatable(initialValue = 0f) },\n        remember { Animatable(initialValue = 0f) },\n        remember { Animatable(initialValue = 0f) }\n    )\n\n    circles.forEachIndexed { index, animatable ->\n        LaunchedEffect(key1 = animatable) {\n            delay(index * 100L)\n            animatable.animateTo(\n                targetValue = 1f,\n                animationSpec = infiniteRepeatable(\n                    animation = keyframes {\n                        durationMillis = 1200\n                        0.0f at 0 with LinearOutSlowInEasing\n                        1.0f at 300 with LinearOutSlowInEasing\n                        0.0f at 600 with LinearOutSlowInEasing\n                        0.0f at 1200 with LinearOutSlowInEasing\n                    },\n                    repeatMode = RepeatMode.Restart\n                )\n            )\n        }\n    }\n\n    val circleValues = circles.map { it.value }\n    val distance = with(LocalDensity.current) { travelDistance.toPx() }\n\n    Row(\n        modifier = modifier,\n        horizontalArrangement = Arrangement.spacedBy(spaceBetween)\n    ) {\n        circleValues.forEach { value ->\n            Box(\n                modifier = Modifier\n                    .size(circleSize)\n                    .graphicsLayer {\n                        translationY = -value * distance\n                    }\n                    .background(\n                        color = circleColor,\n                        shape = CircleShape\n                    )\n            )\n        }\n    }\n\n}"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/compose/RadioGroup.kt",
    "content": "package com.atiurin.sampleapp.compose\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.selection.selectableGroup\nimport androidx.compose.material.RadioButton\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.semantics.semantics\nimport androidx.compose.ui.semantics.testTag\nimport androidx.compose.ui.unit.dp\nimport com.atiurin.sampleapp.activity.ComposeElementsActivity\n\n@Composable\nfun RadioGroup(){\n    var selected by remember { mutableStateOf(\"Male\") }\n    Row (verticalAlignment = Alignment.CenterVertically){\n        val male = \"Male\"\n        val female = \"Female\"\n        RadioButton(selected = selected == male, onClick = { selected = male }, modifier = Modifier\n            .selectableGroup()\n            .semantics { testTag = ComposeElementsActivity.radioButtonMaleTestTag })\n        Text(\n            text = male,\n            modifier = Modifier\n                .clickable(onClick = { selected = male })\n                .padding(start = 4.dp)\n        )\n        Spacer(modifier = Modifier.size(4.dp))\n\n        RadioButton(selected = selected == female, onClick = { selected = female }, modifier = Modifier\n            .selectableGroup()\n            .semantics { testTag = ComposeElementsActivity.radioButtonFemaleTestTag })\n        Text(\n            text = female,\n            modifier = Modifier\n                .clickable(onClick = { selected = female })\n                .padding(start = 4.dp)\n        )\n    }\n}\n"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/compose/RegionsClickListener.kt",
    "content": "package com.atiurin.sampleapp.compose\n\nimport android.annotation.SuppressLint\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.defaultMinSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material.Button\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.MutableState\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.semantics.semantics\nimport androidx.compose.ui.semantics.testTag\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport com.atiurin.sampleapp.compose.RegionsClickListenerTestTags.regionsNode\n\nobject RegionsClickListenerTestTags {\n    const val regionsNode = \"regionsClickListener\"\n    const val regionsClickedText = \"regionsClickedText\"\n}\n@Composable\nfun RegionsClickListener(clickState: MutableState<String>) {\n\n    Column(modifier = Modifier.semantics { testTag = regionsNode }) {\n        Row (Modifier.defaultMinSize(minHeight = 1.dp).padding(vertical = 0.dp)){//top\n            Button(modifier = Modifier.padding(0.dp),onClick = { clickState.value = RegionName.TopLeft.name }) {\n                Text(text = \"TL\")\n            }\n            Button(modifier = Modifier.padding(0.dp),onClick = { clickState.value = RegionName.TopCenter.name }) {\n                Text(text = \"TC\")\n            }\n            Button(modifier = Modifier.padding(0.dp),onClick = { clickState.value = RegionName.TopRight.name }) {\n                Text(text = \"TR\")\n            }\n        }\n        Row (Modifier.defaultMinSize(minHeight = 1.dp).padding(vertical = 0.dp)){//Center\n            Button(onClick = { clickState.value = RegionName.CenterLeft.name }) {\n                Text(text = \"CL\")\n            }\n            Button(onClick = { clickState.value = RegionName.Center.name }) {\n                Text(text = \"CC\")\n            }\n            Button(onClick = { clickState.value = RegionName.CenterRight.name }) {\n                Text(text = \"CR\")\n            }\n        }\n        Row (Modifier.padding(vertical = 0.dp)){//Bottom\n            Button(modifier = Modifier.padding(0.dp), onClick = { clickState.value = RegionName.BottomLeft.name }) {\n                Text(text = \"BL\")\n            }\n            Button(onClick = { clickState.value = RegionName.BottomCenter.name }) {\n                Text(text = \"BC\")\n            }\n            Button(onClick = { clickState.value = RegionName.BottomRight.name }) {\n                Text(text = \"BR\")\n            }\n        }\n    }\n\n}\n\n@SuppressLint(\"UnrememberedMutableState\")\n@Preview\n@Composable\nfun RegionsClickListenerPreview(){\n    RegionsClickListener(mutableStateOf(\"\"))\n}\n\nenum class RegionName {\n    TopLeft, TopCenter, TopRight, CenterLeft, Center, CenterRight, BottomLeft, BottomCenter, BottomRight\n}"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/compose/SimpleOutlinedText.kt",
    "content": "package com.atiurin.sampleapp.compose\n\nimport androidx.compose.foundation.text.selection.SelectionContainer\nimport androidx.compose.material.OutlinedTextField\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.semantics.semantics\nimport androidx.compose.ui.semantics.testTag\n\n@Composable\nfun SimpleOutlinedText(defaultValue: String = \"\",  myTestTag: String = \"outlinedText\") {\n    var text by remember { mutableStateOf(defaultValue) }\n    SelectionContainer {\n        OutlinedTextField(\n            value = text,\n            onValueChange = { text = it },\n            label = { Text(\"Label\") },\n            modifier = Modifier.semantics { testTag = myTestTag },\n\n        )\n    }\n}"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/compose/SwipeableNode.kt",
    "content": "package com.atiurin.sampleapp.compose\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.gestures.detectDragGestures\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.MutableState\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.input.pointer.consumeAllChanges\nimport androidx.compose.ui.input.pointer.pointerInput\nimport androidx.compose.ui.semantics.semantics\nimport androidx.compose.ui.semantics.testTag\nimport androidx.compose.ui.unit.dp\nimport com.atiurin.sampleapp.activity.ActionsStatus\nimport com.atiurin.sampleapp.activity.ComposeElementsActivity\n\n@Composable\nfun SwipeableNode(statusState: MutableState<String>) {\n    Box(\n        modifier = Modifier\n            .pointerInput(Unit) {\n                detectDragGestures { change, dragAmount ->\n                    change.consumeAllChanges()\n\n                    val (x, y) = dragAmount\n                    when {\n                        x > 0 -> {\n                            statusState.value = ActionsStatus.SwipeRight.name\n                        }\n                        x < 0 -> {\n                            statusState.value = ActionsStatus.SwipeLeft.name\n                        }\n                    }\n                    when {\n                        y > 0 -> {\n                            statusState.value = ActionsStatus.SwipeDown.name\n                        }\n                        y < 0 -> {\n                            statusState.value = ActionsStatus.SwipeUp.name\n                        }\n                    }\n                }\n            }\n            .semantics { testTag = ComposeElementsActivity.swipeableNode }\n            .width(100.dp)\n            .height(100.dp)\n            .background(Color.Blue)\n    )\n}"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/compose/app/App.kt",
    "content": "package com.atiurin.sampleapp.compose.app\n\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material.Scaffold\nimport androidx.compose.material3.Snackbar\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport androidx.navigation.NavHostController\nimport androidx.navigation.compose.NavHost\nimport androidx.navigation.compose.composable\nimport androidx.navigation.compose.currentBackStackEntryAsState\nimport androidx.navigation.compose.rememberNavController\nimport com.atiurin.sampleapp.compose.screen.DatePickerScreen\nimport com.atiurin.sampleapp.compose.screen.NavigationScreen\n\n@Composable\nfun App(\n    navController: NavHostController = rememberNavController(),\n) {\n    // Get current back stack entry\n    val backStackEntry by navController.currentBackStackEntryAsState()\n    val snackbarHostState = remember { SnackbarHostState() }\n    val currentScreen = AppScreen.valueOf(\n        backStackEntry?.destination?.route ?: AppScreen.DataPicker.name\n    )\n    Scaffold(\n        topBar = {\n            AppBar(\n                currentScreen = currentScreen,\n                canNavigateBack = navController.previousBackStackEntry != null,\n                navigateUp = { navController.navigateUp() },\n                onRefresh = {}\n            )\n        },\n        snackbarHost = {\n            SnackbarHost(snackbarHostState) { snackbarData ->\n                Snackbar(\n                    snackbarData = snackbarData,\n                    modifier = Modifier.padding(16.dp)\n                )\n            }\n        }\n    ) { innerPadding ->\n        NavHost(\n            navController = navController,\n            startDestination = AppScreen.Navigation.name,\n            modifier = Modifier\n                .fillMaxSize()\n                .padding(innerPadding)\n        ) {\n            composable(route = AppScreen.Navigation.name) {\n                NavigationScreen { screen ->\n                    navController.navigate(screen.name)\n                }\n            }\n            composable(route = AppScreen.DataPicker.name) {\n                DatePickerScreen()\n            }\n        }\n    }\n}"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/compose/app/AppBar.kt",
    "content": "package com.atiurin.sampleapp.compose.app\n\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.ArrowBack\nimport androidx.compose.material.icons.filled.Refresh\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun AppBar(\n    currentScreen: AppScreen,\n    canNavigateBack: Boolean,\n    navigateUp: () -> Unit,\n    modifier: Modifier = Modifier,\n    onRefresh: (() -> Unit)? = null\n) {\n    TopAppBar(\n        title = { Text(currentScreen.title) },\n        colors = TopAppBarDefaults.mediumTopAppBarColors(\n            containerColor = Color(0xffffde02)\n        ),\n        modifier = modifier,\n        navigationIcon = {\n            if (canNavigateBack) {\n                IconButton(onClick = navigateUp) {\n                    Icon(\n                        imageVector = Icons.AutoMirrored.Filled.ArrowBack,\n                        contentDescription = \"BackButton\"\n                    )\n                }\n            }\n        },\n        actions = {\n            if (onRefresh != null) {\n                IconButton(onClick = onRefresh) {\n                    Icon(Icons.Default.Refresh, contentDescription = \"Refresh List\")\n                }\n            }\n        }\n    )\n}"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/compose/app/AppScreen.kt",
    "content": "package com.atiurin.sampleapp.compose.app\n\nenum class AppScreen(val title: String) {\n    Navigation(\"Navigation\"),\n    DataPicker(\"Date Picker\")\n}"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/compose/screen/DatePickerScreen.kt",
    "content": "package com.atiurin.sampleapp.compose.screen\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.runtime.Composable\nimport com.atiurin.sampleapp.compose.DatePickerDocked\n\n@Composable\nfun DatePickerScreen() {\n    Column {\n        DatePickerDocked()\n\n//        val modalDate = remember { mutableStateOf(\"No modal date selected\") }\n//        val showModal = remember { mutableStateOf(false) }\n//        Text(modalDate.value)\n//        if (showModal.value){\n//            DatePickerModal({ date ->\n//                date?.let { modalDate.value = convertMillisToDate(date) }\n//                showModal.value = false\n//            }) {\n//                showModal.value = false\n//            }\n//        }\n    }\n\n}\n\n@Composable\nfun ShowModalButton(){\n\n}"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/compose/screen/NavigationScreen.kt",
    "content": "package com.atiurin.sampleapp.compose.screen\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material.Button\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.semantics.semantics\nimport androidx.compose.ui.semantics.testTag\nimport androidx.compose.ui.unit.dp\nimport com.atiurin.sampleapp.compose.app.AppScreen\n\nobject NavigationTestTags {\n    const val DatePicker = \"DatePicker\"\n}\n@Composable\nfun NavigationScreen(\n    onNavButtonClicked: (AppScreen) -> Unit\n){\n    Column(\n        modifier = Modifier\n            .fillMaxSize()\n            .padding(start = 16.dp),\n        verticalArrangement = Arrangement.Top,\n        horizontalAlignment = Alignment.Start\n    ) {\n        NavButton(\"DatePicker\", NavigationTestTags.DatePicker){\n            onNavButtonClicked(AppScreen.DataPicker)\n        }\n    }\n}\n\n@Composable\nfun NavButton(name: String, testId: String, onClick: () -> Unit){\n    Button(modifier = Modifier.semantics {\n        testTag = testId\n    },\n        onClick = onClick,\n        content = { Text(text = name) },\n    )\n}"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/data/Tags.kt",
    "content": "package com.atiurin.sampleapp.data\n\nenum class Tags{\n    CONTACTS_LIST,\n    MESSAGES_LIST\n}"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/data/entities/Contact.kt",
    "content": "package com.atiurin.sampleapp.data.entities\n\ndata class Contact( val id: Int,val name: String, val status: String, val avatar: Int)"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/data/entities/Message.kt",
    "content": "package com.atiurin.sampleapp.data.entities\n\ndata class Message(val authorId: Int,\n                   val receiverId: Int,\n                   val text: String)"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/data/entities/User.kt",
    "content": "package com.atiurin.sampleapp.data.entities\n\ndata class User( val id: Int,val name: String, val avatar: Int, val login: String, val password: String)"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/data/loaders/MessageLoader.kt",
    "content": "package com.atiurin.sampleapp.data.loaders\n\nimport com.atiurin.sampleapp.data.entities.Message\nimport com.atiurin.sampleapp.data.repositories.MESSAGES\n\nopen class MessageLoader{\n    open fun load() : ArrayList<Message>{\n        return MESSAGES\n    }\n}"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/data/repositories/ContactRepositoty.kt",
    "content": "package com.atiurin.sampleapp.data.repositories\n\nimport com.atiurin.sampleapp.data.entities.Contact\nobject ContactRepositoty {\n\n    fun getContact(id: Int) : Contact{\n        return contacts.find { it.id == id }!!\n    }\n\n    fun getFirst(): Contact {\n        return contacts.first()\n    }\n    fun getLast() : Contact{\n        return contacts.last()\n    }\n    fun all() = contacts.toList()\n\n    private val contacts = CONTACTS\n}"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/data/repositories/MessageRepository.kt",
    "content": "package com.atiurin.sampleapp.data.repositories\n\nimport com.atiurin.sampleapp.data.entities.Message\nimport com.atiurin.sampleapp.data.loaders.MessageLoader\n\n\nobject MessageRepository {\n    var messages : ArrayList<Message>\n\n    init {\n        messages = loadMessages(MessageLoader())\n    }\n\n    fun loadMessages(loader: MessageLoader)\n            : ArrayList<Message>{\n        messages = loader.load()\n        return messages\n    }\n\n    fun searchMessage(author: Int, recipient: Int, text: String) : Message?{\n        return messages.find { it.authorId == author &&  it.receiverId == recipient && it.text == text }\n    }\n\n\n    fun getChatMessages(contactId: Int): ArrayList<Message>{\n        return ArrayList(messages.filter {message ->\n            (message.authorId == contactId && message.receiverId == CURRENT_USER.id) ||\n                    (message.authorId == CURRENT_USER.id && message.receiverId == contactId)\n        })\n    }\n\n    fun clearMessages(){\n        messages.clear()\n    }\n\n    fun addMessage(message: Message){\n        messages.add(message)\n    }\n\n    fun getMessagesCount() : Int{\n        return messages.size\n    }\n}"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/data/repositories/Storage.kt",
    "content": "package com.atiurin.sampleapp.data.repositories\n\nimport com.atiurin.sampleapp.R\nimport com.atiurin.sampleapp.data.entities.Contact\nimport com.atiurin.sampleapp.data.entities.Message\nimport com.atiurin.sampleapp.data.entities.User\n\nval CURRENT_USER = User(1, \"Joey Tribbiani\", Avatars.JOEY.drawable, \"joey\", \"1234\")\n\nval CONTACTS = arrayListOf(\n    Contact(2, \"Chandler Bing\", \"Joey doesn't share food!\", Avatars.CHANDLER.drawable),\n    Contact(3, \"Ross Geller\", \"UNAGI\", Avatars.ROSS.drawable),\n    Contact(4, \"Rachel Green\", \"I got off the plane!\", Avatars.RACHEL.drawable),\n    Contact(5, \"Phoebe Buffay\", \"Smelly cat, smelly cat..\", Avatars.PHOEBE.drawable),\n    Contact(6, \"Monica Geller\", \"I need to clean up..\", Avatars.MONICA.drawable),\n    Contact(7, \"Gunther\", \"They were on break :(\", Avatars.GUNTHER.drawable),\n    Contact(8, \"Janice\", \"Oh. My. God\", Avatars.JANICE.drawable),\n    Contact(9, \"Bob\", \"I wanna drink\", Avatars.DEFAULT.drawable),\n    Contact(10, \"Marty McFly\", \"Back to the ...\", Avatars.DEFAULT.drawable),\n    Contact(12, \"Emmet Brown\", \"Time fluid capacitor\", Avatars.DEFAULT.drawable),\n    Contact(13, \"Friend1\", \"Time fluid capacitor\", Avatars.DEFAULT.drawable),\n    Contact(14, \"Friend2\", \"Time fluid capacitor\", Avatars.DEFAULT.drawable),\n    Contact(15, \"Friend3\", \"Time fluid capacitor\", Avatars.DEFAULT.drawable),\n    Contact(16, \"Friend4\", \"Time fluid capacitor\", Avatars.DEFAULT.drawable),\n    Contact(17, \"Friend5\", \"Time fluid capacitor\", Avatars.DEFAULT.drawable),\n    Contact(18, \"Friend6\", \"Time fluid capacitor\", Avatars.DEFAULT.drawable),\n    Contact(19, \"Friend7\", \"Time fluid capacitor\", Avatars.DEFAULT.drawable),\n    Contact(20, \"Friend8\", \"Time fluid capacitor\", Avatars.DEFAULT.drawable),\n    Contact(21, \"Friend9\", \"Time fluid capacitor\", Avatars.DEFAULT.drawable),\n    Contact(22, \"Friend10\", \"Time fluid capacitor\", Avatars.DEFAULT.drawable),\n    Contact(23, \"Friend11\", \"Time fluid capacitor\", Avatars.DEFAULT.drawable),\n    Contact(24, \"Friend12\", \"Time fluid capacitor\", Avatars.DEFAULT.drawable),\n    Contact(25, \"Friend13\", \"Time fluid capacitor\", Avatars.DEFAULT.drawable),\n    Contact(26, \"Friend14\", \"Time fluid capacitor\", Avatars.DEFAULT.drawable),\n    Contact(27, \"Friend15\", \"Time fluid capacitor\", Avatars.DEFAULT.drawable),\n    Contact(28, \"Friend16\", \"Time fluid capacitor\", Avatars.DEFAULT.drawable),\n    Contact(29, \"Friend17\", \"Time fluid capacitor\", Avatars.DEFAULT.drawable),\n    Contact(30, \"Friend18\", \"Time fluid capacitor\", Avatars.DEFAULT.drawable),\n    Contact(31, \"Friend19\", \"Time fluid capacitor\", Avatars.DEFAULT.drawable),\n    Contact(32, \"Friend20\", \"Time fluid capacitor\", Avatars.DEFAULT.drawable)\n)\n\nenum class Avatars(val drawable: Int) {\n    CHANDLER(R.drawable.chandler),\n    ROSS(R.drawable.ross),\n    MONICA(R.drawable.monica),\n    RACHEL(R.drawable.rachel),\n    PHOEBE(R.drawable.phoebe),\n    GUNTHER(R.drawable.gunther),\n    JOEY(R.drawable.joey),\n    JANICE(R.drawable.janice),\n    DEFAULT(R.drawable.default_avatar)\n}\n\n\nval MESSAGES = arrayListOf(\n    Message(1, 2, \"What's up Chandler\"),\n    Message(2, 1, \"Hi Joey\"),\n    Message(1, 2, \"Let's drink coffee\"),\n    Message(2, 1, \"Ok\"),\n    Message(1, 3, \"Do u wanna coffee?\"),\n    Message(3, 1, \"yep, let's go\")\n)"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/data/viewmodel/ContactsViewModel.kt",
    "content": "package com.atiurin.sampleapp.data.viewmodel\n\nimport androidx.lifecycle.MutableLiveData\nimport androidx.lifecycle.ViewModel\nimport com.atiurin.sampleapp.data.entities.Contact\n\nclass ContactsViewModel : ViewModel(){\n    val contacts: MutableLiveData<List<Contact>> by lazy {\n        MutableLiveData()\n    }\n}"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/data/viewmodel/DataViewModel.kt",
    "content": "package com.atiurin.sampleapp.data.viewmodel\n\nimport androidx.lifecycle.MutableLiveData\nimport androidx.lifecycle.ViewModel\nimport com.atiurin.sampleapp.data.entities.Contact\n\nclass DataViewModel : ViewModel(){\n    val data: MutableLiveData<String> by lazy {\n        MutableLiveData()\n    }\n}"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/idlingresources/AbstractIdlingResource.kt",
    "content": "package com.atiurin.sampleapp.idlingresources\n\nimport androidx.annotation.Nullable\nimport androidx.test.espresso.IdlingResource\nimport java.util.concurrent.atomic.AtomicBoolean\nimport androidx.test.espresso.IdlingResource.ResourceCallback\n\nabstract class AbstractIdlingResource : IdlingResource {\n    @Nullable\n    @Volatile\n    private var mCallback: ResourceCallback? = null\n    private val mIsIdleNow = AtomicBoolean(true)\n    override fun getName(): String {\n        return this.javaClass.name\n    }\n\n    override fun isIdleNow(): Boolean {\n        return mIsIdleNow.get()\n    }\n\n    override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {\n        mCallback = callback\n    }\n\n    fun setIdleState(isIdleNow: Boolean) {\n        mIsIdleNow.set(isIdleNow)\n        if (isIdleNow && mCallback != null) {\n            mCallback?.onTransitionToIdle()\n        }\n    }\n}"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/idlingresources/Holder.kt",
    "content": "package com.atiurin.sampleapp.idlingresources\n\nimport androidx.annotation.VisibleForTesting\n\nopen class Holder<out T>(private val constructor: () -> T) {\n\n    @Volatile\n    private var instance: T? = null\n\n    @VisibleForTesting\n    fun getInstanceFromTest(): T? {\n        return when {\n            instance != null -> instance\n            else -> synchronized(this) {\n                instance = constructor()\n                instance\n            }\n        }\n    }\n\n    fun getInstanceFromApp(): T? {\n        return instance\n    }\n}"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/idlingresources/IdlingHelper.kt",
    "content": "package com.atiurin.sampleapp.idlingresources\nconst val RELEASE_BUILD = false;\n\nobject IdlingHelper{\n    @JvmStatic\n    fun ifAllowed(resourceAction:() -> Unit){\n        if (!RELEASE_BUILD){\n            resourceAction()\n        }\n    }\n}"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/idlingresources/resources/ChatIdlingResource.kt",
    "content": "package com.atiurin.sampleapp.idlingresources.resources\n\nimport com.atiurin.sampleapp.idlingresources.AbstractIdlingResource\nimport com.atiurin.sampleapp.idlingresources.Holder\n\nclass ChatIdlingResource : AbstractIdlingResource(){\n    companion object : Holder<ChatIdlingResource>(::ChatIdlingResource)\n}"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/idlingresources/resources/ContactsIdlingResource.kt",
    "content": "package com.atiurin.sampleapp.idlingresources.resources\n\nimport com.atiurin.sampleapp.idlingresources.AbstractIdlingResource\nimport com.atiurin.sampleapp.idlingresources.Holder\n\nclass ContactsIdlingResource : AbstractIdlingResource(){\n    companion object : Holder<ContactsIdlingResource>(::ContactsIdlingResource)\n}\n"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/managers/AccountManager.kt",
    "content": "package com.atiurin.sampleapp.managers\n\nimport android.content.Context\n\nclass AccountManager(val context: Context){\n    companion object {\n        private const val expectedUserName = \"joey\"\n        private const val expectedPassword = \"1234\"\n        private const val USER_KEY = \"username\"\n        private const val PASSWORD_KEY = \"password\"\n    }\n\n    fun login(user: String, password: String) : Boolean{\n        var success = false\n        // there should be some network request to app server\n        if ((user == Companion.expectedUserName) &&(password == expectedPassword)){\n            success = true\n            with(PrefsManager(context)){\n                savePref(USER_KEY, user)\n                savePref(PASSWORD_KEY, password)\n            }\n        }\n        return success\n    }\n\n    fun isLogedIn() : Boolean{\n        var userName = \"\"\n        var password = \"\"\n        with(PrefsManager(context)){\n            userName = getPref(USER_KEY)\n            password = getPref(PASSWORD_KEY)\n        }\n        if (userName.isEmpty() || password.isEmpty()) return false\n        return true\n    }\n\n    fun logout(){\n        with(PrefsManager(context)){\n            remove(USER_KEY)\n            remove(PASSWORD_KEY)\n        }\n    }\n}"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/managers/PrefsManager.kt",
    "content": "package com.atiurin.sampleapp.managers\n\nimport android.content.Context.MODE_PRIVATE\nimport android.content.Context\n\n\nclass PrefsManager(val context: Context){\n    val PREFS_NAME = \"MyPrefsFileName\"\n\n    fun savePref(key: String, value: String){\n        val editor = context.getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit()\n        editor.putString(key, value)\n        editor.apply()\n    }\n\n    fun getPref(key: String) : String{\n        val prefs = context.getSharedPreferences(PREFS_NAME, MODE_PRIVATE)\n        var value = prefs.getString(key, null)\n        if (value == null) value = \"\"\n        return value\n    }\n\n    fun remove(key: String){\n        val editor = context.getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit()\n        editor.remove(key)\n        editor.commit()\n    }\n}\n\n"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/utils/TimeUtils.kt",
    "content": "package com.atiurin.sampleapp.utils\n\nimport java.text.SimpleDateFormat\nimport java.util.Date\nimport java.util.Locale\n\nfun convertMillisToDate(millis: Long): String {\n    val formatter = SimpleDateFormat(\"MM/dd/yyyy\", Locale.getDefault())\n    return formatter.format(Date(millis))\n}"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/view/CircleImageView.java",
    "content": "package com.atiurin.sampleapp.view;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.content.res.TypedArray;\nimport android.graphics.Bitmap;\nimport android.graphics.BitmapShader;\nimport android.graphics.Canvas;\nimport android.graphics.Color;\nimport android.graphics.ColorFilter;\nimport android.graphics.Matrix;\nimport android.graphics.Outline;\nimport android.graphics.Paint;\nimport android.graphics.Rect;\nimport android.graphics.RectF;\nimport android.graphics.Shader;\nimport android.graphics.drawable.BitmapDrawable;\nimport android.graphics.drawable.ColorDrawable;\nimport android.graphics.drawable.Drawable;\nimport android.net.Uri;\nimport android.os.Build;\nimport android.util.AttributeSet;\nimport android.view.MotionEvent;\nimport android.view.View;\nimport android.view.ViewOutlineProvider;\n\nimport androidx.annotation.ColorInt;\nimport androidx.annotation.ColorRes;\nimport androidx.annotation.DrawableRes;\nimport androidx.annotation.RequiresApi;\nimport androidx.appcompat.widget.AppCompatImageView;\nimport com.atiurin.sampleapp.R;\n\n@SuppressWarnings(\"UnusedDeclaration\")\npublic class CircleImageView extends AppCompatImageView {\n\n    private static final ScaleType SCALE_TYPE = ScaleType.CENTER_CROP;\n\n    private static final Bitmap.Config BITMAP_CONFIG = Bitmap.Config.ARGB_8888;\n    private static final int COLORDRAWABLE_DIMENSION = 2;\n\n    private static final int DEFAULT_BORDER_WIDTH = 0;\n    private static final int DEFAULT_BORDER_COLOR = Color.BLACK;\n    private static final int DEFAULT_CIRCLE_BACKGROUND_COLOR = Color.TRANSPARENT;\n    private static final boolean DEFAULT_BORDER_OVERLAY = false;\n\n    private final RectF mDrawableRect = new RectF();\n    private final RectF mBorderRect = new RectF();\n\n    private final Matrix mShaderMatrix = new Matrix();\n    private final Paint mBitmapPaint = new Paint();\n    private final Paint mBorderPaint = new Paint();\n    private final Paint mCircleBackgroundPaint = new Paint();\n\n    private int mBorderColor = DEFAULT_BORDER_COLOR;\n    private int mBorderWidth = DEFAULT_BORDER_WIDTH;\n    private int mCircleBackgroundColor = DEFAULT_CIRCLE_BACKGROUND_COLOR;\n\n    private Bitmap mBitmap;\n    private BitmapShader mBitmapShader;\n    private int mBitmapWidth;\n    private int mBitmapHeight;\n\n    private float mDrawableRadius;\n    private float mBorderRadius;\n\n    private ColorFilter mColorFilter;\n\n    private boolean mReady;\n    private boolean mSetupPending;\n    private boolean mBorderOverlay;\n    private boolean mDisableCircularTransformation;\n\n    public CircleImageView(Context context) {\n        super(context);\n\n        init();\n    }\n\n    public CircleImageView(Context context, AttributeSet attrs) {\n        this(context, attrs, 0);\n    }\n\n    public CircleImageView(Context context, AttributeSet attrs, int defStyle) {\n        super(context, attrs, defStyle);\n\n        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleImageView, defStyle, 0);\n\n        mBorderWidth = a.getDimensionPixelSize(R.styleable.CircleImageView_civ_border_width, DEFAULT_BORDER_WIDTH);\n        mBorderColor = a.getColor(R.styleable.CircleImageView_civ_border_color, DEFAULT_BORDER_COLOR);\n        mBorderOverlay = a.getBoolean(R.styleable.CircleImageView_civ_border_overlay, DEFAULT_BORDER_OVERLAY);\n        mCircleBackgroundColor = a.getColor(R.styleable.CircleImageView_civ_circle_background_color, DEFAULT_CIRCLE_BACKGROUND_COLOR);\n\n        a.recycle();\n\n        init();\n    }\n\n    private void init() {\n        super.setScaleType(SCALE_TYPE);\n        mReady = true;\n\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {\n            setOutlineProvider(new OutlineProvider());\n        }\n\n        if (mSetupPending) {\n            setup();\n            mSetupPending = false;\n        }\n    }\n\n    @Override\n    public ScaleType getScaleType() {\n        return SCALE_TYPE;\n    }\n\n    @Override\n    public void setScaleType(ScaleType scaleType) {\n        if (scaleType != SCALE_TYPE) {\n            throw new IllegalArgumentException(String.format(\"ScaleType %s not supported.\", scaleType));\n        }\n    }\n\n    @Override\n    public void setAdjustViewBounds(boolean adjustViewBounds) {\n        if (adjustViewBounds) {\n            throw new IllegalArgumentException(\"adjustViewBounds not supported.\");\n        }\n    }\n\n    @Override\n    protected void onDraw(Canvas canvas) {\n        if (mDisableCircularTransformation) {\n            super.onDraw(canvas);\n            return;\n        }\n\n        if (mBitmap == null) {\n            return;\n        }\n\n        if (mCircleBackgroundColor != Color.TRANSPARENT) {\n            canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mCircleBackgroundPaint);\n        }\n        canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mBitmapPaint);\n        if (mBorderWidth > 0) {\n            canvas.drawCircle(mBorderRect.centerX(), mBorderRect.centerY(), mBorderRadius, mBorderPaint);\n        }\n    }\n\n    @Override\n    protected void onSizeChanged(int w, int h, int oldw, int oldh) {\n        super.onSizeChanged(w, h, oldw, oldh);\n        setup();\n    }\n\n    @Override\n    public void setPadding(int left, int top, int right, int bottom) {\n        super.setPadding(left, top, right, bottom);\n        setup();\n    }\n\n    @Override\n    public void setPaddingRelative(int start, int top, int end, int bottom) {\n        super.setPaddingRelative(start, top, end, bottom);\n        setup();\n    }\n\n    public int getBorderColor() {\n        return mBorderColor;\n    }\n\n    public void setBorderColor(@ColorInt int borderColor) {\n        if (borderColor == mBorderColor) {\n            return;\n        }\n\n        mBorderColor = borderColor;\n        mBorderPaint.setColor(mBorderColor);\n        invalidate();\n    }\n\n    public int getCircleBackgroundColor() {\n        return mCircleBackgroundColor;\n    }\n\n    public void setCircleBackgroundColor(@ColorInt int circleBackgroundColor) {\n        if (circleBackgroundColor == mCircleBackgroundColor) {\n            return;\n        }\n\n        mCircleBackgroundColor = circleBackgroundColor;\n        mCircleBackgroundPaint.setColor(circleBackgroundColor);\n        invalidate();\n    }\n\n    public void setCircleBackgroundColorResource(@ColorRes int circleBackgroundRes) {\n        setCircleBackgroundColor(getContext().getResources().getColor(circleBackgroundRes));\n    }\n\n    public int getBorderWidth() {\n        return mBorderWidth;\n    }\n\n    public void setBorderWidth(int borderWidth) {\n        if (borderWidth == mBorderWidth) {\n            return;\n        }\n\n        mBorderWidth = borderWidth;\n        setup();\n    }\n\n    public boolean isBorderOverlay() {\n        return mBorderOverlay;\n    }\n\n    public void setBorderOverlay(boolean borderOverlay) {\n        if (borderOverlay == mBorderOverlay) {\n            return;\n        }\n\n        mBorderOverlay = borderOverlay;\n        setup();\n    }\n\n    public boolean isDisableCircularTransformation() {\n        return mDisableCircularTransformation;\n    }\n\n    public void setDisableCircularTransformation(boolean disableCircularTransformation) {\n        if (mDisableCircularTransformation == disableCircularTransformation) {\n            return;\n        }\n\n        mDisableCircularTransformation = disableCircularTransformation;\n        initializeBitmap();\n    }\n\n    @Override\n    public void setImageBitmap(Bitmap bm) {\n        super.setImageBitmap(bm);\n        initializeBitmap();\n    }\n\n    @Override\n    public void setImageDrawable(Drawable drawable) {\n        super.setImageDrawable(drawable);\n        initializeBitmap();\n    }\n\n    @Override\n    public void setImageResource(@DrawableRes int resId) {\n        super.setImageResource(resId);\n        initializeBitmap();\n    }\n\n    @Override\n    public void setImageURI(Uri uri) {\n        super.setImageURI(uri);\n        initializeBitmap();\n    }\n\n    @Override\n    public void setColorFilter(ColorFilter cf) {\n        if (cf == mColorFilter) {\n            return;\n        }\n\n        mColorFilter = cf;\n        applyColorFilter();\n        invalidate();\n    }\n\n    @Override\n    public ColorFilter getColorFilter() {\n        return mColorFilter;\n    }\n\n    private void applyColorFilter() {\n        mBitmapPaint.setColorFilter(mColorFilter);\n    }\n\n    private Bitmap getBitmapFromDrawable(Drawable drawable) {\n        if (drawable == null) {\n            return null;\n        }\n\n        if (drawable instanceof BitmapDrawable) {\n            return ((BitmapDrawable) drawable).getBitmap();\n        }\n\n        try {\n            Bitmap bitmap;\n\n            if (drawable instanceof ColorDrawable) {\n                bitmap = Bitmap.createBitmap(COLORDRAWABLE_DIMENSION, COLORDRAWABLE_DIMENSION, BITMAP_CONFIG);\n            } else {\n                bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), BITMAP_CONFIG);\n            }\n\n            Canvas canvas = new Canvas(bitmap);\n            drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());\n            drawable.draw(canvas);\n            return bitmap;\n        } catch (Exception e) {\n            e.printStackTrace();\n            return null;\n        }\n    }\n\n    private void initializeBitmap() {\n        if (mDisableCircularTransformation) {\n            mBitmap = null;\n        } else {\n            mBitmap = getBitmapFromDrawable(getDrawable());\n        }\n        setup();\n    }\n\n    private void setup() {\n        if (!mReady) {\n            mSetupPending = true;\n            return;\n        }\n\n        if (getWidth() == 0 && getHeight() == 0) {\n            return;\n        }\n\n        if (mBitmap == null) {\n            invalidate();\n            return;\n        }\n\n        mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);\n\n        mBitmapPaint.setAntiAlias(true);\n        mBitmapPaint.setShader(mBitmapShader);\n\n        mBorderPaint.setStyle(Paint.Style.STROKE);\n        mBorderPaint.setAntiAlias(true);\n        mBorderPaint.setColor(mBorderColor);\n        mBorderPaint.setStrokeWidth(mBorderWidth);\n\n        mCircleBackgroundPaint.setStyle(Paint.Style.FILL);\n        mCircleBackgroundPaint.setAntiAlias(true);\n        mCircleBackgroundPaint.setColor(mCircleBackgroundColor);\n\n        mBitmapHeight = mBitmap.getHeight();\n        mBitmapWidth = mBitmap.getWidth();\n\n        mBorderRect.set(calculateBounds());\n        mBorderRadius = Math.min((mBorderRect.height() - mBorderWidth) / 2.0f, (mBorderRect.width() - mBorderWidth) / 2.0f);\n\n        mDrawableRect.set(mBorderRect);\n        if (!mBorderOverlay && mBorderWidth > 0) {\n            mDrawableRect.inset(mBorderWidth - 1.0f, mBorderWidth - 1.0f);\n        }\n        mDrawableRadius = Math.min(mDrawableRect.height() / 2.0f, mDrawableRect.width() / 2.0f);\n\n        applyColorFilter();\n        updateShaderMatrix();\n        invalidate();\n    }\n\n    private RectF calculateBounds() {\n        int availableWidth  = getWidth() - getPaddingLeft() - getPaddingRight();\n        int availableHeight = getHeight() - getPaddingTop() - getPaddingBottom();\n\n        int sideLength = Math.min(availableWidth, availableHeight);\n\n        float left = getPaddingLeft() + (availableWidth - sideLength) / 2f;\n        float top = getPaddingTop() + (availableHeight - sideLength) / 2f;\n\n        return new RectF(left, top, left + sideLength, top + sideLength);\n    }\n\n    private void updateShaderMatrix() {\n        float scale;\n        float dx = 0;\n        float dy = 0;\n\n        mShaderMatrix.set(null);\n\n        if (mBitmapWidth * mDrawableRect.height() > mDrawableRect.width() * mBitmapHeight) {\n            scale = mDrawableRect.height() / (float) mBitmapHeight;\n            dx = (mDrawableRect.width() - mBitmapWidth * scale) * 0.5f;\n        } else {\n            scale = mDrawableRect.width() / (float) mBitmapWidth;\n            dy = (mDrawableRect.height() - mBitmapHeight * scale) * 0.5f;\n        }\n\n        mShaderMatrix.setScale(scale, scale);\n        mShaderMatrix.postTranslate((int) (dx + 0.5f) + mDrawableRect.left, (int) (dy + 0.5f) + mDrawableRect.top);\n\n        mBitmapShader.setLocalMatrix(mShaderMatrix);\n    }\n\n    @SuppressLint(\"ClickableViewAccessibility\")\n    @Override\n    public boolean onTouchEvent(MotionEvent event) {\n        return inTouchableArea(event.getX(), event.getY()) && super.onTouchEvent(event);\n    }\n\n    private boolean inTouchableArea(float x, float y) {\n        return Math.pow(x - mBorderRect.centerX(), 2) + Math.pow(y - mBorderRect.centerY(), 2) <= Math.pow(mBorderRadius, 2);\n    }\n\n    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)\n    private class OutlineProvider extends ViewOutlineProvider {\n\n        @Override\n        public void getOutline(View view, Outline outline) {\n            Rect bounds = new Rect();\n            mBorderRect.roundOut(bounds);\n            outline.setRoundRect(bounds, bounds.width() / 2.0f);\n        }\n\n    }\n\n}"
  },
  {
    "path": "sample-app/src/main/java/com/atiurin/sampleapp/view/listeners/OnSwipeTouchListener.kt",
    "content": "package com.atiurin.sampleapp.view.listeners\n\nimport android.content.Context\nimport android.view.GestureDetector\nimport android.view.GestureDetector.SimpleOnGestureListener\nimport android.view.MotionEvent\nimport android.view.View\nimport android.view.View.OnTouchListener\n\n\nopen class OnSwipeTouchListener(ctx: Context?) : OnTouchListener {\n    private val gestureDetector: GestureDetector\n    override fun onTouch(v: View?, event: MotionEvent): Boolean {\n        return gestureDetector.onTouchEvent(event)\n    }\n\n    private inner class GestureListener : SimpleOnGestureListener() {\n        override fun onDown(e: MotionEvent): Boolean {\n            return true\n        }\n\n        override fun onFling(e1: MotionEvent?, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {\n            var result = false\n            try {\n                val diffY = e2.y - e1!!.y\n                val diffX = e2.x - e1.x\n                if (Math.abs(diffX) > Math.abs(diffY)) {\n                    if (Math.abs(diffX) > Companion.SWIPE_THRESHOLD && Math.abs(velocityX) > Companion.SWIPE_VELOCITY_THRESHOLD) {\n                        if (diffX > 0) {\n                            onSwipeRight()\n                        } else {\n                            onSwipeLeft()\n                        }\n                        result = true\n                    }\n                } else if (Math.abs(diffY) > Companion.SWIPE_THRESHOLD && Math.abs(velocityY) > Companion.SWIPE_VELOCITY_THRESHOLD) {\n                    if (diffY > 0) {\n                        onSwipeDown()\n                    } else {\n                        onSwipeUp()\n                    }\n                    result = true\n                }\n            } catch (exception: Exception) {\n                exception.printStackTrace()\n            }\n            return result\n        }\n\n\n    }\n\n    open fun onSwipeRight() {}\n    open fun onSwipeLeft() {}\n    open fun onSwipeUp() {}\n    open fun onSwipeDown() {}\n\n    init {\n        gestureDetector = GestureDetector(ctx, GestureListener())\n    }\n\n    companion object {\n        private const val SWIPE_THRESHOLD = 100\n        private const val SWIPE_VELOCITY_THRESHOLD = 100\n    }\n}"
  },
  {
    "path": "sample-app/src/main/res/drawable/background_splash.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <item\n            android:drawable=\"@color/colorPrimaryDark\"/>\n\n    <!--<item>-->\n        <!--<bitmap-->\n                <!--android:gravity=\"center\"-->\n                <!--android:src=\"@mipmap/ic_launcher\"/>-->\n    <!--</item>-->\n\n</layer-list>"
  },
  {
    "path": "sample-app/src/main/res/drawable/circle.xml",
    "content": "<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n       android:innerRadius=\"0dp\"\n       android:shape=\"ring\"\n       android:thicknessRatio=\"1.9\"\n       android:useLevel=\"false\" >\n    <solid android:color=\"@android:color/transparent\" />\n\n    <stroke\n            android:width=\"2dp\"\n            android:color=\"@android:color/holo_green_light\" />\n</shape>"
  },
  {
    "path": "sample-app/src/main/res/drawable/ic_launcher_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector\n        xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:height=\"108dp\"\n        android:width=\"108dp\"\n        android:viewportHeight=\"108\"\n        android:viewportWidth=\"108\">\n    <path android:fillColor=\"#008577\"\n          android:pathData=\"M0,0h108v108h-108z\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M9,0L9,108\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M19,0L19,108\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M29,0L29,108\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M39,0L39,108\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M49,0L49,108\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M59,0L59,108\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M69,0L69,108\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M79,0L79,108\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M89,0L89,108\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M99,0L99,108\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M0,9L108,9\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M0,19L108,19\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M0,29L108,29\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M0,39L108,39\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M0,49L108,49\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M0,59L108,59\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M0,69L108,69\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M0,79L108,79\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M0,89L108,89\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M0,99L108,99\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M19,29L89,29\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M19,39L89,39\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M19,49L89,49\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M19,59L89,59\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M19,69L89,69\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M19,79L89,79\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M29,19L29,89\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M39,19L39,89\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M49,19L49,89\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M59,19L59,89\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M69,19L69,89\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M79,19L79,89\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n</vector>\n"
  },
  {
    "path": "sample-app/src/main/res/drawable/ic_menu_camera.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\">\n    <path\n            android:fillColor=\"#FF000000\"\n            android:pathData=\"M12,12m-3.2,0a3.2,3.2 0,1 1,6.4 0a3.2,3.2 0,1 1,-6.4 0\"/>\n    <path\n            android:fillColor=\"#FF000000\"\n            android:pathData=\"M9,2L7.17,4H4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V6c0,-1.1 -0.9,-2 -2,-2h-3.17L15,2H9zm3,15c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5z\"/>\n</vector>\n"
  },
  {
    "path": "sample-app/src/main/res/drawable/ic_menu_gallery.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\">\n    <path\n            android:fillColor=\"#FF000000\"\n            android:pathData=\"M22,16V4c0,-1.1 -0.9,-2 -2,-2H8c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2zm-11,-4l2.03,2.71L16,11l4,5H8l3,-4zM2,6v14c0,1.1 0.9,2 2,2h14v-2H4V6H2z\"/>\n</vector>\n"
  },
  {
    "path": "sample-app/src/main/res/drawable/ic_menu_manage.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\">\n    <path\n            android:fillColor=\"#FF000000\"\n            android:pathData=\"M22.7,19l-9.1,-9.1c0.9,-2.3 0.4,-5 -1.5,-6.9 -2,-2 -5,-2.4 -7.4,-1.3L9,6 6,9 1.6,4.7C0.4,7.1 0.9,10.1 2.9,12.1c1.9,1.9 4.6,2.4 6.9,1.5l9.1,9.1c0.4,0.4 1,0.4 1.4,0l2.3,-2.3c0.5,-0.4 0.5,-1.1 0.1,-1.4z\"/>\n</vector>"
  },
  {
    "path": "sample-app/src/main/res/drawable/ic_menu_send.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\">\n    <path\n            android:fillColor=\"#FF000000\"\n            android:pathData=\"M2.01,21L23,12 2.01,3 2,10l15,2 -15,2z\"/>\n</vector>\n"
  },
  {
    "path": "sample-app/src/main/res/drawable/ic_menu_share.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\">\n    <path\n            android:fillColor=\"#FF000000\"\n            android:pathData=\"M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z\"/>\n</vector>\n"
  },
  {
    "path": "sample-app/src/main/res/drawable/ic_menu_slideshow.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\">\n    <path\n            android:fillColor=\"#FF000000\"\n            android:pathData=\"M4,6H2v14c0,1.1 0.9,2 2,2h14v-2H4V6zm16,-4H8c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V4c0,-1.1 -0.9,-2 -2,-2zm-8,12.5v-9l6,4.5 -6,4.5z\"/>\n</vector>\n"
  },
  {
    "path": "sample-app/src/main/res/drawable/img.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\" >\n\n    <item android:drawable=\"@drawable/chandler\"/>\n    <item android:drawable=\"@drawable/circle\"/>\n\n\n</layer-list>"
  },
  {
    "path": "sample-app/src/main/res/drawable/side_nav_bar.xml",
    "content": "<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n       android:shape=\"rectangle\">\n    <gradient\n            android:angle=\"135\"\n            android:centerColor=\"#009688\"\n            android:endColor=\"#00695C\"\n            android:startColor=\"#4DB6AC\"\n            android:type=\"linear\"/>\n</shape>"
  },
  {
    "path": "sample-app/src/main/res/drawable-anydpi/ic_account.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"#00574B\">\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,5c1.66,0 3,1.34 3,3s-1.34,3 -3,3 -3,-1.34 -3,-3 1.34,-3 3,-3zM12,19.2c-2.5,0 -4.71,-1.28 -6,-3.22 0.03,-1.99 4,-3.08 6,-3.08 1.99,0 5.97,1.09 6,3.08 -1.29,1.94 -3.5,3.22 -6,3.22z\"/>\n</vector>\n"
  },
  {
    "path": "sample-app/src/main/res/drawable-anydpi/ic_attach_file.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"#00574B\">\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M16.5,6v11.5c0,2.21 -1.79,4 -4,4s-4,-1.79 -4,-4V5c0,-1.38 1.12,-2.5 2.5,-2.5s2.5,1.12 2.5,2.5v10.5c0,0.55 -0.45,1 -1,1s-1,-0.45 -1,-1V6H10v9.5c0,1.38 1.12,2.5 2.5,2.5s2.5,-1.12 2.5,-2.5V5c0,-2.21 -1.79,-4 -4,-4S7,2.79 7,5v12.5c0,3.04 2.46,5.5 5.5,5.5s5.5,-2.46 5.5,-5.5V6h-1.5z\"/>\n</vector>\n"
  },
  {
    "path": "sample-app/src/main/res/drawable-anydpi/ic_exit.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"#00574B\">\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M10.09,15.59L11.5,17l5,-5 -5,-5 -1.41,1.41L12.67,11H3v2h9.67l-2.58,2.59zM19,3H5c-1.11,0 -2,0.9 -2,2v4h2V5h14v14H5v-4H3v4c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2z\"/>\n</vector>\n"
  },
  {
    "path": "sample-app/src/main/res/drawable-anydpi/ic_messages.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"#00574B\">\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M20,2L4,2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM18,14L6,14v-2h12v2zM18,11L6,11L6,9h12v2zM18,8L6,8L6,6h12v2z\"/>\n</vector>\n"
  },
  {
    "path": "sample-app/src/main/res/drawable-anydpi/ic_send.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"#00574B\">\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M2.01,21L23,12 2.01,3 2,10l15,2 -15,2z\"/>\n</vector>\n"
  },
  {
    "path": "sample-app/src/main/res/drawable-v24/ic_launcher_foreground.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        xmlns:aapt=\"http://schemas.android.com/aapt\"\n        android:width=\"108dp\"\n        android:height=\"108dp\"\n        android:viewportHeight=\"108\"\n        android:viewportWidth=\"108\">\n    <path\n            android:fillType=\"evenOdd\"\n            android:pathData=\"M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z\"\n            android:strokeColor=\"#00000000\"\n            android:strokeWidth=\"1\">\n        <aapt:attr name=\"android:fillColor\">\n            <gradient\n                    android:endX=\"78.5885\"\n                    android:endY=\"90.9159\"\n                    android:startX=\"48.7653\"\n                    android:startY=\"61.0927\"\n                    android:type=\"linear\">\n                <item\n                        android:color=\"#44000000\"\n                        android:offset=\"0.0\"/>\n                <item\n                        android:color=\"#00000000\"\n                        android:offset=\"1.0\"/>\n            </gradient>\n        </aapt:attr>\n    </path>\n    <path\n            android:fillColor=\"#FFFFFF\"\n            android:fillType=\"nonZero\"\n            android:pathData=\"M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z\"\n            android:strokeColor=\"#00000000\"\n            android:strokeWidth=\"1\"/>\n</vector>\n"
  },
  {
    "path": "sample-app/src/main/res/layout/activity_chat.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n                xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n                xmlns:tools=\"http://schemas.android.com/tools\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                android:background=\"#E1E1E1\"\n                tools:context=\".activity.ChatActivity\">\n\n    <com.google.android.material.appbar.AppBarLayout\n            android:id=\"@+id/app_toolbar\"\n            android:layout_height=\"wrap_content\"\n            android:layout_width=\"match_parent\"\n            android:theme=\"@style/AppTheme.AppBarOverlay\">\n\n        <androidx.appcompat.widget.Toolbar\n                android:id=\"@+id/toolbar\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"?attr/actionBarSize\"\n                android:background=\"?attr/colorPrimary\"\n                app:popupTheme=\"@style/AppTheme.PopupOverlay\">\n            <com.atiurin.sampleapp.view.CircleImageView\n                    android:id=\"@+id/toolbar_avatar\"\n                    android:layout_width=\"45dp\"\n                    android:layout_height=\"45dp\"\n                    android:layout_centerInParent=\"true\"\n                    android:src=\"@drawable/chandler\"\n                    app:civ_border_width=\"2dp\"\n                    app:civ_border_color=\"@android:color/transparent\"/>\n            <TextView\n                    android:id=\"@+id/toolbar_title\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:textSize=\"18sp\"\n                    android:textStyle=\"bold\"\n                    android:textColor=\"@android:color/white\"\n                    android:paddingLeft=\"16dp\"\n                    />\n        </androidx.appcompat.widget.Toolbar>\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <androidx.recyclerview.widget.RecyclerView\n            android:id=\"@+id/messages_list\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:layout_margin=\"10dp\"\n            android:layout_below=\"@+id/app_toolbar\"\n            android:layout_above=\"@+id/message_panel\"\n    />\n\n\n    <LinearLayout android:id=\"@+id/message_panel\"\n                  android:layout_width=\"match_parent\"\n                  android:layout_height=\"wrap_content\"\n                  android:layout_alignParentBottom=\"true\"\n                  android:layout_alignParentEnd=\"true\"\n                  android:background=\"#FFFFFF\"\n                  android:paddingBottom=\"2dp\"\n                  android:orientation=\"vertical\">\n\n        <View style=\"@style/Divider\"\n              android:background=\"#8E8E8E\"/>\n\n        <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:paddingTop=\"2dp\"\n                android:orientation=\"horizontal\">\n\n            <ImageView\n                    android:id=\"@+id/attach_button\"\n                    android:layout_width=\"50dp\"\n                    android:layout_height=\"50dp\"\n                    android:layout_alignParentBottom=\"true\"\n                    android:layout_marginStart=\"10dp\"\n                    android:src=\"@drawable/ic_attach_file\"\n                    android:layout_weight=\"1\"\n                    android:layout_margin=\"5dp\"\n            />\n            <EditText\n                    android:id=\"@+id/message_input_text\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:minHeight=\"48sp\"\n                    android:maxHeight=\"110sp\"\n                    android:maxLines=\"4\"\n                    android:background=\"@null\"\n                    android:layout_alignParentBottom=\"true\"\n                    android:layout_weight=\"3\"\n                    android:paddingStart=\"16dp\"\n                    android:paddingLeft=\"6dp\"\n                    android:paddingTop=\"8dp\"\n                    android:layout_marginLeft=\"5sp\"\n                    android:layout_marginRight=\"5sp\"\n                    android:hint=\"Enter text\"/>\n\n            <ImageView\n                    android:id=\"@+id/send_button\"\n                    android:layout_width=\"50dp\"\n                    android:layout_height=\"50dp\"\n                    android:layout_alignParentBottom=\"true\"\n                    android:layout_alignParentEnd=\"true\"\n                    android:layout_margin=\"5dp\"\n                    android:layout_marginEnd=\"10dp\"\n                    android:layout_weight=\"1\"\n                    android:src=\"@drawable/ic_send\"/>\n        </LinearLayout>\n\n    </LinearLayout>\n\n</RelativeLayout>"
  },
  {
    "path": "sample-app/src/main/res/layout/activity_custom_clicks.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/frameLayout\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:background=\"#123FE3\"\n    tools:context=\".activity.CustomClicksActivity\">\n\n    <ImageView\n        android:id=\"@+id/imageView\"\n        android:layout_width=\"264dp\"\n        android:layout_height=\"285dp\"\n        android:layout_marginStart=\"72dp\"\n        android:layout_marginTop=\"128dp\"\n        android:adjustViewBounds=\"false\"\n        android:background=\"#FFFFFF\"\n        android:contentDescription=\"picture\"\n        android:visibility=\"visible\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\"\n        app:srcCompat=\"@drawable/background_splash\"\n        tools:srcCompat=\"@drawable/background_splash\" />\n\n    <Button\n        android:id=\"@+id/button\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginStart=\"161dp\"\n        android:layout_marginTop=\"58dp\"\n        android:text=\"Button\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@+id/imageView\" />\n\n    <RadioButton\n        android:id=\"@+id/rB_top_left\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginStart=\"72dp\"\n        android:layout_marginTop=\"128dp\"\n        android:backgroundTint=\"#DA1414\"\n        android:minWidth=\"48dp\"\n        android:minHeight=\"48dp\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n\n    <RadioButton\n        android:id=\"@+id/rB_center_left\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginEnd=\"216dp\"\n        android:layout_marginBottom=\"120dp\"\n        android:backgroundTint=\"#DA1414\"\n        android:minWidth=\"48dp\"\n        android:minHeight=\"48dp\"\n        app:layout_constraintBottom_toBottomOf=\"@+id/imageView\"\n        app:layout_constraintEnd_toEndOf=\"@+id/imageView\" />\n\n    <RadioButton\n        android:id=\"@+id/rB_bottom_left\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:backgroundTint=\"#DA1414\"\n        android:minWidth=\"48dp\"\n        android:minHeight=\"48dp\"\n        app:layout_constraintBottom_toBottomOf=\"@+id/imageView\"\n        app:layout_constraintStart_toStartOf=\"@+id/imageView\" />\n\n    <RadioButton\n        android:id=\"@+id/rB_top_right\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:backgroundTint=\"#DA1414\"\n        android:minWidth=\"48dp\"\n        android:minHeight=\"48dp\"\n        app:layout_constraintEnd_toEndOf=\"@+id/imageView\"\n        app:layout_constraintTop_toTopOf=\"@+id/imageView\" />\n\n    <RadioButton\n        android:id=\"@+id/rB_center_right\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"117dp\"\n        android:backgroundTint=\"#DA1414\"\n        android:minWidth=\"48dp\"\n        android:minHeight=\"48dp\"\n        app:layout_constraintEnd_toEndOf=\"@+id/imageView\"\n        app:layout_constraintTop_toTopOf=\"@+id/imageView\" />\n\n    <RadioButton\n        android:id=\"@+id/rB_bottom_right\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:backgroundTint=\"#DA1414\"\n        android:minWidth=\"48dp\"\n        android:minHeight=\"48dp\"\n        app:layout_constraintBottom_toBottomOf=\"@+id/imageView\"\n        app:layout_constraintEnd_toEndOf=\"@+id/imageView\" />\n\n    <RadioButton\n        android:id=\"@+id/rB_top_center\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginEnd=\"104dp\"\n        android:backgroundTint=\"#DA1414\"\n        android:minWidth=\"48dp\"\n        android:minHeight=\"48dp\"\n        app:layout_constraintEnd_toEndOf=\"@+id/imageView\"\n        app:layout_constraintTop_toTopOf=\"@+id/imageView\" />\n\n    <RadioButton\n        android:id=\"@+id/rB_bottom_center\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"237dp\"\n        android:layout_marginEnd=\"104dp\"\n        android:backgroundTint=\"#DA1414\"\n        android:minWidth=\"48dp\"\n        android:minHeight=\"48dp\"\n        app:layout_constraintEnd_toEndOf=\"@+id/imageView\"\n        app:layout_constraintTop_toTopOf=\"@+id/imageView\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "sample-app/src/main/res/layout/activity_login.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n              xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n              xmlns:tools=\"http://schemas.android.com/tools\"\n              android:orientation=\"vertical\"\n              android:layout_width=\"match_parent\"\n              android:layout_height=\"match_parent\"\n              android:layout_margin=\"10dp\"\n              tools:context=\".activity.LoginActivity\">\n\n        <EditText\n                android:id=\"@+id/et_username\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center_vertical|center_horizontal\"\n                android:textColorHighlight=\"@color/colorPrimary\"\n                android:hint=\"Enter user name\"/>\n\n        <EditText\n                android:id=\"@+id/et_password\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center_vertical|center_horizontal\"\n                android:password=\"true\"\n                android:hint=\"Enter password\"/>\n\n        <Button\n                android:id=\"@+id/login_button\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"Login\"\n                android:layout_marginTop=\"10dp\"\n                android:background=\"@color/colorPrimaryDark\"\n                android:textColor=\"@android:color/white\"/>\n        <Button\n                android:id=\"@+id/sign_up_button\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"Sign Up\"\n                android:layout_marginTop=\"10dp\"\n                android:background=\"@color/colorPrimaryDark\"\n                android:textColor=\"@android:color/white\"/>\n\n</LinearLayout>"
  },
  {
    "path": "sample-app/src/main/res/layout/activity_main.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.drawerlayout.widget.DrawerLayout\n        xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n        xmlns:tools=\"http://schemas.android.com/tools\"\n        android:id=\"@+id/drawer_layout\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:fitsSystemWindows=\"true\"\n        tools:openDrawer=\"start\">\n\n    <include\n            layout=\"@layout/app_bar_main\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"/>\n\n    <com.google.android.material.navigation.NavigationView\n            android:id=\"@+id/nav_view\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"match_parent\"\n            android:layout_gravity=\"start\"\n            android:fitsSystemWindows=\"true\"\n            app:headerLayout=\"@layout/nav_header_main\"\n            app:menu=\"@menu/activity_main_drawer\"/>\n    <!-- A RecyclerView with some commonly used attributes -->\n\n\n</androidx.drawerlayout.widget.DrawerLayout>\n"
  },
  {
    "path": "sample-app/src/main/res/layout/activity_profile.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n              xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n              xmlns:tools=\"http://schemas.android.com/tools\"\n              android:orientation=\"vertical\"\n              android:layout_width=\"match_parent\"\n              android:layout_height=\"match_parent\"\n              android:fitsSystemWindows=\"true\"\n              tools:context=\".activity.ProfileActivity\">\n\n    <com.atiurin.sampleapp.view.CircleImageView\n            android:id=\"@+id/avatar\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_centerInParent=\"true\"\n            android:src=\"@drawable/chandler\"\n            app:civ_border_width=\"2dp\"\n            app:civ_border_color=\"@android:color/transparent\"/>\n    <EditText\n            android:id=\"@+id/et_username\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"20dp\"\n            android:layout_gravity=\"center_vertical|center_horizontal\"\n            android:textColorHighlight=\"@color/colorPrimary\"\n            android:hint=\"Enter user name\"/>\n</LinearLayout>"
  },
  {
    "path": "sample-app/src/main/res/layout/activity_uiblock.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:orientation=\"vertical\">\n\n    <androidx.constraintlayout.widget.ConstraintLayout\n        android:id=\"@+id/contact_items\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n\n        <include\n            android:id=\"@+id/contact_item_1\"\n            layout=\"@layout/ui_block_contact_item\"\n            app:layout_constraintTop_toTopOf=\"parent\"\n            app:layout_constraintStart_toStartOf=\"parent\"\n            app:layout_constraintEnd_toEndOf=\"parent\" />\n\n        <include\n            android:id=\"@+id/contact_item_2\"\n            layout=\"@layout/ui_block_contact_item\"\n            app:layout_constraintTop_toBottomOf=\"@id/contact_item_1\"\n            app:layout_constraintStart_toStartOf=\"parent\"\n            app:layout_constraintEnd_toEndOf=\"parent\" />\n\n    </androidx.constraintlayout.widget.ConstraintLayout>\n</LinearLayout>\n\n"
  },
  {
    "path": "sample-app/src/main/res/layout/activity_uielements.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\">\n\n    <TextView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:shadowColor=\"@color/colorShadow\"\n        android:text=\"@string/ui_elements_activity_title\"\n        android:layout_marginBottom=\"10dp\"\n        android:textColor=\"@color/colorPrimary\"\n        android:textColorHighlight=\"@color/colorHighlight\"\n        android:textColorHint=\"@color/colorHint\" />\n\n    <Button\n        android:id=\"@+id/button1\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:contentDescription=\"@string/button_default_content_desc\"\n        android:text=\"@string/button_text\"\n        android:textAllCaps=\"false\" />\n\n    <TextView\n        android:id=\"@+id/last_event_status\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:shadowColor=\"@color/colorShadow\"\n        android:text=\"@string/button_text\"\n        android:textColor=\"@color/colorPrimary\"\n        android:textColorHighlight=\"@color/colorHighlight\"\n        android:textColorHint=\"@color/colorHint\" />\n\n    <RadioGroup\n        android:id=\"@+id/radio_group_visibility\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"horizontal\">\n\n        <RadioButton\n            android:id=\"@+id/radio_visible\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:checked=\"true\"\n            android:text=\"@string/radio_visible\" />\n\n        <RadioButton\n            android:id=\"@+id/radio_invisible\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"@string/radio_invisible\" />\n\n        <RadioButton\n            android:id=\"@+id/radio_gone\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"@string/radio_gone\" />\n    </RadioGroup>\n\n    <CheckBox\n        android:id=\"@+id/checkbox_enable\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:checked=\"true\"\n        android:text=\"@string/checkbox_enable\" />\n\n    <CheckBox\n        android:id=\"@+id/checkbox_clickable\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:checked=\"true\"\n        android:text=\"@string/checkbox_clickable\" />\n\n    <CheckBox\n        android:id=\"@+id/checkbox_selected\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:checked=\"false\"\n        android:text=\"@string/checkbox_selected\" />\n\n    <CheckBox\n        android:id=\"@+id/checkbox_focusable\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:checked=\"true\"\n        android:text=\"@string/checkbox_focusable\"\n        android:visibility=\"invisible\" />\n\n    <EditText\n        android:id=\"@+id/et_contentDesc\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:text=\"@string/button_default_content_desc\" />\n\n    <WebView\n        android:id=\"@+id/webview\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\" />\n\n    <CheckBox\n        android:id=\"@+id/checkbox_js_enabled\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:checked=\"true\"\n\n        android:text=\"@string/checkbox_js_enabled\" />\n\n    <androidx.appcompat.widget.AppCompatTextView\n        android:id=\"@+id/app_compat_text\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_margin=\"10dp\"\n        android:text=\"@string/app_compat_text\"\n        android:textColor=\"@color/colorPrimary\" />\n\n    <LinearLayout\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"horizontal\">\n\n        <ImageView\n            android:id=\"@+id/image_view2\"\n            android:layout_width=\"100dp\"\n            android:layout_height=\"100dp\"\n            android:layout_margin=\"10dp\"\n            android:src=\"@drawable/ic_account\" />\n\n        <ImageView\n            android:id=\"@+id/swipe_image_view\"\n            android:layout_width=\"100dp\"\n            android:layout_height=\"100dp\"\n            android:layout_margin=\"10dp\"\n            android:src=\"@drawable/ic_account\" />\n\n    </LinearLayout>\n\n    <ImageView\n        android:id=\"@+id/empty_image_view\"\n        android:layout_width=\"10dp\"\n        android:layout_height=\"10dp\"\n        android:layout_margin=\"10dp\"\n        android:background=\"@color/colorAccent\"\n        android:clickable=\"false\" />\n\n    <Button\n        android:id=\"@+id/exist_hidden_button\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:textAllCaps=\"false\"\n        android:visibility=\"invisible\" />\n</LinearLayout>\n"
  },
  {
    "path": "sample-app/src/main/res/layout/activity_webview.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:orientation=\"vertical\">\n\n    <WebView\n            android:id=\"@+id/webview\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            />\n\n</LinearLayout>"
  },
  {
    "path": "sample-app/src/main/res/layout/app_bar_main.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout\n        xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n        xmlns:tools=\"http://schemas.android.com/tools\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        tools:context=\".activity.MainActivity\">\n\n    <com.google.android.material.appbar.AppBarLayout\n            android:layout_height=\"wrap_content\"\n            android:layout_width=\"match_parent\"\n            android:theme=\"@style/AppTheme.AppBarOverlay\">\n\n        <androidx.appcompat.widget.Toolbar\n                android:id=\"@+id/toolbar\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"?attr/actionBarSize\"\n                android:background=\"?attr/colorPrimary\"\n                app:popupTheme=\"@style/AppTheme.PopupOverlay\"/>\n\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <include layout=\"@layout/content_main\"/>\n\n\n</androidx.coordinatorlayout.widget.CoordinatorLayout>"
  },
  {
    "path": "sample-app/src/main/res/layout/content_main.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout\n        xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        xmlns:tools=\"http://schemas.android.com/tools\"\n        xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        app:layout_behavior=\"@string/appbar_scrolling_view_behavior\"\n        tools:showIn=\"@layout/app_bar_main\"\n        tools:context=\".activity.MainActivity\">\n\n    <androidx.recyclerview.widget.RecyclerView\n            android:id=\"@+id/recycler_friends\"\n            android:scrollbars=\"vertical\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"/>\n\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "sample-app/src/main/res/layout/list_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n              xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n              android:layout_width=\"match_parent\"\n              android:layout_height=\"wrap_content\"\n              android:orientation=\"vertical\"\n              >\n    <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"102dp\"\n            android:paddingBottom=\"10dp\"\n            android:paddingLeft=\"16dp\"\n            android:paddingTop=\"12dp\"\n            android:orientation=\"horizontal\">\n        <LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n                      android:layout_width=\"wrap_content\"\n                      android:layout_height=\"match_parent\"\n                      android:layout_alignParentLeft=\"true\">\n            <com.atiurin.sampleapp.view.CircleImageView\n                    android:id=\"@+id/avatar\"\n                    android:layout_width=\"80dp\"\n                    android:layout_height=\"80dp\"\n                    android:layout_centerInParent=\"true\"\n                    android:src=\"@drawable/chandler\"\n                    app:civ_border_width=\"2dp\"\n                    app:civ_border_color=\"@android:color/transparent\"/>\n        </LinearLayout>\n        <LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n                          android:layout_width=\"wrap_content\"\n                          android:layout_height=\"match_parent\"\n                          android:orientation=\"vertical\"\n                          android:paddingLeft=\"16dp\"\n                          android:layout_alignParentRight=\"true\"\n            >\n                <TextView\n                        android:id=\"@+id/tv_name\"\n                        android:layout_width=\"wrap_content\"\n                        android:layout_height=\"wrap_content\"\n                        android:textColor=\"#000000\"\n                        android:textSize=\"20sp\"\n                        android:textStyle=\"bold\"\n                        android:layout_marginTop=\"10dp\"/>\n\n                <TextView\n                        android:id=\"@+id/tv_status\"\n                        android:gravity=\"bottom\"\n                        android:layout_width=\"wrap_content\"\n                        android:layout_height=\"wrap_content\"\n                        android:textColor=\"#2B2B2B\"\n                        android:textStyle=\"italic\"\n                        android:textSize=\"16sp\" />\n            </LinearLayout>\n    </LinearLayout>\n    <View style=\"@style/Divider\"\n          android:layout_marginLeft=\"16dp\"\n          android:background=\"#8E8E8E\"/>\n\n\n</LinearLayout>"
  },
  {
    "path": "sample-app/src/main/res/layout/message_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n              android:orientation=\"vertical\"\n              android:layout_width=\"match_parent\"\n              android:layout_height=\"wrap_content\"\n              xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n              xmlns:emojicon=\"http://schemas.android.com/apk/res-auto\"\n              xmlns:tools=\"http://schemas.android.com/tools\">\n    <androidx.cardview.widget.CardView\n            android:id=\"@+id/card_view\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginBottom=\"4dp\"\n            android:layout_marginTop=\"4dp\"\n            app:cardBackgroundColor=\"#FFFFFF\"\n            app:cardCornerRadius=\"5dp\"\n            app:cardElevation=\"1dp\"\n            app:cardPreventCornerOverlap=\"false\">\n\n        <LinearLayout\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_margin=\"16dp\">\n\n            <LinearLayout\n                    android:id=\"@+id/topWrapper\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:orientation=\"vertical\">\n\n                <TextView\n                        android:id=\"@+id/author\"\n                        android:visibility=\"gone\"\n                        android:layout_width=\"wrap_content\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_marginEnd=\"5dp\"\n                        android:ellipsize=\"end\"\n                        android:maxLines=\"1\"\n                        android:textStyle=\"italic\"/>\n\n                <TextView\n                        android:id=\"@+id/message_text\"\n                        android:layout_width=\"wrap_content\"\n                        android:layout_height=\"wrap_content\"\n                        android:textSize=\"20sp\"\n                        android:layout_marginEnd=\"5dp\"/>\n                       </LinearLayout>\n\n            <LinearLayout\n                    android:id=\"@+id/bottomWrapper2\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_below=\"@+id/topWrapper\"\n                    android:gravity=\"center_vertical|start\"\n                    android:orientation=\"horizontal\">\n\n                <TextView\n                        android:id=\"@+id/messageDate\"\n                        android:layout_width=\"wrap_content\"\n                        android:layout_height=\"wrap_content\" />\n\n            </LinearLayout>\n        </LinearLayout>\n    </androidx.cardview.widget.CardView>\n</LinearLayout>"
  },
  {
    "path": "sample-app/src/main/res/layout/my_text_view.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<TextView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n          android:layout_width=\"match_parent\"\n          android:layout_height=\"wrap_content\"\n          android:textSize=\"16sp\">\n</TextView>"
  },
  {
    "path": "sample-app/src/main/res/layout/nav_header_main.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout\n        xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"@dimen/nav_header_height\"\n        android:background=\"@drawable/side_nav_bar\"\n        android:paddingBottom=\"@dimen/activity_vertical_margin\"\n        android:paddingLeft=\"@dimen/activity_horizontal_margin\"\n        android:paddingRight=\"@dimen/activity_horizontal_margin\"\n        android:paddingTop=\"@dimen/activity_vertical_margin\"\n        android:theme=\"@style/ThemeOverlay.AppCompat.Dark\"\n        android:orientation=\"vertical\"\n        android:gravity=\"bottom\">\n\n    <com.atiurin.sampleapp.view.CircleImageView\n            android:layout_width=\"95dp\"\n            android:layout_height=\"95dp\"\n            android:src=\"@drawable/joey\"\n            app:civ_border_width=\"1dp\"\n            app:civ_border_color=\"@android:color/background_light\"\n            android:contentDescription=\"@string/nav_header_desc\"\n            android:id=\"@+id/navigation_user_avatar\"/>\n\n    <TextView\n            android:id=\"@+id/navigation_user_name\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:paddingTop=\"@dimen/nav_header_vertical_spacing\"\n            android:text=\"Joey Tribbiani\"\n            android:textAppearance=\"@style/TextAppearance.AppCompat.Body1\"/>\n\n</LinearLayout>\n"
  },
  {
    "path": "sample-app/src/main/res/layout/ui_block_contact_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:orientation=\"vertical\">\n\n    <TextView\n        android:id=\"@+id/name\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:shadowColor=\"@color/colorShadow\"\n        android:textColor=\"@color/colorPrimary\"\n        android:text=\"name\"\n        android:textColorHighlight=\"@color/colorHighlight\"\n        android:textColorHint=\"@color/colorHint\" />\n\n    <TextView\n        android:id=\"@+id/status\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:shadowColor=\"@color/colorShadow\"\n        android:text=\"status\"\n        android:textColor=\"@color/colorPrimary\"\n        android:textColorHighlight=\"@color/colorHighlight\"\n        android:textColorHint=\"@color/colorHint\" />\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"vertical\"\n        android:id=\"@+id/deep_search_parent\">\n        <TextView\n            android:id=\"@+id/deep_search_child\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:shadowColor=\"@color/colorShadow\"\n            android:textColor=\"@color/colorPrimary\"\n            android:text=\"@string/deep_search\"\n            android:textColorHighlight=\"@color/colorHighlight\"\n            android:textColorHint=\"@color/colorHint\" />\n    </LinearLayout>\n</LinearLayout>"
  },
  {
    "path": "sample-app/src/main/res/menu/activity_main_drawer.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n      xmlns:tools=\"http://schemas.android.com/tools\"\n      tools:showIn=\"navigation_view\">\n\n    <group android:checkableBehavior=\"single\">\n        <item\n                android:id=\"@+id/nav_settings\"\n                android:icon=\"@drawable/ic_messages\"\n                android:title=\"@string/menu_settings\"/>\n        <item\n                android:id=\"@+id/nav_saved_messages\"\n                android:icon=\"@drawable/ic_messages\"\n                android:title=\"@string/menu_saved_messages\"/>\n        <item\n                android:id=\"@+id/nav_profile\"\n                android:icon=\"@drawable/ic_account\"\n                android:title=\"@string/menu_profile\"/>\n        <item\n                android:id=\"@+id/nav_logout\"\n                android:icon=\"@drawable/ic_exit\"\n                android:title=\"@string/menu_logout\"/>\n\n        <item\n                android:id=\"@+id/ui_elements\"\n                android:icon=\"@drawable/ic_exit\"\n                android:title=\"@string/menu_ui_elements\"/>\n\n        <item\n            android:id=\"@+id/web_view_nav_item\"\n            android:icon=\"@drawable/ic_menu_slideshow\"\n            android:title=\"@string/menu_web_view\" />\n        <item\n            android:id=\"@+id/custom_clicks_nav_item\"\n            android:icon=\"@android:drawable/ic_dialog_dialer\"\n            android:title=\"@string/menu_custom_clicks\" />\n            android:title=\"@string/menu_web_view\"/>\n        <item\n            android:id=\"@+id/compose_elements\"\n            android:icon=\"@android:drawable/ic_menu_preferences\"\n            android:title=\"@string/menu_compose_ui_elements\"/>\n        <item\n            android:id=\"@+id/compose_router\"\n            android:icon=\"@android:drawable/ic_dialog_map\"\n            android:title=\"@string/menu_compose_router\"/>\n        <item\n            android:id=\"@+id/compose_list\"\n            android:icon=\"@android:drawable/ic_menu_gallery\"\n            android:title=\"@string/menu_compose_list\"/>\n    </group>\n\n    <!--<item android:title=\"Communicate\">-->\n        <!--<menu>-->\n            <!--<item-->\n                    <!--android:id=\"@+id/nav_share\"-->\n                    <!--android:icon=\"@drawable/ic_menu_share\"-->\n                    <!--android:title=\"@string/menu_share\"/>-->\n            <!--<item-->\n                    <!--android:id=\"@+id/nav_send\"-->\n                    <!--android:icon=\"@drawable/ic_menu_send\"-->\n                    <!--android:title=\"@string/menu_send\"/>-->\n        <!--</menu>-->\n    <!--</item>-->\n\n</menu>\n"
  },
  {
    "path": "sample-app/src/main/res/menu/main.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n      xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n    <item android:id=\"@+id/action_clear\"\n          android:title=\"@string/action_clear_history\"\n          android:orderInCategory=\"100\"\n          app:showAsAction=\"never\"/>\n</menu>\n"
  },
  {
    "path": "sample-app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@drawable/ic_launcher_background\"/>\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\"/>\n</adaptive-icon>"
  },
  {
    "path": "sample-app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@drawable/ic_launcher_background\"/>\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\"/>\n</adaptive-icon>"
  },
  {
    "path": "sample-app/src/main/res/values/attrs.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <declare-styleable name=\"CircleImageView\">\n        <attr name=\"civ_border_width\" format=\"dimension\" />\n        <attr name=\"civ_border_color\" format=\"color\" />\n        <attr name=\"civ_border_overlay\" format=\"boolean\" />\n        <attr name=\"civ_circle_background_color\" format=\"color\" />\n    </declare-styleable>\n</resources>"
  },
  {
    "path": "sample-app/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"colorPrimary\">#008577</color>\n    <color name=\"colorPrimaryDark\">#00574B</color>\n    <color name=\"colorAccent\">#2A4B9F</color>\n    <color name=\"colorLight\">#58E1CA</color>\n    <color name=\"colorHint\">#8158E1</color>\n    <color name=\"colorShadow\">#A1E158</color>\n    <color name=\"colorHighlight\">#E158BF</color>\n    <color name=\"invalid\">#221F21</color>\n\n\n</resources>\n"
  },
  {
    "path": "sample-app/src/main/res/values/dimens.xml",
    "content": "<resources>\n    <!-- Default screen margins, per the Android Design guidelines. -->\n    <dimen name=\"activity_horizontal_margin\">16dp</dimen>\n    <dimen name=\"activity_vertical_margin\">16dp</dimen>\n    <dimen name=\"nav_header_vertical_spacing\">8dp</dimen>\n    <dimen name=\"nav_header_height\">176dp</dimen>\n    <dimen name=\"fab_margin\">16dp</dimen>\n</resources>"
  },
  {
    "path": "sample-app/src/main/res/values/strings.xml",
    "content": "<resources>\n    <string name=\"app_name\">EspressoGuide</string>\n    <string name=\"navigation_drawer_open\">Open navigation drawer</string>\n    <string name=\"navigation_drawer_close\">Close navigation drawer</string>\n    <string name=\"nav_header_title\">Android Studio</string>\n    <string name=\"nav_header_subtitle\">android.studio@android.com</string>\n    <string name=\"nav_header_desc\">Navigation header</string>\n    <string name=\"action_settings\">Settings</string>\n    <string name=\"action_logout\">Logout</string>\n    <string name=\"action_clear_history\">Clear history</string>\n\n    <string name=\"menu_settings\">Settings</string>\n    <string name=\"menu_saved_messages\">Saved messages</string>\n    <string name=\"menu_profile\">My Profile</string>\n    <string name=\"menu_logout\">Logout</string>\n    <string name=\"menu_ui_elements\">UiElements</string>\n    <string name=\"menu_web_view\">Web View</string>\n    <string name=\"menu_compose_ui_elements\">Compose UiElements</string>\n    <string name=\"menu_compose_router\">Compose Router</string>\n    <string name=\"menu_compose_list\">Compose List</string>\n    <string name=\"title_friends_list\">Friends</string>\n\n    <string name=\"button_text\">Simple Button</string>\n    <string name=\"button_default_content_desc\">Default content description</string>\n    <string name=\"radio_visible\">VISIBLE</string>\n    <string name=\"radio_invisible\">INVISIBLE</string>\n    <string name=\"radio_gone\">GONE</string>\n    <string name=\"checkbox_enable\">Enable button</string>\n    <string name=\"checkbox_clickable\">Clickable button</string>\n    <string name=\"checkbox_selected\">Selected button</string>\n    <string name=\"checkbox_focusable\">Focusable button</string>\n    <string name=\"checkbox_js_enabled\">WebView Javascript enabled</string>\n    <string name=\"checkbox_unavailable\">Unavailable</string>\n\n    <string name=\"button_event_click\">Click</string>\n    <string name=\"button_event_long_click\">Long Click</string>\n    <string name=\"button_event_double_click\">Double Click</string>\n\n    <string name=\"app_compat_text\">Default AppCompatTextView</string>\n    <string name=\"menu_custom_clicks\">Custom Clicks</string>\n    <string name=\"ui_elements_activity_title\">UI elements activity title</string>\n    <string name=\"deep_search\">Deep search</string>\n\n</resources>\n"
  },
  {
    "path": "sample-app/src/main/res/values/styles.xml",
    "content": "<resources>\n\n    <!-- Base application theme. -->\n    <style name=\"AppTheme\" parent=\"Theme.AppCompat.Light.DarkActionBar\">\n        <!-- Customize your theme here. -->\n        <item name=\"colorPrimary\">@color/colorPrimary</item>\n        <item name=\"colorPrimaryDark\">@color/colorPrimaryDark</item>\n        <item name=\"colorAccent\">@color/colorAccent</item>\n    </style>\n    <style name=\"AppTheme.NoActionBar\">\n        <item name=\"windowActionBar\">false</item>\n        <item name=\"windowNoTitle\">true</item>\n    </style>\n    <style name=\"AppTheme.AppBarOverlay\" parent=\"ThemeOverlay.AppCompat.Dark.ActionBar\"/>\n    <style name=\"AppTheme.PopupOverlay\" parent=\"ThemeOverlay.AppCompat.Light\"/>\n    <style name=\"SplashTheme\" parent=\"Theme.AppCompat.NoActionBar\">\n        <item name=\"android:windowBackground\">@drawable/background_splash</item>\n    </style>\n\n    <style name=\"Divider\">\n        <item name=\"android:layout_width\">match_parent</item>\n        <item name=\"android:layout_height\">1dp</item>\n        <item name=\"android:background\">?android:attr/listDivider</item>\n    </style>\n\n</resources>\n"
  },
  {
    "path": "sample-app/src/main/res/values-v21/styles.xml",
    "content": "<resources>\n    <style name=\"AppTheme.NoActionBar\">\n        <item name=\"windowActionBar\">false</item>\n        <item name=\"windowNoTitle\">true</item>\n        <item name=\"android:statusBarColor\">@android:color/transparent</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "settings.gradle.kts",
    "content": "pluginManagement {\n    repositories {\n        google()\n        google {\n            mavenContent {\n                includeGroupAndSubgroups(\"androidx\")\n                includeGroupAndSubgroups(\"com.android\")\n                includeGroupAndSubgroups(\"com.google\")\n            }\n        }\n        gradlePluginPortal()\n        mavenCentral()\n    }\n}\n\n\n//rootProject.name = \"Ultron\"\ninclude(\":sample-app\")\ninclude(\":ultron-android\")\ninclude(\":ultron-compose\")\ninclude(\":ultron-allure\")\ninclude(\":ultron-common\")\ninclude(\":composeApp\")\n"
  },
  {
    "path": "ultron-allure/.gitignore",
    "content": "/build"
  },
  {
    "path": "ultron-allure/build.gradle.kts",
    "content": "import com.vanniktech.maven.publish.SonatypeHost\n\nplugins {\n    alias(libs.plugins.androidLibrary)\n    id(\"kotlin-android\")\n    id(\"org.jetbrains.dokka\")\n    alias(libs.plugins.vanniktech.mavenPublish)\n}\n\ngroup = project.findProperty(\"GROUP\")!!\nversion =  project.findProperty(\"VERSION_NAME\")!!\n\nkotlin {\n    jvmToolchain(11)\n}\n\nandroid {\n    namespace = \"com.atiurin.ultron.allure\"\n    compileSdk = 35\n    defaultConfig {\n        minSdk = 21\n    }\n\n    sourceSets {\n        named(\"main\").configure {\n            java.srcDir(\"src/main/java\")\n        }\n        named(\"test\").configure {\n            java.srcDir(\"src/test/java\")\n        }\n    }\n    compileOptions {\n        targetCompatibility = JavaVersion.VERSION_11\n        sourceCompatibility = JavaVersion.VERSION_11\n    }\n\n    publishing {\n        // Publishing configuration is handled by mavenPublishing block\n    }\n}\ndependencies {\n    implementation(Libs.kotlinStdlib)\n    api(project(\":ultron-common\"))\n    api(Libs.allureAndroid)\n    api(Libs.allureCommon)\n    api(Libs.allureModel)\n    api(Libs.allureJunit4)\n    api(Libs.espressoCore)\n}\n\nval dokkaOutputDir = buildDir.resolve(\"dokka\")\ntasks.dokkaHtml { outputDirectory.set(file(dokkaOutputDir)) }\nval deleteDokkaOutputDir by tasks.register<Delete>(\"deleteDokkaOutputDirectory\") { delete(dokkaOutputDir) }\nval javadocJar = tasks.register<Jar>(\"javadocJar\") {\n    archiveClassifier.set(\"javadoc\")\n    duplicatesStrategy = DuplicatesStrategy.EXCLUDE\n    dependsOn(deleteDokkaOutputDir, tasks.dokkaHtml)\n    from(dokkaOutputDir)\n}\n\n\nmavenPublishing {\n    publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)\n    signAllPublications()\n    coordinates(artifactId = \"ultron-allure\")\n\n    pom {\n        name = \"Ultron Common\"\n        description = \"Android & Compose Multiplatform UI testing framework\"\n        url = \"https://github.com/open-tool/ultron\"\n        inceptionYear = \"2021\"\n\n        licenses {\n            license {\n                name.set(\"The Apache License, Version 2.0\")\n                url.set(\"http://www.apache.org/licenses/LICENSE-2.0.txt\")\n            }\n        }\n\n        issueManagement {\n            system = \"GitHub Issues\"\n            url = \"https://github.com/open-tool/ultron/issues\"\n        }\n\n        developers {\n            developer {\n                id = \"alex-tiurin\"\n                name = \"Aleksei Tiurin\"\n                url = \"https://github.com/open-tool\"\n            }\n        }\n\n        scm {\n            url = \"https://github.com/open-tool/ultron\"\n            connection = \"scm:git@github.com:open-tool/ultron.git\"\n            developerConnection = \"scm:git@github.com:open-tool/ultron.git\"\n        }\n    }\n}"
  },
  {
    "path": "ultron-allure/gradle.properties",
    "content": "GROUP=com.atiurin\nPOM_ARTIFACT_ID=ultron-allure\n\nPOM_NAME=ultron-allure\nPOM_PACKAGING=aar\n\nPOM_DESCRIPTION=Ultron support of Allure report\nPOM_INCEPTION_YEAR=2024\n"
  },
  {
    "path": "ultron-allure/src/main/java/com/atiurin/ultron/allure/UltronAllureTestRunner.kt",
    "content": "package com.atiurin.ultron.allure\n\nimport android.app.Instrumentation\nimport android.os.Bundle\nimport com.atiurin.ultron.allure.runner.UltronAllureRunInformer\nimport com.atiurin.ultron.allure.runner.UltronTestRunListener\nimport com.atiurin.ultron.extensions.putArguments\nimport com.atiurin.ultron.runner.UltronRunInformer\nimport io.qameta.allure.android.runners.AllureAndroidJUnitRunner\n\nopen class UltronAllureTestRunner : AllureAndroidJUnitRunner() {\n    val informer: UltronRunInformer = UltronAllureRunInformer()\n\n    override fun onCreate(arguments: Bundle) {\n        arguments.putArguments(\"listener\", UltronTestRunListener::class.qualifiedName!!)\n        super.onCreate(arguments)\n    }\n}\n\nfun Instrumentation.getRunInformer() : UltronRunInformer {\n    return requireNotNull((this as? UltronAllureTestRunner)?.informer) {\n        \"Set testInstrumentationRunner = ${UltronAllureTestRunner::class.qualifiedName}\"\n    }\n}"
  },
  {
    "path": "ultron-allure/src/main/java/com/atiurin/ultron/allure/attachment/AllureDirectoryUtil.kt",
    "content": "package com.atiurin.ultron.allure.attachment\n\nimport androidx.test.platform.app.InstrumentationRegistry\nimport io.qameta.allure.kotlin.util.PropertiesUtils\nimport java.io.File\n\nobject AllureDirectoryUtil {\n\n    fun getResultsDirectoryName(): String = PropertiesUtils.resultsDirectoryPath\n\n    /**\n     * From Allure source code\n     * see [https://github.com/allure-framework/allure-kotlin/blob/master/allure-kotlin-android/src/main/kotlin/io/qameta/allure/android/AllureAndroidLifecycle.kt]\n     */\n    fun getOriginalResultsDirectory(): File {\n        return File(InstrumentationRegistry.getInstrumentation().targetContext.filesDir, getResultsDirectoryName())\n    }\n}"
  },
  {
    "path": "ultron-allure/src/main/java/com/atiurin/ultron/allure/attachment/AttachUtil.kt",
    "content": "package com.atiurin.ultron.allure.attachment\n\nimport com.atiurin.ultron.file.MimeType\nimport io.qameta.allure.kotlin.Allure\nimport io.qameta.allure.kotlin.AllureLifecycle\nimport java.io.File\nimport java.io.InputStream\n\nobject AttachUtil {\n    /**\n     * @return allure file name\n     */\n    fun attachFile(file: File, mimeType: MimeType) = attachFile(\n        name = file.name,\n        file = file,\n        mimeType = mimeType\n    )\n\n    /**\n     * @return allure file name\n     */\n    fun attachFile(name: String, file: File, mimeType: MimeType) = Allure.lifecycle.writeFile(\n        name = name,\n        stream = file.inputStream(),\n        type = mimeType.value,\n        fileExtension = mimeType.extension\n    )\n}\n\nfun AllureLifecycle.writeFile(name: String, stream: InputStream, type: String?, fileExtension: String?): String {\n    val source = prepareAttachment(\n        name = name,\n        type = type,\n        fileExtension = fileExtension\n    )\n    writeAttachment(\n        attachmentSource = source,\n        stream = stream\n    )\n    return source\n}"
  },
  {
    "path": "ultron-allure/src/main/java/com/atiurin/ultron/allure/condition/AllureConditionExecutorWrapper.kt",
    "content": "package com.atiurin.ultron.allure.condition\n\nimport com.atiurin.ultron.allure.step.step\nimport com.atiurin.ultron.testlifecycle.setupteardown.Condition\nimport com.atiurin.ultron.testlifecycle.setupteardown.ConditionExecutorWrapper\n\nclass AllureConditionExecutorWrapper : ConditionExecutorWrapper {\n    override fun execute(condition: Condition) {\n        val stepName = condition.name.ifEmpty {\n            \"${condition.counter} - ${condition.key}\"\n        }\n        step(stepName) { condition.actions() }\n    }\n}"
  },
  {
    "path": "ultron-allure/src/main/java/com/atiurin/ultron/allure/condition/AllureConditionsExecutor.kt",
    "content": "package com.atiurin.ultron.allure.condition\n\nimport com.atiurin.ultron.allure.step.step\nimport com.atiurin.ultron.testlifecycle.setupteardown.Condition\nimport com.atiurin.ultron.testlifecycle.setupteardown.DefaultConditionsExecutor\n\nclass AllureConditionsExecutor : DefaultConditionsExecutor() {\n    override fun execute(conditions: List<Condition>, keys: List<String>, description: String) {\n        val stepName = description.ifEmpty { \"Conditions\" }\n        step(stepName) { super.execute(conditions, keys, description) }\n    }\n}"
  },
  {
    "path": "ultron-allure/src/main/java/com/atiurin/ultron/allure/config/AllureAttachStrategy.kt",
    "content": "package com.atiurin.ultron.allure.config\n\nenum class AllureAttachStrategy {\n    TEST_FAILURE,\n    OPERATION_FAILURE, // attach artifact for failed operation\n    OPERATION_SUCCESS, // attach artifact for each succeeded operation\n    OPERATION_FINISH, // attach artifact for each operation\n    NONE\n}"
  },
  {
    "path": "ultron-allure/src/main/java/com/atiurin/ultron/allure/config/AllureConfigParams.kt",
    "content": "package com.atiurin.ultron.allure.config\n\nimport androidx.test.platform.app.InstrumentationRegistry\nimport com.atiurin.ultron.allure.attachment.AllureDirectoryUtil\nimport com.atiurin.ultron.allure.attachment.AttachUtil\nimport java.io.File\n\ndata class AllureConfigParams(\n    var addScreenshotPolicy: MutableSet<AllureAttachStrategy> = mutableSetOf(\n        AllureAttachStrategy.TEST_FAILURE,\n        AllureAttachStrategy.OPERATION_FAILURE\n    ),\n    var addHierarchyPolicy: MutableSet<AllureAttachStrategy> = mutableSetOf(\n        AllureAttachStrategy.TEST_FAILURE,\n        AllureAttachStrategy.OPERATION_FAILURE\n    ),\n    var attachUltronLog: Boolean = true,\n    var attachLogcat: Boolean = true,\n    var addConditionsToReport: Boolean = true,\n    var detailedAllureReport: Boolean = true\n)"
  },
  {
    "path": "ultron-allure/src/main/java/com/atiurin/ultron/allure/config/UltronAllureConfig.kt",
    "content": "package com.atiurin.ultron.allure.config\n\nimport android.os.Environment\nimport androidx.test.platform.app.InstrumentationRegistry\nimport com.atiurin.ultron.allure.attachment.AllureDirectoryUtil\nimport com.atiurin.ultron.allure.condition.AllureConditionExecutorWrapper\nimport com.atiurin.ultron.allure.condition.AllureConditionsExecutor\nimport com.atiurin.ultron.allure.getRunInformer\nimport com.atiurin.ultron.allure.listeners.DetailedOperationAllureListener\nimport com.atiurin.ultron.allure.listeners.ScreenshotAttachListener\nimport com.atiurin.ultron.allure.listeners.WindowHierarchyAttachListener\nimport com.atiurin.ultron.allure.runner.LogcatAttachRunListener\nimport com.atiurin.ultron.allure.runner.ScreenshotAttachRunListener\nimport com.atiurin.ultron.allure.runner.UltronAllureResultsTransferListener\nimport com.atiurin.ultron.allure.runner.UltronLogAttachRunListener\nimport com.atiurin.ultron.allure.runner.UltronLogCleanerRunListener\nimport com.atiurin.ultron.allure.runner.WindowHierarchyAttachRunListener\nimport com.atiurin.ultron.core.config.UltronAndroidCommonConfig\nimport com.atiurin.ultron.core.config.UltronCommonConfig\nimport com.atiurin.ultron.extensions.simpleClassName\nimport com.atiurin.ultron.listeners.AbstractListener\nimport com.atiurin.ultron.log.UltronLog\nimport com.atiurin.ultron.runner.UltronRunListener\nimport java.io.File\n\nobject UltronAllureConfig {\n    var params: AllureConfigParams = AllureConfigParams()\n\n    fun setAllureConditionExecutor() {\n        UltronAndroidCommonConfig.Conditions.conditionsExecutor = AllureConditionsExecutor()\n    }\n\n    fun setAllureConditionsExecutorWrapper() {\n        UltronAndroidCommonConfig.Conditions.conditionExecutorWrapper = AllureConditionExecutorWrapper()\n    }\n\n    /**\n     * Sets the directory where Allure results will be stored.\n     * Original results artifacts are saved in [AllureDirectoryUtil.getOriginalResultsDirectory]\n     *\n     * The results will be copied from [AllureDirectoryUtil.getOriginalResultsDirectory]\n     * to [targetDirectory] once test run is finished.\n     *\n     * @param targetDirectory The directory where the Allure results will be stored.\n     *\n     * @see AllureDirectoryUtil.getOriginalResultsDirectory\n     */\n    fun setAllureResultsDirectory(targetDirectory: File) {\n        removeRunListener(UltronLogCleanerRunListener::class.java)\n        addRunListener(\n            UltronAllureResultsTransferListener(\n                sourceDir = AllureDirectoryUtil.getOriginalResultsDirectory(),\n                targetDir = targetDirectory\n            )\n        )\n        addRunListener(UltronLogCleanerRunListener())\n    }\n\n    /**\n     * Sets the directory where Allure results will be stored with default public directory.\n     *\n     * @param publicDirectory The public directory where the Allure results will be stored.\n     *                        Default is [Environment.DIRECTORY_DOWNLOADS].\n     *\n     * So, the final path by default will be like '/sdcard/Download/allure-result'\n     */\n    fun setAllureResultsDirectory(publicDirectory: String = Environment.DIRECTORY_DOWNLOADS) {\n        val targetDir = Environment.getExternalStoragePublicDirectory(publicDirectory).resolve(AllureDirectoryUtil.getResultsDirectoryName())\n        setAllureResultsDirectory(targetDir)\n    }\n\n    private fun modify() {\n        if (params.detailedAllureReport) {\n            UltronCommonConfig.addListener(DetailedOperationAllureListener())\n        }\n        if (!params.addScreenshotPolicy.contains(AllureAttachStrategy.NONE)) {\n            UltronCommonConfig.addListener(ScreenshotAttachListener(params.addScreenshotPolicy))\n            addRunListener(ScreenshotAttachRunListener(params.addScreenshotPolicy))\n        }\n        if (!params.addHierarchyPolicy.contains(AllureAttachStrategy.NONE)) {\n            UltronCommonConfig.addListener(WindowHierarchyAttachListener(params.addHierarchyPolicy))\n            addRunListener(WindowHierarchyAttachRunListener(params.addHierarchyPolicy))\n        }\n        if (params.addConditionsToReport) {\n            setAllureConditionsExecutorWrapper()\n            setAllureConditionExecutor()\n        }\n        if (!params.attachUltronLog) {\n            removeRunListener(UltronLogAttachRunListener::class.java)\n        }\n        if (!params.attachLogcat) {\n            removeRunListener(LogcatAttachRunListener::class.java)\n        }\n        UltronLog.info(\"UltronAllureConfig applied with params $params\")\n    }\n\n    fun applyRecommended() {\n        params = AllureConfigParams()\n        modify()\n    }\n\n    fun apply(block: AllureConfigParams.() -> Unit) {\n        params.block()\n        modify()\n    }\n\n    fun addRunListener(listener: UltronRunListener) {\n        UltronLog.info(\"Add ${listener::class.simpleName} run listener\")\n        InstrumentationRegistry.getInstrumentation().getRunInformer().addListener(listener)\n    }\n\n    fun <T : AbstractListener> removeRunListener(listenerClass: Class<T>) {\n        UltronLog.info(\"Remove ${listenerClass.simpleClassName()} run listener\")\n        InstrumentationRegistry.getInstrumentation().getRunInformer().removeListener(listenerClass.kotlin)\n    }\n}\n"
  },
  {
    "path": "ultron-allure/src/main/java/com/atiurin/ultron/allure/hierarchy/AllureHierarchyDumper.kt",
    "content": "package com.atiurin.ultron.allure.hierarchy\n\nimport com.atiurin.ultron.allure.attachment.AttachUtil\nimport com.atiurin.ultron.hierarchy.HierarchyDumper\nimport com.atiurin.ultron.hierarchy.UiDeviceHierarchyDumper\nimport com.atiurin.ultron.log.UltronLog\nimport com.atiurin.ultron.utils.createCacheFile\n\nclass AllureHierarchyDumper {\n    private val dumper: HierarchyDumper = UiDeviceHierarchyDumper()\n\n    fun dumpAndAttach(name: String = \"window_hierarchy\"): Boolean {\n        val tempFile = createCacheFile()\n        val result = dumper.dumpFullWindowHierarchy(tempFile)\n        val fileName = AttachUtil.attachFile(\n            name = \"$name${result.mimeType.extension}\",\n            file = tempFile,\n            mimeType = result.mimeType\n        )\n        UltronLog.info(\"WindowHierarchy file '$fileName' has attached to Allure report\")\n        return result.isSuccess\n    }\n}"
  },
  {
    "path": "ultron-allure/src/main/java/com/atiurin/ultron/allure/listeners/DetailedOperationAllureListener.kt",
    "content": "package com.atiurin.ultron.allure.listeners\n\nimport com.atiurin.ultron.core.common.Operation\nimport com.atiurin.ultron.core.common.OperationResult\nimport com.atiurin.ultron.exceptions.UltronException\nimport com.atiurin.ultron.listeners.UltronLifecycleListener\nimport io.qameta.allure.kotlin.Allure\nimport io.qameta.allure.kotlin.model.Status\nimport io.qameta.allure.kotlin.model.StepResult\nimport io.qameta.allure.kotlin.util.ResultsUtils\nimport java.util.UUID\n\nclass DetailedOperationAllureListener : UltronLifecycleListener() {\n    private val stepsMap = mutableMapOf<Operation, String>()\n\n    override fun after(operationResult: OperationResult<Operation>) {\n        Allure.lifecycle.stopStep(stepsMap.getUuid(operationResult.operation))\n        stepsMap.remove(operationResult.operation)\n    }\n\n    override fun afterFailure(operationResult: OperationResult<Operation>) {\n        val exception = operationResult.operationIterationResult?.exception\n        Allure.lifecycle.updateStep {\n            with(it) {\n                status = ResultsUtils.getStatus(exception) ?: Status.BROKEN\n                statusDetails = ResultsUtils.getStatusDetails(exception)\n            }\n        }\n    }\n\n    override fun afterSuccess(operationResult: OperationResult<Operation>) {\n        Allure.lifecycle.updateStep(stepsMap.getUuid(operationResult.operation)) {\n            it.status = Status.PASSED\n        }\n    }\n\n    override fun before(operation: Operation) {\n        val uuid = UUID.randomUUID().toString()\n        stepsMap[operation] = uuid\n        val stepName = operation.name\n        Allure.lifecycle.startStep(uuid, StepResult().apply {\n            this.name = stepName\n        })\n    }\n\n    private fun Map<Operation, String>.getUuid(operation: Operation): String {\n        return this[operation] ?: throw UltronException(\"No Allure UUID defined for operation ${operation.name}\")\n    }\n}"
  },
  {
    "path": "ultron-allure/src/main/java/com/atiurin/ultron/allure/listeners/ScreenshotAttachListener.kt",
    "content": "package com.atiurin.ultron.allure.listeners\n\nimport com.atiurin.ultron.allure.config.AllureAttachStrategy\nimport com.atiurin.ultron.allure.screenshot.AllureScreenshot\nimport com.atiurin.ultron.core.common.Operation\nimport com.atiurin.ultron.core.common.OperationResult\nimport com.atiurin.ultron.listeners.UltronLifecycleListener\n\nclass ScreenshotAttachListener(val policies: Set<AllureAttachStrategy> = setOf(AllureAttachStrategy.OPERATION_FAILURE)) : UltronLifecycleListener() {\n    val screenshot = AllureScreenshot()\n\n    override fun afterFailure(operationResult: OperationResult<Operation>) {\n        if (policies.contains(AllureAttachStrategy.OPERATION_FAILURE)) {\n            screenshot.takeAndAttach(\"$prefix${operationResult.operation.name}\")\n        }\n    }\n\n    override fun afterSuccess(operationResult: OperationResult<Operation>) {\n        if (policies.contains(AllureAttachStrategy.OPERATION_SUCCESS)) {\n            screenshot.takeAndAttach(\"$prefix${operationResult.operation.name}\")\n        }\n    }\n\n    override fun after(operationResult: OperationResult<Operation>) {\n        if (policies.contains(AllureAttachStrategy.OPERATION_FINISH)) {\n            screenshot.takeAndAttach(\"$prefix${operationResult.operation.name}\")\n        }\n    }\n    companion object{\n        private const val prefix = \"screenshot_\"\n    }\n}"
  },
  {
    "path": "ultron-allure/src/main/java/com/atiurin/ultron/allure/listeners/WindowHierarchyAttachListener.kt",
    "content": "package com.atiurin.ultron.allure.listeners\n\nimport com.atiurin.ultron.allure.config.AllureAttachStrategy\nimport com.atiurin.ultron.allure.hierarchy.AllureHierarchyDumper\nimport com.atiurin.ultron.core.common.Operation\nimport com.atiurin.ultron.core.common.OperationResult\nimport com.atiurin.ultron.listeners.UltronLifecycleListener\n\nclass WindowHierarchyAttachListener(val policies: Set<AllureAttachStrategy> = setOf(AllureAttachStrategy.OPERATION_FAILURE)) : UltronLifecycleListener() {\n    val dumper = AllureHierarchyDumper()\n\n    override fun afterFailure(operationResult: OperationResult<Operation>) {\n        if (policies.contains(AllureAttachStrategy.OPERATION_FAILURE)) {\n            dumper.dumpAndAttach(\"$prefix${operationResult.operation.name}\")\n        }\n    }\n\n    override fun afterSuccess(operationResult: OperationResult<Operation>) {\n        if (policies.contains(AllureAttachStrategy.OPERATION_SUCCESS)) {\n            dumper.dumpAndAttach(\"$prefix${operationResult.operation.name}\")\n        }\n    }\n\n    override fun after(operationResult: OperationResult<Operation>) {\n        if (policies.contains(AllureAttachStrategy.OPERATION_FINISH)) {\n            dumper.dumpAndAttach(\"$prefix${operationResult.operation.name}\")\n        }\n    }\n\n    companion object{\n        private const val prefix = \"hierarchy_\"\n    }\n}"
  },
  {
    "path": "ultron-allure/src/main/java/com/atiurin/ultron/allure/runner/LogcatAttachRunListener.kt",
    "content": "package com.atiurin.ultron.allure.runner\n\nimport com.atiurin.ultron.allure.attachment.AttachUtil\nimport com.atiurin.ultron.file.MimeType\nimport com.atiurin.ultron.log.UltronLog\nimport com.atiurin.ultron.runner.UltronRunListener\nimport com.atiurin.ultron.utils.createCacheFile\nimport org.junit.runner.Description\nimport org.junit.runner.notification.Failure\nimport java.text.SimpleDateFormat\nimport java.util.*\n\nclass LogcatAttachRunListener : UltronRunListener() {\n    private val sdf = SimpleDateFormat(\"MM-dd HH:mm:ss.SSS\", Locale.getDefault())\n    private lateinit var startTime: String\n\n    override fun testStarted(description: Description) {\n        startTime = sdf.format(Date())\n    }\n\n    override fun testFailure(failure: Failure) {\n        val file = createCacheFile(\"logcat_\", \".log\")\n        val command = prepareCommand(dumpTime = startTime)\n        val process = Runtime.getRuntime().exec(arrayOf(\"sh\", \"-c\", command))\n        runCatching {\n            file.outputStream().use { process.inputStream.copyTo(it) }\n        }.onSuccess {\n            UltronLog.info(\"Dump logcat to file\")\n            val fileName = AttachUtil.attachFile(file, MimeType.PLAIN_TEXT)\n            UltronLog.info(\"LOGCAT file '$fileName' has attached to Allure report\")\n        }.onFailure {\n            UltronLog.error(\"Failed to dump logcat ${it.message}\")\n        }\n        process.destroy()\n    }\n\n    private fun prepareCommand(\n        dumpTime: String,\n        buffer: Buffer = Buffer.Default,\n    ): String {\n        return \"logcat -b ${buffer.value} -d -t \\\"$dumpTime\\\"\"\n    }\n\n    /**\n     * Logcat buffers\n     * https://developer.android.com/studio/command-line/logcat#alternativeBuffers\n     */\n    enum class Buffer(val value: String) {\n        Radio(\"radio\"),     // View the buffer that contains radio/telephony related messages.\n        Events(\"events\"),   // View the interpreted binary system event buffer messages.\n        Main(\"main\"),       // View the main log buffer (default) does not contain system and crash log messages.\n        System(\"system\"),   // View the system log buffer (default).\n        Crash(\"crash\"),     // View the crash log buffer (default).\n        All(\"all\"),         // View all buffers.\n        Default(\"default\")  // Reports main, system, and crash buffers.\n    }\n}"
  },
  {
    "path": "ultron-allure/src/main/java/com/atiurin/ultron/allure/runner/ScreenshotAttachRunListener.kt",
    "content": "package com.atiurin.ultron.allure.runner\n\nimport com.atiurin.ultron.allure.config.AllureAttachStrategy\nimport com.atiurin.ultron.allure.screenshot.AllureScreenshot\nimport com.atiurin.ultron.extensions.fullTestName\nimport com.atiurin.ultron.runner.UltronRunListener\nimport org.junit.runner.notification.Failure\n\nclass ScreenshotAttachRunListener(val policies: Set<AllureAttachStrategy>) : UltronRunListener() {\n\n    val screenshot = AllureScreenshot()\n\n    override fun testFailure(failure: Failure) {\n        if (policies.contains(AllureAttachStrategy.TEST_FAILURE)){\n            screenshot.takeAndAttach(\"$prefix${failure.description.fullTestName()}\")\n        }\n    }\n    companion object{\n        private const val prefix = \"screenshot_\"\n    }\n}"
  },
  {
    "path": "ultron-allure/src/main/java/com/atiurin/ultron/allure/runner/UltronAllureResultsTransferListener.kt",
    "content": "package com.atiurin.ultron.allure.runner\n\nimport com.atiurin.ultron.extensions.createDirectoryIfNotExists\nimport com.atiurin.ultron.log.UltronLog\nimport com.atiurin.ultron.runner.UltronRunListener\nimport org.junit.runner.Description\nimport org.junit.runner.Result\nimport java.io.File\nimport kotlin.system.measureTimeMillis\n\n/**\n * Transfer files from original Allure results directory [sourceDir]\n * to custom directory [targetDir] provided by user [UltronAllureConfig.setAllureResultsDirectory]\n */\nclass UltronAllureResultsTransferListener(private val sourceDir: File, private val targetDir: File) : UltronRunListener() {\n    override fun testFinished(description: Description) {\n        transferFiles()\n    }\n\n    override fun testRunFinished(result: Result) {\n        transferFiles()\n    }\n\n    private fun transferFiles(){\n        UltronLog.info(\"Copy Allure results from '${sourceDir.absolutePath}' to '${targetDir.absolutePath}'\")\n        targetDir.createDirectoryIfNotExists()\n        var isSuccessfullyCopied = true\n        val time = measureTimeMillis {\n            sourceDir.copyRecursively(targetDir, true, onError = { file, ioException ->\n                UltronLog.error(\"\"\"\n                |Unable to copy Allure results file '${file.absolutePath}' to '${targetDir.absolutePath}'.\n                |Got exception : '${ioException.message}'.\n                |Source directory '${sourceDir.absolutePath}' won't be deleted.\n                \"\"\".trimMargin()\n                )\n                isSuccessfullyCopied = false\n                OnErrorAction.SKIP\n            })\n            if (isSuccessfullyCopied) sourceDir.deleteRecursively()\n        }\n        UltronLog.info(\"Allure results files transfer time = $time ms\")\n    }\n}\n\n"
  },
  {
    "path": "ultron-allure/src/main/java/com/atiurin/ultron/allure/runner/UltronAllureRunInformer.kt",
    "content": "package com.atiurin.ultron.allure.runner\n\nimport com.atiurin.ultron.runner.*\n\nclass UltronAllureRunInformer : UltronRunInformer() {\n    init {\n        addListener(UltronLogRunListener())\n        addListener(LogcatAttachRunListener())\n        addListener(UltronLogAttachRunListener())\n        addListener(UltronLogCleanerRunListener())\n    }\n}"
  },
  {
    "path": "ultron-allure/src/main/java/com/atiurin/ultron/allure/runner/UltronLogAttachRunListener.kt",
    "content": "package com.atiurin.ultron.allure.runner\n\nimport com.atiurin.ultron.allure.attachment.AttachUtil\nimport com.atiurin.ultron.allure.config.UltronAllureConfig\nimport com.atiurin.ultron.core.config.UltronCommonConfig\nimport com.atiurin.ultron.file.MimeType\nimport com.atiurin.ultron.log.UltronLog\nimport com.atiurin.ultron.runner.UltronRunListener\nimport org.junit.runner.notification.Failure\nimport java.io.File\n\nclass UltronLogAttachRunListener : UltronRunListener() {\n    override fun testFailure(failure: Failure) {\n        if (UltronAllureConfig.params.attachUltronLog ){\n            if (!UltronCommonConfig.logToFile){\n                UltronLog.error(\"Ultron doesn't log into file. \" +\n                        \"Change config param UltronConfig.edit { logToFile = true }\"\n                )\n                return\n            }\n            val fileName = AttachUtil.attachFile(\n                file = File(UltronLog.fileLogger.getLogFilePath()),\n                mimeType = MimeType.PLAIN_TEXT\n            )\n            UltronLog.info(\"Ultron log file '$fileName' has attached to Allure report\")\n        }\n    }\n}"
  },
  {
    "path": "ultron-allure/src/main/java/com/atiurin/ultron/allure/runner/UltronLogCleanerRunListener.kt",
    "content": "package com.atiurin.ultron.allure.runner\n\nimport com.atiurin.ultron.core.config.UltronCommonConfig\nimport com.atiurin.ultron.log.UltronLog\nimport com.atiurin.ultron.runner.UltronRunListener\nimport org.junit.runner.Description\n\nclass UltronLogCleanerRunListener : UltronRunListener() {\n    override fun testFinished(description: Description) {\n        if (UltronCommonConfig.logToFile){\n            UltronLog.info(\"Clear log file\")\n            UltronLog.fileLogger.clearFile()\n        }\n    }\n}"
  },
  {
    "path": "ultron-allure/src/main/java/com/atiurin/ultron/allure/runner/UltronTestRunListener.kt",
    "content": "package com.atiurin.ultron.allure.runner\n\nimport androidx.test.platform.app.InstrumentationRegistry\nimport com.atiurin.ultron.allure.getRunInformer\nimport org.junit.runner.Description\nimport org.junit.runner.Result\nimport org.junit.runner.notification.Failure\nimport org.junit.runner.notification.RunListener\n\nclass UltronTestRunListener : RunListener() {\n    private val informer = InstrumentationRegistry.getInstrumentation().getRunInformer()\n\n    override fun testRunStarted(description: Description) = informer.testRunStarted(description)\n    override fun testStarted(description: Description) = informer.testStarted(description)\n    override fun testFinished(description: Description) = informer.testFinished(description)\n    override fun testFailure(failure: Failure) = informer.testFailure(failure)\n    override fun testAssumptionFailure(failure: Failure) = informer.testAssumptionFailure(failure)\n    override fun testIgnored(description: Description) = informer.testIgnored(description)\n    override fun testRunFinished(result: Result) = informer.testRunFinished(result)\n}\n\n"
  },
  {
    "path": "ultron-allure/src/main/java/com/atiurin/ultron/allure/runner/WindowHierarchyAttachRunListener.kt",
    "content": "package com.atiurin.ultron.allure.runner\n\nimport com.atiurin.ultron.allure.config.AllureAttachStrategy\nimport com.atiurin.ultron.allure.hierarchy.AllureHierarchyDumper\nimport com.atiurin.ultron.extensions.fullTestName\nimport com.atiurin.ultron.runner.UltronRunListener\nimport org.junit.runner.notification.Failure\n\nclass WindowHierarchyAttachRunListener(val policies: Set<AllureAttachStrategy>) : UltronRunListener() {\n    val dumper = AllureHierarchyDumper()\n\n    override fun testFailure(failure: Failure) {\n        if (policies.contains(AllureAttachStrategy.TEST_FAILURE)){\n            dumper.dumpAndAttach(\"$prefix${failure.description.fullTestName()}\")\n        }\n    }\n    companion object{\n        private const val prefix = \"hierarchy_\"\n    }\n}"
  },
  {
    "path": "ultron-allure/src/main/java/com/atiurin/ultron/allure/screenshot/AllureScreenshot.kt",
    "content": "package com.atiurin.ultron.allure.screenshot\n\nimport com.atiurin.ultron.allure.attachment.AttachUtil\nimport com.atiurin.ultron.log.UltronLog\nimport com.atiurin.ultron.screenshot.Screenshoter\nimport com.atiurin.ultron.screenshot.UiAutomationScreenshoter\nimport com.atiurin.ultron.screenshot.ViewScreenshoter\nimport com.atiurin.ultron.utils.createCacheFile\n\nclass AllureScreenshot(private val quality: Int = 90) {\n    private val mainScreenshoter: Screenshoter = UiAutomationScreenshoter(quality)\n    private val backupScreenshoter: Screenshoter = ViewScreenshoter(quality)\n\n    fun takeAndAttach(name: String = \"screenshot\"): Boolean {\n        val tempFile = createCacheFile()\n        var result = mainScreenshoter.takeFullScreenShot(tempFile)\n        if (!result.isSuccess) {\n            result = backupScreenshoter.takeFullScreenShot(tempFile)\n            if (!result.isSuccess) UltronLog.error(\"Failed to take the screenshot \")\n        }\n        val fileName = AttachUtil.attachFile(\n            name = \"$name${result.mimeType.extension}\",\n            file = tempFile,\n            mimeType = result.mimeType\n        )\n        UltronLog.info(\"SCREENSHOT file '$fileName' has attached to Allure report\")\n        return result.isSuccess\n    }\n}\n\n"
  },
  {
    "path": "ultron-allure/src/main/java/com/atiurin/ultron/allure/step/UltronStep.kt",
    "content": "package com.atiurin.ultron.allure.step\n\nimport com.atiurin.ultron.log.UltronLogUtil.logTextBlock\nimport com.atiurin.ultron.log.UltronLogUtil.stepDelimiter\nimport io.qameta.allure.kotlin.Allure\n\ninline fun <T> step (description: String, crossinline block: () -> T): T {\n    logTextBlock(\"Begin STEP '$description'\", delimiter = stepDelimiter)\n    val result = Allure.step(description) {\n        block()\n    }\n    logTextBlock(\"End STEP '$description'\", delimiter = stepDelimiter)\n    return result\n}"
  },
  {
    "path": "ultron-allure/src/test/java/com/atiurin/ultron/allure/ExampleUnitTest.kt",
    "content": "package com.atiurin.ultron.allure\n\nimport org.junit.Test\n\nimport org.junit.Assert.*\n\n/**\n * Example local unit test, which will execute on the development machine (host).\n *\n * See [testing documentation](http://d.android.com/tools/testing).\n */\nclass ExampleUnitTest {\n    @Test\n    fun addition_isCorrect() {\n        assertEquals(4, 2 + 2)\n    }\n}"
  },
  {
    "path": "ultron-android/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "ultron-android/build.gradle.kts",
    "content": "import com.vanniktech.maven.publish.SonatypeHost\n\nplugins {\n    alias(libs.plugins.androidLibrary)\n    id(\"kotlin-android\")\n    id(\"org.jetbrains.dokka\")\n    alias(libs.plugins.vanniktech.mavenPublish)\n}\n\ngroup = project.findProperty(\"GROUP\")!!\nversion = project.findProperty(\"VERSION_NAME\")!!\n\nkotlin {\n    jvmToolchain(11)\n}\n\nandroid {\n    namespace = \"com.atiurin.ultron.android\"\n    compileSdk = 35\n\n    defaultConfig {\n        minSdk = 21\n        multiDexEnabled = true\n    }\n\n    compileOptions {\n        sourceCompatibility = JavaVersion.VERSION_11\n        targetCompatibility = JavaVersion.VERSION_11\n    }\n\n    publishing {\n        // Publishing configuration is handled by mavenPublishing block\n    }\n}\n\ndependencies {\n    api(project(\":ultron-common\"))\n    implementation(Libs.kotlinReflect)\n    implementation(Libs.kotlinStdlib)\n    implementation(Libs.recyclerView)\n    api(Libs.espressoCore)\n    api(Libs.espressoContrib)\n    api(Libs.espressoWeb)\n    api(Libs.accessibility)\n    api(Libs.hamcrestCore)\n}\n\nval dokkaOutputDir = buildDir.resolve(\"dokka\")\n\ntasks.dokkaHtml.configure { outputDirectory.set(file(dokkaOutputDir)) }\n\nval deleteDokkaOutputDir by tasks.register<Delete>(\"deleteDokkaOutputDirectory\") {\n    delete(dokkaOutputDir)\n}\n\nval javadocJar = tasks.register<Jar>(\"javadocJar\") {\n    archiveClassifier.set(\"javadoc\")\n    duplicatesStrategy = DuplicatesStrategy.EXCLUDE\n    dependsOn(deleteDokkaOutputDir, tasks.dokkaHtml)\n    from(dokkaOutputDir)\n}\nmavenPublishing {\n    publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)\n    signAllPublications()\n    coordinates(artifactId = \"ultron-android\")\n\n    pom {\n        name = \"Ultron Common\"\n        description = \"Android & Compose Multiplatform UI testing framework\"\n        url = \"https://github.com/open-tool/ultron\"\n        inceptionYear = \"2021\"\n\n        licenses {\n            license {\n                name.set(\"The Apache License, Version 2.0\")\n                url.set(\"http://www.apache.org/licenses/LICENSE-2.0.txt\")\n            }\n        }\n\n        issueManagement {\n            system = \"GitHub Issues\"\n            url = \"https://github.com/open-tool/ultron/issues\"\n        }\n\n        developers {\n            developer {\n                id = \"alex-tiurin\"\n                name = \"Aleksei Tiurin\"\n                url = \"https://github.com/open-tool\"\n            }\n        }\n\n        scm {\n            url = \"https://github.com/open-tool/ultron\"\n            connection = \"scm:git@github.com:open-tool/ultron.git\"\n            developerConnection = \"scm:git@github.com:open-tool/ultron.git\"\n        }\n    }\n}"
  },
  {
    "path": "ultron-android/gradle.properties",
    "content": "GROUP=com.atiurin\nPOM_ARTIFACT_ID=ultron-android\n\nPOM_NAME=ultron-android\nPOM_PACKAGING=aar\n\nPOM_DESCRIPTION=Ultron support of Espresso and UIAutomator for Android\nPOM_INCEPTION_YEAR=2024\n"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/config/UltronConfig.kt",
    "content": "package com.atiurin.ultron.core.config\n\nimport android.annotation.SuppressLint\nimport android.view.View\nimport android.webkit.WebView\nimport androidx.test.espresso.AmbiguousViewMatcherException\nimport androidx.test.espresso.DaggerBaseLayerComponent\nimport androidx.test.espresso.NoMatchingViewException\nimport androidx.test.espresso.PerformException\nimport androidx.test.espresso.UiController\nimport androidx.test.espresso.base.ActiveRootLister\nimport androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom\nimport androidx.test.espresso.matcher.ViewMatchers.isDisplayed\nimport androidx.test.internal.platform.os.ControlledLooper\nimport androidx.test.platform.app.InstrumentationRegistry\nimport androidx.test.uiautomator.Configurator\nimport androidx.test.uiautomator.UiDevice\nimport androidx.test.uiautomator.UiObjectNotFoundException\nimport com.atiurin.ultron.core.common.Operation\nimport com.atiurin.ultron.core.common.OperationResult\nimport com.atiurin.ultron.core.common.resultanalyzer.OperationResultAnalyzer\nimport com.atiurin.ultron.core.espresso.EspressoOperationResult\nimport com.atiurin.ultron.core.espresso.UltronEspressoOperation\nimport com.atiurin.ultron.core.espresso.assertion.EspressoAssertionType\nimport com.atiurin.ultron.core.espresso.recyclerview.UltronRecyclerViewImpl\nimport com.atiurin.ultron.core.espressoweb.operation.WebInteractionOperation\nimport com.atiurin.ultron.core.espressoweb.operation.WebOperationResult\nimport com.atiurin.ultron.core.uiautomator.UiAutomatorOperation\nimport com.atiurin.ultron.core.uiautomator.UiAutomatorOperationResult\nimport com.atiurin.ultron.core.uiautomator.uiobject.UiAutomatorUiSelectorOperation\nimport com.atiurin.ultron.exceptions.UltronAssertionException\nimport com.atiurin.ultron.exceptions.UltronException\nimport com.atiurin.ultron.exceptions.UltronOperationException\nimport com.atiurin.ultron.exceptions.UltronUiAutomatorException\nimport com.atiurin.ultron.exceptions.UltronWrapperException\nimport com.atiurin.ultron.extensions.simpleClassName\nimport com.atiurin.ultron.listeners.LogLifecycleListener\nimport com.atiurin.ultron.listeners.UltronLifecycleListener\nimport com.atiurin.ultron.log.UltronLog\nimport com.atiurin.ultron.log.UltronLogcatLogger\nimport com.atiurin.ultron.testlifecycle.setupteardown.ConditionExecutorWrapper\nimport com.atiurin.ultron.testlifecycle.setupteardown.ConditionsExecutor\nimport junit.framework.AssertionFailedError\nimport org.hamcrest.CoreMatchers.allOf\nimport org.hamcrest.Matcher\nimport kotlin.reflect.KClass\n\nobject UltronConfig {\n    init {\n        UltronCommonConfig.operationsExcludedFromListeners.apply {\n            add(EspressoAssertionType.IDENTIFY_RECYCLER_VIEW)\n        }\n    }\n\n    @Deprecated(\n        message = \"Default moved to UltronCommonConfig.Defaults\",\n        replaceWith = ReplaceWith(expression = \"UltronCommonConfig.Defaults.OPERATION_TIMEOUT_MS\")\n    )\n    val DEFAULT_OPERATION_TIMEOUT_MS = UltronCommonConfig.Defaults.OPERATION_TIMEOUT_MS\n\n    var params: UltronConfigParams = UltronConfigParams()\n\n    fun applyRecommended() {\n        params = UltronConfigParams()\n        modify()\n    }\n\n    fun apply(block: UltronConfigParams.() -> Unit) {\n        params.block()\n        modify()\n    }\n\n    private fun modify() {\n        Espresso.ACTION_TIMEOUT = params.operationTimeoutMs\n        Espresso.ASSERTION_TIMEOUT = params.operationTimeoutMs\n        UiAutomator.OPERATION_TIMEOUT = params.operationTimeoutMs\n        UltronCommonConfig.addListener(LogLifecycleListener())\n        UltronLog.addLogger(UltronLogcatLogger())\n        if (params.logToFile) {\n            UltronLog.addLogger(UltronLog.fileLogger)\n        } else {\n            UltronLog.removeLogger(UltronLog.fileLogger.id)\n        }\n        if (params.accelerateUiAutomator) {\n            UiAutomator.speedUp()\n        }\n        UltronLog.info(\"UltronConfig applied with params $params\")\n    }\n\n    @Deprecated(\n        message = \"Listeners storage moved to UltronCommonConfig\",\n        replaceWith = ReplaceWith(expression = \"UltronCommonConfig.addListener(Listener)\")\n    )\n    fun addGlobalListener(lifecycleListener: UltronLifecycleListener) {\n        UltronLog.info(\n            \"Add Ultron global listener ${lifecycleListener.simpleClassName()}. \" +\n                    \"It's applied for Espresso, EspressoWeb and UiAutomator operations.\"\n        )\n        UltronCommonConfig.addListener(lifecycleListener)\n    }\n\n    @Deprecated(\n        message = \"Listeners storage moved to UltronCommonConfig\",\n        replaceWith = ReplaceWith(expression = \"UltronCommonConfig.removeListener(listenerId)\")\n    )\n    fun removeGlobalListener(listenerId: String) {\n        UltronLog.info(\"Remove Ultron global listener with id ${listenerId}. \")\n        UltronCommonConfig.removeListener(listenerId)\n    }\n\n    @Deprecated(\n        message = \"Listeners storage moved to UltronCommonConfig\",\n        replaceWith = ReplaceWith(expression = \"UltronCommonConfig.removeListener(Listener::class)\")\n    )\n    fun <T : UltronLifecycleListener> removeGlobalListener(clazz: KClass<T>) {\n        UltronLog.info(\"Remove Ultron global listener  ${clazz.simpleName}. \")\n        UltronCommonConfig.removeListener(clazz)\n    }\n\n    class Log {\n        companion object {\n            @Deprecated(\n                message = \"Log config moved to UltronCommonConfig\",\n                replaceWith = ReplaceWith(expression = \"UltronCommonConfig.logDateFormat\")\n            )\n            var dateFormat = UltronCommonConfig.logDateFormat\n        }\n    }\n\n    class Espresso {\n        companion object {\n            val baseLayerComponent = DaggerBaseLayerComponent.create()\n            val activeRootLister: ActiveRootLister = baseLayerComponent.activeRootLister()\n            val uiController: UiController = baseLayerComponent.uiController()\n\n            @SuppressLint(\"RestrictedApi\")\n            val controlledLooper: ControlledLooper = baseLayerComponent.controlledLooper()\n\n            const val DEFAULT_RECYCLER_VIEW_LOAD_TIMEOUT = 5_000L\n            const val DEFAULT_RECYCLER_VIEW_OPERATION_TIMEOUT = 5_000L\n\n            var ESPRESSO_OPERATION_POLLING_TIMEOUT = UltronCommonConfig.Defaults.POLLING_TIMEOUT_MS\n            var ACTION_TIMEOUT = UltronCommonConfig.operationTimeoutMs\n            var ASSERTION_TIMEOUT = UltronCommonConfig.operationTimeoutMs\n            var RECYCLER_VIEW_LOAD_TIMEOUT = DEFAULT_RECYCLER_VIEW_LOAD_TIMEOUT\n            var RECYCLER_VIEW_OPERATIONS_TIMEOUT = DEFAULT_RECYCLER_VIEW_OPERATION_TIMEOUT\n            var RECYCLER_VIEW_ITEM_SEARCH_LIMIT = -1\n            var RECYCLER_VIEW_IMPLEMENTATION = UltronRecyclerViewImpl.STANDARD\n            var INCLUDE_VIEW_HIERARCHY_TO_EXCEPTION = false //where it applicable\n\n            var resultAnalyzer: OperationResultAnalyzer = UltronCommonConfig.resultAnalyzer\n\n\n            inline fun setResultAnalyzer(crossinline block: (OperationResult<Operation>) -> Boolean) {\n                resultAnalyzer = object : OperationResultAnalyzer {\n                    override fun <Op : Operation, OpRes : OperationResult<Op>> analyze(\n                        operationResult: OpRes\n                    ): Boolean {\n                        return block(operationResult as OperationResult<Operation>)\n                    }\n                }\n            }\n\n            var webViewMatcher: Matcher<View> = allOf(isAssignableFrom(WebView::class.java), isDisplayed())\n        }\n\n        class ViewActionConfig {\n            companion object {\n                var allowedExceptions = mutableListOf<Class<out Throwable>>(\n                    UltronWrapperException::class.java,\n                    UltronException::class.java,\n                    UltronAssertionException::class.java,\n                    PerformException::class.java,\n                    NoMatchingViewException::class.java,\n                    AmbiguousViewMatcherException::class.java,\n                    UltronOperationException::class.java\n                )\n\n                val resultHandler: (EspressoOperationResult<UltronEspressoOperation>) -> Unit = {\n                    UltronCommonConfig.testContext.wrapAnalyzerIfSoftAssertion(resultAnalyzer).analyze(it)\n                }\n\n                var autoScroll = false\n            }\n        }\n\n        class ViewAssertionConfig {\n            companion object {\n                var allowedExceptions = mutableListOf<Class<out Throwable>>(\n                    UltronWrapperException::class.java,\n                    UltronException::class.java,\n                    UltronAssertionException::class.java,\n                    UltronOperationException::class.java,\n                    PerformException::class.java,\n                    NoMatchingViewException::class.java,\n                    AssertionFailedError::class.java,\n                    AmbiguousViewMatcherException::class.java\n                )\n                val resultHandler: (EspressoOperationResult<UltronEspressoOperation>) -> Unit = {\n                    UltronCommonConfig.testContext.wrapAnalyzerIfSoftAssertion(resultAnalyzer).analyze(it)\n                }\n            }\n        }\n\n        class WebInteractionOperationConfig {\n            companion object {\n                var allowedExceptions = mutableListOf<Class<out Throwable>>(\n                    UltronWrapperException::class.java,\n                    UltronException::class.java,\n                    PerformException::class.java,\n                    NoMatchingViewException::class.java,\n                    AssertionFailedError::class.java,\n                    RuntimeException::class.java\n                )\n                val resultHandler: (WebOperationResult<WebInteractionOperation<*>>) -> Unit = {\n                    UltronCommonConfig.testContext.wrapAnalyzerIfSoftAssertion(resultAnalyzer).analyze(it)\n                }\n            }\n        }\n    }\n\n    class UiAutomator {\n        companion object {\n            var UIAUTOMATOR_OPERATION_POLLING_TIMEOUT = UltronCommonConfig.Defaults.POLLING_TIMEOUT_MS\n            var OPERATION_TIMEOUT = UltronCommonConfig.Defaults.OPERATION_TIMEOUT_MS\n\n            var resultAnalyzer: OperationResultAnalyzer = UltronCommonConfig.resultAnalyzer\n\n            inline fun setResultAnalyzer(crossinline block: (OperationResult<Operation>) -> Boolean) {\n                resultAnalyzer = object : OperationResultAnalyzer {\n                    override fun <Op : Operation, OpRes : OperationResult<Op>> analyze(\n                        operationResult: OpRes\n                    ): Boolean {\n                        return block(operationResult as OperationResult<Operation>)\n                    }\n                }\n            }\n\n            var uiDevice: UiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())\n\n            fun setTimeout(timeoutMs: Long) {\n                Configurator.getInstance().apply {\n                    waitForIdleTimeout = timeoutMs\n                    waitForSelectorTimeout = timeoutMs\n                }\n            }\n\n            fun speedUp() {\n                setTimeout(0)\n            }\n        }\n\n        //UiSelector\n        class UiObjectConfig {\n            companion object {\n                var allowedExceptions = mutableListOf<Class<out Throwable>>(\n                    UltronWrapperException::class.java,\n                    UltronAssertionException::class.java,\n                    UltronException::class.java,\n                    UltronUiAutomatorException::class.java,\n                    UiObjectNotFoundException::class.java,\n                    NullPointerException::class.java,\n                )\n                val resultHandler: (UiAutomatorOperationResult<UiAutomatorUiSelectorOperation>) -> Unit = {\n                    UltronCommonConfig.testContext.wrapAnalyzerIfSoftAssertion(Espresso.resultAnalyzer).analyze(it)\n                }\n            }\n        }\n\n        //BySelector\n        class UiObject2Config {\n            companion object {\n                var allowedExceptions = mutableListOf<Class<out Throwable>>(\n                    UltronWrapperException::class.java,\n                    UltronAssertionException::class.java,\n                    UltronException::class.java,\n                    UltronUiAutomatorException::class.java,\n                    UiObjectNotFoundException::class.java,\n                    NullPointerException::class.java,\n                )\n                val resultHandler: (UiAutomatorOperationResult<UiAutomatorOperation>) -> Unit = {\n                    UltronCommonConfig.testContext.wrapAnalyzerIfSoftAssertion(Espresso.resultAnalyzer).analyze(it)\n                }\n            }\n        }\n    }\n\n    class Conditions {\n        companion object {\n            @Deprecated(\n                message = \"ConditionExecutorWrapper moved to UltronAndroidCommonConfig.\",\n                replaceWith = ReplaceWith(\"UltronAndroidCommonConfig.Conditions.conditionExecutorWrapper\")\n            )\n            var conditionExecutorWrapper: ConditionExecutorWrapper = UltronAndroidCommonConfig.Conditions.conditionExecutorWrapper\n                set(value) {\n                    field = value\n                    UltronAndroidCommonConfig.Conditions.conditionExecutorWrapper = value\n                }\n\n            @Deprecated(\n                message = \"ConditionsExecutor moved to UltronAndroidCommonConfig.\",\n                replaceWith = ReplaceWith(\"UltronAndroidCommonConfig.Conditions.conditionsExecutor\")\n            )\n            var conditionsExecutor: ConditionsExecutor = UltronAndroidCommonConfig.Conditions.conditionsExecutor\n                set(value) {\n                    field = value\n                    UltronAndroidCommonConfig.Conditions.conditionsExecutor = value\n                }\n        }\n    }\n}\n"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/config/UltronConfigParams.kt",
    "content": "package com.atiurin.ultron.core.config\n\ndata class UltronConfigParams(\n    var accelerateUiAutomator: Boolean = true,\n    var operationTimeoutMs: Long = UltronCommonConfig.operationTimeoutMs,\n){\n    @Deprecated(\"Use global setting UltronCommonConfig.logToFile\", ReplaceWith(\"UltronCommonConfig.logToFile\"))\n    var logToFile: Boolean = UltronCommonConfig.logToFile\n        set(value) {\n            field = value\n            UltronCommonConfig.logToFile = value\n        }\n}"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/espresso/EspressoOperationExecutor.kt",
    "content": "package com.atiurin.ultron.core.espresso\n\nimport androidx.test.espresso.NoMatchingViewException\nimport com.atiurin.ultron.core.common.Operation\nimport com.atiurin.ultron.core.common.OperationExecutor\nimport com.atiurin.ultron.core.common.OperationIterationResult\nimport com.atiurin.ultron.core.common.ResultDescriptor\nimport com.atiurin.ultron.core.config.UltronConfig\nimport com.atiurin.ultron.core.config.UltronConfig.Espresso.Companion.ESPRESSO_OPERATION_POLLING_TIMEOUT\nimport com.atiurin.ultron.extensions.isAssignedFrom\nimport kotlin.reflect.KClass\n\nabstract class EspressoOperationExecutor<T : Operation>(\n    override val operation: T\n) : OperationExecutor<T, EspressoOperationResult<T>> {\n    override val descriptor: ResultDescriptor\n        get() = ResultDescriptor()\n    override val pollingTimeout: Long\n        get() = ESPRESSO_OPERATION_POLLING_TIMEOUT\n    override var doBetweenOperationRetry: () -> Unit = {}\n\n    override fun generateResult(\n        success: Boolean,\n        exceptions: List<Throwable>,\n        description: String,\n        lastOperationIterationResult: OperationIterationResult?,\n        executionTimeMs: Long\n    ): EspressoOperationResult<T> {\n        return EspressoOperationResult(\n            operation = operation,\n            success = success,\n            exceptions = exceptions,\n            description = description,\n            operationIterationResult = lastOperationIterationResult,\n            executionTimeMs = executionTimeMs\n        )\n    }\n\n    override fun getWrapperException(originalException: Throwable): Throwable {\n        return if (originalException is NoMatchingViewException) {\n            NoMatchingViewException.Builder().from(originalException)\n                .includeViewHierarchy(UltronConfig.Espresso.INCLUDE_VIEW_HIERARCHY_TO_EXCEPTION)\n                .build()\n        } else originalException\n    }\n\n    override fun isExceptionInList(exception: Throwable, exceptionClasses: List<KClass<out Throwable>>): Boolean {\n        return exception::class.java.isAssignedFrom(exceptionClasses.map { it.java })\n    }\n}"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/espresso/EspressoOperationResult.kt",
    "content": "package com.atiurin.ultron.core.espresso\n\nimport com.atiurin.ultron.core.common.Operation\nimport com.atiurin.ultron.core.common.OperationIterationResult\nimport com.atiurin.ultron.core.common.OperationResult\n\nclass EspressoOperationResult<T : Operation>(\n    override val operation: T,\n    override val success: Boolean,\n    override val exceptions: List<Throwable> = emptyList(),\n    override var description: String,\n    override var operationIterationResult: OperationIterationResult?,\n    override val executionTimeMs: Long\n) : OperationResult<T>"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/espresso/UltronEspresso.kt",
    "content": "@file:Suppress(\"DeprecatedCallableAddReplaceWith\")\n\npackage com.atiurin.ultron.core.espresso\n\nimport android.content.Context\nimport androidx.test.espresso.Espresso\nimport androidx.test.espresso.PerformException\nimport androidx.test.platform.app.InstrumentationRegistry\nimport com.atiurin.ultron.core.config.UltronConfig\nimport com.atiurin.ultron.core.espresso.action.EspressoActionExecutor\nimport com.atiurin.ultron.core.espresso.action.EspressoActionType\nimport com.atiurin.ultron.core.espresso.assertion.EspressoAssertionExecutor\n\nobject UltronEspresso {\n    /** Closes soft keyboard if open. */\n    fun closeSoftKeyboard() {\n        executeUltronAction(\n            UltronEspressoOperation(\n                operationBlock = { Espresso.closeSoftKeyboard() },\n                name = \"Espresso.closeSoftKeyboard()\",\n                type = EspressoActionType.CLOSE_SOFT_KEYBOARD,\n                description = \"Espresso.closeSoftKeyboard() during ${UltronConfig.Espresso.ACTION_TIMEOUT} ms\",\n                timeoutMs = UltronConfig.Espresso.ACTION_TIMEOUT\n            ),\n            resultHandler = UltronConfig.Espresso.ViewActionConfig.resultHandler\n        )\n    }\n\n    /**\n     * Press on the back button.\n     *\n     * @throws PerformException if currently displayed activity is root activity, since pressing back\n     *     button would result in application closing.\n     */\n    fun pressBack() {\n        executeUltronAction(\n            UltronEspressoOperation(\n                operationBlock = { Espresso.pressBack() },\n                name = \"Espresso.pressBack()\",\n                type = EspressoActionType.PRESS_BACK,\n                description = \"Espresso.pressBack() during ${UltronConfig.Espresso.ACTION_TIMEOUT} ms\",\n                timeoutMs = UltronConfig.Espresso.ACTION_TIMEOUT\n            ),\n            resultHandler = UltronConfig.Espresso.ViewActionConfig.resultHandler\n        )\n    }\n\n    /**\n     * Opens the overflow menu displayed within an ActionBar.\n     *\n     * <p>This works with both native and SherlockActionBar ActionBars.\n     *\n     * <p>Note the significant differences of UX between ActionMode and ActionBars with respect to\n     * overflows. If a hardware menu key is present, the overflow icon is never displayed in\n     * ActionBars and can only be interacted with via menu key presses.\n     */\n    fun openActionBarOverflowOrOptionsMenu(context: Context = InstrumentationRegistry.getInstrumentation().targetContext) {\n        executeUltronAction(\n            UltronEspressoOperation(\n                operationBlock = { Espresso.openActionBarOverflowOrOptionsMenu(context) },\n                name = \"Espresso.openActionBarOverflowOrOptionsMenu(context)\",\n                type = EspressoActionType.OPEN_ACTION_BAR_OVERFLOW_OR_OPTION_MENU,\n                description = \"Espresso.openActionBarOverflowOrOptionsMenu() during ${UltronConfig.Espresso.ACTION_TIMEOUT} ms\",\n                timeoutMs = UltronConfig.Espresso.ACTION_TIMEOUT\n            ),\n            resultHandler = UltronConfig.Espresso.ViewActionConfig.resultHandler\n        )\n    }\n\n    /**\n     * Opens the overflow menu displayed in the contextual options of an ActionMode.\n     *\n     * <p>This works with both native and SherlockActionBar action modes.\n     *\n     * <p>Note the significant difference in UX between ActionMode and ActionBar overflows -\n     * ActionMode will always present an overflow icon and that icon only responds to clicks. The menu\n     * button (if present) has no impact on it.\n     */\n    fun openContextualActionModeOverflowMenu() {\n        executeUltronAction(\n            UltronEspressoOperation(\n                operationBlock = { Espresso.openContextualActionModeOverflowMenu() },\n                name = \"Espresso.openContextualActionModeOverflowMenu()\",\n                type = EspressoActionType.OPEN_CONTEXTUAL_ACTION_MODE_OVERFLOW_MENU,\n                description = \"Espresso.openContextualActionModeOverflowMenu() during ${UltronConfig.Espresso.ACTION_TIMEOUT} ms\",\n                timeoutMs = UltronConfig.Espresso.ACTION_TIMEOUT\n            ),\n            resultHandler = UltronConfig.Espresso.ViewActionConfig.resultHandler\n        )\n    }\n\n    /**\n     * Executes any espresso action inside Ultron lifecycle\n     */\n    @Deprecated(\"It doesn't support withAssertion(). Consider usage of UltronEspressoInteraction().executeAction() instead\")\n    fun executeAction(\n        operation: UltronEspressoOperation,\n        resultHandler: (EspressoOperationResult<UltronEspressoOperation>) -> Unit = UltronConfig.Espresso.ViewActionConfig.resultHandler\n    ) {\n        UltronEspressoOperationLifecycle.execute(EspressoActionExecutor(operation), resultHandler)\n    }\n\n    /**\n     * Executes any espresso assertion inside Ultron lifecycle\n     */\n    @Deprecated(\"It doesn't support withAssertion(). Consider usage of UltronEspressoInteraction().executeAssertion() instead\")\n    fun executeAssertion(\n        operation: UltronEspressoOperation,\n        resultHandler: (EspressoOperationResult<UltronEspressoOperation>) -> Unit = UltronConfig.Espresso.ViewAssertionConfig.resultHandler\n    ) {\n        UltronEspressoOperationLifecycle.execute(EspressoAssertionExecutor(operation), resultHandler)\n    }\n\n    private fun executeUltronAction(\n        operation: UltronEspressoOperation,\n        resultHandler: (EspressoOperationResult<UltronEspressoOperation>) -> Unit = UltronConfig.Espresso.ViewActionConfig.resultHandler\n    ) {\n        UltronEspressoOperationLifecycle.execute(EspressoActionExecutor(operation), resultHandler)\n    }\n}"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/espresso/UltronEspressoInteraction.kt",
    "content": "package com.atiurin.ultron.core.espresso\n\nimport android.graphics.Rect\nimport android.view.View\nimport androidx.test.espresso.DataInteraction\nimport androidx.test.espresso.NoMatchingViewException\nimport androidx.test.espresso.Root\nimport androidx.test.espresso.UiController\nimport androidx.test.espresso.ViewAction\nimport androidx.test.espresso.ViewAssertion\nimport androidx.test.espresso.ViewInteraction\nimport androidx.test.espresso.action.EspressoKey\nimport androidx.test.espresso.action.GeneralLocation\nimport androidx.test.espresso.action.Press\nimport androidx.test.espresso.action.Swipe\nimport androidx.test.espresso.action.Tap\nimport androidx.test.espresso.action.ViewActions\nimport androidx.test.espresso.assertion.ViewAssertions\nimport androidx.test.espresso.assertion.ViewAssertions.matches\nimport androidx.test.espresso.matcher.ViewMatchers\nimport com.atiurin.ultron.core.common.CommonOperationType\nimport com.atiurin.ultron.core.common.DefaultElementInfo\nimport com.atiurin.ultron.core.common.ElementInfo\nimport com.atiurin.ultron.core.common.UltronOperationType\nimport com.atiurin.ultron.core.common.assertion.DefaultOperationAssertion\nimport com.atiurin.ultron.core.common.assertion.EmptyOperationAssertion\nimport com.atiurin.ultron.core.common.assertion.OperationAssertion\nimport com.atiurin.ultron.core.config.UltronConfig\nimport com.atiurin.ultron.core.espresso.action.EspressoActionExecutor\nimport com.atiurin.ultron.core.espresso.action.EspressoActionType\nimport com.atiurin.ultron.core.espresso.action.UltronCustomClickAction\nimport com.atiurin.ultron.core.espresso.action.UltronEspressoActionParams\nimport com.atiurin.ultron.core.espresso.action.UltronSwipeAction\nimport com.atiurin.ultron.core.espresso.action.UltronSwipeAction.Companion.EDGE_FUZZ_FACTOR\nimport com.atiurin.ultron.core.espresso.action.UltronTypeTextAction\nimport com.atiurin.ultron.core.espresso.assertion.EspressoAssertionExecutor\nimport com.atiurin.ultron.core.espresso.assertion.EspressoAssertionType\nimport com.atiurin.ultron.core.espresso.assertion.UltronEspressoAssertionParams\nimport com.atiurin.ultron.custom.espresso.action.AnonymousViewAction\nimport com.atiurin.ultron.custom.espresso.assertion.ExistsEspressoViewAssertion\nimport com.atiurin.ultron.exceptions.UltronException\nimport com.atiurin.ultron.extensions.getDataMatcher\nimport com.atiurin.ultron.extensions.getRootMatcher\nimport com.atiurin.ultron.extensions.getViewMatcher\nimport com.atiurin.ultron.extensions.simpleClassName\nimport com.atiurin.ultron.listeners.setListenersState\nimport org.hamcrest.BaseMatcher\nimport org.hamcrest.Description\nimport org.hamcrest.Matcher\nimport org.hamcrest.Matchers\nimport java.util.concurrent.atomic.AtomicReference\n\n@Suppress(\"MemberVisibilityCanBePrivate\")\nclass UltronEspressoInteraction<T>(\n    val interaction: T,\n    val timeoutMs: Long? = null,\n    val resultHandler: ((EspressoOperationResult<UltronEspressoOperation>) -> Unit)? = null,\n    val assertion: OperationAssertion = EmptyOperationAssertion(),\n    val elementInfo: ElementInfo = DefaultElementInfo(),\n    var autoScrollForAction: Boolean = UltronConfig.Espresso.ViewActionConfig.autoScroll\n) {\n    init {\n        if (interaction !is ViewInteraction && interaction !is DataInteraction) throw UltronException(\n            \"Invalid interaction class provided ${interaction.simpleClassName()}. Use ViewInteraction or DataInteraction\"\n        )\n        if (elementInfo.name.isEmpty()) elementInfo.name = getInteractionMatcher().toString()\n    }\n\n    fun inRoot(rootMatcher: Matcher<Root>) = apply {\n        when (interaction) {\n            is ViewInteraction -> {\n                interaction.inRoot(rootMatcher)\n            }\n\n            is DataInteraction -> {\n                interaction.inRoot(rootMatcher)\n            }\n\n            else -> throw UltronException(\"Unknown type of interaction provided!\")\n        }\n    }\n\n    fun isSuccess(operation: UltronEspressoInteraction<T>.() -> Any): Boolean =\n        runCatching { operation() }.isSuccess\n\n    fun getActionTimeout(): Long = timeoutMs ?: UltronConfig.Espresso.ACTION_TIMEOUT\n\n    fun getAssertionTimeout(): Long = timeoutMs ?: UltronConfig.Espresso.ASSERTION_TIMEOUT\n\n    fun withResultHandler(resultHandler: (EspressoOperationResult<UltronEspressoOperation>) -> Unit): UltronEspressoInteraction<T> {\n        return UltronEspressoInteraction(\n            this.interaction,\n            this.timeoutMs,\n            resultHandler,\n            this.assertion,\n            this.elementInfo\n        )\n    }\n\n    fun withTimeout(timeoutMs: Long): UltronEspressoInteraction<T> {\n        return UltronEspressoInteraction(\n            this.interaction,\n            timeoutMs,\n            this.resultHandler,\n            this.assertion,\n            this.elementInfo\n        )\n    }\n\n    fun withAssertion(assertion: OperationAssertion) = UltronEspressoInteraction(\n        interaction,\n        timeoutMs,\n        resultHandler,\n        assertion,\n        this.elementInfo\n    )\n\n    fun withAssertion(name: String = \"\", isListened: Boolean = false, block: () -> Unit) =\n        UltronEspressoInteraction(\n            interaction,\n            timeoutMs,\n            resultHandler,\n            DefaultOperationAssertion(name, block.setListenersState(isListened)),\n            this.elementInfo\n        )\n\n    fun withName(name: String) = apply { elementInfo.name = name }\n\n    fun withMetaInfo(meta: Any) = apply { elementInfo.meta = meta }\n\n    fun withAutoScroll(autoscroll: Boolean) = apply { this.autoScrollForAction = autoscroll }\n\n    // =========== CUSTOM CLICKS ============================\n    private fun customClick(\n        type: EspressoActionType,\n        location: GeneralLocation,\n        offsetX: Int,\n        offsetY: Int\n    ) = apply {\n        executeAction(\n            operationBlock = getInteractionActionBlock(\n                UltronCustomClickAction(\n                    Tap.SINGLE,\n                    location,\n                    areaPercentage = 20,\n                    offsetX = offsetX,\n                    offsetY = offsetY\n                )\n            ),\n            name = \"$type for '${elementInfo.name}'\",\n            type = type,\n            description = \"${interaction.simpleClassName()} action '${type}' to '${elementInfo.name}' with root '${getInteractionRootMatcher()}' during ${getActionTimeout()} ms\"\n        )\n    }\n\n    fun clickTopLeft(offsetX: Int, offsetY: Int) = apply {\n        customClick(\n            EspressoActionType.CLICK_TOP_LEFT,\n            GeneralLocation.TOP_LEFT,\n            offsetX = offsetX,\n            offsetY = offsetY\n        )\n    }\n\n    fun clickTopCenter(offsetY: Int) = apply {\n        customClick(\n            EspressoActionType.CLICK_TOP_CENTER,\n            GeneralLocation.TOP_CENTER,\n            offsetY = offsetY,\n            offsetX = 0\n        )\n    }\n\n    fun clickTopRight(offsetX: Int, offsetY: Int) = apply {\n        customClick(\n            EspressoActionType.CLICK_TOP_RIGHT,\n            GeneralLocation.TOP_RIGHT,\n            offsetY = offsetY,\n            offsetX = offsetX\n        )\n    }\n\n    fun clickCenterRight(offsetX: Int) = apply {\n        customClick(\n            EspressoActionType.CLICK_CENTER_RIGHT,\n            GeneralLocation.CENTER_RIGHT,\n            offsetY = 0,\n            offsetX = offsetX\n        )\n    }\n\n    fun clickBottomRight(offsetX: Int, offsetY: Int) = apply {\n        customClick(\n            EspressoActionType.CLICK_BOTTOM_RIGHT,\n            GeneralLocation.BOTTOM_RIGHT,\n            offsetY = offsetY,\n            offsetX = offsetX\n        )\n    }\n\n    fun clickBottomCenter(offsetY: Int) = apply {\n        customClick(\n            EspressoActionType.CLICK_BOTTOM_CENTER,\n            GeneralLocation.BOTTOM_CENTER,\n            offsetY = offsetY,\n            offsetX = 0\n        )\n    }\n\n    fun clickBottomLeft(offsetX: Int, offsetY: Int) = apply {\n        customClick(\n            EspressoActionType.CLICK_BOTTOM_LEFT,\n            GeneralLocation.BOTTOM_LEFT,\n            offsetY = offsetY,\n            offsetX = offsetX\n        )\n    }\n\n    fun clickCenterLeft(offsetX: Int) = apply {\n        customClick(\n            EspressoActionType.CLICK_CENTER_LEFT,\n            GeneralLocation.CENTER_LEFT,\n            offsetY = 0,\n            offsetX = offsetX\n        )\n    }\n    // ======================================================\n\n    fun click() = apply {\n        executeAction(\n            operationBlock = getInteractionActionBlock(\n                if (autoScrollForAction) {\n                    UltronCustomClickAction(\n                        tapper = Tap.SINGLE,\n                        coordinatesProvider = GeneralLocation.CENTER,\n                        areaPercentage = 90,\n                        precisionDescriber = Press.FINGER\n                    )\n                } else {\n                    ViewActions.click()\n                }\n            ),\n            name = \"Click to '${elementInfo.name}'\",\n            type = EspressoActionType.CLICK,\n            description = \"${interaction.simpleClassName()} action '${EspressoActionType.CLICK}' to '${elementInfo.name}' with root '${getInteractionRootMatcher()}' during ${getActionTimeout()} ms\"\n        )\n    }\n\n    fun doubleClick() = apply {\n        executeAction(\n            operationBlock = getInteractionActionBlock(\n                if (autoScrollForAction) {\n                    UltronCustomClickAction(\n                        tapper = Tap.DOUBLE,\n                        coordinatesProvider = GeneralLocation.CENTER,\n                        areaPercentage = 90,\n                        precisionDescriber = Press.FINGER\n                    )\n                } else {\n                    ViewActions.doubleClick()\n                }\n            ),\n            name = \"DoubleClick to '${elementInfo.name}'\",\n            type = EspressoActionType.DOUBLE_CLICK,\n            description = \"${interaction.simpleClassName()} action '${EspressoActionType.DOUBLE_CLICK}' to '${elementInfo.name}' with root '${getInteractionRootMatcher()}' during ${getActionTimeout()} ms\"\n        )\n    }\n\n    fun longClick() = apply {\n        executeAction(\n            operationBlock = getInteractionActionBlock(\n                if (autoScrollForAction) {\n                    UltronCustomClickAction(\n                        tapper = Tap.LONG,\n                        coordinatesProvider = GeneralLocation.CENTER,\n                        areaPercentage = 90,\n                        precisionDescriber = Press.FINGER\n                    )\n                } else {\n                    ViewActions.longClick()\n                }\n            ),\n            name = \"LongClick to '${elementInfo.name}'\",\n            type = EspressoActionType.LONG_CLICK,\n            description = \"${interaction.simpleClassName()} action '${EspressoActionType.LONG_CLICK}' to '${elementInfo.name}' with root '${getInteractionRootMatcher()}' during ${getActionTimeout()} ms\"\n        )\n    }\n\n    fun typeText(text: String) = apply {\n        executeAction(\n            operationBlock = getInteractionActionBlock(\n                if (autoScrollForAction) {\n                    UltronTypeTextAction(\n                        stringToBeTyped = text,\n                        tapToFocus = true,\n                        clickAction = UltronCustomClickAction(\n                            tapper = Tap.SINGLE,\n                            coordinatesProvider = GeneralLocation.CENTER,\n                            areaPercentage = 90,\n                            precisionDescriber = Press.FINGER\n                        )\n                    )\n                } else {\n                    ViewActions.typeText(text)\n                }\n            ),\n            name = \"Type text '$text' to '${elementInfo.name}'\",\n            type = EspressoActionType.TYPE_TEXT,\n            description = \"${interaction.simpleClassName()} action '${EspressoActionType.TYPE_TEXT}' '$text' to '${elementInfo.name}' with root '${getInteractionRootMatcher()}' during ${getActionTimeout()} ms\"\n        )\n    }\n\n    fun replaceText(text: String) = apply {\n        executeAction(\n            operationBlock = getInteractionActionBlock(ViewActions.replaceText(text)),\n            name = \"Replace text '$text' to '${elementInfo.name}'\",\n            type = EspressoActionType.REPLACE_TEXT,\n            description = \"${interaction.simpleClassName()} action '${EspressoActionType.REPLACE_TEXT}' '$text' to '${elementInfo.name}' with root '${getInteractionRootMatcher()}' during ${getActionTimeout()} ms\"\n        )\n    }\n\n    fun clearText() = apply {\n        executeAction(\n            operationBlock = getInteractionActionBlock(ViewActions.clearText()),\n            name = \"Clear text in '${elementInfo.name}'\",\n            type = EspressoActionType.CLEAR_TEXT,\n            description = \"${interaction.simpleClassName()} action '${EspressoActionType.CLEAR_TEXT}' in '${elementInfo.name}' with root '${getInteractionRootMatcher()}' during ${getActionTimeout()} ms\"\n        )\n    }\n\n    fun pressKey(keyCode: Int) = apply {\n        executeAction(\n            operationBlock = getInteractionActionBlock(ViewActions.pressKey(keyCode)),\n            name = \"PressKey code '$keyCode' on '${elementInfo.name}'\",\n            type = EspressoActionType.PRESS_KEY,\n            description = \"${interaction.simpleClassName()} action '${EspressoActionType.PRESS_KEY}' '$keyCode' on '${elementInfo.name}' with root '${getInteractionRootMatcher()}' during ${getActionTimeout()} ms\"\n        )\n    }\n\n    fun pressKey(key: EspressoKey) = apply {\n        executeAction(\n            operationBlock = getInteractionActionBlock(ViewActions.pressKey(key)),\n            name = \"Press EspressoKey '$key' on '${elementInfo.name}'\",\n            type = EspressoActionType.PRESS_KEY,\n            description = \"${interaction.simpleClassName()} action '${EspressoActionType.PRESS_KEY}' EspressoKey '$key' on '${elementInfo.name}' with root '${getInteractionRootMatcher()}' during ${getActionTimeout()} ms\"\n        )\n    }\n\n    fun closeSoftKeyboard() = apply {\n        executeAction(\n            operationBlock = getInteractionActionBlock(ViewActions.closeSoftKeyboard()),\n            name = \"CloseSoftKeyboard with '${elementInfo.name}'\",\n            type = EspressoActionType.CLOSE_SOFT_KEYBOARD,\n            description = \"${interaction.simpleClassName()} action '${EspressoActionType.CLOSE_SOFT_KEYBOARD}' of '${elementInfo.name}' with root '${getInteractionRootMatcher()}' during ${getActionTimeout()} ms\"\n        )\n    }\n\n    fun swipeLeft() = apply {\n        executeAction(\n            operationBlock = getInteractionActionBlock(\n                ViewActions.actionWithAssertions(\n                    if (autoScrollForAction) {\n                        UltronSwipeAction(\n                            swiper = Swipe.FAST,\n                            startCoordinatesProvider = GeneralLocation.translate(\n                                GeneralLocation.CENTER_RIGHT,\n                                -EDGE_FUZZ_FACTOR,\n                                0f\n                            ),\n                            endCoordinatesProvider = GeneralLocation.CENTER_LEFT,\n                            precisionDescriber = Press.FINGER\n                        )\n                    } else {\n                        ViewActions.swipeLeft()\n                    }\n                )\n            ),\n            name = \"SwipeLeft with '${elementInfo.name}'\",\n            type = EspressoActionType.SWIPE_LEFT,\n            description = \"${interaction.simpleClassName()} action '${EspressoActionType.SWIPE_LEFT}' of '${elementInfo.name}' with root '${getInteractionRootMatcher()}' during ${getActionTimeout()} ms\"\n        )\n    }\n\n    fun swipeRight() = apply {\n        executeAction(\n            operationBlock = getInteractionActionBlock(\n                ViewActions.actionWithAssertions(\n                    if (autoScrollForAction) {\n                        UltronSwipeAction(\n                            swiper = Swipe.FAST,\n                            startCoordinatesProvider = GeneralLocation.translate(\n                                GeneralLocation.CENTER_LEFT,\n                                EDGE_FUZZ_FACTOR,\n                                0f\n                            ),\n                            endCoordinatesProvider = GeneralLocation.CENTER_RIGHT,\n                            precisionDescriber = Press.FINGER\n                        )\n                    } else {\n                        ViewActions.swipeRight()\n                    }\n                )\n            ),\n            name = \"SwipeRight with '${elementInfo.name}'\",\n            type = EspressoActionType.SWIPE_RIGHT,\n            description = \"${interaction.simpleClassName()} action '${EspressoActionType.SWIPE_RIGHT}' of '${elementInfo.name}' with root '${getInteractionRootMatcher()}' during ${getActionTimeout()} ms\"\n        )\n    }\n\n    fun swipeUp() = apply {\n        executeAction(\n            operationBlock = getInteractionActionBlock(\n                ViewActions.actionWithAssertions(\n                    if (autoScrollForAction) {\n                        UltronSwipeAction(\n                            swiper = Swipe.FAST,\n                            startCoordinatesProvider = GeneralLocation.translate(\n                                GeneralLocation.BOTTOM_CENTER,\n                                0f,\n                                -EDGE_FUZZ_FACTOR\n                            ),\n                            endCoordinatesProvider = GeneralLocation.TOP_CENTER,\n                            precisionDescriber = Press.FINGER\n                        )\n                    } else {\n                        ViewActions.swipeUp()\n                    }\n                )\n            ),\n            name = \"SwipeUp with '${elementInfo.name}'\",\n            type = EspressoActionType.SWIPE_UP,\n            description = \"${interaction.simpleClassName()} action '${EspressoActionType.SWIPE_UP}' of '${elementInfo.name}' with root '${getInteractionRootMatcher()}' during ${getActionTimeout()} ms\"\n        )\n    }\n\n    fun swipeDown() = apply {\n        executeAction(\n            operationBlock = getInteractionActionBlock(\n                ViewActions.actionWithAssertions(\n                    if (autoScrollForAction) {\n                        UltronSwipeAction(\n                            swiper = Swipe.FAST,\n                            startCoordinatesProvider = GeneralLocation.translate(\n                                GeneralLocation.TOP_CENTER,\n                                0f,\n                                EDGE_FUZZ_FACTOR\n                            ),\n                            endCoordinatesProvider = GeneralLocation.BOTTOM_CENTER,\n                            precisionDescriber = Press.FINGER\n                        )\n                    } else {\n                        ViewActions.swipeDown()\n                    }\n                )\n            ),\n            name = \"SwipeDown with '${elementInfo.name}'\",\n            type = EspressoActionType.SWIPE_DOWN,\n            description = \"${interaction.simpleClassName()} action '${EspressoActionType.SWIPE_DOWN}' of '${elementInfo.name}' with root '${getInteractionRootMatcher()}' during ${getActionTimeout()} ms\"\n        )\n    }\n\n    fun scrollTo() = apply {\n        executeAction(\n            operationBlock = getInteractionActionBlock(ViewActions.scrollTo()),\n            name = \"ScrollTo with '${elementInfo.name}'\",\n            type = EspressoActionType.SCROLL,\n            description = \"${interaction.simpleClassName()} action '${EspressoActionType.SCROLL}' of '${elementInfo.name}' with root '${getInteractionRootMatcher()}' during ${getActionTimeout()} ms\"\n        )\n    }\n\n    /**\n     * This method allows you to perform a ViewAction on the interaction's view by providing\n     * the ViewAction and an optional description. The ViewAction represents a specific\n     * interaction with the view, such as clicking or scrolling. The description provides\n     * additional context for the action being performed.\n     *\n     * @param viewAction The ViewAction to perform on the interaction's view.\n     * @param description An optional description for the action being performed.\n     * @return An updated instance of the class.\n     */\n    fun perform(viewAction: ViewAction, description: String = \"\") = apply {\n        executeAction(\n            operationBlock = getInteractionActionBlock(viewAction),\n            name = description.ifEmpty { \"Perform action to ${elementInfo.name}\" },\n            description = \"${interaction.simpleClassName()} action '$description' of '${viewAction.description}' to '${elementInfo.name}' with root '${getInteractionRootMatcher()}' during ${getActionTimeout()} ms\"\n        )\n    }\n\n    /**\n     * This method allows you to perform a custom Espresso action by providing the parameters\n     * for the action and a block of code that defines the action's behavior. The action block\n     * receives a UiController and a View as parameters and is responsible for performing the\n     * desired interaction on the view. After executing the action block, the main thread is\n     * looped until idle.\n     *\n     * @param params The optional parameters for the Espresso action. If null, default action\n     *               parameters are used.\n     * @param block The block of code that defines the behavior of the custom action. The block\n     *              receives a UiController and a View as parameters.\n     * @return An updated instance of the class.\n     */\n    fun perform(\n        params: UltronEspressoActionParams? = null,\n        block: (uiController: UiController, view: View) -> Unit\n    ) = apply {\n        val actionParams = params ?: getDefaultActionParams()\n        val viewAction = object : AnonymousViewAction(actionParams) {\n            override fun perform(uiController: UiController, view: View) {\n                if (autoScrollForAction) {\n                    val rect = Rect()\n                    view.getDrawingRect(rect)\n                    view.requestRectangleOnScreen(rect, true) // immediate is set to true, scrolling will not be animated.\n                    uiController.loopMainThreadUntilIdle()\n                }\n                block(uiController, view)\n                uiController.loopMainThreadUntilIdle()\n            }\n        }\n        executeAction(\n            operationBlock = getInteractionActionBlock(viewAction),\n            name = actionParams.operationName,\n            description = actionParams.operationDescription,\n            type = actionParams.operationType\n        )\n    }\n\n    /**\n     * Executes a custom Espresso action using the provided parameters and action block.\n     *\n     * This method allows you to execute a custom Espresso action by providing the parameters\n     * for the action and a block of code that defines the action's behavior. The action block\n     * receives a UiController and a View as parameters and is responsible for performing the\n     * desired interaction on the view. After executing the action block, the main thread is\n     * looped until idle.\n     *\n     * @param <T> The type of the result returned by the action block.\n     * @param params The optional parameters for the Espresso action. If null, default action\n     *               parameters are used.\n     * @param block The block of code that defines the behavior of the custom action. The block\n     *              receives a UiController and a View as parameters and returns a value of type T.\n     * @return The result of the action block.\n     */\n    fun <T> execute(\n        params: UltronEspressoActionParams? = null,\n        block: (uiController: UiController, view: View) -> T\n    ): T {\n        val actionParams = params ?: getDefaultActionParams()\n        val container = AtomicReference<T>()\n        val viewAction = object : AnonymousViewAction(actionParams) {\n            override fun perform(uiController: UiController, view: View) {\n                container.set(block(uiController, view))\n                uiController.loopMainThreadUntilIdle()\n            }\n        }\n        executeAction(\n            operationBlock = getInteractionActionBlock(viewAction),\n            name = actionParams.operationName,\n            description = actionParams.operationDescription,\n            type = actionParams.operationType\n        )\n        return container.get()\n    }\n\n    //assertion\n    fun isDisplayed() = apply {\n        executeAssertion(\n            operationBlock = getInteractionAssertionBlock(ViewMatchers.isDisplayed()),\n            name = \"IsDisplayed of '${elementInfo.name}'\",\n            type = EspressoAssertionType.IS_DISPLAYED,\n            description = \"${interaction.simpleClassName()} assertion '${EspressoAssertionType.IS_DISPLAYED}' of '${elementInfo.name}' with root '${getInteractionRootMatcher()}' during ${getAssertionTimeout()} ms\"\n        )\n    }\n\n    /**\n     * Assert ui element has effective visibility = VISIBLE\n     */\n    fun isVisible() = apply {\n        executeAssertion(\n            operationBlock = getInteractionAssertionBlock(\n                ViewMatchers.withEffectiveVisibility(\n                    ViewMatchers.Visibility.VISIBLE\n                )\n            ),\n            name = \"IsVisible of '${elementInfo.name}'\",\n            type = EspressoAssertionType.IS_VISIBLE,\n            description = \"${interaction.simpleClassName()} assertion '${EspressoAssertionType.IS_VISIBLE}' of '${elementInfo.name}' with root '${getInteractionRootMatcher()}' during ${getAssertionTimeout()} ms\"\n        )\n    }\n\n    /**\n     * Asserts ui element presents in view hierarchy but isn't displayed\n     * If these is no element in view hierarchy it throws [NoMatchingViewException]. In this case use [doesNotExist]\n     */\n    fun isNotDisplayed() = apply {\n        executeAssertion(\n            operationBlock = getInteractionAssertionBlock(Matchers.not(ViewMatchers.isDisplayed())),\n            name = \"IsNotDisplayed of '${elementInfo.name}'\",\n            type = EspressoAssertionType.IS_NOT_DISPLAYED,\n            description = \"${interaction.simpleClassName()} assertion '${EspressoAssertionType.IS_NOT_DISPLAYED}' of '${elementInfo.name}' with root '${getInteractionRootMatcher()}' during ${getAssertionTimeout()} ms\"\n        )\n    }\n\n    /**\n     * Asserts ui element presents in view hierarchy\n     */\n    fun exists() = apply {\n        executeAssertion(\n            operationBlock = getInteractionAssertionBlock(ExistsEspressoViewAssertion()),\n            name = \"Exists of '${elementInfo.name}'\",\n            type = EspressoAssertionType.EXISTS,\n            description = \"${interaction.simpleClassName()} assertion '${EspressoAssertionType.EXISTS}' of '${elementInfo.name}' with root '${getInteractionRootMatcher()}' during ${getAssertionTimeout()} ms\"\n        )\n    }\n\n    /**\n     * Asserts ui element isn't presents in view hierarchy\n     */\n    fun doesNotExist() = apply {\n        executeAssertion(\n            operationBlock = getInteractionAssertionBlock(ViewAssertions.doesNotExist()),\n            name = \"DoesNotExist of '${elementInfo.name}'\",\n            type = EspressoAssertionType.DOES_NOT_EXIST,\n            description = \"${interaction.simpleClassName()} assertion '${EspressoAssertionType.DOES_NOT_EXIST}' of '${elementInfo.name}' with root '${getInteractionRootMatcher()}' during ${getAssertionTimeout()} ms\"\n        )\n    }\n\n    fun isCompletelyDisplayed() = apply {\n        executeAssertion(\n            operationBlock = getInteractionAssertionBlock(ViewMatchers.isCompletelyDisplayed()),\n            name = \"IsCompletelyDisplayed of '${elementInfo.name}'\",\n            type = EspressoAssertionType.IS_COMPLETELY_DISPLAYED,\n            description = \"${interaction.simpleClassName()} assertion '${EspressoAssertionType.IS_COMPLETELY_DISPLAYED}' of '${elementInfo.name}' with root '${getInteractionRootMatcher()}' during ${getAssertionTimeout()} ms\"\n        )\n    }\n\n    fun isDisplayingAtLeast(percentage: Int) = apply {\n        executeAssertion(\n            operationBlock = getInteractionAssertionBlock(\n                ViewMatchers.isDisplayingAtLeast(\n                    percentage\n                )\n            ),\n            name = \"IsDisplayingAtLeast '$percentage'% of '${elementInfo.name}'\",\n            type = EspressoAssertionType.IS_DISPLAYING_AT_LEAST,\n            description = \"${interaction.simpleClassName()} assertion '${EspressoAssertionType.IS_DISPLAYING_AT_LEAST}' '$percentage'% of '${elementInfo.name}' with root '${getInteractionRootMatcher()}' during ${getAssertionTimeout()} ms\"\n        )\n    }\n\n    fun isEnabled() = apply {\n        executeAssertion(\n            operationBlock = getInteractionAssertionBlock(ViewMatchers.isEnabled()),\n            name = \"IsEnabled of '${elementInfo.name}'\",\n            type = EspressoAssertionType.IS_ENABLED,\n            description = \"${interaction.simpleClassName()} assertion '${EspressoAssertionType.IS_ENABLED}' of '${elementInfo.name}' with root '${getInteractionRootMatcher()}' during ${getAssertionTimeout()} ms\"\n        )\n    }\n\n    fun isNotEnabled() = apply {\n        executeAssertion(\n            operationBlock = getInteractionAssertionBlock(Matchers.not(ViewMatchers.isEnabled())),\n            name = \"IsNotEnabled of '${elementInfo.name}'\",\n            type = EspressoAssertionType.IS_NOT_ENABLED,\n            description = \"${interaction.simpleClassName()} assertion '${EspressoAssertionType.IS_NOT_ENABLED}' of '${elementInfo.name}' with root '${getInteractionRootMatcher()}' during ${getAssertionTimeout()} ms\"\n        )\n    }\n\n    fun isSelected() = apply {\n        executeAssertion(\n            operationBlock = getInteractionAssertionBlock(ViewMatchers.isSelected()),\n            name = \"IsSelected of '${elementInfo.name}'\",\n            type = EspressoAssertionType.IS_SELECTED,\n            description = \"${interaction.simpleClassName()} assertion '${EspressoAssertionType.IS_SELECTED}' of '${elementInfo.name}' with root '${getInteractionRootMatcher()}' during ${getAssertionTimeout()} ms\"\n        )\n    }\n\n    fun isNotSelected() = apply {\n        executeAssertion(\n            operationBlock = getInteractionAssertionBlock(Matchers.not(ViewMatchers.isSelected())),\n            name = \"IsNotSelected of '${elementInfo.name}'\",\n            type = EspressoAssertionType.IS_NOT_SELECTED,\n            description = \"${interaction.simpleClassName()} assertion '${EspressoAssertionType.IS_NOT_SELECTED}' of '${elementInfo.name}' with root '${getInteractionRootMatcher()}' during ${getAssertionTimeout()} ms\"\n        )\n    }\n\n    fun isClickable() = apply {\n        executeAssertion(\n            operationBlock = getInteractionAssertionBlock(ViewMatchers.isClickable()),\n            name = \"IsClickable of '${elementInfo.name}'\",\n            type = EspressoAssertionType.IS_CLICKABLE,\n            description = \"${interaction.simpleClassName()} assertion '${EspressoAssertionType.IS_CLICKABLE}' of '${elementInfo.name}' with root '${getInteractionRootMatcher()}' during ${getAssertionTimeout()} ms\"\n        )\n    }\n\n    fun isNotClickable() = apply {\n        executeAssertion(\n            operationBlock = getInteractionAssertionBlock(Matchers.not(ViewMatchers.isClickable())),\n            name = \"IsNotClickable of '${elementInfo.name}'\",\n            type = EspressoAssertionType.IS_NOT_CLICKABLE,\n            description = \"${interaction.simpleClassName()} assertion '${EspressoAssertionType.IS_NOT_CLICKABLE}' of '${elementInfo.name}' with root '${getInteractionRootMatcher()}' during ${getAssertionTimeout()} ms\"\n        )\n    }\n\n    fun isChecked() = apply {\n        executeAssertion(\n            operationBlock = getInteractionAssertionBlock(ViewMatchers.isChecked()),\n            name = \"IsChecked of '${elementInfo.name}'\",\n            type = EspressoAssertionType.IS_CHECKED,\n            description = \"${interaction.simpleClassName()} assertion '${EspressoAssertionType.IS_CHECKED}' of '${elementInfo.name}' with root '${getInteractionRootMatcher()}' during ${getAssertionTimeout()} ms\"\n        )\n    }\n\n    fun isNotChecked() = apply {\n        executeAssertion(\n            operationBlock = getInteractionAssertionBlock(ViewMatchers.isNotChecked()),\n            name = \"IsNotChecked of '${elementInfo.name}'\",\n            type = EspressoAssertionType.IS_NOT_CHECKED,\n            description = \"${interaction.simpleClassName()} assertion '${EspressoAssertionType.IS_NOT_CHECKED}' of '${elementInfo.name}' with root '${getInteractionRootMatcher()}' during ${getAssertionTimeout()} ms\"\n        )\n    }\n\n    fun isFocusable() = apply {\n        executeAssertion(\n            operationBlock = getInteractionAssertionBlock(ViewMatchers.isFocusable()),\n            name = \"IsFocusable of '${elementInfo.name}'\",\n            type = EspressoAssertionType.IS_FOCUSABLE,\n            description = \"${interaction.simpleClassName()} assertion '${EspressoAssertionType.IS_FOCUSABLE}' of '${elementInfo.name}' with root '${getInteractionRootMatcher()}' during ${getAssertionTimeout()} ms\"\n        )\n    }\n\n    fun isNotFocusable() = apply {\n        executeAssertion(\n            operationBlock = getInteractionAssertionBlock(Matchers.not(ViewMatchers.isFocusable())),\n            name = \"IsNotFocusable of '${elementInfo.name}'\",\n            type = EspressoAssertionType.IS_NOT_FOCUSABLE,\n            description = \"${interaction.simpleClassName()} assertion '${EspressoAssertionType.IS_NOT_FOCUSABLE}' of '${elementInfo.name}' with root '${getInteractionRootMatcher()}' during ${getAssertionTimeout()} ms\"\n        )\n    }\n\n    fun hasFocus() = apply {\n        executeAssertion(\n            operationBlock = getInteractionAssertionBlock(ViewMatchers.hasFocus()),\n            name = \"HasFocus of '${elementInfo.name}'\",\n            type = EspressoAssertionType.HAS_FOCUS,\n            description = \"${interaction.simpleClassName()} assertion '${EspressoAssertionType.HAS_FOCUS}' of '${elementInfo.name}' with root '${getInteractionRootMatcher()}' during ${getAssertionTimeout()} ms\"\n        )\n    }\n\n    fun isJavascriptEnabled() = apply {\n        executeAssertion(\n            operationBlock = getInteractionAssertionBlock(ViewMatchers.isJavascriptEnabled()),\n            name = \"IsJavascriptEnabled of '${elementInfo.name}'\",\n            type = EspressoAssertionType.IS_JS_ENABLED,\n            description = \"${interaction.simpleClassName()} assertion '${EspressoAssertionType.IS_JS_ENABLED}' of '${elementInfo.name}' with root '${getInteractionRootMatcher()}' during ${getAssertionTimeout()} ms\"\n        )\n    }\n\n    fun hasText(text: String) = apply {\n        executeAssertion(\n            operationBlock = getInteractionAssertionBlock(ViewMatchers.withText(text)),\n            name = \"HasText '$text' in '${elementInfo.name}'\",\n            type = EspressoAssertionType.HAS_TEXT,\n            description = \"${interaction.simpleClassName()} assertion '${EspressoAssertionType.HAS_TEXT}' '$text' in '${elementInfo.name}' with root '${getInteractionRootMatcher()}' during ${getAssertionTimeout()} ms\"\n        )\n    }\n\n    fun hasText(resourceId: Int) = apply {\n        executeAction(\n            operationBlock = getInteractionAssertionBlock(ViewMatchers.withText(resourceId)),\n            name = \"HasText with resourceId '$resourceId' in '${elementInfo.name}'\",\n            type = EspressoAssertionType.HAS_TEXT,\n            description = \"${interaction.simpleClassName()} assertion '${EspressoAssertionType.HAS_TEXT}' with resourceId '$resourceId' in '${elementInfo.name}' with root '${getInteractionRootMatcher()}' during ${getAssertionTimeout()} ms\"\n        )\n    }\n\n    fun hasText(stringMatcher: Matcher<String>) = apply {\n        executeAssertion(\n            operationBlock = getInteractionAssertionBlock(ViewMatchers.withText(stringMatcher)),\n            name = \"HasText with matcher '$stringMatcher' in '${elementInfo.name}'\",\n            type = EspressoAssertionType.HAS_TEXT,\n            description = \"${interaction.simpleClassName()} assertion '${EspressoAssertionType.HAS_TEXT}' with matcher '$stringMatcher' in '${elementInfo.name}' with root '${getInteractionRootMatcher()}' during ${getAssertionTimeout()} ms\"\n        )\n    }\n\n    fun textContains(text: String) = apply {\n        executeAssertion(\n            operationBlock = getInteractionAssertionBlock(\n                ViewMatchers.withText(\n                    Matchers.containsString(\n                        text\n                    )\n                )\n            ),\n            name = \"ContainsText '$text' in '${elementInfo.name}'\",\n            type = EspressoAssertionType.CONTAINS_TEXT,\n            description = \"${interaction.simpleClassName()} assertion '${EspressoAssertionType.CONTAINS_TEXT}' '$text' in '${elementInfo.name}' with root '${getInteractionRootMatcher()}' during ${getAssertionTimeout()} ms\"\n        )\n    }\n\n    fun hasContentDescription(text: String) = apply {\n        executeAssertion(\n            operationBlock = getInteractionAssertionBlock(\n                ViewMatchers.withContentDescription(\n                    text\n                )\n            ),\n            name = \"HasContentDescription '$text' in '${elementInfo.name}'\",\n            type = EspressoAssertionType.HAS_CONTENT_DESCRIPTION,\n            description = \"${interaction.simpleClassName()} assertion '${EspressoAssertionType.HAS_CONTENT_DESCRIPTION}' '$text' in '${elementInfo.name}' with root '${getInteractionRootMatcher()}' during ${getAssertionTimeout()} ms\"\n        )\n    }\n\n    fun hasContentDescription(resourceId: Int) = apply {\n        executeAssertion(\n            operationBlock = getInteractionAssertionBlock(\n                ViewMatchers.withContentDescription(\n                    resourceId\n                )\n            ),\n            name = \"HasContentDescription resourceId = '$resourceId' in '${elementInfo.name}'\",\n            type = EspressoAssertionType.HAS_CONTENT_DESCRIPTION,\n            description = \"${interaction.simpleClassName()} assertion '${EspressoAssertionType.HAS_CONTENT_DESCRIPTION}' resourceId = '$resourceId' in '${elementInfo.name}' with root '${getInteractionRootMatcher()}' during ${getAssertionTimeout()} ms\"\n        )\n    }\n\n    fun hasContentDescription(charSequenceMatcher: Matcher<CharSequence>) = apply {\n        executeAssertion(\n            operationBlock = getInteractionAssertionBlock(\n                ViewMatchers.withContentDescription(\n                    charSequenceMatcher\n                )\n            ),\n            name = \"HasContentDescription charSequenceMatcher = '$charSequenceMatcher' in '${elementInfo.name}'\",\n            type = EspressoAssertionType.HAS_CONTENT_DESCRIPTION,\n            description = \"${interaction.simpleClassName()} assertion '${EspressoAssertionType.HAS_CONTENT_DESCRIPTION}' charSequenceMatcher = '$charSequenceMatcher' in '${elementInfo.name}' with root '${getInteractionRootMatcher()}' during ${getAssertionTimeout()} ms\"\n        )\n    }\n\n    fun contentDescriptionContains(text: String) = apply {\n        executeAssertion(\n            operationBlock = getInteractionAssertionBlock(\n                ViewMatchers.withContentDescription(\n                    Matchers.containsString(text)\n                )\n            ),\n            name = \"ContentDescriptionContains text '$text' in '${elementInfo.name}'\",\n            type = EspressoAssertionType.CONTENT_DESCRIPTION_CONTAINS_TEXT,\n            description = \"${interaction.simpleClassName()} assertion '${EspressoAssertionType.CONTENT_DESCRIPTION_CONTAINS_TEXT}' text '$text' in '${elementInfo.name}' with root '${getInteractionRootMatcher()}' during ${getAssertionTimeout()} ms\"\n        )\n    }\n\n    /**\n     * Asserts that the provided condition matches for the interaction's view using a custom Espresso assertion.\n     *\n     * This method allows you to perform a custom Espresso assertion using a Matcher condition. The\n     * provided condition is evaluated against the interaction's view to determine whether the assertion\n     * passes. If the condition matches, the assertion is considered successful.\n     *\n     * @param condition The Matcher condition to evaluate against the interaction's view.\n     * @return An updated instance of the class.\n     */\n    fun assertMatches(condition: Matcher<View>) = apply {\n        executeAssertion(\n            operationBlock = getInteractionAssertionBlock(condition),\n            name = \"Custom assertion with '$condition' of '${elementInfo.name}'\",\n            type = EspressoAssertionType.ASSERT_MATCHES,\n            description = \"${interaction.simpleClassName()} assertion '${EspressoAssertionType.ASSERT_MATCHES}' with '$condition' of '${elementInfo.name}' with root '${getInteractionRootMatcher()}' during ${getAssertionTimeout()} ms\"\n        )\n    }\n\n    /**\n     * Asserts that a custom Espresso assertion matches the given condition using the provided\n     * parameters and assertion block.\n     *\n     * This method allows you to perform a custom Espresso assertion by providing the parameters\n     * for the assertion and a block of code that defines the assertion's condition. The assertion\n     * block receives a View as a parameter and is responsible for evaluating whether the condition\n     * is met for the given view.\n     *\n     * @param params The optional parameters for the Espresso assertion. If null, default assertion\n     *               parameters are used.\n     * @param block The block of code that defines the condition for the custom assertion. The block\n     *              receives a View as a parameter and returns a boolean value indicating whether\n     *              the condition is met.\n     * @return An updated instance of the class.\n     */\n    fun assertMatches(\n        params: UltronEspressoAssertionParams? = null,\n        block: (view: View) -> Boolean\n    ) = apply {\n        val assertionParams = params ?: getDefaultAssertionParams()\n        val matcher = object : BaseMatcher<View>() {\n            override fun describeTo(description: Description) {\n                description.appendText(assertionParams.descriptionToAppend)\n            }\n\n            override fun matches(actual: Any): Boolean = block(actual as View)\n        }\n        executeAssertion(\n            operationBlock = getInteractionAssertionBlock(matcher),\n            name = assertionParams.operationName,\n            description = assertionParams.operationDescription,\n            type = assertionParams.operationType\n        )\n    }\n\n    fun getInteractionActionBlock(viewAction: ViewAction): () -> Unit {\n        return when (interaction) {\n            is ViewInteraction -> {\n                { interaction.perform(viewAction) }\n            }\n\n            is DataInteraction -> {\n                { interaction.perform(viewAction) }\n            }\n\n            else -> throw UltronException(\"Unknown type of interaction provided!\")\n        }\n    }\n\n    fun getInteractionAssertionBlock(matcher: Matcher<View>): () -> Unit {\n        return when (interaction) {\n            is ViewInteraction -> {\n                { interaction.check(matches(matcher)) }\n            }\n\n            is DataInteraction -> {\n                { interaction.check(matches(matcher)) }\n            }\n\n            else -> throw UltronException(\"Unknown type of interaction provided!\")\n        }\n    }\n\n    fun getInteractionAssertionBlock(assertion: ViewAssertion): () -> Unit {\n        return when (interaction) {\n            is ViewInteraction -> {\n                { interaction.check(assertion) }\n            }\n\n            is DataInteraction -> {\n                { interaction.check(assertion) }\n            }\n\n            else -> throw UltronException(\"Unknown type of interaction provided!\")\n        }\n    }\n\n    fun getInteractionMatcher(): Matcher<View>? {\n        return when (interaction) {\n            is ViewInteraction -> {\n                interaction.getViewMatcher()\n            }\n\n            is DataInteraction -> {\n                interaction.getDataMatcher()\n            }\n\n            else -> throw UltronException(\"Unknown type of interaction provided!\")\n        }\n    }\n\n    fun getInteractionRootMatcher(): Matcher<Root>? {\n        return when (interaction) {\n            is ViewInteraction -> {\n                interaction.getRootMatcher()\n            }\n\n            is DataInteraction -> {\n                interaction.getRootMatcher()\n            }\n\n            else -> throw UltronException(\"Unknown type of interaction provided!\")\n        }\n    }\n\n    fun getUltronEspressoActionOperation(\n        operationBlock: () -> Unit, name: String,\n        type: UltronOperationType,\n        description: String\n    ) = UltronEspressoOperation(\n        operationBlock,\n        name,\n        type,\n        description,\n        getActionTimeout(),\n        assertion,\n        elementInfo\n    )\n\n    fun getUltronEspressoAssertionOperation(\n        operationBlock: () -> Unit, name: String,\n        type: UltronOperationType,\n        description: String\n    ) = UltronEspressoOperation(\n        operationBlock,\n        name,\n        type,\n        description,\n        getAssertionTimeout(),\n        assertion,\n        elementInfo\n    )\n\n    fun getActionResultHandler() =\n        this.resultHandler ?: UltronConfig.Espresso.ViewActionConfig.resultHandler\n\n    fun getAssertionResultHandler() =\n        this.resultHandler ?: UltronConfig.Espresso.ViewAssertionConfig.resultHandler\n\n    fun executeAction(operation: UltronEspressoOperation) =\n        UltronEspressoOperationLifecycle.execute(\n            EspressoActionExecutor(operation),\n            getActionResultHandler()\n        )\n\n    fun executeAction(\n        operationBlock: () -> Unit, name: String,\n        type: UltronOperationType = CommonOperationType.DEFAULT,\n        description: String\n    ) = executeAction(getUltronEspressoActionOperation(operationBlock, name, type, description))\n\n    fun executeAssertion(operation: UltronEspressoOperation) =\n        UltronEspressoOperationLifecycle.execute(\n            EspressoAssertionExecutor(operation),\n            getAssertionResultHandler()\n        )\n\n    fun executeAssertion(\n        operationBlock: () -> Unit, name: String,\n        type: UltronOperationType = CommonOperationType.DEFAULT,\n        description: String\n    ) = executeAssertion(\n        getUltronEspressoAssertionOperation(\n            operationBlock,\n            name,\n            type,\n            description\n        )\n    )\n\n    private fun getDefaultActionParams() = UltronEspressoActionParams(\n        operationName = \"Anonymous action to '${elementInfo.name}'\",\n        operationDescription = \"Anonymous action ${interaction.simpleClassName()} on '${elementInfo.name}' with root '${getInteractionRootMatcher()}' during ${getActionTimeout()} ms\"\n    )\n\n    private fun getDefaultAssertionParams() = UltronEspressoAssertionParams(\n        operationName = \"Anonymous assertion on '${elementInfo.name}'\",\n        operationDescription = \"Anonymous assertion on '${elementInfo.name}' with root '${getInteractionRootMatcher()}' during ${getAssertionTimeout()} ms\"\n    )\n}"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/espresso/UltronEspressoOperation.kt",
    "content": "package com.atiurin.ultron.core.espresso\n\nimport com.atiurin.ultron.core.common.DefaultOperationIterationResult\nimport com.atiurin.ultron.core.common.ElementInfo\nimport com.atiurin.ultron.core.common.DefaultElementInfo\nimport com.atiurin.ultron.core.common.Operation\nimport com.atiurin.ultron.core.common.OperationIterationResult\nimport com.atiurin.ultron.core.common.UltronOperationType\nimport com.atiurin.ultron.core.common.assertion.EmptyOperationAssertion\nimport com.atiurin.ultron.core.common.assertion.OperationAssertion\n\n/**\n * @param operationBlock represent an action or assertion block, for example\n * operationBlock = { onView(matcher).perform(viewAction) }\n *\n * if [operationBlock] doesn't throw any exception the operation executes successful\n * @param type specifies the type of operation to be executed.\n * Use one of [com.atiurin.ultron.core.espresso.action.EspressoActionType], [com.atiurin.ultron.core.espresso.assertion.EspressoAssertionType]\n */\nclass UltronEspressoOperation(\n    val operationBlock: () -> Unit,\n    override val name: String,\n    override val type: UltronOperationType,\n    override val description: String,\n    override val timeoutMs: Long,\n    override val assertion: OperationAssertion = EmptyOperationAssertion(),\n    override val elementInfo: ElementInfo = DefaultElementInfo()\n) : Operation {\n    fun withTimeout(timeoutMs: Long) = UltronEspressoOperation(\n        operationBlock,\n        name,\n        type,\n        description,\n        timeoutMs,\n        assertion,\n        elementInfo\n    )\n\n    fun withAssertion(assertion: OperationAssertion) = UltronEspressoOperation(\n        operationBlock,\n        name,\n        type,\n        description,\n        timeoutMs,\n        assertion,\n        elementInfo\n    )\n\n    override fun execute(): OperationIterationResult {\n        var success = true\n        var exception: Throwable? = null\n        try {\n            operationBlock()\n        } catch (error: Throwable) {\n            success = false\n            exception = error\n        }\n        return DefaultOperationIterationResult(success, exception)\n    }\n}"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/espresso/UltronEspressoOperationLifecycle.kt",
    "content": "package com.atiurin.ultron.core.espresso\n\nimport com.atiurin.ultron.core.common.AbstractOperationLifecycle\n\n\nobject UltronEspressoOperationLifecycle : AbstractOperationLifecycle()"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/espresso/UltronEspressoUiBlock.kt",
    "content": "package com.atiurin.ultron.core.espresso\n\nimport android.view.View\nimport androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA\nimport androidx.test.espresso.matcher.ViewMatchers.withParent\nimport com.atiurin.ultron.exceptions.UltronException\nimport com.atiurin.ultron.extensions.ultronInteraction\nimport com.atiurin.ultron.extensions.withName\nimport com.atiurin.ultron.log.UltronLog\nimport org.hamcrest.Matcher\nimport org.hamcrest.Matchers.allOf\nimport kotlin.reflect.typeOf\n\n/**\n * Base class for creating and manipulating UI blocks in Espresso testing.\n *\n * This class provides mechanisms for performing searches within the UI hierarchy,\n * allowing for efficient UI element matching and composition of UI blocks.\n * It supports both descendant and parent searches, focusing on elements within\n * the scope of this block.\n *\n * @param blockMatcher The matcher used to identify this UI block.\n * @param blockDescription A textual description of the UI block, used for better logging and debugging.\n */\nopen class UltronEspressoUiBlock(val blockMatcher: Matcher<View>, val blockDescription: String = \"\") {\n    /**\n     * Searches for a descendant element within the Espresso UI block.\n     *\n     * Constructs a matcher that represents a descendant element of this block,\n     * based on the provided `childMatcher`.\n     *\n     * @param childMatcher Matcher to locate the descendant element within this block.\n     * @return A matcher for the descendant element of this block.\n     */\n    fun _descendantSearch(childMatcher: Matcher<View>): Matcher<View> {\n        return allOf(isDescendantOfA(blockMatcher), childMatcher)\n    }\n\n    /**\n     * Searches for a direct child element of the current Espresso UI block.\n     *\n     * Constructs a matcher that represents a direct child element of this block,\n     * based on the provided `childMatcher`.\n     *\n     * @param childMatcher Matcher to locate the direct child element within this block.\n     * @return A matcher for the direct child element of this block.\n     */\n    fun _childSearch(childMatcher: Matcher<View>): Matcher<View> {\n        return allOf(withParent(blockMatcher), childMatcher)\n    }\n\n    /**\n     * A utility property for interaction with this UI block.\n     *\n     * Appends the block description to the matcher for better logging and debugging.\n     */\n    val uiBlock\n        get() = blockMatcher.let {\n            if (blockDescription.isNotBlank()) {\n                it.withName(blockDescription)\n            } else it.ultronInteraction()\n        }\n\n    /**\n     * Modifies the provided matcher based on the specified search type.\n     *\n     * This method applies a descendant or child search transformation to the given `childMatcher`.\n     * It is useful for updating matchers to narrow down or expand the scope of UI element matching.\n     *\n     * @param childMatcher The original matcher to be modified.\n     * @param descendantSearch Determines the type of search transformation:\n     *                         - `true` (default): Performs a descendant search.\n     *                         - `false`: Performs a direct child search.\n     * @return A new matcher that has been modified according to the search type.\n     */\n    fun child(childMatcher: Matcher<View>, descendantSearch: Boolean = true): Matcher<View> = when (descendantSearch) {\n        true -> _descendantSearch(childMatcher)\n        false -> _childSearch(childMatcher)\n    }\n\n    /**\n     * Creates a child UI block using a matcher modified based on the specified search type.\n     *\n     * This method applies a descendant or parent search transformation to the provided `childMatcher`\n     * and constructs a new UI block instance using the `uiBlockFactory`.\n     *\n     * @param childMatcher The matcher to be modified for the child UI block.\n     * @param descendantSearch Determines the type of search transformation:\n     *                         - `true` (default): Performs a descendant search.\n     *                         - `false`: Performs a direct child search.\n     * @param uiBlockFactory A factory function to create a new instance of the child UI block. The function\n     *                       takes the modified matcher as input and returns an instance of the UI block.\n     * @return A new instance of the child UI block, constructed with the modified matcher.\n     */\n    fun <B : UltronEspressoUiBlock> child(childMatcher: Matcher<View>, descendantSearch: Boolean = true, uiBlockFactory: (Matcher<View>) -> B): B {\n        val newMatcher = when (descendantSearch) {\n            true -> _descendantSearch(childMatcher)\n            false -> _childSearch(childMatcher)\n        }\n        return uiBlockFactory(newMatcher)\n    }\n\n    /**\n     * Creates a child UI block of a specified type using a matcher modified based on the given search type.\n     *\n     * This extension function simplifies the creation of a new instance of a child UI block by automatically invoking\n     * the appropriate constructor of the specified type [T]. The matcher of the current block is transformed\n     * to locate the desired child block based on the specified `descendantSearch` parameter.\n     *\n     * @param T The type of the child UI block to be created. It must extend [UltronEspressoUiBlock].\n     * @param uiBlock The existing instance of the child UI block to use as a template for the new block.\n     *                The `blockMatcher` and `blockDescription` properties of this block are used to create the new instance.\n     * @param descendantSearch Specifies the type of search:\n     *                         - `true` (default): Performs a descendant search.\n     *                         - `false`: Performs a direct parent search.\n     * @return A new instance of the specified type [T], initialized with the updated matcher.\n     *\n     * @throws UltronException If the specified class [T] does not have an appropriate constructor or cannot be instantiated.\n     *\n     * ### Constructor Requirements:\n     * The class [T] must meet the following conditions to be instantiated:\n     * 1. It must not be a nested or inner class. It should be defined at the top level or as a file-level class.\n     * 2. It must have one of the following constructors:\n     *    - A constructor with one parameter of type [Matcher<View>]:\n     *      `class CustomBlock(blockMatcher: Matcher<View>)`\n     *    - A constructor with two parameters: `blockMatcher` of type [Matcher<View>] and `blockDescription` of type [String]:\n     *      `class CustomBlock(blockMatcher: Matcher<View>, blockDescription: String)`\n     *\n     * If neither constructor is available, consider using an alternative method for child creation,\n     * such as `child(childMatcher, descendantSearch, uiBlockFactory)`.\n     *\n     * ### Alternative Method:\n     * If this method does not meet your requirements, consider using the universal method:\n     * ```kotlin\n     * fun <B : UltronEspressoUiBlock> child(\n     *     childMatcher: Matcher<View>,\n     *     descendantSearch: Boolean = true,\n     *     uiBlockFactory: (Matcher<View>) -> B\n     * ): B\n     * ```\n     *\n     * ### Usage Example:\n     * ```kotlin\n     * // UI block declaration\n     * class CustomElementBlock(blockMatcher: Matcher<View>, blockDescription: String = \"\") : UltronEspressoUiBlock(blockMatcher, blockDescription)\n     *\n     * class CustomComplexBlock(blockMatcher: Matcher<View>, blockDescription: String = \"\") : UltronEspressoUiBlock(blockMatcher, blockDescription) {\n     *     val custom = child(CustomElementBlock(withId(R.id.custom_id), \"Custom child block\"))\n     * }\n     *\n     * // Screen object\n     * object MyScreen : Screen<MyScreen>() {\n     *     val complexBlock = CustomComplexBlock(withContentDescription(\"Complex Block\"), \"Descriptive UI Block\")\n     * }\n     *\n     * // In a test\n     * MyScreen {\n     *     complexBlock.custom.performClick()\n     * }\n     * ```\n     */\n    inline fun <reified T : UltronEspressoUiBlock> child(\n        uiBlock: T,\n        descendantSearch: Boolean = true\n    ): T {\n        val newMatcher = when (descendantSearch) {\n            true -> _descendantSearch(uiBlock.blockMatcher)\n            false -> _childSearch(uiBlock.blockMatcher)\n        }\n        val updateBlock = runCatching {\n            T::class.constructors.forEach { constructor ->\n                UltronLog.info(\"Constructor: $constructor, Parameters: ${constructor.parameters.map { it.type }}\")\n            }\n            T::class.constructors.firstOrNull {\n                it.parameters.size == 2 && it.parameters.first().type == typeOf<Matcher<View>>()\n                        && it.parameters[1].type == typeOf<String>()\n            }?.let { constructor ->\n                return@runCatching constructor.call(newMatcher, uiBlock.blockDescription)\n            }\n            T::class.constructors.firstOrNull {\n                it.parameters.size == 1 && it.parameters.first().type == typeOf<Matcher<View>>()\n            }?.let {\n                return@runCatching it.call(newMatcher)\n            }\n            null\n        }.onFailure {\n            if (it is IllegalArgumentException) {\n                throw UltronException(\n                    \"${T::class.simpleName} has hidden java constructor parameters. ${T::class.simpleName} must be defined as a top-level class (not nested inside any other class).\"\n                )\n            } else throw UltronException(\"Unable to create updated ${T::class.simpleName}. Message: ${it.message}\")\n        }.getOrNull()\n        updateBlock?.let {\n            return it\n        } ?: throw UltronException(\n            \"\"\" |${T::class.simpleName} doesn't have an appropriate constructor with arguments: (Matcher<View>, String) or (Matcher<View>)  \n                |Ensure that the class meets the following conditions:\n                |1. ${T::class.simpleName} must not be defined inside another class. It should be a top-level or file-level class.\n                |2. ${T::class.simpleName} must have one of the following constructors:\n                |- A constructor with one parameter of type Matcher<View>:\n                |class ${T::class.simpleName}(blockMatcher: Matcher<View>) : UltronEspressoUiBlock(blockMatcher)\n                |- A constructor with two parameters: blockMatcher of type Matcher<View> and blockDescription of type String:\n                |class ${T::class.simpleName}(blockMatcher: Matcher<View>, blockDescription: String) : UltronEspressoUiBlock(blockMatcher, blockDescription)\n                |If neither constructor is available, consider using another method for child declaration:\n                |fun <B : UltronEspressoUiBlock> child(childMatcher: Matcher<View>, descendantSearch: Boolean = true, uiBlockFactory: (Matcher<View>) -> B): B\n            \"\"\".trimIndent()\n        )\n    }\n}"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/espresso/action/EspressoActionExecutor.kt",
    "content": "package com.atiurin.ultron.core.espresso.action\n\nimport com.atiurin.ultron.core.common.Operation\nimport com.atiurin.ultron.core.config.UltronConfig\nimport com.atiurin.ultron.core.espresso.EspressoOperationExecutor\nimport com.atiurin.ultron.core.espresso.UltronEspressoOperation\nimport kotlin.reflect.KClass\n\ninternal class EspressoActionExecutor(\n    operation: UltronEspressoOperation\n) : EspressoOperationExecutor<UltronEspressoOperation>(operation) {\n    override fun getAllowedExceptions(operation: Operation): List<KClass<out Throwable>> {\n        return UltronConfig.Espresso.ViewActionConfig.allowedExceptions.map { it.kotlin }\n    }\n}"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/espresso/action/EspressoActionType.kt",
    "content": "package com.atiurin.ultron.core.espresso.action\n\nimport com.atiurin.ultron.core.common.UltronOperationType\n\nenum class EspressoActionType : UltronOperationType {\n    CLICK, LONG_CLICK, DOUBLE_CLICK,\n    CLICK_TOP_LEFT, CLICK_TOP_RIGHT, CLICK_TOP_CENTER, CLICK_BOTTOM_CENTER, CLICK_BOTTOM_LEFT, CLICK_BOTTOM_RIGHT, CLICK_CENTER_RIGHT, CLICK_CENTER_LEFT,\n    TYPE_TEXT, REPLACE_TEXT, CLEAR_TEXT, PRESS_KEY,\n    SWIPE_LEFT, SWIPE_RIGHT, SWIPE_UP, SWIPE_DOWN, SCROLL,\n    CLOSE_SOFT_KEYBOARD, PRESS_BACK, OPEN_ACTION_BAR_OVERFLOW_OR_OPTION_MENU, OPEN_CONTEXTUAL_ACTION_MODE_OVERFLOW_MENU\n}"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/espresso/action/UltronCustomClickAction.kt",
    "content": "package com.atiurin.ultron.core.espresso.action\n\nimport android.graphics.Rect\nimport android.view.InputDevice\nimport android.view.MotionEvent\nimport android.view.View\nimport android.view.ViewConfiguration\nimport android.webkit.WebView\nimport androidx.test.espresso.PerformException\nimport androidx.test.espresso.UiController\nimport androidx.test.espresso.ViewAction\nimport androidx.test.espresso.action.CoordinatesProvider\nimport androidx.test.espresso.action.PrecisionDescriber\nimport androidx.test.espresso.action.Press\nimport androidx.test.espresso.action.Tap\nimport androidx.test.espresso.action.Tapper\nimport androidx.test.espresso.matcher.ViewMatchers\nimport androidx.test.espresso.util.HumanReadables\nimport org.hamcrest.Matcher\nimport java.util.Locale\n\n\nclass UltronCustomClickAction(\n    private val tapper: Tapper,\n    private val coordinatesProvider: CoordinatesProvider,\n    private val precisionDescriber: PrecisionDescriber = Press.THUMB,\n    private val inputDevice: Int = InputDevice.SOURCE_UNKNOWN,\n    private val buttonState: Int = MotionEvent.BUTTON_PRIMARY,\n    private val areaPercentage: Int,\n    /**\n     * negative value for LEFT direction, positive value for RIGHT direction\n     */\n    private val offsetX: Int = 0,\n    /**\n     * negative value for TOP direction, positive value for BOTTOM direction\n     */\n    private val offsetY: Int = 0\n) : ViewAction {\n\n    override fun getConstraints(): Matcher<View> {\n        return ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)\n    }\n\n    override fun getDescription(): String {\n        return tapper.toString().lowercase(Locale.ROOT) + \" click\"\n    }\n\n    override fun perform(uiController: UiController, view: View) {\n        if (!ViewMatchers.isDisplayingAtLeast(areaPercentage).matches(view)) {\n            val rect = Rect()\n            view.getDrawingRect(rect)\n            view.requestRectangleOnScreen(rect, true) // immediate is set to true, scrolling will not be animated.\n            uiController.loopMainThreadUntilIdle()\n        }\n\n        if (!ViewMatchers.isDisplayingAtLeast(areaPercentage).matches(view)) {\n            throw PerformException.Builder()\n                .withActionDescription(this.description)\n                .withViewDescription(HumanReadables.describe(view))\n                .withCause(\n                    java.lang.RuntimeException(\n                        \"Auto Scroll to view was attempted, but view is not displayed at least <$areaPercentage> %\"\n                    )\n                )\n                .build()\n        }\n\n\n        val coordinates = coordinatesProvider\n            .calculateCoordinates(view)\n            .apply {\n                this[0] += offsetX.toFloat()\n                this[1] += offsetY.toFloat()\n            }\n        val precision = precisionDescriber.describePrecision()\n\n        var status = Tapper.Status.FAILURE\n        var loopCount = 0\n        // Native event injection is quite a tricky process. A tap is actually 2\n        // seperate motion events which need to get injected into the system. Injection\n        // makes an RPC call from our app under test to the Android system server, the\n        // system server decides which window layer to deliver the event to, the system\n        // server makes an RPC to that window layer, that window layer delivers the event\n        // to the correct UI element, activity, or window object. Now we need to repeat\n        // that 2x. for a simple down and up. Oh and the down event triggers timers to\n        // detect whether or not the event is a long vs. short press. The timers are\n        // removed the moment the up event is received (NOTE: the possibility of eventTime\n        // being in the future is totally ignored by most motion event processors).\n        //\n        // Phew.\n        //\n        // The net result of this is sometimes we'll want to do a regular tap, and for\n        // whatever reason the up event (last half) of the tap is delivered after long\n        // press timeout (depending on system load) and the long press behaviour is\n        // displayed (EG: show a context menu). There is no way to avoid or handle this more\n        // gracefully. Also the longpress behavour is app/widget specific. So if you have\n        // a seperate long press behaviour from your short press, you can pass in a\n        // 'RollBack' SmartViewAction which when executed will undo the effects of long press.\n\n        // Native event injection is quite a tricky process. A tap is actually 2\n        // seperate motion events which need to get injected into the system. Injection\n        // makes an RPC call from our app under test to the Android system server, the\n        // system server decides which window layer to deliver the event to, the system\n        // server makes an RPC to that window layer, that window layer delivers the event\n        // to the correct UI element, activity, or window object. Now we need to repeat\n        // that 2x. for a simple down and up. Oh and the down event triggers timers to\n        // detect whether or not the event is a long vs. short press. The timers are\n        // removed the moment the up event is received (NOTE: the possibility of eventTime\n        // being in the future is totally ignored by most motion event processors).\n        //\n        // Phew.\n        //\n        // The net result of this is sometimes we'll want to do a regular tap, and for\n        // whatever reason the up event (last half) of the tap is delivered after long\n        // press timeout (depending on system load) and the long press behaviour is\n        // displayed (EG: show a context menu). There is no way to avoid or handle this more\n        // gracefully. Also the longpress behavour is app/widget specific. So if you have\n        // a seperate long press behaviour from your short press, you can pass in a\n        // 'RollBack' SmartViewAction which when executed will undo the effects of long press.\n        val action = \"perform: ${\n            String.format(\n                \"%s - At Coordinates: %d, %d and precision: %d, %d\",\n                this.description,\n                coordinates[0].toInt(),\n                coordinates[1].toInt(),\n                precision[0].toInt(),\n                precision[1].toInt()\n            )\n        }\"\n\n        while (status != Tapper.Status.SUCCESS && loopCount < 3) {\n            try {\n                status = tapper.sendTap(\n                    uiController, coordinates, precision, inputDevice, buttonState\n                )\n            } catch (re: RuntimeException) {\n                throw PerformException.Builder()\n                    .withActionDescription(\n                        String.format(\n                            \"%s - At Coordinates: %d, %d and precision: %d, %d\",\n                            this.description,\n                            coordinates[0].toInt(),\n                            coordinates[1].toInt(),\n                            precision[0].toInt(),\n                            precision[1].toInt()\n                        )\n                    )\n                    .withViewDescription(HumanReadables.describe(view))\n                    .withCause(re)\n                    .build()\n            }\n            val duration = ViewConfiguration.getPressedStateDuration()\n            // ensures that all work enqueued to process the tap has been run.\n            if (duration > 0) {\n                uiController.loopMainThreadForAtLeast(duration.toLong())\n            }\n            if (status == Tapper.Status.WARNING) {\n                break\n            }\n            loopCount++\n        }\n        if (status == Tapper.Status.FAILURE) {\n            val actionFail = String.format(\n                \"Couldn't \"\n                        + \"click at: %s,%s precision: %s, %s . Tapper: %s coordinate provider: %s precision \"\n                        + \"describer: %s. Tried %s times.\",\n                coordinates[0],\n                coordinates[1],\n                precision[0],\n                precision[1],\n                tapper,\n                coordinatesProvider,\n                precisionDescriber,\n                loopCount\n            )\n\n            throw PerformException.Builder()\n                .withActionDescription(this.description)\n                .withViewDescription(HumanReadables.describe(view))\n                .withCause(RuntimeException(actionFail)).build()\n        }\n\n        if (tapper === Tap.SINGLE && view is WebView) {\n            // WebViews will not process click events until double tap\n            // timeout. Not the best place for this - but good for now.\n            uiController.loopMainThreadForAtLeast(ViewConfiguration.getDoubleTapTimeout().toLong())\n        }\n    }\n}"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/espresso/action/UltronEspressoActionParams.kt",
    "content": "package com.atiurin.ultron.core.espresso.action\n\nimport android.view.View\nimport com.atiurin.ultron.core.common.CommonOperationType\nimport com.atiurin.ultron.core.common.UltronOperationType\nimport org.hamcrest.Matcher\nimport org.hamcrest.Matchers\n\ndata class UltronEspressoActionParams(\n    val operationName: String,\n    val operationDescription: String,\n    val operationType: UltronOperationType = CommonOperationType.DEFAULT,\n    val viewActionConstraints: Matcher<View> = Matchers.any(View::class.java),\n    val viewActionDescription: String = \"Anonymous ViewAction: specify params in perform/execute method to provide custom info about this action\"\n)\n"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/espresso/action/UltronSwipeAction.kt",
    "content": "package com.atiurin.ultron.core.espresso.action\n\nimport android.graphics.Rect\nimport android.view.View\nimport android.view.ViewConfiguration\nimport androidx.test.espresso.PerformException\nimport androidx.test.espresso.UiController\nimport androidx.test.espresso.ViewAction\nimport androidx.test.espresso.action.CoordinatesProvider\nimport androidx.test.espresso.action.PrecisionDescriber\nimport androidx.test.espresso.action.Swiper\nimport androidx.test.espresso.matcher.ViewMatchers\nimport androidx.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast\nimport androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility\nimport androidx.test.espresso.util.HumanReadables\nimport org.hamcrest.Matcher\nimport java.util.Locale\n\n/** Enables swiping across a view. */\nclass UltronSwipeAction(\n    private val swiper: Swiper,\n    private val startCoordinatesProvider: CoordinatesProvider,\n    private val endCoordinatesProvider: CoordinatesProvider,\n    private val precisionDescriber: PrecisionDescriber\n    ) : ViewAction {\n        companion object {\n            /** Maximum number of times to attempt sending a swipe action. */\n            const val MAX_TRIES: Int = 3\n\n            /** The minimum amount of a view that must be displayed in order to swipe across it. */\n            const val VIEW_DISPLAY_PERCENTAGE: Int = 90\n\n            /**\n             * The distance of a swipe's start position from the view's edge, in terms of the view's length.\n             * We do not start the swipe exactly on the view's edge, but somewhat more inward, since swiping\n             * from the exact edge may behave in an unexpected way (e.g. may open a navigation drawer).\n             */\n            const val EDGE_FUZZ_FACTOR = 0.083f\n        }\n\n    override fun getConstraints(): Matcher<View> {\n        return withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)\n    }\n\n    override fun perform(uiController: UiController, view: View) {\n        if (!isDisplayingAtLeast(VIEW_DISPLAY_PERCENTAGE).matches(view)) {\n            val rect = Rect()\n            view.getDrawingRect(rect)\n            view.requestRectangleOnScreen(rect, true) // immediate is set to true, scrolling will not be animated.\n            uiController.loopMainThreadUntilIdle()\n        }\n\n        if (!isDisplayingAtLeast(VIEW_DISPLAY_PERCENTAGE).matches(view)) {\n            throw PerformException.Builder()\n                .withActionDescription(this.description)\n                .withViewDescription(HumanReadables.describe(view))\n                .withCause(\n                    java.lang.RuntimeException(\n                        \"Auto Scroll to view was attempted, but view is not displayed at least <$VIEW_DISPLAY_PERCENTAGE> %\"\n                    )\n                )\n                .build()\n        }\n\n        val startCoordinates = startCoordinatesProvider.calculateCoordinates(view)\n        val endCoordinates = endCoordinatesProvider.calculateCoordinates(view)\n        val precision = precisionDescriber.describePrecision()\n\n        var status = Swiper.Status.FAILURE\n\n        var tries = 0\n        while (tries < MAX_TRIES && status != Swiper.Status.SUCCESS) {\n            try {\n                status = swiper.sendSwipe(uiController, startCoordinates, endCoordinates, precision)\n            } catch (re: RuntimeException) {\n                throw PerformException.Builder()\n                    .withActionDescription(this.description)\n                    .withViewDescription(HumanReadables.describe(view))\n                    .withCause(re)\n                    .build()\n            }\n\n            val duration = ViewConfiguration.getPressedStateDuration().toLong()\n            // ensures that all work enqueued to process the swipe has been run.\n            if (duration > 0) {\n                uiController.loopMainThreadForAtLeast(duration)\n            }\n            tries++\n        }\n\n        if (status == Swiper.Status.FAILURE) {\n            throw PerformException.Builder()\n                .withActionDescription(description)\n                .withViewDescription(HumanReadables.describe(view))\n                .withCause(\n                    RuntimeException(\n                        String.format(\n                            Locale.ROOT,\n                            \"Couldn't swipe from: %s,%s to: %s,%s precision: %s, %s . Swiper: %s \"\n                                    + \"start coordinate provider: %s precision describer: %s. Tried %s times\",\n                            startCoordinates[0],\n                            startCoordinates[1],\n                            endCoordinates[0],\n                            endCoordinates[1],\n                            precision[0],\n                            precision[1],\n                            swiper,\n                            startCoordinatesProvider,\n                            precisionDescriber,\n                            MAX_TRIES)))\n                .build()\n        }\n    }\n\n    override fun getDescription(): String {\n        return swiper.toString().lowercase(Locale.getDefault()) + \" swipe\"\n    }\n}"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/espresso/action/UltronTypeTextAction.kt",
    "content": "package com.atiurin.ultron.core.espresso.action\n\nimport android.util.Log\nimport android.view.InputDevice\nimport android.view.MotionEvent\nimport android.view.View\nimport android.widget.SearchView\nimport androidx.test.espresso.InjectEventSecurityException\nimport androidx.test.espresso.PerformException\nimport androidx.test.espresso.UiController\nimport androidx.test.espresso.ViewAction\nimport androidx.test.espresso.action.GeneralClickAction\nimport androidx.test.espresso.action.GeneralLocation\nimport androidx.test.espresso.action.Press\nimport androidx.test.espresso.action.Tap\nimport androidx.test.espresso.action.TypeTextAction\nimport androidx.test.espresso.matcher.ViewMatchers\nimport androidx.test.espresso.remote.annotation.RemoteMsgConstructor\nimport androidx.test.espresso.remote.annotation.RemoteMsgField\nimport androidx.test.espresso.util.HumanReadables\nimport org.hamcrest.Matcher\nimport org.hamcrest.Matchers\nimport java.util.Locale\n\n/** Enables typing text on views.  */\nclass UltronTypeTextAction (\n    @RemoteMsgField(order = 0) val stringToBeTyped: String,\n    @RemoteMsgField(order = 1) val tapToFocus: Boolean = true,\n    // The click action to use when tapping to focus is needed before typing in text.\n    private val clickAction: ViewAction? = defaultClickAction()\n) :\n    ViewAction {\n\n    /**\n     * Constructs [androidx.test.espresso.action.TypeTextAction] with given string. If the string is empty it results in no-op\n     * (nothing is typed).\n     *\n     * @param stringToBeTyped String To be typed by [androidx.test.espresso.action.TypeTextAction]\n     * @param tapToFocus indicates whether a tap should be sent to the underlying view before typing.\n     */\n    @RemoteMsgConstructor\n    constructor(stringToBeTyped: String, tapToFocus: Boolean) : this(\n        stringToBeTyped,\n        tapToFocus,\n        null\n    )\n\n    override fun getConstraints(): Matcher<View> {\n        var matchers = Matchers.allOf(ViewMatchers.isDisplayed())\n        if (!tapToFocus) {\n            matchers = Matchers.allOf(matchers, ViewMatchers.hasFocus())\n        }\n        // SearchView does not support input methods itself (rather it delegates to an internal text\n        // view for input).\n        return Matchers.allOf(\n            matchers, Matchers.anyOf(\n                ViewMatchers.supportsInputMethods(), ViewMatchers.isAssignableFrom(\n                    SearchView::class.java\n                )\n            )\n        )\n    }\n\n    override fun perform(uiController: UiController, view: View) {\n        // No-op if string is empty.\n        if (stringToBeTyped.isEmpty()) {\n            Log.w(TAG, \"Supplied string is empty resulting in no-op (nothing is typed).\")\n            return\n        }\n\n        if (tapToFocus) {\n            // Perform a click.\n            if (clickAction == null) {\n                // Uses the default click action if none is specified.\n                defaultClickAction().perform(uiController, view)\n            } else {\n                clickAction.perform(uiController, view)\n            }\n            uiController.loopMainThreadUntilIdle()\n        }\n\n        try {\n            if (!uiController.injectString(stringToBeTyped)) {\n                Log.e(\n                    TAG,\n                    \"Failed to type text: $stringToBeTyped\"\n                )\n                throw PerformException.Builder()\n                    .withActionDescription(this.description)\n                    .withViewDescription(HumanReadables.describe(view))\n                    .withCause(RuntimeException(\"Failed to type text: $stringToBeTyped\"))\n                    .build()\n            }\n        } catch (e: InjectEventSecurityException) {\n            Log.e(\n                TAG,\n                \"Failed to type text: $stringToBeTyped\"\n            )\n            throw PerformException.Builder()\n                .withActionDescription(this.description)\n                .withViewDescription(HumanReadables.describe(view))\n                .withCause(e)\n                .build()\n        }\n    }\n\n    override fun getDescription(): String {\n        return String.format(Locale.ROOT, \"type text(%s)\", stringToBeTyped)\n    }\n\n    companion object {\n        private val TAG: String = TypeTextAction::class.java.simpleName\n\n        private fun defaultClickAction(): GeneralClickAction {\n            return GeneralClickAction(\n                Tap.SINGLE,\n                GeneralLocation.CENTER,\n                Press.FINGER,\n                InputDevice.SOURCE_UNKNOWN,\n                MotionEvent.BUTTON_PRIMARY\n            )\n        }\n    }\n}"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/espresso/assertion/EspressoAssertionExecutor.kt",
    "content": "package com.atiurin.ultron.core.espresso.assertion\n\nimport com.atiurin.ultron.core.common.Operation\nimport com.atiurin.ultron.core.config.UltronConfig\nimport com.atiurin.ultron.core.espresso.EspressoOperationExecutor\nimport com.atiurin.ultron.core.espresso.UltronEspressoOperation\nimport kotlin.reflect.KClass\n\ninternal class EspressoAssertionExecutor(\n    operation: UltronEspressoOperation\n) : EspressoOperationExecutor<UltronEspressoOperation>(operation) {\n    override fun getAllowedExceptions(operation: Operation): List<KClass<out Throwable>> {\n        return UltronConfig.Espresso.ViewAssertionConfig.allowedExceptions.map { it.kotlin }\n    }\n}"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/espresso/assertion/EspressoAssertionType.kt",
    "content": "package com.atiurin.ultron.core.espresso.assertion\n\nimport com.atiurin.ultron.core.common.UltronOperationType\n\nenum class EspressoAssertionType :\n    UltronOperationType {\n    IS_DISPLAYED, IS_NOT_DISPLAYED, IS_COMPLETELY_DISPLAYED, IS_DISPLAYING_AT_LEAST,\n    IS_VISIBLE,\n    DOES_NOT_EXIST, EXISTS,\n    IS_ENABLED, IS_NOT_ENABLED,\n    IS_SELECTED, IS_NOT_SELECTED,\n    IS_CLICKABLE, IS_NOT_CLICKABLE,\n    IS_CHECKED, IS_NOT_CHECKED,\n    IS_FOCUSABLE, IS_NOT_FOCUSABLE, HAS_FOCUS,\n    IS_JS_ENABLED,\n    HAS_TEXT, CONTAINS_TEXT,\n    HAS_CONTENT_DESCRIPTION, CONTENT_DESCRIPTION_CONTAINS_TEXT,\n    ASSERT_MATCHES, IDENTIFY_RECYCLER_VIEW\n}"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/espresso/assertion/UltronEspressoAssertionParams.kt",
    "content": "package com.atiurin.ultron.core.espresso.assertion\n\nimport com.atiurin.ultron.core.common.UltronOperationType\n\ndata class UltronEspressoAssertionParams(\n    val operationName: String,\n    val operationDescription: String,\n    val operationType: UltronOperationType = EspressoAssertionType.ASSERT_MATCHES,\n    val descriptionToAppend: String = \"Default assert matcher description\"\n)\n"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/espresso/recyclerview/RecyclerViewItemExecutor.kt",
    "content": "package com.atiurin.ultron.core.espresso.recyclerview\n\nimport android.view.View\nimport androidx.recyclerview.widget.RecyclerView\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.ViewInteraction\nimport com.atiurin.ultron.core.espresso.UltronEspressoInteraction\nimport org.hamcrest.Matcher\n\ninterface RecyclerViewItemExecutor {\n    fun scrollToItem(offset: Int = 0)\n    fun getItemMatcher(): Matcher<View>\n    fun getItemViewHolder(): RecyclerView.ViewHolder?\n    fun getItemInteraction(): UltronEspressoInteraction<ViewInteraction> = UltronEspressoInteraction(onView(getItemMatcher()))\n    fun getItemChildMatcher(childMatcher: Matcher<View>): Matcher<View>\n    fun getItemChildInteraction(childInteraction: UltronEspressoInteraction<ViewInteraction>): UltronEspressoInteraction<ViewInteraction> = UltronEspressoInteraction(\n        onView((getItemChildMatcher(childInteraction.getInteractionMatcher()!!)))\n    )\n}"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/espresso/recyclerview/RecyclerViewItemMatchingExecutor.kt",
    "content": "package com.atiurin.ultron.core.espresso.recyclerview\n\nimport android.view.View\nimport androidx.recyclerview.widget.RecyclerView\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.ViewInteraction\nimport com.atiurin.ultron.core.espresso.UltronEspressoInteraction\nimport org.hamcrest.Matcher\n\nclass RecyclerViewItemMatchingExecutor(\n    private val ultronRecyclerView: UltronRecyclerView,\n    private val itemViewInteraction: UltronEspressoInteraction<ViewInteraction>,\n) : RecyclerViewItemExecutor {\n    override fun scrollToItem(offset: Int) {\n        ultronRecyclerView.scrollToItem(itemViewInteraction, offset = offset)\n    }\n\n    override fun getItemMatcher(): Matcher<View> {\n        return ultronRecyclerView.atItem(itemViewInteraction.getInteractionMatcher()!!)\n    }\n\n    override fun getItemViewHolder(): RecyclerView.ViewHolder? {\n        return ultronRecyclerView.getViewHolderList(itemViewInteraction.getInteractionMatcher()!!).firstOrNull()\n    }\n\n    override fun getItemInteraction(): UltronEspressoInteraction<ViewInteraction> {\n        return UltronEspressoInteraction(onView(getItemMatcher()))\n            .withName(\"RecyclerViewItem with: '${itemViewInteraction.elementInfo.name}', RecyclerView: ${ultronRecyclerView.recyclerViewInteraction.elementInfo.name}\")\n    }\n\n    override fun getItemChildMatcher(childMatcher: Matcher<View>): Matcher<View> {\n        return ultronRecyclerView.atItemChild(itemViewInteraction.getInteractionMatcher()!!, childMatcher)\n    }\n\n    override fun getItemChildInteraction(childInteraction: UltronEspressoInteraction<ViewInteraction>): UltronEspressoInteraction<ViewInteraction> {\n        return UltronEspressoInteraction(onView(getItemChildMatcher(childInteraction.getInteractionMatcher()!!)))\n            .withName(\"'${childInteraction.elementInfo.name}' of RecyclerViewItem with: '${itemViewInteraction.elementInfo.name}', RecyclerView: ${ultronRecyclerView.recyclerViewInteraction.elementInfo.name}\")\n    }\n}"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/espresso/recyclerview/RecyclerViewItemPositionalExecutor.kt",
    "content": "package com.atiurin.ultron.core.espresso.recyclerview\n\nimport android.view.View\nimport androidx.recyclerview.widget.RecyclerView\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.ViewInteraction\nimport com.atiurin.ultron.core.espresso.UltronEspressoInteraction\nimport com.atiurin.ultron.exceptions.UltronOperationException\nimport org.hamcrest.Matcher\n\nclass RecyclerViewItemPositionalExecutor(\n    private val ultronRecyclerView: UltronRecyclerView,\n    private val position: Int\n) : RecyclerViewItemExecutor {\n    init {\n        if (position < 0) {\n            throw UltronOperationException(\"Position value can't be negative: '$position'\")\n        }\n    }\n\n    override fun scrollToItem(offset: Int) {\n        ultronRecyclerView.scrollToItem(position, offset)\n    }\n\n    override fun getItemMatcher(): Matcher<View> {\n        return ultronRecyclerView.atPosition(position)\n    }\n\n    override fun getItemViewHolder(): RecyclerView.ViewHolder? {\n        return ultronRecyclerView.getViewHolderAtPosition(position)\n    }\n\n    override fun getItemInteraction(): UltronEspressoInteraction<ViewInteraction> {\n        return UltronEspressoInteraction(onView(getItemMatcher()))\n            .withName(\"RecyclerViewItem at position: '$position', RecyclerView: ${ultronRecyclerView.recyclerViewInteraction.elementInfo.name}\")\n    }\n\n    override fun getItemChildMatcher(childMatcher: Matcher<View>): Matcher<View> {\n        return ultronRecyclerView.atPositionItemChild(position, childMatcher)\n    }\n\n    override fun getItemChildInteraction(childInteraction: UltronEspressoInteraction<ViewInteraction>): UltronEspressoInteraction<ViewInteraction> {\n        return UltronEspressoInteraction(onView(getItemChildMatcher(childInteraction.getInteractionMatcher()!!)))\n            .withName(\"'${childInteraction.elementInfo.name}' of RecyclerViewItem at position: '$position', RecyclerView: ${ultronRecyclerView.recyclerViewInteraction.elementInfo.name}\")\n    }\n}"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/espresso/recyclerview/RecyclerViewScrollAction.kt",
    "content": "package com.atiurin.ultron.core.espresso.recyclerview\n\nimport android.view.View\nimport androidx.recyclerview.widget.RecyclerView\nimport androidx.test.espresso.UiController\nimport androidx.test.espresso.ViewAction\nimport androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom\nimport androidx.test.espresso.matcher.ViewMatchers.isDisplayed\nimport com.atiurin.ultron.exceptions.UltronOperationException\nimport com.atiurin.ultron.extensions.instantScrollToPosition\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.runBlocking\nimport org.hamcrest.Matcher\nimport org.hamcrest.Matchers.allOf\n\ninternal class RecyclerViewScrollAction(\n    private val itemMatcher: Matcher<View>,\n    private val itemSearchLimit: Int = -1,\n    private val offset: Int = 0\n) : ViewAction {\n\n    override fun getConstraints(): Matcher<View> {\n        return allOf(\n            isAssignableFrom(RecyclerView::class.java),\n            isDisplayed()\n        )\n    }\n\n    override fun getDescription(): String {\n        return \"Scroll RecyclerView to item $itemMatcher${if (itemSearchLimit >= 0) \" with offset = $offset and search limit = $itemSearchLimit\" else \"\"}\"\n    }\n\n    override fun perform(uiController: UiController, view: View) {\n\n        val recyclerView = view as RecyclerView\n        val viewHolderMatcher: Matcher<RecyclerView.ViewHolder> = viewHolderMatcher(itemMatcher)\n\n        val matchedItem =\n            itemsMatching(recyclerView, viewHolderMatcher, 1, itemSearchLimit).firstOrNull()\n                ?: throw UltronOperationException(\"The scroll action could not be performed because no matching element was found using the matcher: $itemMatcher\")\n\n        val finalPositionToScroll = matchedItem.position + offset\n\n        recyclerView.instantScrollToPosition(finalPositionToScroll, 0.5f)\n        runBlocking { delay(10) }\n    }\n}"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/espresso/recyclerview/RecyclerViewScrollToPositionViewAction.kt",
    "content": "package com.atiurin.ultron.core.espresso.recyclerview\n\nimport android.view.View\nimport androidx.recyclerview.widget.RecyclerView\nimport androidx.test.espresso.UiController\nimport androidx.test.espresso.ViewAction\nimport androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom\nimport androidx.test.espresso.matcher.ViewMatchers.isDisplayed\nimport com.atiurin.ultron.extensions.instantScrollToPosition\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.runBlocking\nimport org.hamcrest.Matcher\nimport org.hamcrest.Matchers.allOf\n\ninternal class RecyclerViewScrollToPositionViewAction (\n    private val position: Int\n) : ViewAction {\n    override fun getConstraints(): Matcher<View> {\n        return allOf(isAssignableFrom(RecyclerView::class.java), isDisplayed())\n    }\n\n    override fun getDescription(): String {\n        return \"scroll RecyclerView to position: $position\"\n    }\n\n    override fun perform(uiController: UiController, view: View) {\n        val recyclerView = view as RecyclerView\n        recyclerView.instantScrollToPosition(position, 0.5f)\n        runBlocking { delay(10) }\n    }\n}"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/espresso/recyclerview/RecyclerViewUtils.kt",
    "content": "package com.atiurin.ultron.core.espresso.recyclerview\n\nimport android.view.View\nimport androidx.recyclerview.widget.RecyclerView\nimport androidx.test.espresso.util.HumanReadables\nimport com.atiurin.ultron.utils.runOnUiThread\nimport org.hamcrest.Description\nimport org.hamcrest.Matcher\nimport org.hamcrest.TypeSafeMatcher\n\n@Suppress(\"UNCHECKED_CAST\")\ninternal fun <VH : RecyclerView.ViewHolder> itemsMatching(\n    recyclerView: RecyclerView,\n    viewHolderMatcher: Matcher<VH>,\n    maxItemsCount: Int = -1,\n    itemSearchLimit: Int = -1\n): List<MatchedItem> {\n    val matchedItems = mutableListOf<MatchedItem>()\n    val adapter = recyclerView.adapter ?: return matchedItems\n    val layoutManager = recyclerView.layoutManager ?: return matchedItems\n\n    val itemCount = adapter.itemCount\n    if (itemCount <= 0) return matchedItems\n\n    val searchLimit = if (itemSearchLimit in 1 until itemCount) itemSearchLimit else itemCount\n    if (maxItemsCount == 0) return matchedItems\n\n    fun addMatch(position: Int, viewHolder: VH) {\n        matchedItems.add(\n            MatchedItem(\n                position,\n                HumanReadables.getViewHierarchyErrorMessage(\n                    viewHolder.itemView,\n                    null,\n                    \"\\n\\n*** Matched ViewHolder at position: $position ***\",\n                    null\n                )\n            )\n        )\n    }\n\n    runOnUiThread {\n        val visibleHolders = mutableSetOf<Int>()\n        for (i in 0 until layoutManager.childCount) {\n            val child = layoutManager.getChildAt(i) ?: continue\n            val position = recyclerView.getChildAdapterPosition(child)\n            if (position in 0 until searchLimit) {\n                val viewHolder = recyclerView.getChildViewHolder(child) as? VH ?: continue\n                if (viewHolderMatcher.matches(viewHolder)) {\n                    addMatch(position, viewHolder)\n                    visibleHolders.add(position)\n                    if (maxItemsCount > 0 && matchedItems.size >= maxItemsCount) return@runOnUiThread\n                }\n            }\n        }\n\n        for (position in 0 until searchLimit) {\n            if (maxItemsCount > 0 && matchedItems.size >= maxItemsCount) break\n            if (position in visibleHolders) continue\n            if (position >= adapter.itemCount) continue\n\n            val itemType = adapter.getItemViewType(position)\n            val tempHolder = adapter.createViewHolder(recyclerView, itemType) as VH\n            adapter.bindViewHolder(tempHolder, position)\n            if (viewHolderMatcher.matches(tempHolder)) {\n                addMatch(position, tempHolder)\n            }\n        }\n    }\n    return matchedItems\n}\n\n/**\n * Wrapper for matched items in recycler view which contains position and description of matched\n * view.\n */\ninternal class MatchedItem(val position: Int, val description: String) {\n\n    override fun toString(): String {\n        return description\n    }\n}\n\n/**\n * Creates matcher for view holder with given item view matcher.\n *\n * @param itemViewMatcher a item view matcher which is used to match item.\n * @return a matcher which matches a view holder containing item matching itemViewMatcher.\n */\ninternal fun <VH : RecyclerView.ViewHolder> viewHolderMatcher(\n    itemViewMatcher: Matcher<View>\n): Matcher<VH> {\n    return object : TypeSafeMatcher<VH>() {\n        override fun matchesSafely(item: VH): Boolean {\n            return itemViewMatcher.matches(item.itemView)\n        }\n\n        override fun describeTo(description: Description) {\n            description.appendText(\"holder with view: \")\n            itemViewMatcher.describeTo(description)\n        }\n    }\n}\n"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/espresso/recyclerview/UltronRecyclerView.kt",
    "content": "package com.atiurin.ultron.core.espresso.recyclerview\n\nimport android.os.SystemClock\nimport android.view.View\nimport androidx.annotation.IntegerRes\nimport androidx.recyclerview.widget.RecyclerView\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.ViewInteraction\nimport androidx.test.espresso.assertion.ViewAssertions.matches\nimport androidx.test.espresso.matcher.BoundedMatcher\nimport androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA\nimport androidx.test.espresso.matcher.ViewMatchers.withId\nimport com.atiurin.ultron.core.config.UltronConfig.Espresso.Companion.RECYCLER_VIEW_IMPLEMENTATION\nimport com.atiurin.ultron.core.config.UltronConfig.Espresso.Companion.RECYCLER_VIEW_ITEM_SEARCH_LIMIT\nimport com.atiurin.ultron.core.config.UltronConfig.Espresso.Companion.RECYCLER_VIEW_LOAD_TIMEOUT\nimport com.atiurin.ultron.core.config.UltronConfig.Espresso.Companion.RECYCLER_VIEW_OPERATIONS_TIMEOUT\nimport com.atiurin.ultron.core.espresso.EspressoOperationResult\nimport com.atiurin.ultron.core.espresso.UltronEspressoInteraction\nimport com.atiurin.ultron.core.espresso.UltronEspressoOperation\nimport com.atiurin.ultron.core.espresso.assertion.EspressoAssertionType\nimport com.atiurin.ultron.exceptions.UltronOperationException\nimport com.atiurin.ultron.extensions.*\nimport com.atiurin.ultron.utils.AssertUtils\nimport org.hamcrest.Description\nimport org.hamcrest.Matcher\nimport org.hamcrest.TypeSafeMatcher\n\n/**\n *  Provides a set of interactions with RecyclerView list.\n *  @param recyclerViewInteraction help ot identify RecyclerView inside view hierarchy\n *  @param loadTimeoutMs specifies a time of waiting while RecyclerView items will be loaded\n *  @param itemSearchLimit set an amount of RecyclerView items to be researched for target item. There is no limit by default\n *  [itemSearchLimit] is applied for matcher search. If you're looking for RecyclerView item by position it isn't used.\n *  @param operationTimeoutMs specifies a timeout for actions and assertions on [UltronRecyclerView]. [UltronRecyclerViewItem] has it own timeout.\n *  @param recyclerImpl specifies a recycler item child matcher implementation\n */\nopen class UltronRecyclerView(\n    val recyclerViewInteraction: UltronEspressoInteraction<ViewInteraction>,\n    val loadTimeoutMs: Long = RECYCLER_VIEW_LOAD_TIMEOUT,\n    private val itemSearchLimit: Int = RECYCLER_VIEW_ITEM_SEARCH_LIMIT,\n    private var operationTimeoutMs: Long = RECYCLER_VIEW_OPERATIONS_TIMEOUT,\n    private val recyclerImpl: UltronRecyclerViewImpl = RECYCLER_VIEW_IMPLEMENTATION\n) {\n    constructor(\n        recyclerViewMatcher: Matcher<View>,\n        loadTimeoutMs: Long = RECYCLER_VIEW_LOAD_TIMEOUT,\n        itemSearchLimit: Int = RECYCLER_VIEW_ITEM_SEARCH_LIMIT,\n        operationTimeoutMs: Long = RECYCLER_VIEW_OPERATIONS_TIMEOUT,\n        recyclerImpl: UltronRecyclerViewImpl = RECYCLER_VIEW_IMPLEMENTATION\n    ): this(\n        recyclerViewInteraction = UltronEspressoInteraction(onView(recyclerViewMatcher)),\n        loadTimeoutMs = loadTimeoutMs,\n        itemSearchLimit = itemSearchLimit,\n        operationTimeoutMs = operationTimeoutMs,\n        recyclerImpl = recyclerImpl\n    )\n\n    val recyclerViewMatcher: Matcher<View>\n        get() = recyclerViewInteraction.getInteractionMatcher() ?: throw UltronOperationException(\"RecyclerView matcher is not specified\")\n\n    private var recyclerView: RecyclerView? = null\n\n    /**\n     * @return current [UltronRecyclerView] operations timeout\n     */\n    fun getTimeout() = operationTimeoutMs\n\n    /** @return [UltronRecyclerViewItem] subclass instance matches [matcher]\n     *\n     * Note: never add inner modifier to [T] class\n     *\n     * Note: [T] class should have a constructor without parameters, eg\n     *\n     *    class SomeRecyclerViewItem : UltronRecyclerViewItem(){...}\n     * */\n    inline fun <reified T : UltronRecyclerViewItem> getItem(\n        matcher: Matcher<View>,\n        autoScroll: Boolean = true,\n        scrollOffset: Int = 0\n    ): T {\n        waitItemsLoaded()\n        return UltronRecyclerViewItem.getInstance(this, UltronEspressoInteraction(onView(matcher)), autoScroll, scrollOffset)\n    }\n\n    inline fun <reified T : UltronRecyclerViewItem> getItem(\n        interaction: UltronEspressoInteraction<ViewInteraction>,\n        autoScroll: Boolean = true,\n        scrollOffset: Int = 0\n    ): T {\n        waitItemsLoaded()\n        return UltronRecyclerViewItem.getInstance(this, interaction, autoScroll, scrollOffset)\n    }\n\n    /** @return [UltronRecyclerViewItem] subclass instance at position [position]\n     *\n     * Note: never add inner modifier to [T] class\n     *\n     * Note: [T] class should have a constructor without parameters, eg\n     *\n     *    class SomeRecyclerViewItem : UltronRecyclerViewItem(){...}\n     * */\n    inline fun <reified T : UltronRecyclerViewItem> getItem(\n        position: Int,\n        autoScroll: Boolean = true,\n        scrollOffset: Int = 0\n    ): T {\n        waitItemsLoaded()\n        return UltronRecyclerViewItem.getInstance(this, position, autoScroll, scrollOffset)\n    }\n\n    /** @return [UltronRecyclerViewItem] subclass instance at first position\n     *\n     * Note: never add inner modifier to [T] class\n     *\n     * Note: [T] class should have a constructor without parameters, eg\n     *\n     *    class SomeRecyclerViewItem : UltronRecyclerViewItem(){...}\n     * */\n    inline fun <reified T : UltronRecyclerViewItem> getFirstItem(autoScroll: Boolean = true) =\n        getItem<T>(0, autoScroll)\n\n    /** @return [UltronRecyclerViewItem] subclass instance at last position\n     *\n     * Note: never add inner modifier to [T] class\n     *\n     * Note: [T] class should have a constructor without parameters, eg\n     *\n     *    class SomeRecyclerViewItem : UltronRecyclerViewItem(){...}\n     * */\n    inline fun <reified T : UltronRecyclerViewItem> getLastItem(autoScroll: Boolean = true): T {\n        waitItemsLoaded()\n        return UltronRecyclerViewItem.getInstance(this, getSize() - 1, autoScroll)\n    }\n\n    /**\n     * @return first [T] matched [matcher]\n     *\n     * it could be used if you can't determine unique matcher for item\n     *\n     * @param matcher determines how to find item\n     * @param autoScroll evaluate scrollTo this item in case of true value\n     */\n    inline fun <reified T : UltronRecyclerViewItem> getFirstItemMatched(\n        matcher: Matcher<View>,\n        autoScroll: Boolean = true,\n        scrollOffset: Int = 0\n    ) = getItemMatched<T>(matcher, 0, autoScroll, scrollOffset)\n\n    /**\n     * @return [T] matched [matcher]\n     *\n     * it could be used if you can't determine unique matcher for item, eg\n     *\n     * RecyclerView has several matched items. You can get any of them by specifying desired [index] value\n     *\n     * @param matcher determines how to find items\n     * @param index value from 0 to lastIndex of matched items\n     * @param autoScroll evaluate scrollTo this item in case of true value\n     */\n    inline fun <reified T : UltronRecyclerViewItem> getItemMatched(\n        matcher: Matcher<View>,\n        index: Int,\n        autoScroll: Boolean = true,\n        scrollOffset: Int = 0\n    ): T {\n        waitItemsLoaded()\n        val position = AssertUtils.assertTrueAndReturnValue(\n            valueBlock = { getItemAdapterPositionAtIndex(matcher, index) },\n            assertionBlock = { value -> value >= 0 },\n            timeoutMs = getTimeout(),\n            desc = \"RecyclerView(${recyclerViewInteraction.elementInfo.name}) has item matched '$matcher' with index $index\"\n        )\n        return UltronRecyclerViewItem.getInstance(this, position, autoScroll, scrollOffset)\n    }\n\n    /**\n     * @return last [T] matched [matcher]\n     *\n     * it could be used if you can't determine unique matcher for item\n     *\n     * @param matcher determines how to find item\n     * @param autoScroll evaluate scrollTo this item in case of true value\n     */\n    inline fun <reified T : UltronRecyclerViewItem> getLastItemMatched(\n        matcher: Matcher<View>,\n        autoScroll: Boolean = true,\n        scrollOffset: Int = 0\n    ): T {\n        waitItemsLoaded()\n        val position = AssertUtils.assertTrueAndReturnValue(\n            valueBlock = { getItemsAdapterPositionList(matcher).lastOrNull() ?: -1 },\n            assertionBlock = { value -> value >= 0 },\n            timeoutMs = getTimeout(),\n            desc = \"RecyclerView(${recyclerViewInteraction.elementInfo.name}) has last item matched '$matcher'\"\n        )\n        return UltronRecyclerViewItem.getInstance(this, position, autoScroll, scrollOffset)\n    }\n\n    /** @return simple [UltronRecyclerViewItem] matches '[matcher]'\n     * @param matcher helps to identify unique item in RecyclerView list\n     * @param autoScroll evaluate scrollTo matched item in case of true value\n     * */\n    fun item(matcher: Matcher<View>, autoScroll: Boolean = true, scrollOffset: Int = 0): UltronRecyclerViewItem {\n        waitItemsLoaded()\n        return UltronRecyclerViewItem(this, matcher, autoScroll, scrollOffset)\n    }\n\n    /** @return simple [UltronRecyclerViewItem] at [position]\n     * @param position of item in RecyclerView list\n     * @param autoScroll evaluate scrollTo item at [position] in case of true value\n     * */\n    fun item(position: Int, autoScroll: Boolean = true, scrollOffset: Int = 0): UltronRecyclerViewItem {\n        waitItemsLoaded()\n        return UltronRecyclerViewItem(this, position, autoScroll, scrollOffset)\n    }\n\n    /** @return [UltronRecyclerViewItem] at first position */\n    fun firstItem(autoScroll: Boolean = true) = item(0, autoScroll)\n\n    /** @return [UltronRecyclerViewItem] at last position */\n    fun lastItem(autoScroll: Boolean = true, scrollOffset: Int = 0): UltronRecyclerViewItem {\n        waitItemsLoaded()\n        return UltronRecyclerViewItem(this, getLastPosition(), autoScroll, scrollOffset)\n    }\n\n    /**\n     * @return first [UltronRecyclerViewItem] matched [matcher]\n     *\n     * it could be used if you can't determine unique matcher for item\n     *\n     * @param matcher determines how to find item\n     * @param autoScroll evaluate scrollTo this item in case of true value\n     */\n    fun firstItemMatched(matcher: Matcher<View>, autoScroll: Boolean = true, scrollOffset: Int = 0) =\n        itemMatched(matcher, 0, autoScroll, scrollOffset)\n\n    /**\n     * @return [UltronRecyclerViewItem] matched [matcher]\n     *\n     * it could be used if you can't determine unique matcher for item, eg\n     *\n     * RecyclerView has several matched items. You can get any of them by specifying desired [index] value\n     *\n     * @param matcher determines how to find items\n     * @param index value from 0 to lastIndex of matched items\n     * @param autoScroll evaluate scrollTo this item in case of true value\n     */\n    fun itemMatched(\n        matcher: Matcher<View>,\n        index: Int,\n        autoScroll: Boolean = true,\n        scrollOffset: Int = 0\n    ): UltronRecyclerViewItem {\n        waitItemsLoaded()\n        val position = AssertUtils.assertTrueAndReturnValue(\n            valueBlock = { getItemAdapterPositionAtIndex(matcher, index) },\n            assertionBlock = { value -> value >= 0 },\n            timeoutMs = getTimeout(),\n            desc = \"RecyclerView(${recyclerViewInteraction.elementInfo.name}) has item matched '$matcher' with index $index\"\n        )\n        return UltronRecyclerViewItem(this, position, autoScroll, scrollOffset)\n    }\n\n    /**\n     * @return last [UltronRecyclerViewItem] matched [matcher]\n     *\n     * it could be used if you can't determine unique matcher for item\n     *\n     * @param matcher determines how to find item\n     * @param autoScroll evaluate scrollTo this item in case of true value\n     */\n    fun lastItemMatched(\n        matcher: Matcher<View>,\n        autoScroll: Boolean = true,\n        scrollOffset: Int = 0\n    ): UltronRecyclerViewItem {\n        waitItemsLoaded()\n        val position = AssertUtils.assertTrueAndReturnValue(\n            valueBlock = { getItemsAdapterPositionList(matcher).lastOrNull() ?: -1 },\n            assertionBlock = { value -> value >= 0 },\n            timeoutMs = getTimeout(),\n            desc = \"RecyclerView(${recyclerViewInteraction.elementInfo.name}) has last item matched '$matcher'\"\n        )\n        return UltronRecyclerViewItem(this, position, autoScroll, scrollOffset)\n    }\n\n    /**\n     * @return RecyclerView representation at the moment of identifying\n     *\n     * in case of original RecyclerView in app under test is changed you have to call this method again to get an actual info\n     * */\n    open fun getRecyclerViewList(): RecyclerView {\n        recyclerViewInteraction.identifyRecyclerView(recyclerViewIdentifierMatcher())\n        return recyclerView\n            ?: throw UltronOperationException(\"Couldn't find recyclerView with '${recyclerViewInteraction.elementInfo.name}'\")\n    }\n\n    /**\n     * identify recyclerView items count immediately at the moment of method calling\n     *\n     * use [assertSize] to make sure the list has correct items count\n     *\n     * in case the list could be not fully loaded use [waitItemsLoaded] method before calling [getSize]\n     */\n    open fun getSize() = getRecyclerViewList().adapter?.itemCount ?: 0\n\n    /**\n     * specifies a list is empty at the moment of method calling\n     *\n     * use [assertEmpty] to assert that list has no items\n     */\n    fun isEmpty() = getSize() == 0\n\n    fun getLastPosition(): Int {\n        val position = getSize() - 1\n        return if (position >= 0) position else 0\n    }\n\n    /**\n     * Asserts RecyclerView has no item\n     */\n    fun assertEmpty() {\n        AssertUtils.assertTrue(\n            { getSize() == 0 }, operationTimeoutMs,\n            { \"RecyclerView(${recyclerViewInteraction.elementInfo.name}) has no items (actual size = ${getSize()})\" }\n        )\n    }\n\n    /**\n     * Asserts RecyclerView has any item\n     */\n    fun assertNotEmpty() {\n        AssertUtils.assertTrue(\n            { getSize() > 0 }, operationTimeoutMs,\n            { \"RecyclerView(${recyclerViewInteraction.elementInfo.name}) is NOT empty\" }\n        )\n    }\n\n    /**\n     * Asserts RecyclerView list size matches the expected condition during [operationTimeoutMs]\n     * @param expected The expected size value to compare against\n     * @param comparison The type of size comparison to perform\n     */\n    open fun assertSize(expected: Int, comparison: SizeComparison = SizeComparison.EQUAL) {\n        val condition: (Int) -> Boolean = when (comparison) {\n            SizeComparison.EQUAL -> { size -> size == expected }\n            SizeComparison.GREATER_THAN -> { size -> size > expected }\n            SizeComparison.LESS_THAN -> { size -> size < expected }\n            SizeComparison.GREATER_THAN_OR_EQUAL -> { size -> size >= expected }\n            SizeComparison.LESS_THAN_OR_EQUAL -> { size -> size <= expected }\n        }\n\n        val operatorSymbol = when (comparison) {\n            SizeComparison.EQUAL -> \"=\"\n            SizeComparison.GREATER_THAN -> \">\"\n            SizeComparison.LESS_THAN -> \"<\"\n            SizeComparison.GREATER_THAN_OR_EQUAL -> \">=\"\n            SizeComparison.LESS_THAN_OR_EQUAL -> \"<=\"\n        }\n\n        AssertUtils.assertTrue(\n            { condition(getSize()) },\n            operationTimeoutMs,\n            { \"RecyclerView(${recyclerViewInteraction.elementInfo.name}) size expected to be $operatorSymbol $expected (actual size = ${getSize()})\" }\n        )\n    }\n\n    /**\n     * Asserts RecyclerView list has item at [position] during [timeoutMs]\n     */\n    open fun assertHasItemAtPosition(position: Int) {\n        AssertUtils.assertTrue(\n            { getSize() >= position }, operationTimeoutMs,\n            { \"Wait RecyclerView(${recyclerViewInteraction.elementInfo.name}) size >= $position (actual size = ${getSize()})\" }\n        )\n    }\n\n    /**\n     * Asserts RecyclerView list hasn't item matched with [matcher].\n     * In case of item not exist it returns immediately.\n     * If, for some reason, item appears later. These assertion can give you false positive result\n     */\n    open fun assertItemNotExistImmediately(matcher: Matcher<View>, timeoutMs: Long) {\n        waitItemsLoaded()\n        AssertUtils.assertTrue(\n            { !isItemExist(matcher) },\n            timeoutMs,\n            { \"RecyclerView(${recyclerViewInteraction.elementInfo.name}) has no item matched '$matcher'\" }\n        )\n    }\n\n    /**\n     * Asserts RecyclerView list hasn't item matched with [matcher] during specified [timeoutMs].\n     * In case item appears in list during [timeoutMs] an exception is going to be thrown.\n     * Note: In positive scenario this assert takes [timeoutMs]\n     */\n    open fun assertItemNotExist(matcher: Matcher<View>, timeoutMs: Long) {\n        waitItemsLoaded()\n        AssertUtils.assertTrueWhileTime(\n            { !isItemExist(matcher) },\n            timeoutMs,\n            \"RecyclerView(${recyclerViewInteraction.elementInfo.name}) has no item matched '$matcher'\"\n        )\n    }\n\n    open fun isDisplayed() = apply { recyclerViewInteraction.withTimeout(getTimeout()).isDisplayed() }\n    open fun isNotDisplayed() =\n        apply { recyclerViewInteraction.withTimeout(getTimeout()).isNotDisplayed() }\n\n    open fun doesNotExist() = apply { recyclerViewInteraction.withTimeout(getTimeout()).doesNotExist() }\n    open fun isEnabled() = apply { recyclerViewInteraction.withTimeout(getTimeout()).isEnabled() }\n    open fun isNotEnabled() = apply { recyclerViewInteraction.withTimeout(getTimeout()).isNotEnabled() }\n    open fun hasContentDescription(contentDescription: String) =\n        apply {\n            recyclerViewInteraction.withTimeout(getTimeout()).hasContentDescription(contentDescription)\n        }\n\n    open fun hasContentDescription(resourceId: Int) =\n        apply { recyclerViewInteraction.withTimeout(getTimeout()).hasContentDescription(resourceId) }\n\n    open fun hasContentDescription(charSequenceMatcher: Matcher<CharSequence>) =\n        apply {\n            recyclerViewInteraction.withTimeout(getTimeout()).hasContentDescription(charSequenceMatcher)\n        }\n\n    open fun contentDescriptionContains(text: String) =\n        apply { recyclerViewInteraction.withTimeout(getTimeout()).contentDescriptionContains(text) }\n\n    open fun assertMatches(matcher: Matcher<View>) =\n        apply { recyclerViewInteraction.withTimeout(getTimeout()).assertMatches(matcher) }\n\n\n    fun scrollToItem(itemMatcher: Matcher<View>, searchLimit: Int = this.itemSearchLimit, offset: Int = 0) = apply {\n        recyclerViewInteraction.withTimeout(getTimeout()).perform(\n            viewAction = RecyclerViewScrollAction(itemMatcher, searchLimit, offset),\n            description = \"Scroll RecyclerView '${recyclerViewInteraction.elementInfo.name}' to item = '$itemMatcher' with searchLimit = $searchLimit and offset = $offset\"\n        )\n    }\n\n    fun scrollToItem(itemInteraction: UltronEspressoInteraction<ViewInteraction>, searchLimit: Int = this.itemSearchLimit, offset: Int = 0) = apply {\n        recyclerViewInteraction.withTimeout(getTimeout()).perform(\n            viewAction = RecyclerViewScrollAction(itemInteraction.getInteractionMatcher()!!, searchLimit, offset),\n            description = \"Scroll RecyclerView '${recyclerViewInteraction.elementInfo.name}' to item with: '${itemInteraction.elementInfo.name}', searchLimit = $searchLimit and offset = $offset\"\n        )\n    }\n\n    @Deprecated(\"Use scrollToItem(itemMatcher, searchLimit, offset)\")\n    fun scrollToIem(itemMatcher: Matcher<View>, searchLimit: Int = this.itemSearchLimit, offset: Int = 0) = scrollToItem(UltronEspressoInteraction(onView(itemMatcher)), searchLimit, offset)\n\n    fun scrollToItem(position: Int, offset: Int = 0) {\n        assertHasItemAtPosition(position)\n        val positionToScroll = position + offset\n        recyclerViewInteraction.withTimeout(getTimeout()).perform(\n            viewAction = RecyclerViewScrollToPositionViewAction(positionToScroll),\n            description = \"Scroll RecyclerView '${recyclerViewInteraction.elementInfo.name}' to position $position with offset = $offset\"\n        )\n    }\n\n    /** set timeout for operations with RecyclerView.\n     * Note: it doesn't modify [loadTimeoutMs] (waiting a RecyclerView to be loaded)\n     * */\n    open fun withTimeout(timeoutMs: Long) =\n        UltronRecyclerView(\n            this.recyclerViewInteraction,\n            this.loadTimeoutMs,\n            this.itemSearchLimit,\n            operationTimeoutMs = timeoutMs,\n            this.recyclerImpl\n        )\n\n    open fun withResultHandler(resultHandler: (EspressoOperationResult<UltronEspressoOperation>) -> Unit) =\n        recyclerViewInteraction.withResultHandler(resultHandler)\n\n    fun isItemExist(matcher: Matcher<View>): Boolean {\n        return getItemAdapterPositionAtIndex(matcher, 0) >= 0\n    }\n\n    /**\n     * It's waiting while RecyclerView items to be loaded\n     * @throws [UltronOperationException] if no item is loaded during [loadTimeoutMs]\n     */\n    fun waitItemsLoaded(\n        recyclerView: RecyclerView = getRecyclerViewList(),\n        minItemsCount: Int = 1\n    ) = apply {\n        if ((recyclerView.adapter?.itemCount ?: 0) >= minItemsCount &&\n            recyclerView.childCount >= minItemsCount\n        ) {\n            return@apply\n        }\n        var isLoaded = false\n        val finishTime = SystemClock.elapsedRealtime() + loadTimeoutMs\n        while (!isLoaded && finishTime > SystemClock.elapsedRealtime()) {\n            isLoaded = (recyclerView.adapter?.itemCount ?: 0) >= minItemsCount &&\n                    recyclerView.childCount >= minItemsCount\n        }\n        if (!isLoaded) throw UltronOperationException(\n            \"RecyclerView failed to load $minItemsCount items in $loadTimeoutMs ms. \" +\n                    \"Current state: adapter.items = ${recyclerView.adapter?.itemCount}, \" +\n                    \"visible.items = ${recyclerView.childCount}\"\n        )\n    }\n\n    /**\n     * @return a list of [RecyclerView.ViewHolder] objects those matched with [itemMatcher]\n     *\n     * Note: only items are displayed on the screen have ViewHolder\n     */\n    fun getViewHolderList(itemMatcher: Matcher<View>): List<RecyclerView.ViewHolder> {\n        val recyclerView = getRecyclerViewList()\n        val viewHolderMatcher: Matcher<RecyclerView.ViewHolder> = viewHolderMatcher(itemMatcher)\n        val matchedItems: List<MatchedItem> =\n            itemsMatching(recyclerView, viewHolderMatcher, itemSearchLimit = itemSearchLimit)\n        val viewHolders = mutableListOf<RecyclerView.ViewHolder>()\n        matchedItems.forEach {\n            recyclerView.findViewHolderForAdapterPosition(it.position)\n                ?.let { vh -> viewHolders.add(vh) }\n        }\n        return viewHolders\n    }\n\n    /**\n     * @return positions of all matched items in the list\n     * @param limitAmountOfMatchedItems restricts the size of list to be returned\n     *\n     * eg RecyclerView contains 100 matched items, [limitAmountOfMatchedItems] = 10 => this method will return positions of first 10 matched RecyclerView items\n     */\n    fun getItemsAdapterPositionList(\n        itemMatcher: Matcher<View>,\n        limitAmountOfMatchedItems: Int = -1\n    ): List<Int> {\n        return itemsMatching(\n            getRecyclerViewList(),\n            viewHolderMatcher(itemMatcher),\n            limitAmountOfMatchedItems,\n            itemSearchLimit\n        ).map { it.position }\n    }\n\n    /**\n     * @return position of matched item with [index]\n     * @return -1 if there is no matched item with [index]\n     */\n    fun getItemAdapterPositionAtIndex(itemMatcher: Matcher<View>, index: Int): Int {\n        val items = getItemsAdapterPositionList(itemMatcher, index + 1)\n        return if (items.size - 1 < index) -1\n        else items[index]\n    }\n\n    /**\n     * @return [RecyclerView.ViewHolder] object at position [position]\n     */\n    fun getViewHolderAtPosition(position: Int): RecyclerView.ViewHolder? {\n        return getRecyclerViewList().findViewHolderForAdapterPosition(position)\n    }\n\n    /**\n     * @param itemMatcher describes how to identify item in the list.\n     * It shouldn't be unique in  view hierarchy\n     * @return the matcher to the exact recyclerView item which it's possible to interact\n     */\n    internal fun atItem(itemMatcher: Matcher<View>): Matcher<View> {\n        return object : TypeSafeMatcher<View>() {\n            var itemView: View? = null\n\n            override fun describeTo(description: Description) {\n                description.appendText(\"RecyclerViewItem recyclerViewMatcher: '$recyclerViewMatcher', itemMatcher: '$itemMatcher'\")\n            }\n\n            override fun matchesSafely(view: View): Boolean {\n                if (itemView == null) itemView = findItemView(itemMatcher, view.rootView)?.itemView\n                return if (itemView != null) itemView == view else false\n            }\n        }\n    }\n\n    /**\n     * @return matcher to RecyclerView item at position [position]\n     */\n    internal fun atPosition(position: Int): Matcher<View> {\n        return object : TypeSafeMatcher<View>() {\n            var itemView: View? = null\n            override fun describeTo(description: Description) {\n                description.appendText(\"RecyclerViewItem recyclerViewMatcher: '$recyclerViewMatcher', itemPosition: '$position'\")\n            }\n\n            override fun matchesSafely(view: View): Boolean {\n                if (itemView == null) itemView =\n                    findItemViewAtPosition(position, view.rootView)?.itemView\n                return if (itemView != null) itemView == view else false\n            }\n        }\n    }\n\n    /**\n     * @param itemMatcher describes how to identify item in the list. It shouldn't be unique in  view hierarchy\n     * @param childMatcher describes how to identify child inside item. It shouldn't be unique in scope of RecyclerView list\n     * @return matcher to a RecyclerView item child which it's possible to interact\n     */\n    internal fun atItemChild(\n        itemMatcher: Matcher<View>,\n        childMatcher: Matcher<View>\n    ): Matcher<View> {\n        return object : TypeSafeMatcher<View>() {\n            var childView: View? = null\n\n            override fun describeTo(description: Description) {\n                description.appendText(\"RecyclerViewItem of '$recyclerViewMatcher', itemMatcher: '$itemMatcher', childMatcher: '$childMatcher'\")\n            }\n\n            override fun matchesSafely(view: View): Boolean {\n                return when (recyclerImpl) {\n                    UltronRecyclerViewImpl.STANDARD -> {\n                        findItemView(itemMatcher, view.rootView)?.itemView?.let {\n                            childView = it.findChildView(childMatcher)\n                        }\n                        if (childView != null) childView == view else false\n                    }\n\n                    UltronRecyclerViewImpl.PERFORMANCE -> {\n                        if (childMatcher.matches(view)) {\n                            isDescendantOfA(atItem(itemMatcher)).matches(view)\n                        } else false\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * @param position of item in a RecyclerView list\n     * @param childMatcher describes how to identify child inside item. It shouldn't be unique in scope of RecyclerView list\n     * @return matcher to a RecyclerView item child which it's possible to interact\n     */\n    internal fun atPositionItemChild(position: Int, childMatcher: Matcher<View>): Matcher<View> {\n        return object : TypeSafeMatcher<View>() {\n            var childView: View? = null\n\n            override fun describeTo(description: Description) {\n                description.appendText(\"RecyclerViewItem of '$recyclerViewMatcher', itemPosition: '$position', childMatcher: '$childMatcher'\")\n            }\n\n            override fun matchesSafely(view: View): Boolean {\n                return when (recyclerImpl) {\n                    UltronRecyclerViewImpl.STANDARD -> {\n                        findItemViewAtPosition(position, view.rootView)?.itemView.let {\n                            childView = it?.findChildView(childMatcher)\n                        }\n                        return if (childView != null) childView == view else false\n                    }\n\n                    UltronRecyclerViewImpl.PERFORMANCE -> {\n                        if (childMatcher.matches(view)) {\n                            isDescendantOfA(atPosition(position)).matches(view)\n                        } else false\n                    }\n                }\n            }\n        }\n    }\n\n    private fun findItemView(itemMatcher: Matcher<View>, rootView: View): RecyclerView.ViewHolder? {\n        val recyclerView = rootView.findChildView(recyclerViewMatcher) as RecyclerView\n        this.recyclerView = recyclerView\n        val viewHolderMatcher: Matcher<RecyclerView.ViewHolder> = viewHolderMatcher(itemMatcher)\n        val matchedItem =\n            itemsMatching(recyclerView, viewHolderMatcher, 1, itemSearchLimit).firstOrNull()\n        return matchedItem?.let { recyclerView.findViewHolderForAdapterPosition(it.position) }\n    }\n\n    private fun findItemViewAtPosition(position: Int, rootView: View): RecyclerView.ViewHolder? {\n        val recyclerView = rootView.findChildView(recyclerViewMatcher) as? RecyclerView\n            ?: return null\n        this.recyclerView = recyclerView\n        return recyclerView.findViewHolderForAdapterPosition(position)\n    }\n\n    private fun recyclerViewIdentifierMatcher(): BoundedMatcher<View, RecyclerView> {\n        return object : BoundedMatcher<View, RecyclerView>(RecyclerView::class.java) {\n            override fun describeTo(description: Description) {\n                description.appendText(\"identify RecyclerView matches \")\n                    .appendValue(recyclerViewMatcher)\n            }\n\n            override fun matchesSafely(view: RecyclerView): Boolean {\n                if (recyclerViewMatcher.matches(view)) {\n                    recyclerView = view\n                }\n                return recyclerView == view\n            }\n        }\n    }\n\n    fun withName(name: String): UltronRecyclerView = apply {\n        recyclerViewInteraction.elementInfo.name = name\n    }\n\n    enum class SizeComparison {\n        EQUAL,\n        GREATER_THAN,\n        LESS_THAN,\n        GREATER_THAN_OR_EQUAL,\n        LESS_THAN_OR_EQUAL\n    }\n}\n\nfun withRecyclerView(\n    recyclerViewMatcher: Matcher<View>,\n    loadTimeout: Long = RECYCLER_VIEW_LOAD_TIMEOUT,\n    itemSearchLimit: Int = RECYCLER_VIEW_ITEM_SEARCH_LIMIT,\n    operationsTimeoutMs: Long = RECYCLER_VIEW_OPERATIONS_TIMEOUT,\n    implementation: UltronRecyclerViewImpl = RECYCLER_VIEW_IMPLEMENTATION\n): UltronRecyclerView {\n    return UltronRecyclerView(recyclerViewMatcher, loadTimeout, itemSearchLimit, operationsTimeoutMs, implementation)\n}\n\nfun withRecyclerView(\n    @IntegerRes resourceId: Int,\n    loadTimeout: Long = RECYCLER_VIEW_LOAD_TIMEOUT,\n    itemSearchLimit: Int = RECYCLER_VIEW_ITEM_SEARCH_LIMIT,\n    operationsTimeoutMs: Long = RECYCLER_VIEW_OPERATIONS_TIMEOUT,\n    implementation: UltronRecyclerViewImpl = RECYCLER_VIEW_IMPLEMENTATION\n): UltronRecyclerView {\n    return UltronRecyclerView(withId(resourceId), loadTimeout, itemSearchLimit, operationsTimeoutMs, implementation)\n}\n\ninternal fun <T> UltronEspressoInteraction<T>.identifyRecyclerView(matcher: Matcher<View>) {\n    this.executeAssertion(\n        UltronEspressoOperation(\n            operationBlock = getInteractionAssertionBlock(matches(matcher)),\n            name = \"Identify RecyclerView matches '${getInteractionMatcher()}'\",\n            type = EspressoAssertionType.IDENTIFY_RECYCLER_VIEW,\n            description = \"${EspressoAssertionType.IDENTIFY_RECYCLER_VIEW} during $timeoutMs ms\",\n            timeoutMs = getAssertionTimeout()\n        )\n    )\n}\n\ninternal fun Matcher<View>.identifyRecyclerView(matcher: Matcher<View>): Unit =\n    UltronEspressoInteraction(onView(this)).identifyRecyclerView(matcher)\n"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/espresso/recyclerview/UltronRecyclerViewImpl.kt",
    "content": "package com.atiurin.ultron.core.espresso.recyclerview\n\nenum class UltronRecyclerViewImpl {\n    STANDARD,\n    PERFORMANCE\n}\n"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/espresso/recyclerview/UltronRecyclerViewItem.kt",
    "content": "package com.atiurin.ultron.core.espresso.recyclerview\n\nimport android.view.View\nimport androidx.recyclerview.widget.RecyclerView\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.ViewAction\nimport androidx.test.espresso.ViewInteraction\nimport com.atiurin.ultron.core.common.assertion.DefaultOperationAssertion\nimport com.atiurin.ultron.core.common.assertion.OperationAssertion\nimport com.atiurin.ultron.core.espresso.EspressoOperationResult\nimport com.atiurin.ultron.core.espresso.UltronEspressoInteraction\nimport com.atiurin.ultron.core.espresso.UltronEspressoOperation\nimport com.atiurin.ultron.custom.espresso.matcher.withSuitableRoot\nimport com.atiurin.ultron.exceptions.UltronException\nimport com.atiurin.ultron.listeners.setListenersState\nimport org.hamcrest.Matcher\n\n/**\n * Represents the item of RecyclerView list.\n * Provides a set of interaction with item.\n */\nopen class UltronRecyclerViewItem {\n    private var executor: RecyclerViewItemExecutor? = null\n\n    /**\n     * Use this constructor to inherit from [UltronRecyclerViewItem]\n     * Don't create an instance of subclass. Use [UltronRecyclerView.getItem] instead\n     */\n    protected constructor()\n\n    fun child(block: () -> Matcher<View>): Lazy<Matcher<View>> = lazy {\n        getChild(block())\n    }\n\n    fun child(interaction: UltronEspressoInteraction<ViewInteraction>): Lazy<UltronEspressoInteraction<ViewInteraction>> = lazy {\n        getChild(interaction)\n    }\n\n    constructor(\n        ultronRecyclerView: UltronRecyclerView,\n        itemViewMatcher: Matcher<View>,\n        autoScroll: Boolean = true,\n        scrollOffset: Int = 0\n    ) {\n        setExecutor(ultronRecyclerView, UltronEspressoInteraction(onView(itemViewMatcher)))\n        if (autoScroll) scrollToItem(scrollOffset)\n    }\n\n    constructor(\n        ultronRecyclerView: UltronRecyclerView,\n        position: Int,\n        autoScroll: Boolean = true,\n        scrollOffset: Int = 0\n    ) {\n        setExecutor(ultronRecyclerView, position)\n        if (autoScroll) scrollToItem(scrollOffset)\n    }\n\n    fun scrollToItem(offset: Int = 0): UltronRecyclerViewItem = apply {\n        executor?.scrollToItem(offset)\n    }\n\n    fun getViewHolder(): RecyclerView.ViewHolder? {\n        return executor?.getItemViewHolder()\n    }\n\n    /**\n     * @return matcher to a child element\n     */\n    fun getChild(childMatcher: Matcher<View>): Matcher<View> {\n        if (executor == null) throw UltronException(\n            \"\"\"\n            |UltronRecyclerViewItem child element should have lazy initialisation in subclasses. \n            |For example, do not use: \n            |val name = getChild(matcher)\n            |Refactor it to:\n            |val name by lazy { getChild(matcher) }\n        \"\"\".trimMargin()\n        )\n        return executor!!.getItemChildMatcher(childMatcher)\n    }\n\n    /**\n     * @return interaction to a child element\n     */\n    fun getChild(childInteraction: UltronEspressoInteraction<ViewInteraction>): UltronEspressoInteraction<ViewInteraction> {\n        if (executor == null) throw UltronException(\n            \"\"\"\n            |UltronRecyclerViewItem child element should have lazy initialisation in subclasses. \n            |For example, do not use: \n            |val name = getChild(matcher)\n            |Refactor it to:\n            |val name by lazy { getChild(matcher) }\n        \"\"\".trimMargin()\n        )\n        return executor!!.getItemChildInteraction(childInteraction)\n    }\n\n    fun withTimeout(timeoutMs: Long) = getInteraction().withTimeout(timeoutMs)\n    fun withResultHandler(resultHandler: (EspressoOperationResult<UltronEspressoOperation>) -> Unit) =\n        getInteraction().withResultHandler(resultHandler)\n\n    fun withAssertion(assertion: OperationAssertion) = getInteraction().withAssertion(assertion)\n    fun withAssertion(name: String = \"\", isListened: Boolean = false, block: () -> Unit) =\n        getInteraction().withAssertion(DefaultOperationAssertion(name, block.setListenersState(isListened)))\n\n    //root view searching\n    fun withSuitableRoot() = apply { this.getMatcher().withSuitableRoot() }\n\n    //actions\n    fun click() = apply { this.getInteraction().click() }\n    fun longClick() = apply { this.getInteraction().longClick() }\n    fun doubleClick() = apply { this.getInteraction().doubleClick() }\n\n    fun clickTopLeft(offsetX: Int = 0, offsetY: Int = 0) = apply { this.getInteraction().clickTopLeft(offsetX, offsetY) }\n    fun clickTopCenter(offsetY: Int) = apply { this.getInteraction().clickTopCenter(offsetY) }\n    fun clickTopRight(offsetX: Int = 0, offsetY: Int = 0) = apply { this.getInteraction().clickTopRight(offsetX, offsetY) }\n    fun clickCenterRight(offsetX: Int = 0) = apply { this.getInteraction().clickCenterRight(offsetX) }\n    fun clickBottomRight(offsetX: Int = 0, offsetY: Int = 0) = apply { this.getInteraction().clickBottomRight(offsetX, offsetY) }\n    fun clickBottomCenter(offsetY: Int = 0) = apply { this.getInteraction().clickBottomCenter(offsetY) }\n    fun clickBottomLeft(offsetX: Int = 0, offsetY: Int = 0) = apply { this.getInteraction().clickBottomLeft(offsetX, offsetY) }\n    fun clickCenterLeft(offsetX: Int = 0) = apply { this.getInteraction().clickCenterLeft(offsetX) }\n\n    fun swipeDown() = apply { this.getInteraction().swipeDown() }\n    fun swipeLeft() = apply { this.getInteraction().swipeLeft() }\n    fun swipeRight() = apply { this.getInteraction().swipeRight() }\n    fun swipeUp() = apply { this.getInteraction().swipeUp() }\n    fun perform(action: ViewAction) = apply { this.getInteraction().perform(action) }\n\n    //assertions\n    fun isDisplayed() = apply { this.getInteraction().isDisplayed() }\n    fun isNotDisplayed() = apply { this.getInteraction().isNotDisplayed() }\n    fun isCompletelyDisplayed() = apply { this.getInteraction().isCompletelyDisplayed() }\n    fun isDisplayingAtLeast(percentage: Int) =\n        apply { this.getInteraction().isDisplayingAtLeast(percentage) }\n\n    fun isClickable() = apply { this.getInteraction().isClickable() }\n    fun isNotClickable() = apply { this.getInteraction().isNotClickable() }\n    fun isEnabled() = apply { this.getInteraction().isEnabled() }\n    fun isNotEnabled() = apply { this.getInteraction().isNotEnabled() }\n    fun assertMatches(condition: Matcher<View>) =\n        apply { this.getInteraction().assertMatches(condition) }\n\n    fun hasContentDescription(contentDescription: String) =\n        apply { this.getInteraction().hasContentDescription(contentDescription) }\n\n    fun hasContentDescription(resourceId: Int) =\n        apply { this.getInteraction().hasContentDescription(resourceId) }\n\n    fun hasContentDescription(charSequenceMatcher: Matcher<CharSequence>) =\n        apply { this.getInteraction().hasContentDescription(charSequenceMatcher) }\n\n    fun contentDescriptionContains(text: String) =\n        apply { this.getInteraction().contentDescriptionContains(text) }\n\n    fun setExecutor(\n        ultronRecyclerView: UltronRecyclerView,\n        itemViewInteraction: UltronEspressoInteraction<ViewInteraction>,\n    ) {\n        this.executor = RecyclerViewItemMatchingExecutor(ultronRecyclerView, itemViewInteraction)\n    }\n\n    fun setExecutor(\n        ultronRecyclerView: UltronRecyclerView,\n        position: Int\n    ) {\n        this.executor = RecyclerViewItemPositionalExecutor(ultronRecyclerView, position)\n    }\n\n    fun getMatcher(): Matcher<View> {\n        return executor!!.getItemMatcher()\n    }\n\n    fun getInteraction(): UltronEspressoInteraction<ViewInteraction> {\n        return executor!!.getItemInteraction()\n    }\n\n    companion object {\n        inline fun <reified T : UltronRecyclerViewItem> getInstance(\n            ultronRecyclerView: UltronRecyclerView,\n            itemViewInteraction: UltronEspressoInteraction<ViewInteraction>,\n            autoScroll: Boolean = true,\n            scrollOffset: Int = 0\n        ): T {\n            val item = this.createUltronRecyclerViewItemInstance<T>()\n            item.setExecutor(ultronRecyclerView, itemViewInteraction)\n            if (autoScroll) item.scrollToItem(scrollOffset)\n            return item\n        }\n\n        inline fun <reified T : UltronRecyclerViewItem> getInstance(\n            ultronRecyclerView: UltronRecyclerView,\n            position: Int,\n            autoScroll: Boolean = true,\n            scrollOffset: Int = 0\n        ): T {\n            val item = this.createUltronRecyclerViewItemInstance<T>()\n            item.setExecutor(ultronRecyclerView, position)\n            if (autoScroll) item.scrollToItem(scrollOffset)\n            return item\n        }\n\n        inline fun <reified T : UltronRecyclerViewItem> createUltronRecyclerViewItemInstance(): T {\n            return try {\n                T::class.java.newInstance()\n            } catch (ex: Exception) {\n                val desc = when {\n                    T::class.isInner -> {\n                        \"${T::class.simpleName} is an inner class so you have to delete inner modifier (It is often when kotlin throws 'has no zero argument constructor' but real reason is an inner modifier)\"\n                    }\n                    T::class.constructors.find { it.parameters.isEmpty() } == null -> {\n                        \"${T::class.simpleName} doesn't have a constructor without params (create an empty constructor)\"\n                    }\n                    else -> ex.message\n                }\n                throw UltronException(\n                    \"\"\"\n                    |Couldn't create an instance of ${T::class.simpleName}. \n                    |Possible reason: $desc \n                    |Original exception: ${ex.message}, cause ${ex.cause}\n                \"\"\".trimMargin()\n                )\n            }\n        }\n    }\n}"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/espressoweb/UltronWebLifecycle.kt",
    "content": "package com.atiurin.ultron.core.espressoweb\n\nimport com.atiurin.ultron.core.common.AbstractOperationLifecycle\n\nobject UltronWebLifecycle : AbstractOperationLifecycle()"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/espressoweb/operation/EspressoWebOperationType.kt",
    "content": "package com.atiurin.ultron.core.espressoweb.operation\n\nimport com.atiurin.ultron.core.common.UltronOperationType\n\nenum class EspressoWebOperationType :\n    UltronOperationType {\n    //element\n    WEB_CLICK, WEB_GET_TEXT, WEB_REPLACE_TEXT,\n    WEB_CLEAR_ELEMENT, WEB_KEYS,\n    WEB_SCROLL_INTO_VIEW, WEB_ASSERT_THAT,\n    WEB_EXISTS, WEB_HAS_TEXT, WEB_CONTAINS_TEXT, WEB_HAS_ATTRIBUTE,\n\n    //elements list\n    WEB_FIND_MULTIPLE_ELEMENTS,\n\n    //document\n    WEB_VIEW_ASSERT_THAT, WEB_EVAL_JS_SCRIPT,\n    WEB_SELECT_ACTIVE_ELEMENT, WEB_SELECT_FRAME_BY_INDEX, WEB_SELECT_FRAME_BY_ID_OR_NAME\n}"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/espressoweb/operation/WebInteractionOperation.kt",
    "content": "package com.atiurin.ultron.core.espressoweb.operation\n\nimport androidx.test.espresso.web.sugar.Web\nimport com.atiurin.ultron.core.common.ElementInfo\nimport com.atiurin.ultron.core.common.DefaultElementInfo\nimport com.atiurin.ultron.core.common.Operation\nimport com.atiurin.ultron.core.common.OperationIterationResult\nimport com.atiurin.ultron.core.common.UltronOperationType\nimport com.atiurin.ultron.core.common.assertion.DefaultOperationAssertion\nimport com.atiurin.ultron.core.common.assertion.OperationAssertion\n\nclass WebInteractionOperation<T>(\n    val webInteractionBlock: () -> Web.WebInteraction<T>,\n    override val name: String,\n    override val type: UltronOperationType,\n    override val description: String,\n    override val timeoutMs: Long,\n    override val assertion: OperationAssertion = DefaultOperationAssertion(\"\") {},\n    override val elementInfo: ElementInfo = DefaultElementInfo()\n) : Operation {\n    override fun execute(): OperationIterationResult {\n        var success = true\n        var exception: Throwable? = null\n        var resultWebInteraction: Web.WebInteraction<T>? = null\n        try {\n            resultWebInteraction = webInteractionBlock()\n        } catch (error: Throwable) {\n            success = false\n            exception = error\n        }\n        return WebInteractionOperationIterationResult(success, exception, resultWebInteraction)\n    }\n}"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/espressoweb/operation/WebInteractionOperationExecutor.kt",
    "content": "package com.atiurin.ultron.core.espressoweb.operation\n\nimport com.atiurin.ultron.core.common.Operation\nimport com.atiurin.ultron.core.common.ResultDescriptor\nimport com.atiurin.ultron.core.config.UltronConfig\nimport kotlin.reflect.KClass\n\ninternal class WebInteractionOperationExecutor<T>(\n    operation: WebInteractionOperation<T>\n) : WebOperationExecutor<WebInteractionOperation<T>>(operation) {\n    override fun getAllowedExceptions(operation: Operation): List<KClass<out Throwable>> {\n        return UltronConfig.Espresso.WebInteractionOperationConfig.allowedExceptions.map {\n            it.kotlin\n        }\n    }\n\n    override val descriptor: ResultDescriptor\n        get() = ResultDescriptor()\n}\n"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/espressoweb/operation/WebInteractionOperationIterationResult.kt",
    "content": "package com.atiurin.ultron.core.espressoweb.operation\n\nimport androidx.test.espresso.web.sugar.Web\nimport com.atiurin.ultron.core.common.OperationIterationResult\n\ninternal data class WebInteractionOperationIterationResult<T>(\n    override val success: Boolean,\n    override val exception: Throwable?,\n    val webInteraction: Web.WebInteraction<T>?\n) : OperationIterationResult"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/espressoweb/operation/WebOperationExecutor.kt",
    "content": "package com.atiurin.ultron.core.espressoweb.operation\n\nimport com.atiurin.ultron.core.common.Operation\nimport com.atiurin.ultron.core.common.OperationExecutor\nimport com.atiurin.ultron.core.common.OperationIterationResult\nimport com.atiurin.ultron.core.config.UltronConfig\nimport com.atiurin.ultron.exceptions.UltronWrapperException\nimport com.atiurin.ultron.extensions.isAssignedFrom\nimport kotlin.reflect.KClass\n\ninternal abstract class WebOperationExecutor<T : Operation>(\n    override val operation: T\n) : OperationExecutor<T, WebOperationResult<T>> {\n    override val pollingTimeout: Long\n        get() = UltronConfig.Espresso.ESPRESSO_OPERATION_POLLING_TIMEOUT\n    override var doBetweenOperationRetry: () -> Unit = {}\n    override fun generateResult(\n        success: Boolean,\n        exceptions: List<Throwable>,\n        description: String,\n        lastOperationIterationResult: OperationIterationResult?,\n        executionTimeMs: Long\n    ): WebOperationResult<T> {\n        return WebOperationResult(\n            operation = operation,\n            success = success,\n            exceptions = exceptions,\n            description = description,\n            operationIterationResult = lastOperationIterationResult,\n            executionTimeMs = executionTimeMs\n        )\n    }\n\n    override fun getWrapperException(originalException: Throwable): Throwable {\n        var modifiedException: Throwable? = null\n        val isMessageContains =\n            originalException.message?.contains(\"Atom evaluation returned null!\")\n        if (isMessageContains != null && isMessageContains) {\n            modifiedException =\n                UltronWrapperException(\"Element not found in WebView!\", originalException)\n        }\n        return modifiedException ?: originalException\n    }\n\n    override fun isExceptionInList(exception: Throwable, exceptionClasses: List<KClass<out Throwable>>): Boolean {\n        return exception::class.java.isAssignedFrom(exceptionClasses.map { it.java })\n    }\n}"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/espressoweb/operation/WebOperationResult.kt",
    "content": "package com.atiurin.ultron.core.espressoweb.operation\n\nimport com.atiurin.ultron.core.common.Operation\nimport com.atiurin.ultron.core.common.OperationIterationResult\nimport com.atiurin.ultron.core.common.OperationResult\n\nclass WebOperationResult<T : Operation>(\n    override val operation: T,\n    override val success: Boolean,\n    override val exceptions: List<Throwable> = emptyList(),\n    override var description: String,\n    override var operationIterationResult: OperationIterationResult?,\n    override val executionTimeMs: Long\n) : OperationResult<T>\n"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/espressoweb/webelement/UltronWebDocument.kt",
    "content": "package com.atiurin.ultron.core.espressoweb.webelement\n\nimport android.view.View\nimport androidx.test.espresso.web.assertion.WebAssertion\nimport androidx.test.espresso.web.model.Atoms\nimport androidx.test.espresso.web.model.ElementReference\nimport androidx.test.espresso.web.model.Evaluation\nimport androidx.test.espresso.web.model.WindowReference\nimport androidx.test.espresso.web.sugar.Web\nimport androidx.test.espresso.web.webdriver.DriverAtoms\nimport com.atiurin.ultron.core.common.UltronOperationType\nimport com.atiurin.ultron.core.common.assertion.DefaultOperationAssertion\nimport com.atiurin.ultron.core.common.assertion.OperationAssertion\nimport com.atiurin.ultron.core.config.UltronConfig\nimport com.atiurin.ultron.core.espressoweb.UltronWebLifecycle\nimport com.atiurin.ultron.core.espressoweb.operation.EspressoWebOperationType\nimport com.atiurin.ultron.core.espressoweb.operation.WebInteractionOperation\nimport com.atiurin.ultron.core.espressoweb.operation.WebInteractionOperationExecutor\nimport com.atiurin.ultron.core.espressoweb.operation.WebInteractionOperationIterationResult\nimport com.atiurin.ultron.core.espressoweb.operation.WebOperationResult\nimport com.atiurin.ultron.exceptions.UltronException\nimport org.hamcrest.Matcher\n\n/**\n * Represents document of webView\n * Provides methods for interaction with document\n */\nclass UltronWebDocument {\n    companion object {\n        /**\n         * Performs a force enable of Javascript on a WebView.\n         *\n         * <p>All WebView interactions are done via Javascript - therefore the WebView we are working on\n         * must support Javascript evaluation.\n         *\n         * <p>Enabling Javascript may cause the WebView under test to be reloaded. This is necessary to\n         * ensure the test infrastructure javascript bridges are loaded by the WebView.\n         */\n        @JvmStatic\n        @JvmOverloads\n        fun forceJavascriptEnabled(\n            webViewMatcher: Matcher<View> = UltronConfig.Espresso.webViewMatcher,\n            timeoutMs: Long = UltronConfig.Espresso.ACTION_TIMEOUT,\n            resultHandler: (WebOperationResult<WebInteractionOperation<Void>>) -> Unit = UltronConfig.Espresso.WebInteractionOperationConfig.resultHandler as (WebOperationResult<WebInteractionOperation<Void>>) -> Unit\n        ) {\n            val webViewInteraction = Web.onWebView(webViewMatcher)\n            executeOperationVoid(\n                webInteractionBlock = { webViewInteraction.forceJavascriptEnabled() },\n                name = \"WebView forceJavascriptEnabled on $webViewMatcher\",\n                type = EspressoWebOperationType.WEB_EVAL_JS_SCRIPT,\n                description = \"WebDocument forceJavascriptEnabled' on $webViewMatcher during $timeoutMs ms\",\n                timeoutMs = timeoutMs,\n                resultHandler = resultHandler\n            )\n        }\n\n        /**\n         * Evaluate JS on webView\n         */\n        @JvmStatic\n        @JvmOverloads\n        fun evalJS(\n            script: String,\n            webViewMatcher: Matcher<View> = UltronConfig.Espresso.webViewMatcher,\n            windowReference: WindowReference? = null,\n            timeoutMs: Long = UltronConfig.Espresso.ACTION_TIMEOUT,\n            resultHandler: (WebOperationResult<WebInteractionOperation<Evaluation>>) -> Unit = UltronConfig.Espresso.WebInteractionOperationConfig.resultHandler as (WebOperationResult<WebInteractionOperation<Evaluation>>) -> Unit\n        ) {\n            val webViewInteraction =\n                if (windowReference != null) Web.onWebView(webViewMatcher).inWindow(windowReference)\n                else Web.onWebView(webViewMatcher)\n            executeOperation(\n                webInteractionBlock = { webViewInteraction.perform(Atoms.script(script)) },\n                name = \"WebView Evaluate JS script on $webViewMatcher\",\n                type = EspressoWebOperationType.WEB_EVAL_JS_SCRIPT,\n                description = \"WebDocument Evaluate JS '$script' on $webViewMatcher during $timeoutMs ms\",\n                timeoutMs = timeoutMs,\n                resultHandler = resultHandler\n            )\n        }\n\n        /** use any webAssertion to assert it safely */\n        @JvmStatic\n        @JvmOverloads\n        fun <T> assertThat(\n            webAssertion: WebAssertion<T>,\n            webViewMatcher: Matcher<View> = UltronConfig.Espresso.webViewMatcher,\n            windowReference: WindowReference? = null,\n            timeoutMs: Long = UltronConfig.Espresso.ASSERTION_TIMEOUT,\n            resultHandler: (WebOperationResult<WebInteractionOperation<T>>) -> Unit = UltronConfig.Espresso.WebInteractionOperationConfig.resultHandler as (WebOperationResult<WebInteractionOperation<T>>) -> Unit\n        ) {\n            val webViewInteraction =\n                if (windowReference != null) Web.onWebView(webViewMatcher).inWindow(windowReference)\n                else Web.onWebView(webViewMatcher)\n            executeOperation(\n                webInteractionBlock = { webViewInteraction.check(webAssertion) },\n                name = \"WebView custom AssertThat\",\n                type = EspressoWebOperationType.WEB_VIEW_ASSERT_THAT,\n                description = \"Assert webView with type ${EspressoWebOperationType.WEB_VIEW_ASSERT_THAT} matches custom condition during $timeoutMs ms, webAssertion atom script ${webAssertion.atom.script}\",\n                timeoutMs = timeoutMs,\n                resultHandler = resultHandler\n            )\n        }\n\n        /** Finds the currently active element in the document. */\n        @JvmStatic\n        @JvmOverloads\n        fun selectActiveElement(\n            webViewMatcher: Matcher<View> = UltronConfig.Espresso.webViewMatcher,\n            windowReference: WindowReference? = null,\n            timeoutMs: Long = UltronConfig.Espresso.ACTION_TIMEOUT,\n            resultHandler: (WebOperationResult<WebInteractionOperation<ElementReference>>) -> Unit = UltronConfig.Espresso.WebInteractionOperationConfig.resultHandler as (WebOperationResult<WebInteractionOperation<ElementReference>>) -> Unit\n        ): ElementReference {\n            val webViewInteraction =\n                if (windowReference != null) Web.onWebView(webViewMatcher).inWindow(windowReference)\n                else Web.onWebView(webViewMatcher)\n            return executeOperation(\n                webInteractionBlock = { webViewInteraction.perform(DriverAtoms.selectActiveElement()) },\n                name = \"WebView selectActiveElement\",\n                type = EspressoWebOperationType.WEB_SELECT_ACTIVE_ELEMENT,\n                description = \"WebView selectActiveElement of $webViewMatcher during $timeoutMs ms\",\n                timeoutMs = timeoutMs,\n                resultHandler = resultHandler\n            )\n        }\n\n        /** Selects a subframe of the currently selected window by it's index. */\n        @JvmStatic\n        @JvmOverloads\n        fun selectFrameByIndex(\n            index: Int,\n            webViewMatcher: Matcher<View> = UltronConfig.Espresso.webViewMatcher,\n            windowReference: WindowReference? = null,\n            timeoutMs: Long = UltronConfig.Espresso.ACTION_TIMEOUT,\n            resultHandler: (WebOperationResult<WebInteractionOperation<WindowReference>>) -> Unit = UltronConfig.Espresso.WebInteractionOperationConfig.resultHandler as (WebOperationResult<WebInteractionOperation<WindowReference>>) -> Unit\n        ): WindowReference {\n            val webViewInteraction =\n                if (windowReference != null) Web.onWebView(webViewMatcher).inWindow(windowReference)\n                else Web.onWebView(webViewMatcher)\n            return executeOperation(\n                webInteractionBlock = {\n                    webViewInteraction.perform(\n                        DriverAtoms.selectFrameByIndex(\n                            index\n                        )\n                    )\n                },\n                name = \"WebView selectFrameByIndex '$index'\",\n                type = EspressoWebOperationType.WEB_SELECT_FRAME_BY_INDEX,\n                description = \"WebView selectFrameByIndex '$index' on $webViewMatcher during $timeoutMs ms\",\n                timeoutMs = timeoutMs,\n                resultHandler\n            )\n        }\n\n        /** Selects a subframe of the given window by it's index. */\n        @JvmStatic\n        @JvmOverloads\n        fun selectFrameByIndex(\n            index: Int,\n            root: WindowReference,\n            webViewMatcher: Matcher<View> = UltronConfig.Espresso.webViewMatcher,\n            windowReference: WindowReference? = null,\n            timeoutMs: Long = UltronConfig.Espresso.ACTION_TIMEOUT,\n            resultHandler: (WebOperationResult<WebInteractionOperation<WindowReference>>) -> Unit = UltronConfig.Espresso.WebInteractionOperationConfig.resultHandler as (WebOperationResult<WebInteractionOperation<WindowReference>>) -> Unit\n        ): WindowReference {\n            val webViewInteraction =\n                if (windowReference != null) Web.onWebView(webViewMatcher).inWindow(windowReference)\n                else Web.onWebView(webViewMatcher)\n            return executeOperation(\n                webInteractionBlock = {\n                    webViewInteraction.perform(\n                        DriverAtoms.selectFrameByIndex(\n                            index,\n                            root\n                        )\n                    )\n                },\n                name = \"WebView selectFrameByIndex '$index' with root\",\n                type = EspressoWebOperationType.WEB_SELECT_FRAME_BY_INDEX,\n                description = \"WebView selectFrameByIndex '$index' with root '$root' on $webViewMatcher during  $timeoutMs ms\",\n                timeoutMs = timeoutMs,\n                resultHandler = resultHandler\n            )\n        }\n\n        /** Selects a subframe of the current window by it's name or id. */\n        @JvmStatic\n        @JvmOverloads\n        fun selectFrameByIdOrName(\n            idOrName: String,\n            webViewMatcher: Matcher<View> = UltronConfig.Espresso.webViewMatcher,\n            windowReference: WindowReference? = null,\n            timeoutMs: Long = UltronConfig.Espresso.ACTION_TIMEOUT,\n            resultHandler: (WebOperationResult<WebInteractionOperation<WindowReference>>) -> Unit = UltronConfig.Espresso.WebInteractionOperationConfig.resultHandler as (WebOperationResult<WebInteractionOperation<WindowReference>>) -> Unit\n        ): WindowReference {\n            val webViewInteraction =\n                if (windowReference != null) Web.onWebView(webViewMatcher).inWindow(windowReference)\n                else Web.onWebView(webViewMatcher)\n            return executeOperation(\n                webInteractionBlock = {\n                    webViewInteraction.perform(\n                        DriverAtoms.selectFrameByIdOrName(\n                            idOrName\n                        )\n                    )\n                },\n                name = \"WebView selectFrameByIdOrName '$idOrName'\",\n                type = EspressoWebOperationType.WEB_SELECT_FRAME_BY_ID_OR_NAME,\n                description = \"WebView selectFrameByIdOrName '$idOrName' on $webViewMatcher during $timeoutMs ms\",\n                timeoutMs = timeoutMs,\n                resultHandler = resultHandler\n            )\n        }\n\n        /** Selects a subframe of the given window by it's name or id. */\n        @JvmStatic\n        @JvmOverloads\n        fun selectFrameByIdOrName(\n            idOrName: String,\n            root: WindowReference,\n            webViewMatcher: Matcher<View> = UltronConfig.Espresso.webViewMatcher,\n            windowReference: WindowReference? = null,\n            timeoutMs: Long = UltronConfig.Espresso.ACTION_TIMEOUT,\n            resultHandler: (WebOperationResult<WebInteractionOperation<WindowReference>>) -> Unit = UltronConfig.Espresso.WebInteractionOperationConfig.resultHandler as (WebOperationResult<WebInteractionOperation<WindowReference>>) -> Unit\n        ): WindowReference {\n            val webViewInteraction =\n                if (windowReference != null) Web.onWebView(webViewMatcher).inWindow(windowReference)\n                else Web.onWebView(webViewMatcher)\n            return executeOperation(\n                webInteractionBlock = {\n                    webViewInteraction.perform(\n                        DriverAtoms.selectFrameByIdOrName(\n                            idOrName,\n                            root\n                        )\n                    )\n                },\n                name = \"WebView selectFrameByIdOrName '$idOrName' with root\",\n                type = EspressoWebOperationType.WEB_SELECT_FRAME_BY_ID_OR_NAME,\n                description = \"WebView selectFrameByIdOrName '$idOrName' with root '$root' on $webViewMatcher during $timeoutMs ms\",\n                timeoutMs = timeoutMs,\n                resultHandler = resultHandler\n            )\n        }\n\n        fun <R> executeOperation(\n            webInteractionBlock: () -> Web.WebInteraction<R>,\n            name: String,\n            type: UltronOperationType,\n            description: String,\n            timeoutMs: Long,\n            resultHandler: (WebOperationResult<WebInteractionOperation<R>>) -> Unit,\n            assertion: OperationAssertion = DefaultOperationAssertion(\"\") {}\n        ): R {\n            val result = UltronWebLifecycle.execute(\n                WebInteractionOperationExecutor(\n                    WebInteractionOperation(\n                        webInteractionBlock = webInteractionBlock,\n                        name = name,\n                        type = type,\n                        description = description,\n                        timeoutMs = timeoutMs,\n                        assertion = assertion\n                    )\n                ), resultHandler\n            )\n            return (result.operationIterationResult as WebInteractionOperationIterationResult<R>).webInteraction?.get()\n                ?: throw UltronException(\"Couldn't get result of $name\")\n        }\n\n        fun executeOperationVoid(\n            webInteractionBlock: () -> Web.WebInteraction<Void>,\n            name: String,\n            type: UltronOperationType,\n            description: String,\n            timeoutMs: Long,\n            resultHandler: (WebOperationResult<WebInteractionOperation<Void>>) -> Unit,\n            assertion: OperationAssertion = DefaultOperationAssertion(\"\") {}\n        ) {\n            UltronWebLifecycle.execute(\n                WebInteractionOperationExecutor(\n                    WebInteractionOperation(\n                        webInteractionBlock = webInteractionBlock,\n                        name = name,\n                        type = type,\n                        description = description,\n                        timeoutMs = timeoutMs,\n                        assertion = assertion\n                    )\n                ), resultHandler\n            )\n        }\n    }\n}"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/espressoweb/webelement/UltronWebElement.kt",
    "content": "package com.atiurin.ultron.core.espressoweb.webelement\n\nimport android.view.View\nimport androidx.test.espresso.web.assertion.WebAssertion\nimport androidx.test.espresso.web.assertion.WebViewAssertions.webContent\nimport androidx.test.espresso.web.assertion.WebViewAssertions.webMatches\nimport androidx.test.espresso.web.model.ElementReference\nimport androidx.test.espresso.web.model.WindowReference\nimport androidx.test.espresso.web.sugar.Web\nimport androidx.test.espresso.web.sugar.Web.onWebView\nimport androidx.test.espresso.web.webdriver.DriverAtoms\nimport androidx.test.espresso.web.webdriver.DriverAtoms.findElement\nimport androidx.test.espresso.web.webdriver.Locator\nimport com.atiurin.ultron.core.common.CommonOperationType\nimport com.atiurin.ultron.core.common.DefaultElementInfo\nimport com.atiurin.ultron.core.common.ElementInfo\nimport com.atiurin.ultron.core.common.UltronOperationType\nimport com.atiurin.ultron.core.common.assertion.DefaultOperationAssertion\nimport com.atiurin.ultron.core.common.assertion.EmptyOperationAssertion\nimport com.atiurin.ultron.core.common.assertion.OperationAssertion\nimport com.atiurin.ultron.core.config.UltronConfig\nimport com.atiurin.ultron.core.espressoweb.UltronWebLifecycle\nimport com.atiurin.ultron.core.espressoweb.operation.EspressoWebOperationType\nimport com.atiurin.ultron.core.espressoweb.operation.WebInteractionOperation\nimport com.atiurin.ultron.core.espressoweb.operation.WebInteractionOperationExecutor\nimport com.atiurin.ultron.core.espressoweb.operation.WebInteractionOperationIterationResult\nimport com.atiurin.ultron.core.espressoweb.operation.WebOperationResult\nimport com.atiurin.ultron.listeners.setListenersState\nimport com.atiurin.ultron.log.UltronLog\nimport org.hamcrest.Matcher\nimport org.hamcrest.Matchers.containsString\nimport org.hamcrest.Matchers.`is`\nimport org.w3c.dom.Document\n\n/**\n * It is required to create WebElement object using method in Companion object\n * like [UltronWebElement.Companion.id], [UltronWebElement.Companion.xpath], [UltronWebElement.Companion.element] and etc\n */\n@Suppress(\"UNCHECKED_CAST\", \"MemberVisibilityCanBePrivate\")\nopen class UltronWebElement internal constructor(\n    open val locator: Locator,\n    open val value: String,\n    private val webViewMatcher: Matcher<View> = UltronConfig.Espresso.webViewMatcher,\n    private val elementReference: ElementReference? = null,\n    private val windowReference: WindowReference? = null,\n    open val timeoutMs: Long? = null,\n    open val resultHandler: (WebOperationResult<WebInteractionOperation<*>>) -> Unit = UltronConfig.Espresso.WebInteractionOperationConfig.resultHandler,\n    open val assertion: OperationAssertion = EmptyOperationAssertion(),\n    open val elementInfo: ElementInfo = DefaultElementInfo(),\n    private val contextualElements: List<UltronWebElement> = mutableListOf<UltronWebElement>()\n) {\n    private fun getActionTimeout(): Long = timeoutMs ?: UltronConfig.Espresso.ACTION_TIMEOUT\n    private fun getAssertionTimeout(): Long = timeoutMs ?: UltronConfig.Espresso.ASSERTION_TIMEOUT\n\n    init {\n        if (elementInfo != null && elementInfo.name.isEmpty()) {\n            this.elementInfo.name = \"WebElement(locator = $locator, value = '$value')\"\n        }\n    }\n\n    fun withContextual(ultronWebElement: UltronWebElement, extendElementName: Boolean = true): UltronWebElement {\n        val newElement = UltronWebElement(\n            this.locator,\n            this.value,\n            this.webViewMatcher,\n            this.elementReference,\n            this.windowReference,\n            this.timeoutMs,\n            this.resultHandler,\n            this.assertion,\n            this.elementInfo.copy(),\n            contextualElements + ultronWebElement\n        )\n        if (extendElementName) {\n            newElement.withName(\"${elementInfo.name} with contextual [locator = ${ultronWebElement.locator}, value = '${ultronWebElement.value}')]\")\n        }\n        return newElement\n    }\n\n    open fun withTimeout(timeoutMs: Long): UltronWebElement {\n        return UltronWebElement(\n            this.locator,\n            this.value,\n            this.webViewMatcher,\n            this.elementReference,\n            this.windowReference,\n            timeoutMs,\n            this.resultHandler,\n            this.assertion,\n            this.elementInfo.copy(),\n            this.contextualElements\n        )\n    }\n\n    open fun withResultHandler(resultHandler: (WebOperationResult<WebInteractionOperation<*>>) -> Unit): UltronWebElement = UltronWebElement(\n        this.locator,\n        this.value,\n        this.webViewMatcher,\n        this.elementReference,\n        this.windowReference,\n        this.timeoutMs,\n        resultHandler,\n        this.assertion,\n        this.elementInfo.copy(),\n        this.contextualElements\n    )\n\n    open fun withAssertion(assertion: OperationAssertion): UltronWebElement = UltronWebElement(\n        this.locator,\n        this.value,\n        this.webViewMatcher,\n        this.elementReference,\n        this.windowReference,\n        this.timeoutMs,\n        this.resultHandler,\n        assertion,\n        this.elementInfo.copy(),\n        this.contextualElements\n    )\n\n    open fun withAssertion(\n        name: String = \"\", isListened: Boolean = false, block: () -> Unit\n    ): UltronWebElement = UltronWebElement(\n        this.locator,\n        this.value,\n        this.webViewMatcher,\n        this.elementReference,\n        this.windowReference,\n        this.timeoutMs,\n        this.resultHandler,\n        DefaultOperationAssertion(name, block.setListenersState(isListened)),\n        this.elementInfo.copy(),\n        this.contextualElements\n    )\n\n    open fun withName(name: String) = apply { elementInfo.name = name }\n\n    open fun withMetaInfo(meta: Any) = apply { elementInfo.meta = meta }\n\n\n    internal val webViewInteraction: Web.WebInteraction<Void>\n        get() {\n            return if (windowReference != null) onWebView(webViewMatcher).inWindow(windowReference)\n            else onWebView(webViewMatcher)\n        }\n\n\n    val webInteractionBlock: () -> Web.WebInteraction<Void>\n        get() = {\n            var wvi = if (elementReference == null) {\n                webViewInteraction.withElement(findElement(locator, value))\n            } else webViewInteraction.withElement(elementReference)\n            UltronLog.info(\"(UContextual): \" + contextualElements.map { \"[${it.locator}. ${it.value}]\" }.toString())\n            contextualElements.forEach {\n                wvi = wvi.withContextualElement(\n                    findElement(it.locator, it.value)\n                )\n            }\n            wvi\n        }\n\n    /** Clears content from an editable element. */\n    fun clearElement() = apply {\n        executeOperation(\n            getUltronWebActionOperation(\n                webInteractionBlock = { webInteractionBlock().perform(DriverAtoms.clearElement()) },\n                name = \"ClearElement of '${elementInfo.name}'\",\n                type = EspressoWebOperationType.WEB_CLEAR_ELEMENT,\n                description = \"ClearElement of '${elementInfo.name}' during $timeoutMs ms\"\n            )\n        )\n    }\n\n    /** Returns the visible text beneath a given DOM element. */\n    fun getText(): String {\n        return executeOperation(\n            getUltronWebActionOperation(\n                webInteractionBlock = { webInteractionBlock().perform(DriverAtoms.getText()) },\n                name = \"GetText of '${elementInfo.name}'\",\n                type = EspressoWebOperationType.WEB_GET_TEXT,\n                description = \"GetText of '${elementInfo.name}' during $timeoutMs ms\"\n            )\n        ) ?: \"\"\n    }\n\n    /** Simulates the javascript events to click on a particular element. */\n    fun webClick() = apply {\n        executeOperation(\n            getUltronWebActionOperation(\n                webInteractionBlock = {\n                    webInteractionBlock().perform(DriverAtoms.webClick())\n                },\n                name = \"WebClick on '${elementInfo.name}'\",\n                type = EspressoWebOperationType.WEB_CLICK,\n                description = \"WebClick on '${elementInfo.name}' during $timeoutMs ms\"\n            )\n        )\n    }\n\n    /** Simulates javascript key events sent to a certain element. */\n    fun webKeys(text: String) = apply {\n        executeOperation(\n            getUltronWebActionOperation(\n                webInteractionBlock = {\n                    webInteractionBlock().perform(DriverAtoms.webKeys(text))\n                },\n                name = \"WebKeys text '$text' on '${elementInfo.name}'\",\n                type = EspressoWebOperationType.WEB_KEYS,\n                description = \"WebKeys text '$text' on '${elementInfo.name}' during $timeoutMs ms\"\n            )\n        )\n    }\n\n    /** Simulates javascript clear and key events sent to a certain element. */\n    fun replaceText(\n        text: String\n    ) = apply {\n        executeOperation(\n            getUltronWebActionOperation(\n                webInteractionBlock = {\n                    webInteractionBlock().perform(DriverAtoms.clearElement()).perform(DriverAtoms.webKeys(text))\n                },\n                name = \"${elementInfo.name} ReplaceText to '$text'\",\n                type = EspressoWebOperationType.WEB_REPLACE_TEXT,\n                description = \"'${elementInfo.name}' ReplaceText to '$text' during $timeoutMs ms\"\n            )\n        )\n    }\n\n    /** Returns {@code true} if the desired element is in view after scrolling. */\n    fun webScrollIntoViewBoolean(): Boolean {\n        return executeOperation(\n            getUltronWebActionOperation(\n                webInteractionBlock = { webInteractionBlock().perform(DriverAtoms.webScrollIntoView()) },\n                name = \"${elementInfo.name} WebScrollIntoView\",\n                type = EspressoWebOperationType.WEB_SCROLL_INTO_VIEW,\n                description = \"'${elementInfo.name}' WebScrollIntoView during $timeoutMs ms\"\n            )\n        ) ?: false\n    }\n\n    /** Executes scroll to view. */\n    fun webScrollIntoView() = apply {\n        executeOperation(\n            getUltronWebActionOperation(\n                webInteractionBlock = { webInteractionBlock().perform(DriverAtoms.webScrollIntoView()) },\n                name = \"${elementInfo.name} WebScrollIntoView\",\n                type = EspressoWebOperationType.WEB_SCROLL_INTO_VIEW,\n                description = \"'${elementInfo.name}' WebScrollIntoView during $timeoutMs ms\"\n            )\n        )\n    }\n\n    /** Asserts that DOM element contains visible text beneath it self. */\n    fun containsText(\n        text: String\n    ) = apply {\n        executeOperation(\n            getUltronWebAssertionOperation(\n                webInteractionBlock = {\n                    webInteractionBlock().check(\n                        webMatches(DriverAtoms.getText(), containsString(text))\n                    )\n                },\n                name = \"${elementInfo.name} ContainsText '$text'\",\n                type = EspressoWebOperationType.WEB_CONTAINS_TEXT,\n                description = \"Assert '${elementInfo.name}' ContainsText = '$text' during $timeoutMs ms\"\n            )\n        )\n    }\n\n    /** Asserts that DOM element has visible text beneath it self. */\n    fun hasText(text: String) = apply {\n        executeOperation(\n            getUltronWebAssertionOperation(\n                webInteractionBlock = {\n                    webInteractionBlock().check(\n                        webMatches(DriverAtoms.getText(), `is`(text))\n                    )\n                },\n                name = \"${elementInfo.name} HasText '$text'\",\n                type = EspressoWebOperationType.WEB_HAS_TEXT,\n                description = \"Assert '${elementInfo.name}' HasText = '$text' during $timeoutMs ms\"\n            )\n        )\n    }\n\n    /** Asserts that element exists in webView */\n    fun exists() = apply {\n        UltronLog.error(\"exists name = '${elementInfo.name}'\")\n        executeOperationVoid(\n            getUltronWebAssertionOperation(\n                webInteractionBlock = {\n                    webInteractionBlock()\n                },\n                name = \"${elementInfo.name} exists\",\n                type = EspressoWebOperationType.WEB_EXISTS,\n                description = \"Assert '${elementInfo.name}' exists during $timeoutMs ms\",\n            )\n        )\n    }\n\n    protected fun hasElementAttribute(\n        attributeName: String, attributeValueMatcher: Matcher<String>, documentMatcher: Matcher<Document>\n    ) = apply {\n        executeOperation(\n            getUltronWebAssertionOperation(\n                webInteractionBlock = {\n                    webViewInteraction.check(\n                        webContent(\n                            documentMatcher\n                        )\n                    )\n                },\n                name = \"${elementInfo.name} hasAttribute '$attributeName' matches $attributeValueMatcher\",\n                type = EspressoWebOperationType.WEB_HAS_ATTRIBUTE,\n                description = \"Assert '${elementInfo.name}' hasAttribute '$attributeName' matches $attributeValueMatcher during $timeoutMs ms\"\n            )\n        )\n    }\n\n\n    /** use any webAssertion to assert it safely */\n    fun <T> assertThat(\n        webAssertion: WebAssertion<T>\n    ) = apply {\n        executeOperation(\n            getUltronWebAssertionOperation(\n                webInteractionBlock = {\n                    webInteractionBlock().check(webAssertion)\n                },\n                name = \"${elementInfo.name} custom webAssertion\",\n                type = EspressoWebOperationType.WEB_ASSERT_THAT,\n                description = \"${elementInfo.name} custom webAssertion during $timeoutMs ms\"\n            )\n        )\n    }\n\n    /** Transforms any action or assertion to Boolean value */\n    fun isSuccess(block: UltronWebElement.() -> Unit): Boolean = runCatching { block() }.isSuccess\n\n    /** Removes the Element and Window references from this interaction */\n    fun reset() = apply { webViewInteraction.reset() }\n\n    fun <R> executeOperation(\n        webInteractionOperation: WebInteractionOperation<R>\n    ): R? {\n        val result = UltronWebLifecycle.execute(\n            executor = WebInteractionOperationExecutor(webInteractionOperation),\n            resultHandler = resultHandler as (WebOperationResult<WebInteractionOperation<R>>) -> Unit\n        )\n        return (result.operationIterationResult as WebInteractionOperationIterationResult<R>).webInteraction?.get()\n    }\n\n    fun <R> executeOperationVoid(\n        webInteractionOperation: WebInteractionOperation<R>\n    ): WebOperationResult<WebInteractionOperation<R>> {\n        return UltronWebLifecycle.execute(\n            executor = WebInteractionOperationExecutor(webInteractionOperation),\n            resultHandler = resultHandler as (WebOperationResult<WebInteractionOperation<R>>) -> Unit\n        )\n    }\n\n    fun <R> getUltronWebActionOperation(\n        webInteractionBlock: () -> Web.WebInteraction<R>, name: String, type: UltronOperationType = CommonOperationType.DEFAULT, description: String\n    ) = WebInteractionOperation(\n        webInteractionBlock, name, type, description, getActionTimeout(), assertion, elementInfo\n    )\n\n    fun <R> getUltronWebAssertionOperation(\n        webInteractionBlock: () -> Web.WebInteraction<R>, name: String, type: UltronOperationType = CommonOperationType.DEFAULT, description: String\n    ) = WebInteractionOperation(\n        webInteractionBlock, name, type, description, getAssertionTimeout(), assertion, elementInfo\n    )\n\n    companion object {\n        fun className(\n            value: String,\n            webViewMatcher: Matcher<View> = UltronConfig.Espresso.webViewMatcher,\n            elementReference: ElementReference? = null,\n            windowReference: WindowReference? = null\n        ): UltronWebElement {\n            return UltronWebElement(\n                Locator.CLASS_NAME, value, webViewMatcher, elementReference, windowReference\n            )\n        }\n\n        fun cssSelector(\n            value: String,\n            webViewMatcher: Matcher<View> = UltronConfig.Espresso.webViewMatcher,\n            elementReference: ElementReference? = null,\n            windowReference: WindowReference? = null\n        ): UltronWebElement {\n            return UltronWebElement(\n                Locator.CSS_SELECTOR, value, webViewMatcher, elementReference, windowReference\n            )\n        }\n\n        fun id(\n            value: String,\n            webViewMatcher: Matcher<View> = UltronConfig.Espresso.webViewMatcher,\n            elementReference: ElementReference? = null,\n            windowReference: WindowReference? = null\n        ): UltronWebElementId {\n            return UltronWebElementId(value, webViewMatcher, elementReference, windowReference)\n        }\n\n        fun linkText(\n            value: String,\n            webViewMatcher: Matcher<View> = UltronConfig.Espresso.webViewMatcher,\n            elementReference: ElementReference? = null,\n            windowReference: WindowReference? = null\n        ): UltronWebElement {\n            return UltronWebElement(\n                Locator.LINK_TEXT, value, webViewMatcher, elementReference, windowReference\n            )\n        }\n\n        fun name(\n            value: String,\n            webViewMatcher: Matcher<View> = UltronConfig.Espresso.webViewMatcher,\n            elementReference: ElementReference? = null,\n            windowReference: WindowReference? = null\n        ): UltronWebElement {\n            return UltronWebElement(\n                Locator.NAME, value, webViewMatcher, elementReference, windowReference\n            )\n        }\n\n        fun partialLinkText(\n            value: String,\n            webViewMatcher: Matcher<View> = UltronConfig.Espresso.webViewMatcher,\n            elementReference: ElementReference? = null,\n            windowReference: WindowReference? = null\n        ): UltronWebElement {\n            return UltronWebElement(\n                Locator.PARTIAL_LINK_TEXT, value, webViewMatcher, elementReference, windowReference\n            )\n        }\n\n        fun tagName(\n            value: String,\n            webViewMatcher: Matcher<View> = UltronConfig.Espresso.webViewMatcher,\n            elementReference: ElementReference? = null,\n            windowReference: WindowReference? = null\n        ): UltronWebElement {\n            return UltronWebElement(\n                Locator.TAG_NAME, value, webViewMatcher, elementReference, windowReference\n            )\n        }\n\n        fun xpath(\n            value: String,\n            webViewMatcher: Matcher<View> = UltronConfig.Espresso.webViewMatcher,\n            elementReference: ElementReference? = null,\n            windowReference: WindowReference? = null\n        ): UltronWebElementXpath {\n            return UltronWebElementXpath(\n                value, webViewMatcher, elementReference, windowReference\n            )\n        }\n\n        fun element(\n            locator: Locator,\n            value: String,\n            webViewMatcher: Matcher<View> = UltronConfig.Espresso.webViewMatcher,\n            elementReference: ElementReference? = null,\n            windowReference: WindowReference? = null\n        ): UltronWebElement {\n            return UltronWebElement(\n                locator, value, webViewMatcher, elementReference, windowReference\n            )\n        }\n    }\n}\n\n\n\n\n"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/espressoweb/webelement/UltronWebElementId.kt",
    "content": "package com.atiurin.ultron.core.espressoweb.webelement\n\nimport android.view.View\nimport androidx.test.espresso.web.matcher.DomMatchers.elementById\nimport androidx.test.espresso.web.model.ElementReference\nimport androidx.test.espresso.web.model.WindowReference\nimport androidx.test.espresso.web.webdriver.Locator\nimport com.atiurin.ultron.core.common.ElementInfo\nimport com.atiurin.ultron.core.common.DefaultElementInfo\nimport com.atiurin.ultron.core.common.assertion.EmptyOperationAssertion\nimport com.atiurin.ultron.core.common.assertion.OperationAssertion\nimport com.atiurin.ultron.core.config.UltronConfig\nimport com.atiurin.ultron.core.espressoweb.operation.WebInteractionOperation\nimport com.atiurin.ultron.core.espressoweb.operation.WebOperationResult\nimport com.atiurin.ultron.custom.espresso.matcher.ElementWithAttributeMatcher.Companion.withAttribute\nimport org.hamcrest.Matcher\nimport org.hamcrest.Matchers.`is`\n\nclass UltronWebElementId(\n    override val value: String,\n    private val webViewMatcher: Matcher<View> = UltronConfig.Espresso.webViewMatcher,\n    private val elementReference: ElementReference? = null,\n    private val windowReference: WindowReference? = null,\n    override val timeoutMs: Long? = null,\n    override val resultHandler: (WebOperationResult<WebInteractionOperation<*>>) -> Unit = UltronConfig.Espresso.WebInteractionOperationConfig.resultHandler,\n    override val assertion: OperationAssertion = EmptyOperationAssertion(),\n    override val elementInfo: ElementInfo = DefaultElementInfo()\n) : UltronWebElement(\n    Locator.ID, value, webViewMatcher, elementReference, windowReference, timeoutMs, resultHandler, assertion, elementInfo\n) {\n    init {\n        if (this.elementInfo.name.isEmpty()) {\n            this.elementInfo.name = \"WebElement(locator = $locator, value = '$value')\"\n        }\n    }\n    override fun withTimeout(timeoutMs: Long) = UltronWebElementId(\n        this.value, this.webViewMatcher, this.elementReference, this.windowReference, timeoutMs, this.resultHandler, this.assertion, this.elementInfo\n    )\n\n    override fun withResultHandler(resultHandler: (WebOperationResult<WebInteractionOperation<*>>) -> Unit) = UltronWebElementId(\n        this.value, this.webViewMatcher, this.elementReference, this.windowReference, this.timeoutMs, resultHandler, this.assertion, this.elementInfo\n    )\n\n    override fun withAssertion(assertion: OperationAssertion) = UltronWebElementId(\n        this.value, this.webViewMatcher, this.elementReference, this.windowReference, this.timeoutMs, this.resultHandler, assertion, this.elementInfo\n    )\n\n    override fun withName(name: String) = apply { elementInfo.name = name }\n\n    override fun withMetaInfo(meta: Any) = apply { elementInfo.meta = meta }\n\n    fun hasAttribute(\n        attributeName: String, attributeValueMatcher: Matcher<String>\n    ) = apply {\n        this.hasElementAttribute(\n            attributeName, attributeValueMatcher, elementById(\n                value, withAttribute(\n                    attributeName, attributeValueMatcher\n                )\n            )\n        )\n    }\n\n    fun hasAttribute(\n        attributeName: String, attributeValue: String\n    ) = apply {\n        val matcher = `is`(attributeValue)\n        this.hasElementAttribute(\n            attributeName, matcher, elementById(\n                value, withAttribute(\n                    attributeName, matcher\n                )\n            )\n        )\n    }\n}"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/espressoweb/webelement/UltronWebElementUiBlock.kt",
    "content": "package com.atiurin.ultron.core.espressoweb.webelement\n\nimport com.atiurin.ultron.exceptions.UltronException\nimport com.atiurin.ultron.log.UltronLog\nimport kotlin.reflect.typeOf\n\nopen class UltronWebElementUiBlock(val blockElement: UltronWebElement, val blockDescription: String = \"\") {\n    /**\n     * A utility property for interaction with this UI block.\n     *\n     * Appends the block description to the matcher for better logging and debugging.\n     */\n    val uiBlock\n        get() = blockElement.let {\n            if (blockDescription.isNotBlank()) {\n                it.withName(blockDescription)\n            } else it\n        }\n\n    /**\n     * Modifies the provided element by contextualizing it with the current block's element.\n     *\n     * This method applies a contextual transformation to the given `element`, associating it with the context\n     * of the current block's `blockElement`.\n     *\n     * @param childElement The element to be contextualized.\n     * @return A new element modified by adding the context of the current block's element.\n     */\n    fun child(childElement: UltronWebElement): UltronWebElement = blockElement.withContextual(childElement)\n\n    /**\n     * Creates a child UI block using an element contextualized with the current block's element.\n     *\n     * This method applies a contextual transformation to the provided `element` and constructs a new UI block instance\n     * using the `uiBlockFactory`.\n     *\n     * @param childElement The element to be contextualized for the child UI block.\n     * @param uiBlockFactory A factory function to create a new instance of the child UI block.\n     *                       The function takes the contextualized element as input and returns a UI block instance.\n     * @return A new instance of the child UI block, constructed with the contextualized element.\n     */\n    fun <B : UltronWebElementUiBlock> child(\n        childElement: UltronWebElement,\n        uiBlockFactory: (UltronWebElement) -> B\n    ): B {\n        return uiBlockFactory(blockElement.withContextual(childElement))\n    }\n\n    /**\n     * Creates a child UI block of a specified type using the provided block element of the parent UI block.\n     *\n     * This extension function simplifies the creation of a new instance of a child UI block by automatically invoking\n     * the appropriate constructor of the specified type [T]. The `blockElement` of the current block is contextualized\n     * with the `blockElement` of the provided `uiBlock` to locate and define the new block.\n     *\n     * @param T The type of the child UI block to be created. It must extend [UltronWebElementUiBlock].\n     * @param uiBlock The existing instance of the child UI block to use as a template for the new block.\n     *                The `blockElement` and `blockDescription` properties of this block are used to create the new instance.\n     * @return A new instance of the specified type [T], initialized with the contextualized element and description.\n     *\n     * @throws UltronException If the specified class [T] does not have an appropriate constructor or cannot be instantiated.\n     *\n     * ### Constructor Requirements:\n     * The class [T] must meet the following conditions to be instantiated:\n     * 1. It must not be a nested or inner class. It should be defined at the top level or as a file-level class.\n     * 2. It must have one of the following constructors:\n     *    - A constructor with two parameters: `blockElement` of type [UltronWebElement] and `blockDescription` of type [String]:\n     *      `class CustomWebElementBlock(blockElement: UltronWebElement, blockDescription: String)`\n     *    - A constructor with one parameter of type [UltronWebElement]:\n     *      `class CustomWebElementBlock(blockElement: UltronWebElement)`\n     *\n     * If neither constructor is available, consider using an alternative method for child creation,\n     * such as `child(element, uiBlockFactory)`.\n     *\n     * ### Usage Example:\n     * ```kotlin\n     * // UI elements declaration\n     * class CustomElementBlock(blockElement: UltronWebElement, blockDescription: String = \"\") : UltronWebElementUiBlock(blockElement, blockDescription)\n     *\n     * class CustomComplexBlock(blockElement: UltronWebElement, blockDescription: String = \"\") : UltronWebElementUiBlock(blockElement, blockDescription) {\n     *     val custom = child(CustomElementBlock(blockElement.withContextual(hasTestTag(\"elementTag\")), \"custom element child of '$blockDescription'\"))\n     * }\n     *\n     * // Screen object\n     * object MyWebPage : Screen<MyWebPage>() {\n     *     val complexBlock = CustomComplexBlock(findElement(\"bigUiBlockLocator\"), \"Presentable description of UI block\")\n     * }\n     *\n     * // Somewhere in the test\n     * MyWebPage {\n     *     complexBlock.custom.isDisplayed()\n     * }\n     * ```\n     */\n    inline fun <reified T : UltronWebElementUiBlock> child(\n        uiBlock: T,\n    ): T {\n        val updatedElement = blockElement.withContextual(uiBlock.blockElement)\n        val updatedBlock = runCatching {\n            T::class.constructors.forEach { constructor ->\n                UltronLog.info(\"Constructor: $constructor, Parameters: ${constructor.parameters.map { it.type }}\")\n            }\n            T::class.constructors.firstOrNull {\n                it.parameters.size == 2\n                        && it.parameters.first().type == typeOf<UltronWebElement>()\n                        && it.parameters[1].type == typeOf<String>()\n            }?.let { constructor ->\n                return@runCatching constructor.call(updatedElement, uiBlock.blockDescription)\n            }\n            T::class.constructors.firstOrNull {\n                it.parameters.size == 1 && it.parameters.first().type == typeOf<UltronWebElement>()\n            }?.let {\n                return@runCatching it.call(updatedElement)\n            }\n            null\n        }.onFailure {\n            if (it is IllegalArgumentException) {\n                throw UltronException(\n                    \"${T::class.simpleName} has hidden java constructor parameters. ${T::class.simpleName} must be defined as a top-level class (not nested inside any other class).\"\n                )\n            } else throw UltronException(\"Unable to create updated ${T::class.simpleName}. Message: ${it.message}\")\n        }.getOrNull()\n        updatedBlock?.let { return it } ?: throw UltronException(\n            \"\"\" |${T::class.simpleName} doesn't have an appropriate constructor with arguments: (UltronWebElement, String) or (UltronWebElement)\n                |Ensure that the class meets the following conditions:\n                |1. ${T::class.simpleName} must not be defined inside another class. It should be a top-level or file-level class.\n                |2. ${T::class.simpleName} must have one of the following constructors:\n                |- A constructor with two parameters: blockElement of type UltronWebElement and blockDescription of type String:\n                |class ${T::class.simpleName}(blockElement: UltronWebElement, blockDescription: String) : UltronWebElementUiBlock(blockElement, blockDescription)\n                |- A constructor with one parameter of type UltronWebElement:\n                |class ${T::class.simpleName}(blockElement: UltronWebElement) : UltronWebElementUiBlock(blockElement)\n                |If neither constructor is available, consider using another method for child declaration:\n                |```\n                |fun <B : UltronWebElementUiBlock> child(\n                |    element: UltronWebElement,\n                |    uiBlockFactory: (UltronWebElement) -> B\n                |): B\n                |```\n            \"\"\".trimMargin()\n        )\n    }\n}"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/espressoweb/webelement/UltronWebElementXpath.kt",
    "content": "package com.atiurin.ultron.core.espressoweb.webelement\n\nimport android.view.View\nimport androidx.test.espresso.web.matcher.DomMatchers.elementByXPath\nimport androidx.test.espresso.web.model.ElementReference\nimport androidx.test.espresso.web.model.WindowReference\nimport androidx.test.espresso.web.webdriver.Locator\nimport com.atiurin.ultron.core.common.ElementInfo\nimport com.atiurin.ultron.core.common.DefaultElementInfo\nimport com.atiurin.ultron.core.common.assertion.EmptyOperationAssertion\nimport com.atiurin.ultron.core.common.assertion.OperationAssertion\nimport com.atiurin.ultron.core.config.UltronConfig\nimport com.atiurin.ultron.core.espressoweb.operation.WebInteractionOperation\nimport com.atiurin.ultron.core.espressoweb.operation.WebOperationResult\nimport com.atiurin.ultron.custom.espresso.matcher.ElementWithAttributeMatcher.Companion.withAttribute\nimport org.hamcrest.CoreMatchers.`is`\nimport org.hamcrest.Matcher\n\nclass UltronWebElementXpath(\n    override val value: String,\n    private val webViewMatcher: Matcher<View> = UltronConfig.Espresso.webViewMatcher,\n    private val elementReference: ElementReference? = null,\n    private val windowReference: WindowReference? = null,\n    override val timeoutMs: Long? = null,\n    override val resultHandler: (WebOperationResult<WebInteractionOperation<*>>) -> Unit = UltronConfig.Espresso.WebInteractionOperationConfig.resultHandler,\n    override val assertion: OperationAssertion = EmptyOperationAssertion(),\n    override val elementInfo: ElementInfo = DefaultElementInfo()\n) : UltronWebElement(\n    Locator.XPATH, value, webViewMatcher, elementReference, windowReference, timeoutMs, resultHandler, assertion, elementInfo\n) {\n    init {\n        if (this.elementInfo.name.isEmpty()) {\n            this.elementInfo.name = \"WebElement(locator = $locator, value = '$value')\"\n        }\n    }\n\n    override fun withTimeout(timeoutMs: Long) = UltronWebElementXpath(\n        this.value, this.webViewMatcher, this.elementReference, this.windowReference, timeoutMs, this.resultHandler, this.assertion, this.elementInfo\n    )\n\n    override fun withResultHandler(resultHandler: (WebOperationResult<WebInteractionOperation<*>>) -> Unit) = UltronWebElementXpath(\n        this.value, this.webViewMatcher, this.elementReference, this.windowReference, this.timeoutMs, resultHandler, this.assertion, this.elementInfo\n    )\n\n\n    override fun withAssertion(assertion: OperationAssertion) = UltronWebElementXpath(\n        this.value, this.webViewMatcher, this.elementReference, this.windowReference, this.timeoutMs, this.resultHandler, assertion, this.elementInfo\n    )\n\n    override fun withName(name: String) = apply { elementInfo.name = name }\n\n    override fun withMetaInfo(meta: Any) = apply { elementInfo.meta = meta }\n\n    fun hasAttribute(\n        attributeName: String, attributeValueMatcher: Matcher<String>\n    ) = apply {\n        this.hasElementAttribute(\n            attributeName, attributeValueMatcher, elementByXPath(\n                value, withAttribute(\n                    attributeName, attributeValueMatcher\n                )\n            )\n        )\n    }\n\n    fun hasAttribute(\n        attributeName: String,\n        attributeValue: String,\n    ) = apply {\n        val matcher = `is`(attributeValue)\n        this.hasElementAttribute(\n            attributeName, matcher, elementByXPath(\n                value, withAttribute(\n                    attributeName, matcher\n                )\n            )\n        )\n    }\n}"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/espressoweb/webelement/UltronWebElements.kt",
    "content": "package com.atiurin.ultron.core.espressoweb.webelement\n\nimport android.view.View\nimport androidx.test.espresso.web.model.ElementReference\nimport androidx.test.espresso.web.model.WindowReference\nimport androidx.test.espresso.web.sugar.Web\nimport androidx.test.espresso.web.sugar.Web.onWebView\nimport androidx.test.espresso.web.webdriver.DriverAtoms\nimport androidx.test.espresso.web.webdriver.Locator\nimport com.atiurin.ultron.core.config.UltronConfig\nimport com.atiurin.ultron.core.espressoweb.operation.EspressoWebOperationType\nimport com.atiurin.ultron.core.espressoweb.operation.WebInteractionOperation\nimport com.atiurin.ultron.core.espressoweb.operation.WebOperationResult\nimport com.atiurin.ultron.core.espressoweb.webelement.UltronWebDocument.Companion.executeOperation\nimport org.hamcrest.Matcher\n\n/**\n * Represents the list of web objects with [locator] and [value]\n * It is required to create [UltronWebElements] object using method in Companion object\n * like [UltronWebElements.Companion.ids], [UltronWebElements.Companion.xpaths], [UltronWebElements.Companion.elements] and etc\n */\nclass UltronWebElements(\n    val locator: Locator,\n    val value: String,\n    private val webViewMatcher: Matcher<View> = UltronConfig.Espresso.webViewMatcher,\n    private val windowReference: WindowReference? = null,\n    private val timeoutMs: Long? = null,\n    private val resultHandler: (WebOperationResult<WebInteractionOperation<*>>) -> Unit = UltronConfig.Espresso.WebInteractionOperationConfig.resultHandler\n) {\n    private val webViewInteraction: Web.WebInteraction<Void>\n        get() {\n            return if (windowReference != null) onWebView(webViewMatcher).inWindow(windowReference)\n            else onWebView(webViewMatcher)\n        }\n\n    fun withTimeout(timeoutMs: Long): UltronWebElements {\n        return UltronWebElements(\n            this.locator,\n            this.value,\n            this.webViewMatcher,\n            this.windowReference,\n            timeoutMs,\n            this.resultHandler\n        )\n    }\n\n    fun withResultHandler(resultHandler: (WebOperationResult<WebInteractionOperation<*>>) -> Unit): UltronWebElements {\n        return UltronWebElements(\n            this.locator,\n            this.value,\n            this.webViewMatcher,\n            this.windowReference,\n            this.timeoutMs,\n            resultHandler\n        )\n    }\n\n    /**\n     * @return list of [UltronWebElement], empty list if no elements found\n     */\n    fun getElements(): List<UltronWebElement> {\n        return findMultipleElements(locator, value).map {\n            UltronWebElement(\n                locator, value, webViewMatcher, it\n            )\n        }\n    }\n\n    /**\n     * @return size of elements, 0 if no elements found\n     */\n    fun getSize() = getElements().size\n\n    private fun findMultipleElements(\n        locator: Locator, matcher: String\n    ): List<ElementReference> {\n        return executeOperation(\n            webInteractionBlock = {\n                webViewInteraction.withNoTimeout()\n                    .perform(DriverAtoms.findMultipleElements(locator, matcher))\n            },\n            name = \"WebElementsList(${locator.type} = '$matcher') findMultipleElements\",\n            type = EspressoWebOperationType.WEB_FIND_MULTIPLE_ELEMENTS,\n            description = \"WebElementsList(${locator.type} = '$matcher') findMultipleElements during $timeoutMs ms\",\n            timeoutMs = timeoutMs ?: UltronConfig.Espresso.ACTION_TIMEOUT,\n            resultHandler as (WebOperationResult<WebInteractionOperation<List<ElementReference>>>) -> Unit\n        )\n    }\n\n    companion object {\n        fun classNames(\n            value: String,\n            webViewMatcher: Matcher<View> = UltronConfig.Espresso.webViewMatcher,\n            windowReference: WindowReference? = null\n        ): UltronWebElements {\n            return UltronWebElements(Locator.CLASS_NAME, value, webViewMatcher, windowReference)\n        }\n\n        fun cssSelectors(\n            value: String,\n            webViewMatcher: Matcher<View> = UltronConfig.Espresso.webViewMatcher,\n            windowReference: WindowReference? = null\n        ): UltronWebElements {\n            return UltronWebElements(Locator.CSS_SELECTOR, value, webViewMatcher, windowReference)\n        }\n\n        fun ids(\n            value: String,\n            webViewMatcher: Matcher<View> = UltronConfig.Espresso.webViewMatcher,\n            windowReference: WindowReference? = null\n        ): UltronWebElements {\n            return UltronWebElements(Locator.ID, value, webViewMatcher, windowReference)\n        }\n\n        fun linkTexts(\n            value: String,\n            webViewMatcher: Matcher<View> = UltronConfig.Espresso.webViewMatcher,\n            windowReference: WindowReference? = null\n        ): UltronWebElements {\n            return UltronWebElements(Locator.LINK_TEXT, value, webViewMatcher, windowReference)\n        }\n\n        fun names(\n            value: String,\n            webViewMatcher: Matcher<View> = UltronConfig.Espresso.webViewMatcher,\n            windowReference: WindowReference? = null\n        ): UltronWebElements {\n            return UltronWebElements(Locator.NAME, value, webViewMatcher, windowReference)\n        }\n\n        fun partialLinkTexts(\n            value: String,\n            webViewMatcher: Matcher<View> = UltronConfig.Espresso.webViewMatcher,\n            windowReference: WindowReference? = null\n        ): UltronWebElements {\n            return UltronWebElements(\n                Locator.PARTIAL_LINK_TEXT,\n                value,\n                webViewMatcher,\n                windowReference\n            )\n        }\n\n        fun tagNames(\n            value: String,\n            webViewMatcher: Matcher<View> = UltronConfig.Espresso.webViewMatcher,\n            windowReference: WindowReference? = null\n        ): UltronWebElements {\n            return UltronWebElements(Locator.TAG_NAME, value, webViewMatcher, windowReference)\n        }\n\n        fun xpaths(\n            value: String,\n            webViewMatcher: Matcher<View> = UltronConfig.Espresso.webViewMatcher,\n            windowReference: WindowReference? = null\n        ): UltronWebElements {\n            return UltronWebElements(Locator.XPATH, value, webViewMatcher, windowReference)\n        }\n\n        fun elements(\n            locator: Locator,\n            matcher: String,\n            webViewMatcher: Matcher<View> = UltronConfig.Espresso.webViewMatcher,\n            windowReference: WindowReference? = null\n        ): UltronWebElements {\n            return UltronWebElements(locator, matcher, webViewMatcher, windowReference)\n        }\n    }\n}"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/uiautomator/UiAutomatorActionType.kt",
    "content": "package com.atiurin.ultron.core.uiautomator\n\nimport com.atiurin.ultron.core.common.UltronOperationType\n\nenum class UiAutomatorActionType :\n    UltronOperationType {\n    CLICK, CLICK_AND_WAIT_FOR_NEW_WINDOW, CLICK_TOP_LEFT, CLICK_BOTTOM_RIGHT,\n    LONG_CLICK, LONG_CLICK_BOTTOM_RIGHT, LONG_CLICK_TOP_LEFT,\n    DRAG, FLING, PINCH_CLOSE, PINCH_OPEN, PINCH_OUT, PINCH_IN,\n    ADD_TEXT, REPLACE_TEXT, CLEAR_TEXT, GET_TEXT, LEGACY_SET_TEXT,\n    GET_APPLICATION_PACKAGE, GET_BOUNDS, GET_VISIBLE_BOUNDS, GET_VISIBLE_CENTER, GET_CLASS_NAME, GET_CONTENT_DESCRIPTION, GET_RESOURCE_NAME,\n    GET_PARENT, GET_CHILDREN, GET_CHILD, GET_CHILD_COUNT, GET_FROM_PARENT, FIND_OBJECT, FIND_OBJECTS,\n    SWIPE, SWIPE_LEFT, SWIPE_RIGHT, SWIPE_UP, SWIPE_DOWN, SCROLL, PERFORM\n}"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/uiautomator/UiAutomatorAssertionType.kt",
    "content": "package com.atiurin.ultron.core.uiautomator\n\nimport com.atiurin.ultron.core.common.UltronOperationType\n\nenum class UiAutomatorAssertionType :\n    UltronOperationType {\n    IS_DISPLAYED, IS_NOT_DISPLAYED, IS_COMPLETELY_DISPLAYED, IS_DISPLAYING_AT_LEAST,\n    IS_ENABLED, IS_NOT_ENABLED,\n    IS_CLICKABLE, IS_NOT_CLICKABLE,\n    IS_LONG_CLICKABLE, IS_NOT_LONG_CLICKABLE,\n    IS_CHECKED, IS_NOT_CHECKED,\n    IS_CHECKABLE, IS_NOT_CHECKABLE,\n    IS_FOCUSABLE, IS_NOT_FOCUSABLE, IS_FOCUSED, IS_NOT_FOCUSED,\n    IS_SELECTED, IS_NOT_SELECTED,\n    IS_SCROLLABLE, IS_NOT_SCROLLABLE,\n    HAS_TEXT, TEXT_CONTAINS, TEST_IS_NULL_OR_EMPTY, TEST_IS_NOT_NULL_OR_EMPTY,\n    HAS_CONTENT_DESCRIPTION, CONTENT_DESCRIPTION_CONTAINS_TEXT, CONTENT_DESCRIPTION_IS_NULL_OR_EMPTY, CONTENT_DESCRIPTION_IS_NOT_NULL_OR_EMPTY,\n    ASSERT_THAT, EXISTS, NOT_EXISTS\n}"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/uiautomator/UiAutomatorOperation.kt",
    "content": "package com.atiurin.ultron.core.uiautomator\n\nimport com.atiurin.ultron.core.common.Operation\n\ninterface UiAutomatorOperation : Operation"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/uiautomator/UiAutomatorOperationExecutor.kt",
    "content": "package com.atiurin.ultron.core.uiautomator\n\nimport com.atiurin.ultron.core.common.Operation\nimport com.atiurin.ultron.core.common.OperationExecutor\nimport com.atiurin.ultron.core.common.OperationIterationResult\nimport com.atiurin.ultron.core.common.ResultDescriptor\nimport com.atiurin.ultron.core.config.UltronConfig.UiAutomator.Companion.UIAUTOMATOR_OPERATION_POLLING_TIMEOUT\nimport com.atiurin.ultron.exceptions.UltronWrapperException\nimport com.atiurin.ultron.extensions.isAssignedFrom\nimport kotlin.reflect.KClass\n\nabstract class UiAutomatorOperationExecutor<T : Operation>(\n    override val operation: T\n) : OperationExecutor<T, UiAutomatorOperationResult<T>> {\n    override val descriptor: ResultDescriptor\n        get() = ResultDescriptor()\n    override val pollingTimeout: Long\n        get() = UIAUTOMATOR_OPERATION_POLLING_TIMEOUT\n    override var doBetweenOperationRetry: () -> Unit = {}\n    override fun generateResult(\n        success: Boolean,\n        exceptions: List<Throwable>,\n        description: String,\n        lastOperationIterationResult: OperationIterationResult?,\n        executionTimeMs: Long\n    ): UiAutomatorOperationResult<T> {\n        return UiAutomatorOperationResult(\n            operation = operation,\n            success = success,\n            exceptions = exceptions,\n            description = description,\n            operationIterationResult = lastOperationIterationResult,\n            executionTimeMs = executionTimeMs\n        )\n    }\n\n    override fun getWrapperException(originalException: Throwable): Throwable {\n        return if (originalException is NullPointerException) {\n            UltronWrapperException(\n                \"\"\"\n                |Looks like UI element not found for execution of [${operation.description}]. \n                |Original error NullPointerException[${originalException.message}] \n                |Usually it happens while searching for an object \n             \"\"\".trimMargin()\n            )\n        } else originalException\n    }\n\n    override fun isExceptionInList(exception: Throwable, exceptionClasses: List<KClass<out Throwable>>): Boolean {\n        return exception::class.java.isAssignedFrom(exceptionClasses.map { it.java })\n    }\n}"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/uiautomator/UiAutomatorOperationResult.kt",
    "content": "package com.atiurin.ultron.core.uiautomator\n\nimport com.atiurin.ultron.core.common.Operation\nimport com.atiurin.ultron.core.common.OperationIterationResult\nimport com.atiurin.ultron.core.common.OperationResult\n\nclass UiAutomatorOperationResult<T : Operation>(\n    override val operation: T,\n    override val success: Boolean,\n    override val exceptions: List<Throwable> = emptyList(),\n    override var description: String = \"\",\n    override var operationIterationResult: OperationIterationResult?,\n    override val executionTimeMs: Long\n) : OperationResult<T>"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/uiautomator/UltronUiAutomatorLifecycle.kt",
    "content": "package com.atiurin.ultron.core.uiautomator\n\nimport com.atiurin.ultron.core.common.AbstractOperationLifecycle\n\nobject UltronUiAutomatorLifecycle : AbstractOperationLifecycle()"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/uiautomator/uiobject/UiAutomatorUiSelectorOperation.kt",
    "content": "package com.atiurin.ultron.core.uiautomator.uiobject\n\nimport com.atiurin.ultron.core.common.DefaultOperationIterationResult\nimport com.atiurin.ultron.core.common.ElementInfo\nimport com.atiurin.ultron.core.common.DefaultElementInfo\nimport com.atiurin.ultron.core.common.OperationIterationResult\nimport com.atiurin.ultron.core.common.UltronOperationType\nimport com.atiurin.ultron.core.common.assertion.DefaultOperationAssertion\nimport com.atiurin.ultron.core.common.assertion.OperationAssertion\nimport com.atiurin.ultron.core.uiautomator.UiAutomatorOperation\nimport com.atiurin.ultron.exceptions.UltronUiAutomatorException\n\nclass UiAutomatorUiSelectorOperation(\n    private val operationBlock: () -> Boolean,\n    override val name: String,\n    override val description: String,\n    override val type: UltronOperationType,\n    override val timeoutMs: Long,\n    override val assertion: OperationAssertion = DefaultOperationAssertion(\"\") {},\n    override val elementInfo: ElementInfo = DefaultElementInfo()\n) : UiAutomatorOperation {\n    override fun execute(): OperationIterationResult {\n        var success = true\n        var exception: Throwable? = null\n        try {\n            success = operationBlock()\n            if (!success) throw UltronUiAutomatorException(\"$name returns false. It means operation failed.\")\n        } catch (error: Throwable) {\n            success = false\n            exception = error\n        }\n        return DefaultOperationIterationResult(success, exception)\n    }\n}\n"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/uiautomator/uiobject/UiAutomatorUiSelectorOperationExecutor.kt",
    "content": "package com.atiurin.ultron.core.uiautomator.uiobject\n\nimport com.atiurin.ultron.core.common.Operation\nimport com.atiurin.ultron.core.config.UltronConfig\nimport com.atiurin.ultron.core.uiautomator.UiAutomatorOperationExecutor\nimport kotlin.reflect.KClass\n\nclass UiAutomatorUiSelectorOperationExecutor(\n    operation: UiAutomatorUiSelectorOperation\n) : UiAutomatorOperationExecutor<UiAutomatorUiSelectorOperation>(operation) {\n    override fun getAllowedExceptions(operation: Operation): List<KClass<out Throwable>> {\n        return UltronConfig.UiAutomator.UiObjectConfig.allowedExceptions.map { it.kotlin }\n    }\n}"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/uiautomator/uiobject/UltronUiObject.kt",
    "content": "package com.atiurin.ultron.core.uiautomator.uiobject\n\nimport android.graphics.Rect\nimport androidx.annotation.IntegerRes\nimport androidx.test.uiautomator.UiObject\nimport androidx.test.uiautomator.UiObjectNotFoundException\nimport androidx.test.uiautomator.UiSelector\nimport com.atiurin.ultron.core.common.CommonOperationType\nimport com.atiurin.ultron.core.common.ElementInfo\nimport com.atiurin.ultron.core.common.DefaultElementInfo\nimport com.atiurin.ultron.core.common.UltronOperationType\nimport com.atiurin.ultron.core.common.assertion.DefaultOperationAssertion\nimport com.atiurin.ultron.core.common.assertion.EmptyOperationAssertion\nimport com.atiurin.ultron.core.common.assertion.OperationAssertion\nimport com.atiurin.ultron.core.config.UltronConfig\nimport com.atiurin.ultron.core.uiautomator.*\nimport com.atiurin.ultron.exceptions.UltronAssertionException\nimport com.atiurin.ultron.exceptions.UltronException\nimport com.atiurin.ultron.listeners.setListenersState\nimport com.atiurin.ultron.utils.getTargetResourceName\nimport org.hamcrest.Matcher\nimport org.hamcrest.Matchers\n\nclass UltronUiObject internal constructor(\n    private val uiObjectProviderBlock: () -> UiObject,\n    private val selectorDesc: String,\n    private val resultHandler: (UiAutomatorOperationResult<UiAutomatorUiSelectorOperation>) -> Unit = UltronConfig.UiAutomator.UiObjectConfig.resultHandler,\n    private val timeoutMs: Long = UltronConfig.UiAutomator.OPERATION_TIMEOUT,\n    private val assertion: OperationAssertion = EmptyOperationAssertion(),\n    val elementInfo: ElementInfo = DefaultElementInfo()\n) {\n    init {\n        if (elementInfo.name.isEmpty()) elementInfo.name = selectorDesc\n    }\n    fun isSuccess(action: UltronUiObject.() -> Unit): Boolean = runCatching { action() }.isSuccess\n\n    fun withResultHandler(resultHandler: (UiAutomatorOperationResult<UiAutomatorUiSelectorOperation>) -> Unit): UltronUiObject =\n        UltronUiObject(\n            this.uiObjectProviderBlock,\n            this.selectorDesc,\n            resultHandler,\n            this.timeoutMs,\n            this.assertion,\n            this.elementInfo\n        )\n\n    fun withTimeout(timeoutMs: Long): UltronUiObject = UltronUiObject(\n        this.uiObjectProviderBlock,\n        this.selectorDesc,\n        this.resultHandler,\n        timeoutMs,\n        this.assertion,\n        this.elementInfo\n    )\n\n    fun withAssertion(assertion: OperationAssertion) = UltronUiObject(\n        this.uiObjectProviderBlock,\n        this.selectorDesc,\n        this.resultHandler,\n        this.timeoutMs,\n        assertion,\n        this.elementInfo\n    )\n\n    fun withAssertion(name: String = \"\", isListened: Boolean = false, block: () -> Unit) =\n        UltronUiObject(\n            this.uiObjectProviderBlock, this.selectorDesc, this.resultHandler, this.timeoutMs,\n            DefaultOperationAssertion(name, block.setListenersState(isListened)),\n            this.elementInfo\n        )\n\n    fun withName(name: String) = apply { elementInfo.name = name }\n\n    fun withMetaInfo(meta: Any) = apply { elementInfo.meta = meta }\n    fun getChild(uiSelector: UiSelector): UltronUiObject {\n        var uiObject: UiObject? = null\n        executeOperation(\n            operationBlock = {\n                uiObject = uiObjectProviderBlock().getChild(uiSelector)\n                uiObject != null\n            },\n            name = \"GetChild of ${elementInfo.name} with uiSelector $uiSelector\",\n            type = UiAutomatorActionType.GET_CHILD,\n            description = \"UiObject action '${UiAutomatorActionType.GET_CHILD}' of ${elementInfo.name} with uiSelector $uiSelector during $timeoutMs ms\"\n        )\n        return if (uiObject != null) UltronUiObject(\n            { uiObject!! },\n            \"child of '${elementInfo.name}' with UiSelector '$uiSelector'\"\n        )\n        else throw UltronException(\"Couldn't build selector of '${elementInfo.name}' with child UiSelector '$uiSelector' \")\n    }\n\n    /**\n     * Creates a new UiObject for a sibling view or a child of the sibling view,\n     * relative to the present UiObject.\n     *\n     * @param selector for a sibling view or children of the sibling view\n     * @return a new UiObject representing the matched view\n     * @throws UiObjectNotFoundException\n     * @since API Level 16\n     */\n    fun getFromParent(uiSelector: UiSelector): UltronUiObject {\n        var uiObject: UiObject? = null\n        executeOperation(\n            operationBlock = {\n                uiObject = uiObjectProviderBlock().getFromParent(uiSelector)\n                uiObject != null\n            },\n            name = \"GetFromParent of ${elementInfo.name} with uiSelector $uiSelector\",\n            type = UiAutomatorActionType.GET_FROM_PARENT,\n            description = \"UiObject action '${UiAutomatorActionType.GET_FROM_PARENT}' of ${elementInfo.name} with uiSelector $uiSelector during $timeoutMs ms\"\n        )\n        return if (uiObject != null) UltronUiObject(\n            { uiObject!! },\n            \"'${elementInfo.name}' with parent UiSelector '$uiSelector'\"\n        )\n        else throw UltronException(\"Couldn't build selector of '${elementInfo.name}' with parent UiSelector '$uiSelector' \")\n    }\n\n    fun getChildCount(): Int {\n        var count = 0\n        executeOperation(\n            operationBlock = {\n                count = uiObjectProviderBlock().childCount\n                true\n            },\n            name = \"GetChildCount of ${elementInfo.name}\",\n            type = UiAutomatorActionType.GET_CHILD_COUNT,\n            description = \"UiObject action '${UiAutomatorActionType.GET_CHILD_COUNT}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n        return count\n    }\n\n    fun getClassName(): String? {\n        var className: String? = null\n        executeOperation(\n            operationBlock = {\n                className = uiObjectProviderBlock().className\n                true\n            },\n            name = \"GetClassName of ${elementInfo.name}\",\n            type = UiAutomatorActionType.GET_CLASS_NAME,\n            description = \"UiObject action '${UiAutomatorActionType.GET_CLASS_NAME}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n        return className\n    }\n\n    fun getContentDescription(): String? {\n        var contentDesc: String? = null\n        executeOperation(\n            operationBlock = {\n                contentDesc = uiObjectProviderBlock().contentDescription\n                true\n            },\n            name = \"GetContentDescription of ${elementInfo.name}\",\n            type = UiAutomatorActionType.GET_CONTENT_DESCRIPTION,\n            description = \"UiObject action '${UiAutomatorActionType.GET_CONTENT_DESCRIPTION}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n        return contentDesc\n\n    }\n\n    fun getPackageName(): String? {\n        var packageName: String? = null\n        executeOperation(\n            operationBlock = {\n                packageName = uiObjectProviderBlock().packageName\n                true\n            },\n            name = \"GetPackageName of ${elementInfo.name}\",\n            type = UiAutomatorActionType.GET_APPLICATION_PACKAGE,\n            description = \"UiObject action '${UiAutomatorActionType.GET_APPLICATION_PACKAGE}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n        return packageName\n    }\n\n    fun getVisibleBounds(): Rect? {\n        var bounds: Rect? = null\n        executeOperation(\n            operationBlock = {\n                bounds = uiObjectProviderBlock().visibleBounds\n                bounds != null\n            },\n            name = \"GetVisibleBounds of ${elementInfo.name}\",\n            type = UiAutomatorActionType.GET_VISIBLE_BOUNDS,\n            description = \"UiObject action '${UiAutomatorActionType.GET_VISIBLE_BOUNDS}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n        return bounds\n    }\n\n    fun getBounds(): Rect? {\n        var bounds: Rect? = null\n        executeOperation(\n            operationBlock = {\n                bounds = uiObjectProviderBlock().bounds\n                bounds != null\n            },\n            name = \"GetBounds of ${elementInfo.name}\",\n            type = UiAutomatorActionType.GET_BOUNDS,\n            description = \"UiObject action '${UiAutomatorActionType.GET_BOUNDS}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n        return bounds\n    }\n\n    fun getText(): String? {\n        var text: String? = null\n        executeOperation(\n            operationBlock = {\n                text = uiObjectProviderBlock().text\n                true\n            },\n            name = \"GetText of ${elementInfo.name}\",\n            type = UiAutomatorActionType.GET_TEXT,\n            description = \"UiObject action '${UiAutomatorActionType.GET_TEXT}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n        return text\n    }\n\n    fun clearTextField() = apply {\n        executeOperation(\n            operationBlock = {\n                uiObjectProviderBlock().clearTextField()\n                true\n            },\n            name = \"ClearTextField of ${elementInfo.name}\",\n            type = UiAutomatorActionType.CLEAR_TEXT,\n            description = \"UiObject action '${UiAutomatorActionType.CLEAR_TEXT}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun replaceText(text: String) = apply {\n        executeOperation(\n            operationBlock = {\n                uiObjectProviderBlock().setText(text)\n            },\n            name = \"ReplaceText of ${elementInfo.name} to '$text'\",\n            type = UiAutomatorActionType.REPLACE_TEXT,\n            description = \"UiObject action '${UiAutomatorActionType.REPLACE_TEXT}' of ${elementInfo.name} to '$text' during $timeoutMs ms\"\n        )\n    }\n\n    /**\n     * Why legacyAddText? Because it actually not replacing the text in textField it adds to the existing one\n     * In case you would like to replace text use [replaceText]\n     */\n    fun legacyAddText(text: String) = apply {\n        executeOperation(\n            operationBlock = {\n                uiObjectProviderBlock().legacySetText(text)\n                true\n            },\n            name = \"LegacySetText of ${elementInfo.name} to '$text'\",\n            type = UiAutomatorActionType.LEGACY_SET_TEXT,\n            description = \"UiObject action '${UiAutomatorActionType.LEGACY_SET_TEXT}' of ${elementInfo.name} to '$text' during $timeoutMs ms\"\n        )\n    }\n\n    fun click() = apply {\n        executeOperation(\n            operationBlock = { uiObjectProviderBlock().click() },\n            name = \"Click to ${elementInfo.name}\",\n            type = UiAutomatorActionType.CLICK,\n            description = \"UiObject action '${UiAutomatorActionType.CLICK}' to ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun clickAndWaitForNewWindow(waitWindowTimeout: Long) = apply {\n        executeOperation(\n            operationBlock = { uiObjectProviderBlock().clickAndWaitForNewWindow(waitWindowTimeout) },\n            name = \"ClickAndWaitForNewWindow to ${elementInfo.name} with waitWindowTimeout = $waitWindowTimeout ms\",\n            type = UiAutomatorActionType.CLICK_AND_WAIT_FOR_NEW_WINDOW,\n            description = \"UiObject action '${UiAutomatorActionType.CLICK_AND_WAIT_FOR_NEW_WINDOW}' to ${elementInfo.name}  with waitWindowTimeout = $waitWindowTimeout ms during $timeoutMs ms\"\n        )\n    }\n\n    fun clickTopLeft() = apply {\n        executeOperation(\n            operationBlock = { uiObjectProviderBlock().clickTopLeft() },\n            name = \"ClickTopLeft to ${elementInfo.name}\",\n            type = UiAutomatorActionType.CLICK_TOP_LEFT,\n            description = \"UiObject action '${UiAutomatorActionType.CLICK_TOP_LEFT}' to ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun clickBottomRight() = apply {\n        executeOperation(\n            operationBlock = { uiObjectProviderBlock().clickBottomRight() },\n            name = \"ClickBottomRight to ${elementInfo.name}\",\n            type = UiAutomatorActionType.CLICK_BOTTOM_RIGHT,\n            description = \"UiObject action '${UiAutomatorActionType.CLICK_BOTTOM_RIGHT}' to ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun longClick() = apply {\n        executeOperation(\n            operationBlock = { uiObjectProviderBlock().longClick() },\n            name = \"LongClick to ${elementInfo.name}\",\n            type = UiAutomatorActionType.LONG_CLICK,\n            description = \"UiObject action '${UiAutomatorActionType.LONG_CLICK}' to ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun longClickTopLeft() = apply {\n        executeOperation(\n            operationBlock = { uiObjectProviderBlock().longClickTopLeft() },\n            name = \"LongClickTopLeft to ${elementInfo.name}\",\n            type = UiAutomatorActionType.LONG_CLICK_TOP_LEFT,\n            description = \"UiObject action '${UiAutomatorActionType.LONG_CLICK_TOP_LEFT}' to ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun longClickBottomRight() = apply {\n        executeOperation(\n            operationBlock = { uiObjectProviderBlock().longClickBottomRight() },\n            name = \"LongClickBottomRight to ${elementInfo.name}\",\n            type = UiAutomatorActionType.LONG_CLICK_BOTTOM_RIGHT,\n            description = \"UiObject action '${UiAutomatorActionType.LONG_CLICK_BOTTOM_RIGHT}' to ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun dragTo(destObj: UiObject, steps: Int) = apply {\n        executeOperation(\n            operationBlock = { uiObjectProviderBlock().dragTo(destObj, steps) },\n            name = \"DragTo of ${elementInfo.name} destObj ${destObj.selector}  with $steps steps\",\n            type = UiAutomatorActionType.DRAG,\n            description = \"UiObject action '${UiAutomatorActionType.DRAG}' of ${elementInfo.name} destObj ${destObj.selector}  with $steps steps during $timeoutMs ms\"\n        )\n    }\n\n    fun dragTo(destX: Int, destY: Int, steps: Int) = apply {\n        executeOperation(\n            operationBlock = { uiObjectProviderBlock().dragTo(destX, destY, steps) },\n            name = \"DragTo of ${elementInfo.name} to destX = $destX, destY = $destY with $steps steps\",\n            type = UiAutomatorActionType.DRAG,\n            description = \"UiObject action '${UiAutomatorActionType.DRAG}' of ${elementInfo.name} to destX = $destX, destY = $destY with $steps steps during $timeoutMs ms\"\n        )\n    }\n\n    fun swipeUp(steps: Int = 100) = apply {\n        executeOperation(\n            operationBlock = { uiObjectProviderBlock().swipeUp(steps) },\n            name = \"SwipeUp of ${elementInfo.name} with $steps steps\",\n            type = UiAutomatorActionType.SWIPE_UP,\n            description = \"UiObject action '${UiAutomatorActionType.SWIPE_UP}' of ${elementInfo.name} with $steps steps during $timeoutMs ms\"\n        )\n    }\n\n    fun swipeDown(steps: Int = 100) = apply {\n        executeOperation(\n            operationBlock = { uiObjectProviderBlock().swipeDown(steps) },\n            name = \"SwipeDown of ${elementInfo.name} with $steps steps\",\n            type = UiAutomatorActionType.SWIPE_DOWN,\n            description = \"UiObject action '${UiAutomatorActionType.SWIPE_DOWN}' of ${elementInfo.name} with $steps steps during $timeoutMs ms\"\n        )\n    }\n\n    fun swipeLeft(steps: Int = 100) = apply {\n        executeOperation(\n            operationBlock = { uiObjectProviderBlock().swipeLeft(steps) },\n            name = \"SwipeLeft of ${elementInfo.name} with $steps steps\",\n            type = UiAutomatorActionType.SWIPE_LEFT,\n            description = \"UiObject action '${UiAutomatorActionType.SWIPE_LEFT}' of ${elementInfo.name} with $steps steps during $timeoutMs ms\"\n        )\n    }\n\n    fun swipeRight(steps: Int = 100) = apply {\n        executeOperation(\n            operationBlock = { uiObjectProviderBlock().swipeRight(steps) },\n            name = \"SwipeRight of ${elementInfo.name} with $steps steps\",\n            type = UiAutomatorActionType.SWIPE_RIGHT,\n            description = \"UiObject action '${UiAutomatorActionType.SWIPE_RIGHT}' of ${elementInfo.name} with $steps steps during $timeoutMs ms\"\n        )\n    }\n\n    fun pinchOut(percent: Int, steps: Int) = apply {\n        executeOperation(\n            operationBlock = { uiObjectProviderBlock().pinchOut(percent, steps) },\n            name = \"PinchOut of ${elementInfo.name} with $percent% and $steps steps\",\n            type = UiAutomatorActionType.PINCH_OUT,\n            description = \"UiObject action '${UiAutomatorActionType.PINCH_OUT}' of ${elementInfo.name} with $percent% and $steps steps steps during $timeoutMs ms\"\n        )\n    }\n\n    fun pinchIn(percent: Int, steps: Int) = apply {\n        executeOperation(\n            operationBlock = { uiObjectProviderBlock().pinchIn(percent, steps) },\n            name = \"PinchIn of ${elementInfo.name} with $percent% and $steps steps\",\n            type = UiAutomatorActionType.PINCH_IN,\n            description = \"UiObject action '${UiAutomatorActionType.PINCH_IN}' of ${elementInfo.name} with $percent% and $steps steps steps during $timeoutMs ms\"\n        )\n    }\n\n    fun perform(actionBlock: UiObject.() -> Boolean, actionDescription: String) = apply {\n        executeOperation(\n            operationBlock = { uiObjectProviderBlock().actionBlock() },\n            name = \"Perform custom action '$actionDescription' on ${elementInfo.name}.\",\n            type = UiAutomatorActionType.PERFORM,\n            description = \"UiObject action '${UiAutomatorActionType.PERFORM}' custom action '$actionDescription' on ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    //asserts\n\n    fun exists() = apply {\n        executeOperation(\n            operationBlock = {\n                uiObjectProviderBlock().exists()\n            },\n            name = \"Exists of ${elementInfo.name}\",\n            type = UiAutomatorAssertionType.EXISTS,\n            description = \"UiObject assertion '${UiAutomatorAssertionType.EXISTS}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun notExists() = apply {\n        executeOperation(\n            operationBlock = {\n                !uiObjectProviderBlock().exists()\n            },\n            name = \"NotExists of ${elementInfo.name}\",\n            type = UiAutomatorAssertionType.NOT_EXISTS,\n            description = \"UiObject assertion '${UiAutomatorAssertionType.NOT_EXISTS}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun isCheckable() = apply {\n        executeOperation(\n            operationBlock = { uiObjectProviderBlock().isCheckable },\n            name = \"IsCheckable of ${elementInfo.name}\",\n            type = UiAutomatorAssertionType.IS_CHECKABLE,\n            description = \"UiObject assertion '${UiAutomatorAssertionType.IS_CHECKABLE}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun isNotCheckable() = apply {\n        executeOperation(\n            operationBlock = { !uiObjectProviderBlock().isCheckable },\n            name = \"IsNotCheckable of ${elementInfo.name}\",\n            type = UiAutomatorAssertionType.IS_NOT_CHECKABLE,\n            description = \"UiObject assertion '${UiAutomatorAssertionType.IS_NOT_CHECKABLE}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun isChecked() = apply {\n        executeOperation(\n            operationBlock = { uiObjectProviderBlock().isChecked },\n            name = \"IsChecked of ${elementInfo.name}\",\n            type = UiAutomatorAssertionType.IS_CHECKED,\n            description = \"UiObject assertion '${UiAutomatorAssertionType.IS_CHECKED}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun isNotChecked() = apply {\n        executeOperation(\n            operationBlock = { !uiObjectProviderBlock().isChecked },\n            name = \"IsNotChecked of ${elementInfo.name}\",\n            type = UiAutomatorAssertionType.IS_NOT_CHECKED,\n            description = \"UiObject assertion '${UiAutomatorAssertionType.IS_NOT_CHECKED}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun isClickable() = apply {\n        executeOperation(\n            operationBlock = { uiObjectProviderBlock().isClickable },\n            name = \"IsClickable of ${elementInfo.name}\",\n            type = UiAutomatorAssertionType.IS_CLICKABLE,\n            description = \"UiObject assertion '${UiAutomatorAssertionType.IS_CLICKABLE}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun isNotClickable() = apply {\n        executeOperation(\n            operationBlock = { !uiObjectProviderBlock().isClickable },\n            name = \"IsNotClickable of ${elementInfo.name}\",\n            type = UiAutomatorAssertionType.IS_NOT_CLICKABLE,\n            description = \"UiObject assertion '${UiAutomatorAssertionType.IS_NOT_CLICKABLE}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun isEnabled() = apply {\n        executeOperation(\n            operationBlock = { uiObjectProviderBlock().isEnabled },\n            name = \"IsEnabled of ${elementInfo.name}\",\n            type = UiAutomatorAssertionType.IS_ENABLED,\n            description = \"UiObject assertion '${UiAutomatorAssertionType.IS_ENABLED}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun isNotEnabled() = apply {\n        executeOperation(\n            operationBlock = { !uiObjectProviderBlock().isEnabled },\n            name = \"IsNotEnabled of ${elementInfo.name}\",\n            type = UiAutomatorAssertionType.IS_NOT_ENABLED,\n            description = \"UiObject assertion '${UiAutomatorAssertionType.IS_NOT_ENABLED}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun isFocusable() = apply {\n        executeOperation(\n            operationBlock = { uiObjectProviderBlock().isFocusable },\n            name = \"IsFocusable of ${elementInfo.name}\",\n            type = UiAutomatorAssertionType.IS_FOCUSABLE,\n            description = \"UiObject assertion '${UiAutomatorAssertionType.IS_FOCUSABLE}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun isNotFocusable() = apply {\n        executeOperation(\n            operationBlock = { !uiObjectProviderBlock().isFocusable },\n            name = \"IsNotFocusable of ${elementInfo.name}\",\n            type = UiAutomatorAssertionType.IS_NOT_FOCUSABLE,\n            description = \"UiObject assertion '${UiAutomatorAssertionType.IS_NOT_FOCUSABLE}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun isFocused() = apply {\n        executeOperation(\n            operationBlock = { uiObjectProviderBlock().isFocused },\n            name = \"IsFocused of ${elementInfo.name}\",\n            type = UiAutomatorAssertionType.IS_FOCUSED,\n            description = \"UiObject assertion '${UiAutomatorAssertionType.IS_FOCUSED}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun isNotFocused() = apply {\n        executeOperation(\n            operationBlock = { !uiObjectProviderBlock().isFocused },\n            name = \"IsNotFocused of ${elementInfo.name}\",\n            type = UiAutomatorAssertionType.IS_NOT_FOCUSED,\n            description = \"UiObject assertion '${UiAutomatorAssertionType.IS_NOT_FOCUSED}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun isLongClickable() = apply {\n        executeOperation(\n            operationBlock = { uiObjectProviderBlock().isLongClickable },\n            name = \"IsLongClickable of ${elementInfo.name}\",\n            type = UiAutomatorAssertionType.IS_LONG_CLICKABLE,\n            description = \"UiObject assertion '${UiAutomatorAssertionType.IS_LONG_CLICKABLE}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun isNotLongClickable() = apply {\n        executeOperation(\n            operationBlock = { !uiObjectProviderBlock().isLongClickable },\n            name = \"IsNotLongClickable of ${elementInfo.name}\",\n            type = UiAutomatorAssertionType.IS_NOT_LONG_CLICKABLE,\n            description = \"UiObject assertion '${UiAutomatorAssertionType.IS_NOT_LONG_CLICKABLE}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun isScrollable() = apply {\n        executeOperation(\n            operationBlock = { uiObjectProviderBlock().isScrollable },\n            name = \"IsScrollable of ${elementInfo.name}\",\n            type = UiAutomatorAssertionType.IS_SCROLLABLE,\n            description = \"UiObject assertion '${UiAutomatorAssertionType.IS_SCROLLABLE}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun isNotScrollable() = apply {\n        executeOperation(\n            operationBlock = { !uiObjectProviderBlock().isScrollable },\n            name = \"IsNotScrollable of ${elementInfo.name}\",\n            type = UiAutomatorAssertionType.IS_NOT_SCROLLABLE,\n            description = \"UiObject assertion '${UiAutomatorAssertionType.IS_NOT_SCROLLABLE}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun isSelected() = apply {\n        executeOperation(\n            operationBlock = { uiObjectProviderBlock().isSelected },\n            name = \"IsSelected of ${elementInfo.name}\",\n            type = UiAutomatorAssertionType.IS_SELECTED,\n            description = \"UiObject assertion '${UiAutomatorAssertionType.IS_SELECTED}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun isNotSelected() = apply {\n        executeOperation(\n            operationBlock = { !uiObjectProviderBlock().isSelected },\n            name = \"IsNotSelected of ${elementInfo.name}\",\n            type = UiAutomatorAssertionType.IS_NOT_SELECTED,\n            description = \"UiObject assertion '${UiAutomatorAssertionType.IS_NOT_SELECTED}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun hasText(text: String) = apply { hasText(Matchers.equalTo(text)) }\n\n    fun hasText(textMatcher: Matcher<String>) = apply {\n        executeOperation(\n            operationBlock = {\n                val actualText = uiObjectProviderBlock().text\n                if (!textMatcher.matches(actualText)) {\n                    throw UltronAssertionException(\"Expected: text matches '$textMatcher', got '$actualText'.\")\n                }\n                true\n            },\n            name = \"HasText $textMatcher in ${elementInfo.name}\",\n            type = UiAutomatorAssertionType.HAS_TEXT,\n            description = \"UiObject assertion '${UiAutomatorAssertionType.HAS_TEXT}' matches '$textMatcher' in ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun textContains(textSubstring: String) =\n        apply { hasText(Matchers.containsString(textSubstring)) }\n\n    fun textIsNullOrEmpty() = apply {\n        hasText(Matchers.isEmptyOrNullString())\n    }\n\n    fun textIsNotNullOrEmpty() = apply {\n        hasText(Matchers.not(Matchers.isEmptyOrNullString()))\n    }\n\n    fun hasContentDescription(contentDesc: String) = apply {\n        hasContentDescription(Matchers.equalTo(contentDesc))\n    }\n\n    fun hasContentDescription(contentDescMatcher: Matcher<String>) = apply {\n        executeOperation(\n            operationBlock = {\n                val contentDesc = uiObjectProviderBlock().contentDescription\n                if (!contentDescMatcher.matches(contentDesc)) {\n                    throw UltronAssertionException(\"Expected: contentDescription matches '$contentDescMatcher', got '$contentDesc'.\")\n                }\n                true\n            },\n            name = \"HasContentDescription $contentDescMatcher in ${elementInfo.name}\",\n            type = UiAutomatorAssertionType.HAS_CONTENT_DESCRIPTION,\n            description = \"UiObject assertion '${UiAutomatorAssertionType.HAS_CONTENT_DESCRIPTION}' matches '$contentDescMatcher' in ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun contentDescriptionContains(contentDescSubstring: String) =\n        apply { hasContentDescription(Matchers.containsString(contentDescSubstring)) }\n\n    fun contentDescriptionIsNullOrEmpty() =\n        apply { hasContentDescription(Matchers.isEmptyOrNullString()) }\n\n    fun contentDescriptionIsNotNullOrEmpty() =\n        apply { hasContentDescription(Matchers.not(Matchers.isEmptyOrNullString())) }\n\n    fun assertThat(assertBlock: UiObject.() -> Boolean, assertionDescription: String) = apply {\n        executeOperation(\n            operationBlock = { uiObjectProviderBlock().assertBlock() },\n            name = \"AssertThat $assertionDescription in ${elementInfo.name}\",\n            type = UiAutomatorAssertionType.ASSERT_THAT,\n            description = \"UiObject assertion '${UiAutomatorAssertionType.ASSERT_THAT}' $assertionDescription in ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun executeOperation(\n        operationBlock: () -> Boolean,\n        name: String = \"empty name\",\n        description: String = \"empty name\",\n        type: UltronOperationType = CommonOperationType.DEFAULT\n    ) {\n        UltronUiAutomatorLifecycle.execute(\n            UiAutomatorUiSelectorOperationExecutor(\n                UiAutomatorUiSelectorOperation(\n                    operationBlock = operationBlock,\n                    name = name,\n                    type = type,\n                    description = description,\n                    timeoutMs = timeoutMs,\n                    assertion = assertion,\n                    elementInfo = elementInfo\n                )\n            ), resultHandler\n        )\n    }\n\n    companion object {\n        @JvmStatic\n        fun uiResId(@IntegerRes resourceId: Int): UltronUiObject {\n            val uiSelector = uiSelector(resourceId)\n            return UltronUiObject(\n                { UltronConfig.UiAutomator.uiDevice.findObject(uiSelector) },\n                uiSelector.toString()\n            )\n        }\n\n        @JvmStatic\n        fun uiText(text: String): UltronUiObject {\n            return ui(UiSelector().text(text))\n        }\n\n        @JvmStatic\n        fun ui(uiSelector: UiSelector): UltronUiObject {\n            return UltronUiObject(\n                { UltronConfig.UiAutomator.uiDevice.findObject(uiSelector) },\n                uiSelector.toString()\n            )\n        }\n\n        @JvmStatic\n        fun uiSelector(@IntegerRes resourceId: Int): UiSelector {\n            return UiSelector().resourceId(getTargetResourceName(resourceId))\n        }\n    }\n}"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/uiautomator/uiobject2/UiAutomatorBySelectorAction.kt",
    "content": "package com.atiurin.ultron.core.uiautomator.uiobject2\n\nimport com.atiurin.ultron.core.common.DefaultOperationIterationResult\nimport com.atiurin.ultron.core.common.ElementInfo\nimport com.atiurin.ultron.core.common.DefaultElementInfo\nimport com.atiurin.ultron.core.common.OperationIterationResult\nimport com.atiurin.ultron.core.common.UltronOperationType\nimport com.atiurin.ultron.core.common.assertion.DefaultOperationAssertion\nimport com.atiurin.ultron.core.common.assertion.OperationAssertion\nimport com.atiurin.ultron.core.uiautomator.UiAutomatorOperation\n\nclass UiAutomatorBySelectorAction(\n    private val actionBlock: () -> Unit,\n    override val name: String,\n    override val description: String,\n    override val type: UltronOperationType,\n    override val timeoutMs: Long,\n    override val assertion: OperationAssertion = DefaultOperationAssertion(\"\") {},\n    override val elementInfo: ElementInfo = DefaultElementInfo()\n) : UiAutomatorOperation {\n    override fun execute(): OperationIterationResult {\n        var success = true\n        var exception: Throwable? = null\n        try {\n            actionBlock()\n        } catch (error: Throwable) {\n            success = false\n            exception = error\n        }\n        return DefaultOperationIterationResult(success, exception)\n    }\n}"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/uiautomator/uiobject2/UiAutomatorBySelectorActionExecutor.kt",
    "content": "package com.atiurin.ultron.core.uiautomator.uiobject2\n\nimport com.atiurin.ultron.core.common.Operation\nimport com.atiurin.ultron.core.config.UltronConfig\nimport com.atiurin.ultron.core.uiautomator.UiAutomatorOperation\nimport com.atiurin.ultron.core.uiautomator.UiAutomatorOperationExecutor\nimport kotlin.reflect.KClass\n\nclass UiAutomatorBySelectorActionExecutor(\n    action: UiAutomatorBySelectorAction\n) : UiAutomatorOperationExecutor<UiAutomatorOperation>(action) {\n    override fun getAllowedExceptions(operation: Operation): List<KClass<out Throwable>> {\n        return UltronConfig.UiAutomator.UiObject2Config.allowedExceptions.map { it.kotlin }\n    }\n}"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/uiautomator/uiobject2/UiAutomatorBySelectorAssertion.kt",
    "content": "package com.atiurin.ultron.core.uiautomator.uiobject2\n\nimport com.atiurin.ultron.core.common.DefaultOperationIterationResult\nimport com.atiurin.ultron.core.common.ElementInfo\nimport com.atiurin.ultron.core.common.DefaultElementInfo\nimport com.atiurin.ultron.core.common.OperationIterationResult\nimport com.atiurin.ultron.core.common.UltronOperationType\nimport com.atiurin.ultron.core.common.assertion.DefaultOperationAssertion\nimport com.atiurin.ultron.core.common.assertion.OperationAssertion\nimport com.atiurin.ultron.core.uiautomator.UiAutomatorOperation\nimport com.atiurin.ultron.exceptions.UltronUiAutomatorException\n\nclass UiAutomatorBySelectorAssertion(\n    private val assertionBlock: () -> Boolean,\n    override val name: String,\n    override val description: String,\n    override val type: UltronOperationType,\n    override val timeoutMs: Long,\n    override val assertion: OperationAssertion = DefaultOperationAssertion(\"\") {},\n    override val elementInfo: ElementInfo = DefaultElementInfo()\n) : UiAutomatorOperation {\n    override fun execute(): OperationIterationResult {\n        var success = true\n        var exception: Throwable? = null\n        try {\n            success = assertionBlock()\n            if (!success) throw UltronUiAutomatorException(\"$name returns false. It means assertion failed.\")\n        } catch (error: Throwable) {\n            success = false\n            exception = error\n        }\n        return DefaultOperationIterationResult(success, exception)\n    }\n}"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/uiautomator/uiobject2/UiAutomatorBySelectorAssertionExecutor.kt",
    "content": "package com.atiurin.ultron.core.uiautomator.uiobject2\n\nimport com.atiurin.ultron.core.common.Operation\nimport com.atiurin.ultron.core.config.UltronConfig\nimport com.atiurin.ultron.core.uiautomator.UiAutomatorOperation\nimport com.atiurin.ultron.core.uiautomator.UiAutomatorOperationExecutor\nimport kotlin.reflect.KClass\n\nclass UiAutomatorBySelectorAssertionExecutor(\n    assertion: UiAutomatorBySelectorAssertion\n) : UiAutomatorOperationExecutor<UiAutomatorOperation>(assertion) {\n    override fun getAllowedExceptions(operation: Operation): List<KClass<out Throwable>> {\n        return UltronConfig.UiAutomator.UiObject2Config.allowedExceptions.map { it.kotlin }\n    }\n}"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/uiautomator/uiobject2/UltronUiObject2.kt",
    "content": "package com.atiurin.ultron.core.uiautomator.uiobject2\n\nimport android.graphics.Point\nimport android.graphics.Rect\nimport androidx.annotation.IntegerRes\nimport androidx.test.uiautomator.By\nimport androidx.test.uiautomator.BySelector\nimport androidx.test.uiautomator.Direction\nimport androidx.test.uiautomator.UiObject2\nimport com.atiurin.ultron.core.common.CommonOperationType\nimport com.atiurin.ultron.core.common.DefaultElementInfo\nimport com.atiurin.ultron.core.common.ElementInfo\nimport com.atiurin.ultron.core.common.UltronOperationType\nimport com.atiurin.ultron.core.common.assertion.DefaultOperationAssertion\nimport com.atiurin.ultron.core.common.assertion.EmptyOperationAssertion\nimport com.atiurin.ultron.core.common.assertion.OperationAssertion\nimport com.atiurin.ultron.core.config.UltronConfig\nimport com.atiurin.ultron.core.uiautomator.UiAutomatorActionType\nimport com.atiurin.ultron.core.uiautomator.UiAutomatorAssertionType\nimport com.atiurin.ultron.core.uiautomator.UiAutomatorOperation\nimport com.atiurin.ultron.core.uiautomator.UiAutomatorOperationResult\nimport com.atiurin.ultron.core.uiautomator.UltronUiAutomatorLifecycle\nimport com.atiurin.ultron.exceptions.UltronAssertionException\nimport com.atiurin.ultron.extensions.getBySelector\nimport com.atiurin.ultron.listeners.setListenersState\nimport com.atiurin.ultron.utils.getTargetResourceName\nimport org.hamcrest.Matcher\nimport org.hamcrest.Matchers.containsString\nimport org.hamcrest.Matchers.equalTo\nimport org.hamcrest.Matchers.isEmptyOrNullString\nimport org.hamcrest.Matchers.not\n\n\nclass UltronUiObject2 internal constructor(\n    val uiObject2ProviderBlock: () -> UiObject2?,\n    val selectorDesc: String,\n    val resultHandler: (UiAutomatorOperationResult<UiAutomatorOperation>) -> Unit = UltronConfig.UiAutomator.UiObject2Config.resultHandler,\n    val timeoutMs: Long = UltronConfig.UiAutomator.OPERATION_TIMEOUT,\n    val assertion: OperationAssertion = EmptyOperationAssertion(),\n    val elementInfo: ElementInfo = DefaultElementInfo()\n) {\n    init {\n        if (elementInfo.name.isEmpty()) elementInfo.name = selectorDesc\n    }\n    fun getBySelector() = uiObject2ProviderBlock()?.getBySelector()\n    fun isSuccess(action: UltronUiObject2.() -> Unit): Boolean = runCatching { action() }.isSuccess\n\n    fun withResultHandler(resultHandler: (UiAutomatorOperationResult<UiAutomatorOperation>) -> Unit): UltronUiObject2 {\n        return UltronUiObject2(\n            this.uiObject2ProviderBlock,\n            this.selectorDesc,\n            resultHandler,\n            this.timeoutMs,\n            this.assertion,\n            this.elementInfo\n        )\n    }\n\n    fun withTimeout(timeoutMs: Long): UltronUiObject2 = UltronUiObject2(\n        this.uiObject2ProviderBlock,\n        this.selectorDesc,\n        this.resultHandler,\n        timeoutMs,\n        this.assertion,\n        this.elementInfo\n    )\n\n    fun withAssertion(assertion: OperationAssertion) = UltronUiObject2(\n        this.uiObject2ProviderBlock,\n        this.selectorDesc,\n        this.resultHandler,\n        this.timeoutMs,\n        assertion,\n        this.elementInfo\n    )\n\n    fun withAssertion(name: String = \"\", isListened: Boolean = false, block: () -> Unit) =\n        UltronUiObject2(\n            this.uiObject2ProviderBlock, this.selectorDesc, this.resultHandler, this.timeoutMs,\n            DefaultOperationAssertion(name, block.setListenersState(isListened)),\n            this.elementInfo\n        )\n\n\n    fun withName(name: String) = apply { elementInfo.name = name }\n\n    fun withMetaInfo(meta: Any) = apply { elementInfo.meta = meta }\n    // Search functions\n    /** @return this object's parent, or null if it has no parent. */\n    fun getParent(): UltronUiObject2? {\n        var uiobject2: UiObject2? = null\n        executeAction(\n            actionBlock = { uiobject2 = uiObject2ProviderBlock()!!.parent },\n            name = \"GetParent of ${elementInfo.name}\",\n            type = UiAutomatorActionType.GET_PARENT,\n            description = \"UiObject2 action '${UiAutomatorActionType.GET_PARENT}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n        return uiobject2?.let {\n            UltronUiObject2(\n                { it }, \"Parent of ${elementInfo.name}.\"\n            )\n        }\n    }\n\n    /**\n     * @return a collection of the child elements directly under this object. Empty list if no child exist\n     * */\n    fun getChildren(): List<UltronUiObject2> {\n        val children = mutableListOf<UiObject2>()\n        executeAction(\n            actionBlock = { children.addAll(uiObject2ProviderBlock()!!.children) },\n            name = \"GetChildren of ${elementInfo.name}\",\n            type = UiAutomatorActionType.GET_CHILDREN,\n            description = \"UiObject2 action '${UiAutomatorActionType.GET_CHILDREN}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n        return children.map { UltronUiObject2({ it }, \"Child of ${elementInfo.name}\") }\n    }\n\n\n    /** Returns the number of child elements directly under this object, 0 if it has no child*/\n    fun getChildCount(): Int {\n        var count = 0\n        executeAction(\n            actionBlock = { count = uiObject2ProviderBlock()!!.childCount },\n            name = \"GetChildCount of ${elementInfo.name}\",\n            type = UiAutomatorActionType.GET_CHILD_COUNT,\n            description = \"UiObject2 action '${UiAutomatorActionType.GET_CHILD_COUNT}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n        return count\n    }\n\n    /**\n     * Searches all elements under this object and returns the first object to match the criteria,\n     * or null if no matching objects are found.\n     */\n    fun findObject(bySelector: BySelector): UltronUiObject2? {\n        var uiobject2: UiObject2? = null\n        executeAction(\n            actionBlock = { uiobject2 = uiObject2ProviderBlock()!!.findObject(bySelector) },\n            name = \"FindObject of ${elementInfo.name} using $bySelector\",\n            type = UiAutomatorActionType.FIND_OBJECT,\n            description = \"UiObject2 action '${UiAutomatorActionType.FIND_OBJECT}' of ${elementInfo.name} using $bySelector during $timeoutMs ms\"\n        )\n        return uiobject2?.let {\n            UltronUiObject2(\n                { it }, \"First child of ${elementInfo.name} with bySelector $bySelector.\"\n            )\n        } ?: null\n    }\n\n    /** Searches all elements under this object and returns all objects that match the criteria. */\n    fun findObjects(bySelector: BySelector): List<UltronUiObject2> {\n        val objects = mutableListOf<UiObject2>()\n        executeAction(\n            actionBlock = { objects.addAll(uiObject2ProviderBlock()!!.findObjects(bySelector)) },\n            name = \"FindObjects of ${elementInfo.name} using $bySelector\",\n            type = UiAutomatorActionType.FIND_OBJECTS,\n            description = \"UiObject2 action '${UiAutomatorActionType.FIND_OBJECTS}' of ${elementInfo.name} using $bySelector during $timeoutMs ms\"\n        )\n        return objects.map {\n            UltronUiObject2({ it }, \"Child of ${elementInfo.name} with bySelector $bySelector\")\n        }\n    }\n\n    // Attribute accessors\n    /**\n     * @return view.text or null if view has no text\n     * if you would like to assert view.text use [hasText] and [textContains]\n     */\n    fun getText(): String? {\n        var text: String? = null\n        executeAction(\n            actionBlock = { text = uiObject2ProviderBlock()!!.text },\n            name = \"GetText of ${elementInfo.name}\",\n            type = UiAutomatorActionType.GET_TEXT,\n            description = \"UiObject2 action '${UiAutomatorActionType.GET_TEXT}' of ${elementInfo.name}  during $timeoutMs ms\"\n        )\n        return text\n    }\n\n    /** @return the class name of the view represented by this object.\n     *  or null if it impossible to get className */\n    fun getClassName(): String? {\n        var className: String? = null\n        executeAction(\n            actionBlock = { className = uiObject2ProviderBlock()!!.className },\n            name = \"GetClassName of ${elementInfo.name}\",\n            type = UiAutomatorActionType.GET_CLASS_NAME,\n            description = \"UiObject2 action '${UiAutomatorActionType.GET_CLASS_NAME}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n        return className\n    }\n\n    /** @return the package name of the app that this object belongs to.\n     * or null if it is impossible to identify app\n     * */\n    fun getApplicationPackage(): String? {\n        var packageName: String? = null\n        executeAction(\n            actionBlock = { packageName = uiObject2ProviderBlock()!!.applicationPackage },\n            name = \"GetApplicationPackage of ${elementInfo.name}\",\n            type = UiAutomatorActionType.GET_APPLICATION_PACKAGE,\n            description = \"UiObject2 action '${UiAutomatorActionType.GET_APPLICATION_PACKAGE}' of ${elementInfo.name}  during $timeoutMs ms\"\n        )\n        return packageName\n    }\n\n    /**  @return the visible bounds of this object in screen coordinates. */\n    fun getVisibleBounds(): Rect? {\n        var visibleBounds: Rect? = null\n        executeAction(\n            actionBlock = { visibleBounds = uiObject2ProviderBlock()!!.visibleBounds },\n            name = \"GetVisibleBounds of ${elementInfo.name}\",\n            type = UiAutomatorActionType.GET_VISIBLE_BOUNDS,\n            description = \"UiObject2 action '${UiAutomatorActionType.GET_VISIBLE_BOUNDS}' of ${elementInfo.name}  during $timeoutMs ms\"\n        )\n        return visibleBounds\n    }\n\n    /**  @return a point in the center of the visible bounds of this object. */\n    fun getVisibleCenter(): Point? {\n        var visibleCenter: Point? = null\n        executeAction(\n            actionBlock = { visibleCenter = uiObject2ProviderBlock()!!.visibleCenter },\n            name = \"GetVisibleCenter of ${elementInfo.name}\",\n            type = UiAutomatorActionType.GET_VISIBLE_CENTER,\n            description = \"UiObject2 action '${UiAutomatorActionType.GET_VISIBLE_CENTER}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n        return visibleCenter\n    }\n\n    /**\n     * @return the fully qualified resource name for this object's id.\n     * or null if resource name isn't specified\n     */\n    fun getResourceName(): String? {\n        var resName: String? = null\n        executeAction(\n            actionBlock = { resName = uiObject2ProviderBlock()!!.resourceName },\n            name = \"GetResourceName of ${elementInfo.name}\",\n            type = UiAutomatorActionType.GET_RESOURCE_NAME,\n            description = \"UiObject2 action '${UiAutomatorActionType.GET_RESOURCE_NAME}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n        return resName\n    }\n\n    /**\n     * @return the content description for this object\n     * or null if it isn't specified for view\n     */\n    fun getContentDescription(): String? {\n        var contentDesc: String? = null\n        executeAction(\n            actionBlock = { contentDesc = uiObject2ProviderBlock()!!.contentDescription },\n            name = \"GetContentDescription of ${elementInfo.name}\",\n            type = UiAutomatorActionType.GET_CONTENT_DESCRIPTION,\n            description = \"UiObject2 action '${UiAutomatorActionType.GET_CONTENT_DESCRIPTION}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n        return contentDesc\n    }\n\n    // Actions\n    /** Clicks on object. */\n    fun click(\n        duration: Long = 0 // A basic click is a touch down and touch up over the same point with no delay.\n    ) = apply {\n        executeAction(\n            actionBlock = { uiObject2ProviderBlock()!!.click(duration) },\n            name = \"Click to ${elementInfo.name} with duration = $duration\",\n            type = UiAutomatorActionType.CLICK,\n            description = \"UiObject2 action '${UiAutomatorActionType.CLICK}' to ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    /** Performs a long click on object. */\n    fun longClick() = apply {\n        executeAction(\n            actionBlock = { uiObject2ProviderBlock()!!.longClick() },\n            name = \"LongClick to ${elementInfo.name}\",\n            type = UiAutomatorActionType.LONG_CLICK,\n            description = \"UiObject2 action '${UiAutomatorActionType.LONG_CLICK}' to ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    /** Clears the text content if object is an editable field. */\n    fun clear() = apply {\n        executeAction(\n            actionBlock = { uiObject2ProviderBlock()!!.clear() },\n            name = \"Clear of ${elementInfo.name}\",\n            type = UiAutomatorActionType.CLEAR_TEXT,\n            description = \"UiObject2 action '${UiAutomatorActionType.CLEAR_TEXT}' in ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    /** Add the text content if object is an editable field. */\n    fun addText(text: String) = apply {\n        executeAction(\n            actionBlock = { uiObject2ProviderBlock()!!.text += text },\n            name = \"AddText of ${elementInfo.name} to '$text'\",\n            type = UiAutomatorActionType.ADD_TEXT,\n            description = \"UiObject2 action '${UiAutomatorActionType.ADD_TEXT}' = '$text' to ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    /** Set the text content by sending individual key codes.\n     * @throws NullPointerException if you are trying to apply it on uneditable object\n     * */\n    fun legacySetText(text: String) = apply {\n        executeAction(\n            actionBlock = { uiObject2ProviderBlock()!!.legacySetText(text) },\n            name = \"LegacySetText of ${elementInfo.name} to '$text'\",\n            type = UiAutomatorActionType.LEGACY_SET_TEXT,\n            description = \"UiObject2 action '${UiAutomatorActionType.LEGACY_SET_TEXT}' in ${elementInfo.name} to '$text' during $timeoutMs ms\"\n        )\n    }\n\n    /** Sets the text content if object is an editable field. */\n    fun replaceText(text: String) = apply {\n        executeAction(\n            actionBlock = { uiObject2ProviderBlock()!!.text = text },\n            name = \"ReplaceText of ${elementInfo.name} to '$text'\",\n            type = UiAutomatorActionType.REPLACE_TEXT,\n            description = \"UiObject2 action '${UiAutomatorActionType.REPLACE_TEXT}' in ${elementInfo.name} to '$text' during $timeoutMs ms\"\n        )\n    }\n\n    /**\n     * Drags object to the specified location.\n     *\n     * @param dest The end point that this object should be dragged to.\n     * @param speed The speed at which to perform this gesture in pixels per second.\n     */\n    fun drag(dest: Point, speed: Int = DEFAULT_DRAG_SPEED) = apply {\n        executeAction(\n            actionBlock = { uiObject2ProviderBlock()!!.drag(dest, speed) },\n            name = \"Drag of ${elementInfo.name} to dest = '$dest' with speed = $speed\",\n            type = UiAutomatorActionType.DRAG,\n            description = \"UiObject2 action '${UiAutomatorActionType.DRAG}' of ${elementInfo.name} to dest = '$dest' with speed = $speed during $timeoutMs ms\"\n        )\n    }\n\n    /**\n     * Performs a pinch close gesture on this object.\n     *\n     * @param percent The size of the pinch as a percentage of this object's size.\n     * @param speed The speed at which to perform this gesture in pixels per second.\n     */\n    fun pinchClose(percent: Float, speed: Int = DEFAULT_PINCH_SPEED) = apply {\n        executeAction(\n            actionBlock = { uiObject2ProviderBlock()!!.pinchClose(percent, speed) },\n            name = \"PinchClose of ${elementInfo.name} with $percent% and $speed speed\",\n            type = UiAutomatorActionType.PINCH_CLOSE,\n            description = \"UiObject2 action '${UiAutomatorActionType.PINCH_CLOSE}' of ${elementInfo.name} with $percent% and $speed speed during $timeoutMs ms\"\n        )\n    }\n\n    /**\n     * Performs a pinch open gesture on this object.\n     *\n     * @param percent The size of the pinch as a percentage of this object's size.\n     * @param speed The speed at which to perform this gesture in pixels per second.\n     */\n    fun pinchOpen(percent: Float, speed: Int = DEFAULT_PINCH_SPEED) = apply {\n        executeAction(\n            actionBlock = { uiObject2ProviderBlock()!!.pinchOpen(percent, speed) },\n            name = \"PinchOpen of ${elementInfo.name} with $percent% and $speed speed\",\n            type = UiAutomatorActionType.PINCH_OPEN,\n            description = \"UiObject2 action '${UiAutomatorActionType.PINCH_OPEN}' of ${elementInfo.name} with $percent% and $speed speed during $timeoutMs ms\"\n        )\n    }\n\n    /**\n     * Performs a swipe gesture on this object.\n     *\n     * @param direction The direction in which to swipe.\n     * @param percent The length of the swipe as a percentage of this object's size.\n     * @param speed The speed at which to perform this gesture in pixels per second.\n     */\n    private fun swipe(direction: Direction, percent: Float, speed: Int = DEFAULT_SWIPE_SPEED) =\n        apply {\n            executeAction(\n                actionBlock = { uiObject2ProviderBlock()!!.swipe(direction, percent, speed) },\n                name = \"Swipe of ${elementInfo.name} to direction = '${direction.name}' with $percent% and $speed speed\",\n                type = UiAutomatorActionType.SWIPE,\n                description = \"UiObject2 action '${UiAutomatorActionType.SWIPE}' of ${elementInfo.name} with direction = '${direction.name}' with $percent% and $speed speed during $timeoutMs ms\"\n            )\n        }\n\n    /**\n     * Performs a swipe up gesture on this object.\n     *\n     * @param percent The length of the swipe as a percentage of this object's size.\n     * @param speed The speed at which to perform this gesture in pixels per second.\n     */\n    fun swipeUp(percent: Float = 0.95F, speed: Int = DEFAULT_SWIPE_SPEED) = apply {\n        this@UltronUiObject2.swipe(Direction.UP, percent, speed)\n    }\n\n    /**\n     * Performs a swipe down gesture on this object.\n     *\n     * @param percent The length of the swipe as a percentage of this object's size.\n     * @param speed The speed at which to perform this gesture in pixels per second.\n     */\n    fun swipeDown(percent: Float = 0.95F, speed: Int = DEFAULT_SWIPE_SPEED) = apply {\n        this@UltronUiObject2.swipe(Direction.DOWN, percent, speed)\n    }\n\n    /**\n     * Performs a swipe left gesture on this object.\n     *\n     * @param percent The length of the swipe as a percentage of this object's size.\n     * @param speed The speed at which to perform this gesture in pixels per second.\n     */\n    fun swipeLeft(percent: Float = 0.95F, speed: Int = DEFAULT_SWIPE_SPEED) = apply {\n        this@UltronUiObject2.swipe(Direction.LEFT, percent, speed)\n    }\n\n    /**\n     * Performs a swipe right gesture on this object.\n     *\n     * @param percent The length of the swipe as a percentage of this object's size.\n     * @param speed The speed at which to perform this gesture in pixels per second.\n     */\n    fun swipeRight(percent: Float = 0.95F, speed: Int = DEFAULT_SWIPE_SPEED) = apply {\n        this@UltronUiObject2.swipe(Direction.RIGHT, percent, speed)\n    }\n\n    /**\n     * Performs a scroll gesture on this object.\n     *\n     * @param direction The direction in which to scroll.\n     * @param percent The distance to scroll as a percentage of this object's visible size.\n     * @param speed The speed at which to perform this gesture in pixels per second.\n     * @return Whether the object can still scroll in the given direction.\n     */\n    private fun scroll(\n        direction: Direction,\n        percent: Float,\n        speed: Int = DEFAULT_SCROLL_SPEED\n    ): Boolean {\n        var result = false\n        executeAction(\n            actionBlock = {\n                result = uiObject2ProviderBlock()!!.scroll(direction, percent, speed)\n            },\n            name = \"Scroll of ${elementInfo.name} to direction = '${direction.name}' with $percent% and $speed speed\",\n            type = UiAutomatorActionType.SCROLL,\n            description = \"UiObject2 action '${UiAutomatorActionType.SCROLL}' of ${elementInfo.name} with direction = '${direction.name}' with $percent% and $speed speed during $timeoutMs ms\"\n        )\n        return result\n    }\n\n    /**\n     * Performs a scroll up gesture on this object.\n     *\n     * @param percent The distance to scroll as a percentage of this object's visible size.\n     * @param speed The speed at which to perform this gesture in pixels per second.\n     * @return Whether the object can still scroll in the given direction.\n     */\n    fun scrollUp(percent: Float = 0.95F, speed: Int = DEFAULT_SCROLL_SPEED): Boolean {\n        return scroll(Direction.UP, percent, speed)\n    }\n\n    /**\n     * Performs a scroll down gesture on this object.\n     *\n     * @param percent The distance to scroll as a percentage of this object's visible size.\n     * @param speed The speed at which to perform this gesture in pixels per second.\n     * @return Whether the object can still scroll in the given direction.\n     */\n    fun scrollDown(percent: Float = 0.95F, speed: Int = DEFAULT_SCROLL_SPEED): Boolean {\n        return scroll(Direction.DOWN, percent, speed)\n    }\n\n    /**\n     * Performs a scroll left gesture on this object.\n     *\n     * @param percent The distance to scroll as a percentage of this object's visible size.\n     * @param speed The speed at which to perform this gesture in pixels per second.\n     * @return Whether the object can still scroll in the given direction.\n     */\n    fun scrollLeft(percent: Float = 0.95F, speed: Int = DEFAULT_SCROLL_SPEED): Boolean {\n        return scroll(Direction.LEFT, percent, speed)\n    }\n\n    /**\n     * Performs a scroll right gesture on this object.\n     *\n     * @param percent The distance to scroll as a percentage of this object's visible size.\n     * @param speed The speed at which to perform this gesture in pixels per second.\n     * @return Whether the object can still scroll in the given direction.\n     */\n    fun scrollRight(percent: Float = 0.95F, speed: Int = DEFAULT_SCROLL_SPEED): Boolean {\n        return scroll(Direction.RIGHT, percent, speed)\n    }\n\n    /**\n     * Performs a fling gesture on this object.\n     *\n     * @param direction The direction in which to fling.\n     * @param speed The speed at which to perform this gesture in pixels per second.\n     * @return Whether the object can still scroll in the given direction.\n     */\n    fun fling(direction: Direction, speed: Int = DEFAULT_FLING_SPEED): Boolean {\n        var result: Boolean = false\n        executeAction(\n            actionBlock = { result = uiObject2ProviderBlock()!!.fling(direction, speed) },\n            name = \"FLing of ${elementInfo.name} to direction = '${direction.name}' with speed = $speed\",\n            type = UiAutomatorActionType.FLING,\n            description = \"UiObject2 action '${UiAutomatorActionType.FLING}' of ${elementInfo.name} to direction = '${direction.name}' with speed = $speed during $timeoutMs ms\"\n        )\n        return result\n    }\n\n    /**\n     * @param actionBlock provides an access to UiObject2.\n     * @param actionDescription used for logging and exception description\n     */\n    fun perform(actionBlock: UiObject2.() -> Unit, actionDescription: String) = apply {\n        executeAction(\n            actionBlock = { uiObject2ProviderBlock()!!.actionBlock() },\n            name = \"Perform custom action '$actionDescription' on ${elementInfo.name}.\",\n            type = UiAutomatorActionType.PERFORM,\n            description = \"UiObject2 action '${UiAutomatorActionType.PERFORM}' custom action '$actionDescription' on ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    //asserts\n    fun hasText(textMatcher: Matcher<String>) = apply {\n        executeAssertion(\n            assertionBlock = {\n                val actualText = uiObject2ProviderBlock()!!.text\n                if (!textMatcher.matches(actualText)) {\n                    throw UltronAssertionException(\"Expected: text matches '$textMatcher', got '$actualText'.\")\n                }\n                true\n            },\n            name = \"HasText '$textMatcher' in ${elementInfo.name}\",\n            type = UiAutomatorAssertionType.HAS_TEXT,\n            description = \"UiObject2 assertion '${UiAutomatorAssertionType.HAS_TEXT}' matches '$textMatcher' in ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun hasText(text: String) = apply { hasText(equalTo(text)) }\n    fun textContains(textSubstring: String) = apply { hasText(containsString(textSubstring)) }\n    fun textIsNullOrEmpty() = apply { hasText(isEmptyOrNullString()) }\n    fun textIsNotNullOrEmpty() = apply { hasText(not(isEmptyOrNullString())) }\n\n    fun hasContentDescription(contentDescMatcher: Matcher<String>) = apply {\n        executeAssertion(\n            assertionBlock = {\n                val actualContentDesc = uiObject2ProviderBlock()!!.contentDescription\n                if (!contentDescMatcher.matches(actualContentDesc)) {\n                    throw UltronAssertionException(\"Expected: contentDescription matches '$contentDescMatcher', got '$actualContentDesc'.\")\n                }\n                true\n            },\n            name = \"HasContentDescription '$contentDescMatcher' in ${elementInfo.name}\",\n            type = UiAutomatorAssertionType.HAS_CONTENT_DESCRIPTION,\n            description = \"UiObject2 assertion '${UiAutomatorAssertionType.HAS_CONTENT_DESCRIPTION}' matches '$contentDescMatcher' in ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun hasContentDescription(contentDesc: String) =\n        apply { hasContentDescription(equalTo(contentDesc)) }\n\n    fun contentDescriptionContains(contentDescSubstring: String) =\n        apply { hasContentDescription(containsString(contentDescSubstring)) }\n\n    fun contentDescriptionIsNullOrEmpty() = apply { hasContentDescription(isEmptyOrNullString()) }\n    fun contentDescriptionIsNotNullOrEmpty() =\n        apply { hasContentDescription(not(isEmptyOrNullString())) }\n\n    fun isCheckable() = apply {\n        executeAssertion(\n            assertionBlock = { uiObject2ProviderBlock()!!.isCheckable },\n            name = \"IsCheckable of ${elementInfo.name}\",\n            type = UiAutomatorAssertionType.IS_CHECKABLE,\n            description = \"UiObject2 assertion '${UiAutomatorAssertionType.IS_CHECKABLE}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun isNotCheckable() = apply {\n        executeAssertion(\n            assertionBlock = { !uiObject2ProviderBlock()!!.isCheckable },\n            name = \"IsNotCheckable of ${elementInfo.name}\",\n            type = UiAutomatorAssertionType.IS_NOT_CHECKABLE,\n            description = \"UiObject2 assertion '${UiAutomatorAssertionType.IS_NOT_CHECKABLE}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun isChecked() = apply {\n        executeAssertion(\n            assertionBlock = { uiObject2ProviderBlock()!!.isChecked },\n            name = \"IsChecked of ${elementInfo.name}\",\n            type = UiAutomatorAssertionType.IS_CHECKED,\n            description = \"UiObject2 assertion '${UiAutomatorAssertionType.IS_CHECKED}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun isNotChecked() = apply {\n        executeAssertion(\n            assertionBlock = { !uiObject2ProviderBlock()!!.isChecked },\n            name = \"IsNotChecked of ${elementInfo.name}\",\n            type = UiAutomatorAssertionType.IS_NOT_CHECKED,\n            description = \"UiObject2 assertion '${UiAutomatorAssertionType.IS_NOT_CHECKED}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun isClickable() = apply {\n        executeAssertion(\n            assertionBlock = { uiObject2ProviderBlock()!!.isClickable },\n            name = \"IsClickable of ${elementInfo.name}\",\n            type = UiAutomatorAssertionType.IS_CLICKABLE,\n            description = \"UiObject2 assertion '${UiAutomatorAssertionType.IS_CLICKABLE}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun isNotClickable() = apply {\n        executeAssertion(\n            assertionBlock = { !uiObject2ProviderBlock()!!.isClickable },\n            name = \"IsNotClickable of ${elementInfo.name}\",\n            type = UiAutomatorAssertionType.IS_NOT_CLICKABLE,\n            description = \"UiObject2 assertion '${UiAutomatorAssertionType.IS_NOT_CLICKABLE}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun isEnabled() = apply {\n        executeAssertion(\n            assertionBlock = { uiObject2ProviderBlock()!!.isEnabled },\n            name = \"IsEnabled of ${elementInfo.name}\",\n            type = UiAutomatorAssertionType.IS_ENABLED,\n            description = \"UiObject2 assertion '${UiAutomatorAssertionType.IS_ENABLED}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun isNotEnabled() = apply {\n        executeAssertion(\n            assertionBlock = { !uiObject2ProviderBlock()!!.isEnabled },\n            name = \"IsNotEnabled of ${elementInfo.name}\",\n            type = UiAutomatorAssertionType.IS_NOT_ENABLED,\n            description = \"UiObject2 assertion '${UiAutomatorAssertionType.IS_NOT_ENABLED}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun isFocusable() = apply {\n        executeAssertion(\n            assertionBlock = { uiObject2ProviderBlock()!!.isFocusable },\n            name = \"IsFocusable of ${elementInfo.name}\",\n            type = UiAutomatorAssertionType.IS_FOCUSABLE,\n            description = \"UiObject2 assertion '${UiAutomatorAssertionType.IS_FOCUSABLE}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun isNotFocusable() = apply {\n        executeAssertion(\n            assertionBlock = { !uiObject2ProviderBlock()!!.isFocusable },\n            name = \"IsNotFocusable of ${elementInfo.name}\",\n            type = UiAutomatorAssertionType.IS_NOT_FOCUSABLE,\n            description = \"UiObject2 assertion '${UiAutomatorAssertionType.IS_NOT_FOCUSABLE}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun isFocused() = apply {\n        executeAssertion(\n            assertionBlock = { uiObject2ProviderBlock()!!.isFocused },\n            name = \"IsFocused of ${elementInfo.name}\",\n            type = UiAutomatorAssertionType.IS_FOCUSED,\n            description = \"UiObject2 assertion '${UiAutomatorAssertionType.IS_FOCUSED}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun isNotFocused() = apply {\n        executeAssertion(\n            assertionBlock = { !uiObject2ProviderBlock()!!.isFocused },\n            name = \"IsNotFocused of ${elementInfo.name}\",\n            type = UiAutomatorAssertionType.IS_NOT_FOCUSED,\n            description = \"UiObject2 assertion '${UiAutomatorAssertionType.IS_NOT_FOCUSED}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun isLongClickable() = apply {\n        executeAssertion(\n            assertionBlock = { uiObject2ProviderBlock()!!.isLongClickable },\n            name = \"IsLongClickable of ${elementInfo.name}\",\n            type = UiAutomatorAssertionType.IS_LONG_CLICKABLE,\n            description = \"UiObject2 assertion '${UiAutomatorAssertionType.IS_LONG_CLICKABLE}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun isNotLongClickable() = apply {\n        executeAssertion(\n            assertionBlock = { !uiObject2ProviderBlock()!!.isLongClickable },\n            name = \"IsNotLongClickable of ${elementInfo.name}\",\n            type = UiAutomatorAssertionType.IS_NOT_LONG_CLICKABLE,\n            description = \"UiObject2 assertion '${UiAutomatorAssertionType.IS_NOT_LONG_CLICKABLE}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun isScrollable() = apply {\n        executeAssertion(\n            assertionBlock = { uiObject2ProviderBlock()!!.isScrollable },\n            name = \"IsScrollable of ${elementInfo.name}\",\n            type = UiAutomatorAssertionType.IS_SCROLLABLE,\n            description = \"UiObject2 assertion '${UiAutomatorAssertionType.IS_SCROLLABLE}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun isNotScrollable() = apply {\n        executeAssertion(\n            assertionBlock = { !uiObject2ProviderBlock()!!.isScrollable },\n            name = \"IsNotScrollable of ${elementInfo.name}\",\n            type = UiAutomatorAssertionType.IS_NOT_SCROLLABLE,\n            description = \"UiObject2 assertion '${UiAutomatorAssertionType.IS_NOT_SCROLLABLE}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun isSelected() = apply {\n        executeAssertion(\n            assertionBlock = { uiObject2ProviderBlock()!!.isSelected },\n            name = \"IsSelected of ${elementInfo.name}\",\n            type = UiAutomatorAssertionType.IS_SELECTED,\n            description = \"UiObject2 assertion '${UiAutomatorAssertionType.IS_SELECTED}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun isNotSelected() = apply {\n        executeAssertion(\n            assertionBlock = { !uiObject2ProviderBlock()!!.isSelected },\n            name = \"IsNotSelected of ${elementInfo.name}\",\n            type = UiAutomatorAssertionType.IS_NOT_SELECTED,\n            description = \"UiObject2 assertion '${UiAutomatorAssertionType.IS_NOT_SELECTED}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun isDisplayed() = apply {\n        executeAssertion(\n            assertionBlock = { uiObject2ProviderBlock() != null },\n            name = \"IsDisplayed of ${elementInfo.name}\",\n            type = UiAutomatorAssertionType.IS_DISPLAYED,\n            description = \"UiObject2 assertion '${UiAutomatorAssertionType.IS_DISPLAYED}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    fun isNotDisplayed() = apply {\n        executeAssertion(\n            assertionBlock = { uiObject2ProviderBlock() == null },\n            name = \"IsNotDisplayed of ${elementInfo.name}\",\n            type = UiAutomatorAssertionType.IS_NOT_DISPLAYED,\n            description = \"UiObject2 assertion '${UiAutomatorAssertionType.IS_NOT_DISPLAYED}' of ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    /**\n     * @param assertBlock provides an access to UiObject2.\n     * If [assertBlock] returns false it means the assertion failed and an exception will be thrown\n     * @param assertionDescription used for logging and exception description\n     */\n    fun assertThat(assertBlock: UiObject2.() -> Boolean, assertionDescription: String) = apply {\n        executeAssertion(\n            assertionBlock = { uiObject2ProviderBlock()!!.assertBlock() },\n            name = \"AssertThat $assertionDescription in ${elementInfo.name}\",\n            type = UiAutomatorAssertionType.ASSERT_THAT,\n            description = \"UiObject2 assertion '${UiAutomatorAssertionType.ASSERT_THAT}' $assertionDescription in ${elementInfo.name} during $timeoutMs ms\"\n        )\n    }\n\n    @Suppress(\"UNCHECKED_CAST\")\n    fun executeAction(\n        actionBlock: () -> Unit,\n        name: String = \"empty name\",\n        description: String = \"empty description\",\n        type: UltronOperationType = CommonOperationType.DEFAULT,\n        timeoutMs: Long = this.timeoutMs,\n        resultHandler: (UiAutomatorOperationResult<UiAutomatorOperation>) -> Unit = this.resultHandler\n    ) {\n        UltronUiAutomatorLifecycle.execute(\n            UiAutomatorBySelectorActionExecutor(\n                UiAutomatorBySelectorAction(\n                    actionBlock = actionBlock,\n                    name = name,\n                    type = type,\n                    description = description,\n                    timeoutMs = timeoutMs,\n                    assertion = assertion,\n                    elementInfo = elementInfo\n                )\n            ), resultHandler\n        )\n    }\n\n    @Suppress(\"UNCHECKED_CAST\")\n    fun executeAssertion(\n        assertionBlock: () -> Boolean,\n        name: String = \"empty name\",\n        description: String = \"empty description\",\n        type: UltronOperationType = CommonOperationType.DEFAULT,\n        timeoutMs: Long = this.timeoutMs,\n        resultHandler: (UiAutomatorOperationResult<UiAutomatorOperation>) -> Unit = this.resultHandler\n    ) {\n        UltronUiAutomatorLifecycle.execute(\n            UiAutomatorBySelectorAssertionExecutor(\n                UiAutomatorBySelectorAssertion(\n                    assertionBlock = assertionBlock,\n                    name = name,\n                    type = type,\n                    description = description,\n                    timeoutMs = timeoutMs,\n                    assertion = assertion,\n                    elementInfo = elementInfo\n                )\n            ), resultHandler\n        )\n    }\n\n    companion object {\n        /** value from [UiObject2.DEFAULT_SWIPE_SPEED] */\n        private const val DEFAULT_SWIPE_SPEED = 5000\n\n        /** value from [UiObject2.DEFAULT_SCROLL_SPEED] */\n        private const val DEFAULT_SCROLL_SPEED = 5000\n\n        /** value from [UiObject2.DEFAULT_FLING_SPEED] */\n        private const val DEFAULT_FLING_SPEED = 7500\n\n        /** value from [UiObject2.DEFAULT_DRAG_SPEED] */\n        private const val DEFAULT_DRAG_SPEED = 2500\n\n        /** value from [UiObject2.DEFAULT_P INCH_SPEED] */\n        private const val DEFAULT_PINCH_SPEED = 2500\n\n        @JvmStatic\n        fun byResId(@IntegerRes resourceId: Int): UltronUiObject2 {\n            val bySelector = bySelector(resourceId)\n            return UltronUiObject2(\n                { UltronConfig.UiAutomator.uiDevice.findObject(bySelector) }, bySelector.toString()\n            )\n        }\n        @JvmStatic\n        fun byText(text: String): UltronUiObject2 {\n            return by(By.text(text))\n        }\n\n        @JvmStatic\n        fun by(bySelector: BySelector): UltronUiObject2 {\n            return UltronUiObject2(\n                { UltronConfig.UiAutomator.uiDevice.findObject(bySelector) }, bySelector.toString()\n            )\n        }\n\n        @JvmStatic\n        fun bySelector(@IntegerRes resourceId: Int): BySelector {\n            return By.res(getTargetResourceName(resourceId))\n        }\n    }\n}"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/core/uiautomator/uiobject2/UltronUiObject2UiBlock.kt",
    "content": "package com.atiurin.ultron.core.uiautomator.uiobject2\n\nimport androidx.test.uiautomator.BySelector\nimport androidx.test.uiautomator.UiObject2\nimport com.atiurin.ultron.core.config.UltronConfig.UiAutomator.Companion.uiDevice\nimport com.atiurin.ultron.exceptions.UltronException\nimport com.atiurin.ultron.exceptions.UltronUiAutomatorException\nimport com.atiurin.ultron.extensions.getBySelector\nimport kotlin.reflect.typeOf\n\n/**\n * Represents a UI block based on the UiAutomator framework using [UiObject2] elements.\n *\n * This class provides a foundation for defining and working with blocks in UI tests,\n * offering methods to locate and interact with descendant elements or child blocks.\n *\n * ### Constructor Requirements:\n * To define a new block type inheriting from this class, ensure the following:\n * 1. The block class must be a **top-level or file-level class** (not nested inside another class).\n *    Nested class definitions may result in hidden Java constructor parameters.\n * 2. The block class must have the following constructor:\n *    `class CustomBlock(blockDesc: String, blockSelector: () -> BySelector) : UltronUiObject2UiBlock(blockDesc, blockSelector)`\n *\n * ### Methods:\n * - [child]: Creates and retrieves child UI elements or blocks of the specified type [T].\n */\nopen class UltronUiObject2UiBlock(val blockDesc: String = \"Empty block description\", val blockSelector: () -> BySelector) {\n    /**\n     * Provides a search mechanism for descendant elements within this block.\n     *\n     * @param childSelector A [BySelector] to locate the child element.\n     * @return A function that retrieves the descendant element or throws an exception if not found.\n     */\n    private fun descendantSearchProvider(childSelector: BySelector): () -> UiObject2 = {\n        uiDevice.findObject(blockSelector())\n            ?.findObject(childSelector)\n            ?: throw UltronUiAutomatorException(\"'$childSelector' in block ${this::class.simpleName} '$blockDesc' not found!\")\n    }\n\n    /**\n     * A utility property for interaction with this UI block.\n     *\n     * Appends the block description to the matcher for better logging and debugging.\n     */\n    val uiBlock\n        get() = UltronUiObject2({ uiDevice.findObject(blockSelector()) }, blockDesc)\n\n    /**\n     * Searches for a child element within the block and returns it as an [UltronUiObject2].\n     *\n     * @param selector The selector for the child element.\n     * @param description A textual description of the child element.\n     * @return The [UltronUiObject2] representing the child element.\n     */\n    fun child(selector: BySelector, description: String = selector.toString()): UltronUiObject2 = UltronUiObject2(\n        uiObject2ProviderBlock = descendantSearchProvider(selector),\n        selectorDesc = \"Child of block '$blockDesc' with selector '$description'\"\n    )\n\n    /**\n     * Searches for a child block within this block and returns it as the specified type [T].\n     *\n     * @param uiBlock An instance of the desired block type, acting as a template.\n     * @param description A textual description of the block.\n     * @return A new instance of the specified block type [T].\n     * @throws UltronException If the block's constructor is incompatible or the block cannot be created.\n     */\n    inline fun <reified T : UltronUiObject2UiBlock> child(\n        uiBlock: T,\n        description: String = uiBlock.blockDesc\n    ): T {\n        val modifiedSelectorBlock = {\n            uiDevice.findObject(blockSelector())\n                ?.findObject(uiBlock.blockSelector())\n                ?.getBySelector()\n        }\n        val updatedBLock = runCatching {\n            T::class.constructors.firstOrNull {\n                it.parameters.size == 2\n                        && it.parameters.first().type == typeOf<String>()\n                        && it.parameters[1].type == typeOf<() -> BySelector>()\n            }?.call(description, modifiedSelectorBlock)\n        }.onFailure {\n            if (it is IllegalArgumentException) {\n                throw UltronException(\n                    \"${T::class.simpleName} has hidden java constructor parameters. ${T::class.simpleName} must be defined as a top-level class (not nested inside any other class).\"\n                )\n            } else throw UltronException(\"Unable to create updated ${T::class.simpleName}. Message: ${it.message}\")\n        }.getOrNull()\n        updatedBLock?.let { return it } ?: throw UltronException(\n            \"\"\"\n            |${T::class.simpleName} doesn't have an appropriate constructor with arguments: String, () -> BySelector.\n            |Ensure that the class meets the following conditions:\n            |1. ${T::class.simpleName} must not be defined inside another class. It should be a top-level or file-level class.\n            |2. ${T::class.simpleName} must have the following constructor:\n            |class ${T::class.simpleName}(blockDesc: String, blockSelector: () -> BySelector) : UltronUiObject2UiBlock(blockDesc, blockSelector)\n            |If neither constructor is available, consider using another method for child declaration:\n            |```\n            |fun <B : UltronUiObject2UiBlock> child(\n            |    selector: BySelector,\n            |    description: String = selector.toString(),\n            |    uiBlockFactory: (String, () -> BySelector) -> B\n            |): B\n            | ```\n        \"\"\".trimMargin()\n        )\n    }\n\n    /**\n     * Searches for a child block using a factory function.\n     *\n     * @param selector The selector for the child block.\n     * @param description A textual description of the block.\n     * @param uiBlockFactory A factory function to create the block.\n     * @return The created block instance.\n     */\n    fun <B : UltronUiObject2UiBlock> child(\n        selector: BySelector,\n        description: String = selector.toString(),\n        uiBlockFactory: (String, () -> BySelector) -> B\n    ): B {\n        val modifiedSelectorBlock = {\n            val block = uiDevice.findObject(blockSelector())\n                ?: throw UltronUiAutomatorException(\"'$blockDesc' with type ${this::class.simpleName} not found!\")\n            block.findObject(selector)?.getBySelector()\n                ?: throw UltronUiAutomatorException(\n                    \"'$description' child with selector '$selector' not found in block '$blockDesc' with type ${this::class.simpleName}!\"\n                )\n\n        }\n        return uiBlockFactory(description, modifiedSelectorBlock)\n    }\n}"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/custom/espresso/action/AnonymousViewAction.kt",
    "content": "package com.atiurin.ultron.custom.espresso.action\n\nimport android.view.View\nimport androidx.test.espresso.ViewAction\nimport com.atiurin.ultron.core.espresso.action.UltronEspressoActionParams\nimport org.hamcrest.Matcher\n\nabstract class AnonymousViewAction(val params: UltronEspressoActionParams) : ViewAction {\n    override fun getConstraints(): Matcher<View> = params.viewActionConstraints\n    override fun getDescription(): String = params.viewActionDescription\n}"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/custom/espresso/action/CustomEspressoActionType.kt",
    "content": "package com.atiurin.ultron.custom.espresso.action\n\nimport com.atiurin.ultron.core.common.UltronOperationType\n\nenum class CustomEspressoActionType : UltronOperationType {\n    GET_TEXT, GET_CONTENT_DESCRIPTION, GET_DRAWABLE, GET_VIEW, GET_VIEW_FORCIBLY,\n    PERFORM_ON_VIEW, PERFORM_ON_VIEW_FORCIBLY\n}\n"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/custom/espresso/action/GetContentDescriptionAction.kt",
    "content": "package com.atiurin.ultron.custom.espresso.action\n\nimport android.view.View\nimport androidx.test.espresso.DataInteraction\nimport androidx.test.espresso.Espresso\nimport androidx.test.espresso.ViewInteraction\nimport androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom\nimport com.atiurin.ultron.core.espresso.UltronEspressoInteraction\nimport com.atiurin.ultron.core.espresso.action.UltronEspressoActionParams\nimport com.atiurin.ultron.extensions.simpleClassName\nimport org.hamcrest.Matcher\n\nfun <T> UltronEspressoInteraction<T>.getContentDescription(): String? = execute(\n    UltronEspressoActionParams(\n        operationName = \"GetContentDescription from view with '${getInteractionMatcher()}'\",\n        operationDescription = \"${interaction.simpleClassName()} action '${CustomEspressoActionType.GET_CONTENT_DESCRIPTION}' of '${getInteractionMatcher()}' with root '${getInteractionRootMatcher()}' during ${getActionTimeout()} ms\",\n        operationType = CustomEspressoActionType.GET_CONTENT_DESCRIPTION,\n        viewActionConstraints = isAssignableFrom(View::class.java),\n        viewActionDescription = \"getting content description from view\"\n    )\n) { _, view ->\n    view.contentDescription?.toString()\n}\n\nfun ViewInteraction.getContentDescription() =\n    UltronEspressoInteraction(this).getContentDescription()\n\nfun DataInteraction.getContentDescription() =\n    UltronEspressoInteraction(this).getContentDescription()\n\nfun Matcher<View>.getContentDescription() =\n    UltronEspressoInteraction(Espresso.onView(this)).getContentDescription()\n"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/custom/espresso/action/GetDrawableAction.kt",
    "content": "package com.atiurin.ultron.custom.espresso.action\n\nimport android.graphics.drawable.Drawable\nimport android.view.View\nimport android.widget.ImageView\nimport androidx.test.espresso.DataInteraction\nimport androidx.test.espresso.Espresso\nimport androidx.test.espresso.ViewInteraction\nimport androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom\nimport com.atiurin.ultron.core.espresso.UltronEspressoInteraction\nimport com.atiurin.ultron.core.espresso.action.UltronEspressoActionParams\nimport com.atiurin.ultron.extensions.simpleClassName\nimport org.hamcrest.Matcher\n\nfun <T> UltronEspressoInteraction<T>.getDrawable(): Drawable? = execute(\n    UltronEspressoActionParams(\n        operationName = \"GetDrawable from TextView with '${getInteractionMatcher()}'\",\n        operationDescription = \"${interaction.simpleClassName()} action '${CustomEspressoActionType.GET_DRAWABLE}' of '${getInteractionMatcher()}' with root '${getInteractionRootMatcher()}' during ${getActionTimeout()} ms\",\n        operationType = CustomEspressoActionType.GET_DRAWABLE,\n        viewActionConstraints = isAssignableFrom(ImageView::class.java),\n        viewActionDescription = \"getting Drawable from ImageView\"\n    )\n) { _, view ->\n    (view as ImageView).drawable\n}\n\nfun ViewInteraction.getDrawable() = UltronEspressoInteraction(this).getDrawable()\nfun DataInteraction.getDrawable() = UltronEspressoInteraction(this).getDrawable()\nfun Matcher<View>.getDrawable() = UltronEspressoInteraction(Espresso.onView(this)).getDrawable()\n"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/custom/espresso/action/GetTextAction.kt",
    "content": "package com.atiurin.ultron.custom.espresso.action\n\nimport android.view.View\nimport android.widget.TextView\nimport androidx.test.espresso.DataInteraction\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.ViewInteraction\nimport androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom\nimport com.atiurin.ultron.core.espresso.UltronEspressoInteraction\nimport com.atiurin.ultron.core.espresso.action.UltronEspressoActionParams\nimport com.atiurin.ultron.extensions.simpleClassName\nimport org.hamcrest.Matcher\n\nfun <T> UltronEspressoInteraction<T>.getText(): String = execute(\n    UltronEspressoActionParams(\n        operationName = \"GetText from TextView with '${getInteractionMatcher()}'\",\n        operationDescription = \"${interaction.simpleClassName()} action '${CustomEspressoActionType.GET_TEXT}' of '${getInteractionMatcher()}' with root '${getInteractionRootMatcher()}' during ${getActionTimeout()} ms\",\n        operationType = CustomEspressoActionType.GET_TEXT,\n        viewActionDescription = \"getting text from TextView\",\n        viewActionConstraints = isAssignableFrom(TextView::class.java)\n    )\n) { _, view ->\n    (view as TextView).text.toString()\n}\n\nfun ViewInteraction.getText() = UltronEspressoInteraction(this).getText()\nfun DataInteraction.getText() = UltronEspressoInteraction(this).getText()\nfun Matcher<View>.getText() = UltronEspressoInteraction(onView(this)).getText()\n"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/custom/espresso/action/GetViewAction.kt",
    "content": "package com.atiurin.ultron.custom.espresso.action\n\nimport android.view.View\nimport androidx.test.espresso.DataInteraction\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.ViewInteraction\nimport androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom\nimport com.atiurin.ultron.core.espresso.UltronEspressoInteraction\nimport com.atiurin.ultron.core.espresso.action.UltronEspressoActionParams\nimport com.atiurin.ultron.extensions.simpleClassName\nimport org.hamcrest.Matcher\n\nfun <T> UltronEspressoInteraction<T>.getView(): View = execute(\n    UltronEspressoActionParams(\n        operationName = \"Get view with '${getInteractionMatcher()}'\",\n        operationDescription = \"${interaction.simpleClassName()} action '${CustomEspressoActionType.GET_VIEW}' of '${getInteractionMatcher()}' with root '${getInteractionRootMatcher()}' during ${getActionTimeout()} ms\",\n        operationType = CustomEspressoActionType.GET_VIEW,\n        viewActionDescription = \"getting view\",\n        viewActionConstraints = isAssignableFrom(View::class.java)\n    )\n) { _, view ->\n    view\n}\n\n/**\n * Gets the view that is matched by the matcher.\n *\n * This method is bound to the common Espresso idle state mechanism. This means that it will\n * wait for the view to become visible before returning it.\n *\n * @return The view that is matched by the matcher.\n *\n * In case you need to bypass Espresso idle state mechanism use `getViewForcibly()` extension method.\n */\nfun Matcher<View>.getView() = UltronEspressoInteraction(onView(this)).getView()\nfun ViewInteraction.getView() = UltronEspressoInteraction(this).getView()\nfun DataInteraction.getView() = UltronEspressoInteraction(this).getView()\n"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/custom/espresso/assertion/AnyRootAssertions.kt",
    "content": "package com.atiurin.ultron.custom.espresso.assertion\n\nimport android.view.View\nimport androidx.test.espresso.DataInteraction\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.ViewInteraction\nimport androidx.test.espresso.assertion.ViewAssertions\nimport androidx.test.espresso.matcher.RootMatchers.withDecorView\nimport com.atiurin.ultron.core.espresso.UltronEspressoInteraction\nimport com.atiurin.ultron.core.espresso.assertion.EspressoAssertionType\nimport com.atiurin.ultron.custom.espresso.base.getVisibleRootViews\nimport com.atiurin.ultron.extensions.simpleClassName\nimport org.hamcrest.CoreMatchers.`is`\nimport org.hamcrest.Matcher\n\nfun <T> UltronEspressoInteraction<T>.doesNotExistInAnyVisibleRoot() : UltronEspressoInteraction<T> {\n    val defaultUltronEspressoInteraction = this\n    getVisibleRootViews().forEach { root ->\n        this.inRoot(withDecorView(`is`(root.decorView)))\n        executeAssertion(\n            operationBlock = getInteractionAssertionBlock(ViewAssertions.doesNotExist()),\n            name = \"DoesNotExist of '${getInteractionMatcher()}'\",\n            type = EspressoAssertionType.DOES_NOT_EXIST,\n            description = \"${interaction.simpleClassName()} assertion '${EspressoAssertionType.DOES_NOT_EXIST}' of '${getInteractionMatcher()}' with root '${getInteractionRootMatcher()}' during ${getAssertionTimeout()} ms\"\n        )\n    }\n    return defaultUltronEspressoInteraction\n}\n\nfun Matcher<View>.doesNotExistInAnyVisibleRoot() = UltronEspressoInteraction(onView(this)).doesNotExistInAnyVisibleRoot()\nfun ViewInteraction.doesNotExistInAnyVisibleRoot() = UltronEspressoInteraction(this).doesNotExistInAnyVisibleRoot()\nfun DataInteraction.doesNotExistInAnyVisibleRoot() = UltronEspressoInteraction(this).doesNotExistInAnyVisibleRoot()"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/custom/espresso/assertion/CustomEspressoAssertionType.kt",
    "content": "package com.atiurin.ultron.custom.espresso.assertion\n\nimport com.atiurin.ultron.core.common.UltronOperationType\n\nenum class CustomEspressoAssertionType : UltronOperationType {\n    HAS_DRAWABLE, HAS_ANY_DRAWABLE,\n    HAS_CURRENT_TEXT_COLOR, HAS_CURRENT_HINT_TEXT_COLOR, HAS_HIGHLIGHT_COLOR, HAS_SHADOW_COLOR\n}\n"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/custom/espresso/assertion/DrawableAssertion.kt",
    "content": "package com.atiurin.ultron.custom.espresso.assertion\n\nimport android.view.View\nimport androidx.annotation.DrawableRes\nimport androidx.test.espresso.DataInteraction\nimport androidx.test.espresso.Espresso\nimport androidx.test.espresso.ViewInteraction\nimport com.atiurin.ultron.core.espresso.UltronEspressoInteraction\nimport com.atiurin.ultron.custom.espresso.matcher.withDrawable\nimport com.atiurin.ultron.extensions.simpleClassName\nimport com.atiurin.ultron.utils.getTargetResourceName\nimport org.hamcrest.Matcher\n\nfun <T> UltronEspressoInteraction<T>.hasDrawable(@DrawableRes resourceId: Int) = apply {\n    val resName = getTargetResourceName(resourceId)\n    executeAssertion(\n        operationBlock = getInteractionAssertionBlock(withDrawable(resourceId)),\n        name = \"HasDrawable with target resource $resName in '${getInteractionMatcher()}'\",\n        type = CustomEspressoAssertionType.HAS_DRAWABLE,\n        description = \"${interaction.simpleClassName()} assertion '${CustomEspressoAssertionType.HAS_DRAWABLE}' with drawable resource '$resName' in '${getInteractionMatcher()}' with root '${getInteractionRootMatcher()}' during ${getAssertionTimeout()} ms\",\n    )\n}\n\n/**\n * Assert view has any drawable\n */\nfun <T> UltronEspressoInteraction<T>.hasAnyDrawable() = apply {\n    executeAssertion(\n        operationBlock = getInteractionAssertionBlock(com.atiurin.ultron.custom.espresso.matcher.hasAnyDrawable()),\n        name = \"HasAnyDrawable in '${getInteractionMatcher()}'\",\n        type = CustomEspressoAssertionType.HAS_ANY_DRAWABLE,\n        description = \"${interaction.simpleClassName()} assertion '${CustomEspressoAssertionType.HAS_ANY_DRAWABLE}' with any drawable in '${getInteractionMatcher()}' with root '${getInteractionRootMatcher()}' during ${getAssertionTimeout()} ms\",\n    )\n}\n\nfun DataInteraction.hasDrawable(@DrawableRes resourceId: Int) = UltronEspressoInteraction(this).hasDrawable(resourceId)\nfun ViewInteraction.hasDrawable(@DrawableRes resourceId: Int) = UltronEspressoInteraction(this).hasDrawable(resourceId)\nfun Matcher<View>.hasDrawable(@DrawableRes resourceId: Int) = UltronEspressoInteraction(Espresso.onView(this)).hasDrawable(resourceId)\n\nfun DataInteraction.hasAnyDrawable() = UltronEspressoInteraction(this).hasAnyDrawable()\nfun ViewInteraction.hasAnyDrawable() = UltronEspressoInteraction(this).hasAnyDrawable()\nfun Matcher<View>.hasAnyDrawable() = UltronEspressoInteraction(Espresso.onView(this)).hasAnyDrawable()"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/custom/espresso/assertion/ExistsEspressoViewAssertion.kt",
    "content": "package com.atiurin.ultron.custom.espresso.assertion\n\nimport android.view.View\nimport androidx.test.espresso.NoMatchingViewException\nimport androidx.test.espresso.ViewAssertion\nimport com.atiurin.ultron.exceptions.UltronAssertionException\nimport com.atiurin.ultron.exceptions.UltronOperationException\n\nclass ExistsEspressoViewAssertion : ViewAssertion {\n    override fun check(view: View?, noViewFoundException: NoMatchingViewException?) {\n        if (view == null){\n            val ex = noViewFoundException ?: UltronAssertionException(\"View does not exist in hierarchy\")\n            throw ex\n        }\n    }\n}"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/custom/espresso/assertion/TextColorAssertion.kt",
    "content": "package com.atiurin.ultron.custom.espresso.assertion\n\nimport android.view.View\nimport androidx.annotation.ColorRes\nimport androidx.test.espresso.DataInteraction\nimport androidx.test.espresso.Espresso\nimport androidx.test.espresso.ViewInteraction\nimport com.atiurin.ultron.core.espresso.UltronEspressoInteraction\nimport com.atiurin.ultron.custom.espresso.matcher.textViewHasCurrentHintTextColor\nimport org.hamcrest.Matcher\nimport com.atiurin.ultron.custom.espresso.matcher.textViewHasCurrentTextColor\nimport com.atiurin.ultron.custom.espresso.matcher.textViewHasHighlightColor\nimport com.atiurin.ultron.custom.espresso.matcher.textViewHasShadowColor\nimport com.atiurin.ultron.extensions.simpleClassName\nimport com.atiurin.ultron.utils.getColorHex\nimport com.atiurin.ultron.utils.getTargetColor\n\n/**\n * Assert TextView has current text color = [colorResId]\n */\nfun <T> UltronEspressoInteraction<T>.hasCurrentTextColor(@ColorRes colorResId: Int) = apply {\n    val hexColor = getColorHex(getTargetColor(colorResId))\n    executeAssertion(\n        operationBlock = getInteractionAssertionBlock(textViewHasCurrentTextColor(colorResId)),\n        name = \"Has current text color $hexColor in '${getInteractionMatcher()}'\",\n        type = CustomEspressoAssertionType.HAS_CURRENT_TEXT_COLOR,\n        description = \"${interaction.simpleClassName()} assertion '${CustomEspressoAssertionType.HAS_CURRENT_TEXT_COLOR}' with color resource = '$hexColor' in '${getInteractionMatcher()}' with root '${getInteractionRootMatcher()}' during ${getAssertionTimeout()} ms\",\n    )\n}\n\nfun DataInteraction.hasCurrentTextColor(@ColorRes colorRes: Int) = UltronEspressoInteraction(this).hasCurrentTextColor(colorRes)\nfun ViewInteraction.hasCurrentTextColor(@ColorRes colorRes: Int) = UltronEspressoInteraction(this).hasCurrentTextColor(colorRes)\nfun Matcher<View>.hasCurrentTextColor(@ColorRes colorRes: Int) = UltronEspressoInteraction(Espresso.onView(this)).hasCurrentTextColor(colorRes)\n\n/**\n * Assert TextView has current hint text color = [colorResId]\n */\nfun <T> UltronEspressoInteraction<T>.hasCurrentHintTextColor(@ColorRes colorResId: Int) = apply {\n    val hexColor = getColorHex(getTargetColor(colorResId))\n    executeAssertion(\n        operationBlock = getInteractionAssertionBlock(textViewHasCurrentHintTextColor(colorResId)),\n        name = \"Has current hint text color $hexColor in '${getInteractionMatcher()}'\",\n        type = CustomEspressoAssertionType.HAS_CURRENT_HINT_TEXT_COLOR,\n        description = \"${interaction.simpleClassName()} assertion '${CustomEspressoAssertionType.HAS_CURRENT_HINT_TEXT_COLOR}' with color resource = '$hexColor' in '${getInteractionMatcher()}' with root '${getInteractionRootMatcher()}' during ${getAssertionTimeout()} ms\",\n    )\n}\n\nfun DataInteraction.hasCurrentHintTextColor(@ColorRes colorRes: Int) = UltronEspressoInteraction(this).hasCurrentHintTextColor(colorRes)\nfun ViewInteraction.hasCurrentHintTextColor(@ColorRes colorRes: Int) = UltronEspressoInteraction(this).hasCurrentHintTextColor(colorRes)\nfun Matcher<View>.hasCurrentHintTextColor(@ColorRes colorRes: Int) = UltronEspressoInteraction(Espresso.onView(this)).hasCurrentHintTextColor(colorRes)\n\n/**\n * Assert TextView has highlight color = [colorResId]\n */\nfun <T> UltronEspressoInteraction<T>.hasHighlightColor(@ColorRes colorResId: Int) = apply {\n    val hexColor = getColorHex(getTargetColor(colorResId))\n    executeAssertion(\n        operationBlock = getInteractionAssertionBlock(textViewHasHighlightColor(colorResId)),\n        name = \"Has highlight color $hexColor in '${getInteractionMatcher()}'\",\n        type = CustomEspressoAssertionType.HAS_HIGHLIGHT_COLOR,\n        description = \"${interaction.simpleClassName()} assertion '${CustomEspressoAssertionType.HAS_HIGHLIGHT_COLOR}' with color resource = '$hexColor' in '${getInteractionMatcher()}' with root '${getInteractionRootMatcher()}' during ${getAssertionTimeout()} ms\",\n    )\n}\n\nfun DataInteraction.hasHighlightColor(@ColorRes colorRes: Int) = UltronEspressoInteraction(this).hasHighlightColor(colorRes)\nfun ViewInteraction.hasHighlightColor(@ColorRes colorRes: Int) = UltronEspressoInteraction(this).hasHighlightColor(colorRes)\nfun Matcher<View>.hasHighlightColor(@ColorRes colorRes: Int) = UltronEspressoInteraction(Espresso.onView(this)).hasHighlightColor(colorRes)\n\n\n/**\n* Assert TextView has shadow color = [colorResId]\n*/\nfun <T> UltronEspressoInteraction<T>.hasShadowColor(@ColorRes colorResId: Int) = apply {\n    val hexColor = getColorHex(getTargetColor(colorResId))\n    executeAssertion(\n        operationBlock = getInteractionAssertionBlock(textViewHasShadowColor(colorResId)),\n        name = \"Has shadow color $hexColor in '${getInteractionMatcher()}'\",\n        type = CustomEspressoAssertionType.HAS_SHADOW_COLOR,\n        description = \"${interaction.simpleClassName()} assertion '${CustomEspressoAssertionType.HAS_SHADOW_COLOR}' with color resource = '$hexColor' in '${getInteractionMatcher()}' with root '${getInteractionRootMatcher()}' during ${getAssertionTimeout()} ms\",\n    )\n}\n\nfun DataInteraction.hasShadowColor(@ColorRes colorRes: Int) = UltronEspressoInteraction(this).hasShadowColor(colorRes)\nfun ViewInteraction.hasShadowColor(@ColorRes colorRes: Int) = UltronEspressoInteraction(this).hasShadowColor(colorRes)\nfun Matcher<View>.hasShadowColor(@ColorRes colorRes: Int) = UltronEspressoInteraction(Espresso.onView(this)).hasShadowColor(colorRes)\n"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/custom/espresso/base/Checker.kt",
    "content": "package com.atiurin.ultron.custom.espresso.base\n\nimport android.os.Looper\n\n\ninternal object Checker {\n    fun checkMainThread() {\n        checkState(\n            Thread.currentThread() == Looper.getMainLooper().thread,\n            \"Method cannot be called off the main application thread (on: %s)\",\n            Thread.currentThread().name\n        )\n    }\n\n    fun checkState(\n        expression: Boolean, errorMessageTemplate: String, vararg errorMessageArgs: Any,\n    ) {\n        check(expression) { format(errorMessageTemplate, *errorMessageArgs) }\n    }\n\n    private fun format(template: String?, vararg args: Any): String {\n        var templ = template\n        templ = templ.toString()\n\n        // start substituting the arguments into the '%s' placeholders\n        val builder = StringBuilder(templ.length + 16 * args.size)\n        var templateStart = 0\n        var i = 0\n        while (i < args.size) {\n            val placeholderStart = templ.indexOf(\"%s\", templateStart)\n            if (placeholderStart == -1) {\n                break\n            }\n            builder.append(templ.substring(templateStart, placeholderStart))\n            builder.append(args[i++])\n            templateStart = placeholderStart + 2\n        }\n        builder.append(templ.substring(templateStart))\n\n        // if we run out of placeholders, append the extra args in square braces\n        if (i < args.size) {\n            builder.append(\" [\")\n            builder.append(args[i++])\n            while (i < args.size) {\n                builder.append(\", \")\n                builder.append(args[i++])\n            }\n            builder.append(']')\n        }\n        return builder.toString()\n    }\n}"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/custom/espresso/base/IterableUtils.kt",
    "content": "package com.atiurin.ultron.custom.espresso.base\n\nimport org.hamcrest.Matcher\n\ninternal fun <T> filter(iterable: Iterable<T>, matcher: Matcher<T>): Iterable<T> {\n    return iterable.filter { matcher.matches(it) }\n}\n\ninternal fun <T> filterToList(iterable: Iterable<T>, matcher: Matcher<T>): List<T> {\n    return filter(iterable, matcher).toList()\n}\n\ninternal fun joinToString(iterable: Iterable<Any>, delimiter: String): String {\n    return iterable.joinToString(separator = delimiter)\n}\n\ninternal fun <T> toArray(iterator: Iterator<T>, clazz: Class<T>): Array<T> {\n    val arrayList = ArrayList<T>()\n    while (iterator.hasNext()) {\n        arrayList.add(iterator.next())\n    }\n    return arrayList.toArray(\n        java.lang.reflect.Array.newInstance(\n            clazz,\n            arrayList.size\n        ) as Array<T>\n    )\n}"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/custom/espresso/base/RootViewPickerCreator.kt",
    "content": "package com.atiurin.ultron.custom.espresso.base\n\nimport android.view.View\nimport androidx.test.espresso.base.RootViewPicker\nimport androidx.test.platform.app.InstrumentationRegistry\nimport androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry\nimport com.atiurin.ultron.core.config.UltronConfig.Espresso\nimport com.atiurin.ultron.custom.espresso.matcher.withSuitableRoot\nimport org.hamcrest.Matcher\nimport java.lang.reflect.Constructor\nimport java.util.concurrent.atomic.AtomicReference\n\nfun createRootViewPicker(viewMatcher: Matcher<View>): RootViewPicker {\n    val rootViewPickerClass: Class<*> = Class.forName(\"androidx.test.espresso.base.RootViewPicker\")\n    val rootViewPickerConstructor: Constructor<*> = rootViewPickerClass.declaredConstructors.first()\n    val rootResultFetcherClass: Class<*> =\n        rootViewPickerClass.declaredClasses.first { clazz -> clazz.simpleName.contains(\"RootResultFetcher\") }\n    val rootResultFetcherConstructor: Constructor<*> =\n        rootResultFetcherClass.declaredConstructors.first()\n    rootViewPickerConstructor.isAccessible = true\n    rootResultFetcherConstructor.isAccessible = true\n    val rootResultFetcher = rootResultFetcherConstructor.newInstance(\n        Espresso.activeRootLister,\n        AtomicReference(withSuitableRoot(viewMatcher))\n    )\n\n    val commonArgs = arrayOf(\n        Espresso.uiController,\n        rootResultFetcher,\n        ActivityLifecycleMonitorRegistry.getInstance(),\n        AtomicReference(true),\n        Espresso.controlledLooper\n    )\n\n    var rootViewPicker: RootViewPicker? = null\n    runCatching {\n        rootViewPicker = rootViewPickerConstructor.newInstance(*commonArgs) as RootViewPicker\n    }.onFailure {\n        val argsWithTargetContext = commonArgs + InstrumentationRegistry.getInstrumentation().targetContext\n        rootViewPicker = rootViewPickerConstructor.newInstance(*argsWithTargetContext) as RootViewPicker\n    }\n\n    return rootViewPicker ?: throw IllegalStateException(\"RootViewPicker is not created\")\n}"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/custom/espresso/base/UltronRootViewFinder.kt",
    "content": "package com.atiurin.ultron.custom.espresso.base\n\nimport androidx.test.espresso.Root\nimport com.atiurin.ultron.core.config.UltronConfig\nimport com.atiurin.ultron.utils.isVisible\nimport com.atiurin.ultron.utils.runOnUiThread\n\nfun getRootViewsList(): List<Root> = runOnUiThread {\n    UltronConfig.Espresso.activeRootLister.listActiveRoots()\n}\n\nfun getVisibleRootViews(): List<Root> = getRootViewsList().filter { it.decorView.isVisible }"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/custom/espresso/base/UltronViewFinder.kt",
    "content": "package com.atiurin.ultron.custom.espresso.base\n\nimport android.view.View\nimport android.widget.AdapterView\nimport androidx.test.espresso.AmbiguousViewMatcherException\nimport androidx.test.espresso.AmbiguousViewMatcherException.Builder\nimport androidx.test.espresso.DataInteraction\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.NoMatchingViewException\nimport androidx.test.espresso.ViewInteraction\nimport androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom\nimport androidx.test.espresso.util.TreeIterables\nimport com.atiurin.ultron.core.espresso.UltronEspressoInteraction\nimport com.atiurin.ultron.custom.espresso.action.CustomEspressoActionType.GET_VIEW_FORCIBLY\nimport com.atiurin.ultron.exceptions.UltronException\nimport com.atiurin.ultron.extensions.getTargetMatcher\nimport com.atiurin.ultron.extensions.getViewMatcher\nimport com.atiurin.ultron.extensions.simpleClassName\nimport com.atiurin.ultron.utils.runOnUiThread\nimport org.hamcrest.Matcher\nimport java.util.Locale\nimport java.util.concurrent.atomic.AtomicReference\n\nclass UltronViewFinder<T>(val interaction: T) {\n\n    private val viewMatcher: Matcher<View> = when (interaction) {\n        is ViewInteraction -> interaction.getViewMatcher()\n            ?: throw NullPointerException(\"Matcher<View> is null\")\n\n        is DataInteraction -> interaction.getTargetMatcher()\n            ?: throw NullPointerException(\"Matcher<View> is null\")\n\n        else -> throw UltronException(\"Unknown type of interaction provided!\")\n    }\n    private val root: View by lazy {\n        getVisibleRootViews().find { it.isReady }?.decorView\n            ?: throw UltronException(\"There is no root View in ready and visible state\")\n    }\n\n    val view: View by lazy {\n        runOnUiThread {\n            fetchView()\n        }\n    }\n\n    @Throws(AmbiguousViewMatcherException::class, NoMatchingViewException::class)\n    fun fetchView(): View {\n        Checker.checkMainThread()\n        val matchedViewIterator =\n            filter(TreeIterables.breadthFirstViewTraversal(root), viewMatcher).iterator()\n        var matchedView: View? = null\n\n        while (matchedViewIterator.hasNext()) {\n            if (matchedView != null) {\n                // Ambiguous!\n                throw Builder()\n                    .withViewMatcher(viewMatcher)\n                    .withRootView(root)\n                    .withView1(matchedView)\n                    .withView2(matchedViewIterator.next())\n                    .withOtherAmbiguousViews(\n                        *toArray(\n                            matchedViewIterator,\n                            View::class.java\n                        )\n                    )\n                    .build()\n            } else {\n                matchedView = matchedViewIterator.next()\n            }\n        }\n        if (null == matchedView) {\n            val adapterViews =\n                filterToList(\n                    TreeIterables.breadthFirstViewTraversal(root),\n                    isAssignableFrom(AdapterView::class.java)\n                )\n\n            if (adapterViews.isEmpty()) {\n                throw NoMatchingViewException.Builder()\n                    .withViewMatcher(viewMatcher)\n                    .withRootView(root)\n                    .build()\n            }\n\n            val warning = String.format(\n                Locale.ROOT,\n                \"\"\"\n                    If the target view is not part of the view hierarchy, you may need to use Espresso.onData to load it from one of the following AdapterViews:%s\n                \"\"\".trimIndent(),\n                joinToString(adapterViews, \"\\n- \")\n            )\n            throw NoMatchingViewException.Builder()\n                .withViewMatcher(viewMatcher)\n                .withRootView(root)\n                .withAdapterViews(adapterViews)\n                .withAdapterViewWarning(warning)\n                .build()\n        } else {\n            return matchedView\n        }\n    }\n\n\n}\n\n\nfun <T> UltronEspressoInteraction<T>.getViewForcibly(): View {\n    val viewContainer = AtomicReference<View>()\n    executeAction(\n        operationBlock = { viewContainer.set(UltronViewFinder(interaction).view) },\n        name = \"Get view forcibly with '${getInteractionMatcher()}' in root '${getInteractionRootMatcher()}'\",\n        type = GET_VIEW_FORCIBLY,\n        description = \"${interaction.simpleClassName()} get view forcibly action '$GET_VIEW_FORCIBLY' of '${getInteractionMatcher()}' with root '${getInteractionRootMatcher()}' during ${getActionTimeout()} ms\",\n    )\n    return viewContainer.get()\n}\n\n/**\n * Returns the view associated with this matcher, bypassing the common Espresso idle state mechanism.\n *\n * The `getViewForcibly()` extension method allows obtaining the view directly, without waiting for Espresso's internal idle state.\n * This is useful in scenarios where the idle state mechanism may cause delays or interfere with certain operations.\n *\n * Compared to the `getView()` method, which works with Espresso's idle state mechanism, `getViewForcibly()` provides immediate access to the view without considering the idle state.\n *\n * @receiver The matcher to obtain the view from.\n * @return The view associated with the matcher.\n */\nfun Matcher<View>.getViewForcibly() = UltronEspressoInteraction(onView(this)).getViewForcibly()\nfun ViewInteraction.getViewForcibly() = UltronEspressoInteraction(this).getViewForcibly()\nfun DataInteraction.getViewForcibly() = UltronEspressoInteraction(this).getViewForcibly()"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/custom/espresso/matcher/AppCompatTextMatcher.kt",
    "content": "package com.atiurin.ultron.custom.espresso.matcher\n\nimport android.view.View\nimport androidx.annotation.StringRes\nimport androidx.appcompat.widget.AppCompatTextView\nimport androidx.test.espresso.matcher.BoundedMatcher\nimport com.atiurin.ultron.utils.getTargetString\nimport org.hamcrest.Description\nimport org.hamcrest.Matcher\n\n/**\n * Matcher for [AppCompatTextView]\n * @param textMatcher to match with [AppCompatTextView.getText]\n */\nfun withAppCompatText(textMatcher: Matcher<String>): Matcher<View> {\n    return object : BoundedMatcher<View, AppCompatTextView>(AppCompatTextView::class.java) {\n        override fun describeTo(description: Description) {\n            description.appendText(\"withAppCompactText: \")\n            textMatcher.describeTo(description)\n        }\n\n        override fun matchesSafely(item: AppCompatTextView): Boolean {\n            val value = item.text.toString().replace(\"\\n\".toRegex(), \" \")\n            return textMatcher.matches(value)\n        }\n    }\n}\n\n/**\n * Matcher for [AppCompatTextView]\n * @param text to match with [AppCompatTextView.getText]\n */\nfun withAppCompatText(text: String): Matcher<View> {\n    return object : BoundedMatcher<View, AppCompatTextView>(AppCompatTextView::class.java) {\n        override fun describeTo(description: Description) {\n            description.appendText(\"withAppCompactText: $text\")\n        }\n\n        override fun matchesSafely(item: AppCompatTextView): Boolean {\n            val value = item.text.toString().replace(\"\\n\".toRegex(), \" \")\n            return text == value\n        }\n    }\n}\n\n/**\n * Matcher for [AppCompatTextView]\n * @param stringId resource id of string to match with [AppCompatTextView.getText]\n */\nfun withAppCompatText(@StringRes stringId: Int): Matcher<View> {\n    val text = getTargetString(stringId)\n    return object : BoundedMatcher<View, AppCompatTextView>(AppCompatTextView::class.java) {\n        override fun describeTo(description: Description) {\n            description.appendText(\"withAppCompactText: $text\")\n        }\n\n        override fun matchesSafely(item: AppCompatTextView): Boolean {\n            val value = item.text.toString().replace(\"\\n\".toRegex(), \" \")\n            return text == value\n        }\n    }\n}"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/custom/espresso/matcher/DrawableMatchers.kt",
    "content": "package com.atiurin.ultron.custom.espresso.matcher\n\nimport android.content.res.Resources\nimport android.graphics.drawable.Drawable\nimport android.view.View\nimport android.widget.ImageView\nimport com.atiurin.ultron.extensions.isSameAs\nimport org.hamcrest.Description\nimport org.hamcrest.Matcher\nimport org.hamcrest.TypeSafeMatcher\n\n\nclass ResourceDrawableMatcher(private val expectedId: Int = 0) : TypeSafeMatcher<View>() {\n    private var resourceName: String? = null\n\n    override fun matchesSafely(targetView: View): Boolean {\n        if (targetView !is ImageView) {\n            return false\n        }\n        val imageView: ImageView = targetView\n        if (expectedId < 0) {\n            return imageView.drawable != null\n        }\n        val resources: Resources = targetView.context.resources\n        val expectedDrawable: Drawable = resources.getDrawable(expectedId)\n        resourceName = resources.getResourceEntryName(expectedId)\n        return imageView.drawable?.isSameAs(expectedDrawable) == true\n    }\n\n\n    override fun describeTo(description: Description) {\n        if (expectedId < 0) {\n            description.appendValue(\"has any drawable\")\n        } else {\n            description.appendValue(\"has drawable from resource id: $expectedId\")\n        }\n        if (resourceName != null) {\n            description.appendText(\"[\")\n            description.appendText(resourceName)\n            description.appendText(\"]\")\n        }\n    }\n}\n\n/**\n * Matches view bitmap with resource object\n * @param resourceId the object id against which the matcher is evaluated\n * @return matchesSafely returns `true` if bitmaps are the same, otherwise `false`.\n */\nfun withDrawable(resourceId: Int): Matcher<View> {\n    return ResourceDrawableMatcher(resourceId)\n}\n\n/**\n * Matches view has any drawable or not\n * @return matchesSafely returns `true` if view has any drawable, otherwise `false`.\n */\nfun hasAnyDrawable(): Matcher<View> {\n    return ResourceDrawableMatcher(-1)\n}"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/custom/espresso/matcher/ElementWithAttributeMatcher.kt",
    "content": "package com.atiurin.ultron.custom.espresso.matcher\n\nimport org.hamcrest.Description\nimport org.hamcrest.Matcher\nimport org.hamcrest.TypeSafeMatcher\nimport org.w3c.dom.Element\n\nopen class ElementWithAttributeMatcher(\n    val attributeName: String,\n    val attributeValueMatcher: Matcher<String>\n) :\n    TypeSafeMatcher<Element>() {\n    override fun matchesSafely(element: Element): Boolean {\n        return attributeValueMatcher.matches(element.getAttribute(attributeName))\n    }\n\n    override fun describeTo(description: Description) {\n        description.appendText(\"with text content: \")\n        attributeValueMatcher.describeTo(description)\n    }\n\n    companion object {\n        fun withAttribute(attributeName: String, attributeValueMatcher: Matcher<String>) =\n            ElementWithAttributeMatcher(attributeName, attributeValueMatcher)\n    }\n}\n\n"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/custom/espresso/matcher/NotUniqueViewMatchers.kt",
    "content": "package com.atiurin.ultron.custom.espresso.matcher\n\nimport android.view.View\nimport org.hamcrest.Description\nimport org.hamcrest.Matcher\nimport org.hamcrest.TypeSafeMatcher\n\n/**\n * Should be the last description of element\n */\nfun Matcher<View>.first(): Matcher<View> {\n    val initialMatcher = this\n    return object : TypeSafeMatcher<View>(View::class.java) {\n        var isFirstMatchedItemFound = false\n        override fun describeTo(description: Description) {\n            description.appendText(\"first matched view: \")\n            initialMatcher.describeTo(description)\n        }\n\n        override fun matchesSafely(item: View): Boolean {\n            return if (!isFirstMatchedItemFound) {\n                isFirstMatchedItemFound = initialMatcher.matches(item)\n                isFirstMatchedItemFound\n            } else false\n        }\n    }\n}\n\n/**\n * Should be the last description of element\n * @param number of matched view in hierarchy. Starts from 0\n */\nfun Matcher<View>.hierarchyNumber(number: Int): Matcher<View> {\n    val initialMatcher = this\n    return object : TypeSafeMatcher<View>(View::class.java) {\n        var lastMatchedItemNumber = -1\n        override fun describeTo(description: Description) {\n            description.appendText(\"first matched view: \")\n            initialMatcher.describeTo(description)\n        }\n\n        override fun matchesSafely(item: View): Boolean {\n            val isMatched = initialMatcher.matches(item)\n            if (isMatched) lastMatchedItemNumber++\n            return (lastMatchedItemNumber == number && isMatched)\n        }\n    }\n}"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/custom/espresso/matcher/SuitableRootMatcher.kt",
    "content": "package com.atiurin.ultron.custom.espresso.matcher\n\nimport android.view.View\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.Root\nimport com.atiurin.ultron.core.espresso.UltronEspressoInteraction\nimport com.atiurin.ultron.core.espresso.recyclerview.UltronRecyclerView\nimport com.atiurin.ultron.extensions.withSuitableRoot\nimport com.atiurin.ultron.utils.allViews\nimport com.atiurin.ultron.utils.isVisible\nimport org.hamcrest.Description\nimport org.hamcrest.Matcher\nimport org.hamcrest.TypeSafeMatcher\n\nfun withSuitableRoot(viewMatcher: Matcher<View>): Matcher<Root> {\n    return SuitableRootMatcher(viewMatcher)\n}\n\ninternal class SuitableRootMatcher(private val viewMatcher: Matcher<View>) :\n    TypeSafeMatcher<Root>() {\n\n    override fun describeTo(description: Description) {\n        description.appendText(\"find suitable root view \")\n    }\n\n    override fun matchesSafely(root: Root): Boolean {\n        return root.decorView.isVisible &&\n                root.isReady &&\n                root.decorView.allViews.any { viewInRoot -> viewMatcher.matches(viewInRoot) }\n    }\n\n}\n\nfun Matcher<View>.withSuitableRoot() = UltronEspressoInteraction(onView(this).withSuitableRoot())\nfun UltronRecyclerView.withSuitableRoot() = apply { recyclerViewMatcher.withSuitableRoot() }"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/custom/espresso/matcher/TextColorMatchers.kt",
    "content": "package com.atiurin.ultron.custom.espresso.matcher\n\nimport android.content.Context\nimport android.os.Build\nimport android.view.View\nimport android.widget.TextView\nimport androidx.test.espresso.matcher.BoundedDiagnosingMatcher\nimport com.atiurin.ultron.utils.getColorHex\nimport org.hamcrest.Description\nimport org.hamcrest.Matcher\n\n/**\n * Returns a matcher that matches [android.widget.TextView] based on it's color.\n *\n *\n * **This API is currently in beta.**\n */\ninternal fun hasColor(colorResId: Int, textViewColorPropertyName: String = \"\", viewColorSelector: TextView.() -> Int): Matcher<View> {\n  return object : BoundedDiagnosingMatcher<View, TextView>(TextView::class.java) {\n    var context: Context? = null\n    override fun matchesSafely(textView: TextView, mismatchDescription: Description): Boolean {\n      val viewContext = textView.context\n      context = viewContext\n      val textViewColor = viewColorSelector.invoke(textView)\n      val expectedColor: Int = if (Build.VERSION.SDK_INT <= 22) {\n        viewContext.resources.getColor(colorResId)\n      } else {\n        viewContext.getColor(colorResId)\n      }\n      mismatchDescription\n        .appendText(\"textView $textViewColorPropertyName was \")\n        .appendText(getColorHex(textViewColor))\n      return textViewColor == expectedColor\n    }\n\n    override fun describeMoreTo(description: Description) {\n      description.appendText(\"textView $textViewColorPropertyName is color with \")\n      if (context == null) {\n        description.appendText(\"ID \").appendValue(colorResId)\n      } else {\n        val color = if (Build.VERSION.SDK_INT <= 22) context!!.resources.getColor(colorResId) else context!!.getColor(colorResId)\n        description.appendText(\"value \" + getColorHex(color))\n      }\n    }\n  }\n}\n\nfun textViewHasCurrentTextColor(colorResId: Int) = hasColor(colorResId, \"currentTextColor\") { this.currentTextColor }\nfun textViewHasHighlightColor(colorResId: Int) = hasColor(colorResId, \"highlightColor\") { this.highlightColor }\nfun textViewHasShadowColor(colorResId: Int) = hasColor(colorResId, \"shadowColor\") { this.shadowColor }\nfun textViewHasCurrentHintTextColor(colorResId: Int) = hasColor(colorResId, \"currentHintTextColor\") { this.currentHintTextColor }"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/extensions/BitmapExt.kt",
    "content": "package com.atiurin.ultron.extensions\n\nimport android.graphics.Bitmap\nimport java.nio.ByteBuffer\nimport java.util.Arrays\n\nfun Bitmap.isSameAs(expected: Bitmap): Boolean {\n    val buffer1 = ByteBuffer.allocate(this.height * this.rowBytes);\n    this.copyPixelsToBuffer(buffer1)\n\n    val buffer2 = ByteBuffer.allocate(expected.height * expected.rowBytes);\n    expected.copyPixelsToBuffer(buffer2)\n    return Arrays.equals(buffer1.array(), buffer2.array())\n}"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/extensions/DataInterationExt.kt",
    "content": "package com.atiurin.ultron.extensions\n\nimport android.view.View\nimport androidx.test.espresso.DataInteraction\nimport androidx.test.espresso.UiController\nimport androidx.test.espresso.ViewAction\nimport androidx.test.espresso.action.EspressoKey\nimport com.atiurin.ultron.core.common.assertion.DefaultOperationAssertion\nimport com.atiurin.ultron.core.common.assertion.OperationAssertion\nimport com.atiurin.ultron.core.espresso.EspressoOperationResult\nimport com.atiurin.ultron.core.espresso.UltronEspressoInteraction\nimport com.atiurin.ultron.core.espresso.UltronEspressoOperation\nimport com.atiurin.ultron.core.espresso.action.UltronEspressoActionParams\nimport com.atiurin.ultron.listeners.setListenersState\nimport org.hamcrest.Matcher\n\nfun DataInteraction.isSuccess(action: DataInteraction.() -> Unit): Boolean =\n    runCatching { action() }.isSuccess\n\nfun DataInteraction.withTimeout(timeoutMs: Long) =\n    UltronEspressoInteraction(this).withTimeout(timeoutMs)\n\nfun DataInteraction.withResultHandler(resultHandler: (EspressoOperationResult<UltronEspressoOperation>) -> Unit) =\n    UltronEspressoInteraction(this).withResultHandler(resultHandler)\n\nfun DataInteraction.withAssertion(assertion: OperationAssertion) =\n    UltronEspressoInteraction(this).withAssertion(assertion)\n\nfun DataInteraction.withAssertion(\n    name: String = \"\",\n    isListened: Boolean = false,\n    block: () -> Unit\n) =\n    UltronEspressoInteraction(this).withAssertion(\n        DefaultOperationAssertion(\n            name,\n            block.setListenersState(isListened)\n        )\n    )\n\nfun DataInteraction.withName(name: String) = UltronEspressoInteraction(this).withName(name)\nfun DataInteraction.withMetaInfo(meta: Any) = UltronEspressoInteraction(this).withMetaInfo(meta)\n\n//actions\nfun DataInteraction.click() = UltronEspressoInteraction(this).click()\nfun DataInteraction.doubleClick() = UltronEspressoInteraction(this).doubleClick()\nfun DataInteraction.longClick() = UltronEspressoInteraction(this).longClick()\n\nfun DataInteraction.clickTopLeft(offsetX: Int = 0, offsetY: Int = 0) =\n    UltronEspressoInteraction(this).clickTopLeft(offsetX, offsetY)\n\nfun DataInteraction.clickTopCenter(offsetY: Int) =\n    UltronEspressoInteraction(this).clickTopCenter(offsetY)\n\nfun DataInteraction.clickTopRight(offsetX: Int = 0, offsetY: Int = 0) =\n    UltronEspressoInteraction(this).clickTopRight(offsetX, offsetY)\n\nfun DataInteraction.clickCenterRight(offsetX: Int = 0) =\n    UltronEspressoInteraction(this).clickCenterRight(offsetX)\n\nfun DataInteraction.clickBottomRight(offsetX: Int = 0, offsetY: Int = 0) =\n    UltronEspressoInteraction(this).clickBottomRight(offsetX, offsetY)\n\nfun DataInteraction.clickBottomCenter(offsetY: Int = 0) =\n    UltronEspressoInteraction(this).clickBottomCenter(offsetY)\n\nfun DataInteraction.clickBottomLeft(offsetX: Int = 0, offsetY: Int = 0) =\n    UltronEspressoInteraction(this).clickBottomLeft(offsetX, offsetY)\n\nfun DataInteraction.clickCenterLeft(offsetX: Int = 0) =\n    UltronEspressoInteraction(this).clickCenterLeft(offsetX)\n\nfun DataInteraction.typeText(text: String) = UltronEspressoInteraction(this).typeText(text)\nfun DataInteraction.replaceText(text: String) = UltronEspressoInteraction(this).replaceText(text)\nfun DataInteraction.clearText() = UltronEspressoInteraction(this).clearText()\nfun DataInteraction.pressKey(keyCode: Int) = UltronEspressoInteraction(this).pressKey(keyCode)\nfun DataInteraction.pressKey(key: EspressoKey) = UltronEspressoInteraction(this).pressKey(key)\nfun DataInteraction.closeSoftKeyboard() = UltronEspressoInteraction(this).closeSoftKeyboard()\nfun DataInteraction.swipeLeft() = UltronEspressoInteraction(this).swipeLeft()\nfun DataInteraction.swipeRight() = UltronEspressoInteraction(this).swipeRight()\nfun DataInteraction.swipeUp() = UltronEspressoInteraction(this).swipeUp()\nfun DataInteraction.swipeDown() = UltronEspressoInteraction(this).swipeDown()\nfun DataInteraction.scrollTo() = UltronEspressoInteraction(this).scrollTo()\nfun DataInteraction.perform(viewAction: ViewAction, description: String = \"\") =\n    UltronEspressoInteraction(this).perform(viewAction, description)\n\nfun DataInteraction.perform(\n    params: UltronEspressoActionParams? = null,\n    block: (uiController: UiController, view: View) -> Unit\n) =\n    UltronEspressoInteraction(this).perform(params, block)\n\nfun <T> DataInteraction.execute(\n    params: UltronEspressoActionParams? = null,\n    block: (uiController: UiController, view: View) -> T\n): T =\n    UltronEspressoInteraction(this).execute(params, block)\n\n//assertions\nfun DataInteraction.isDisplayed() = UltronEspressoInteraction(this).isDisplayed()\nfun DataInteraction.isNotDisplayed() = UltronEspressoInteraction(this).isNotDisplayed()\nfun DataInteraction.exists() = UltronEspressoInteraction(this).exists()\nfun DataInteraction.doesNotExist() = UltronEspressoInteraction(this).doesNotExist()\nfun DataInteraction.isCompletelyDisplayed() =\n    UltronEspressoInteraction(this).isCompletelyDisplayed()\n\nfun DataInteraction.isDisplayingAtLeast(percentage: Int) =\n    UltronEspressoInteraction(this).isDisplayingAtLeast(percentage)\n\nfun DataInteraction.isEnabled() = UltronEspressoInteraction(this).isEnabled()\nfun DataInteraction.isNotEnabled() = UltronEspressoInteraction(this).isNotEnabled()\nfun DataInteraction.isSelected() = UltronEspressoInteraction(this).isSelected()\nfun DataInteraction.isNotSelected() = UltronEspressoInteraction(this).isNotSelected()\nfun DataInteraction.isClickable() = UltronEspressoInteraction(this).isClickable()\nfun DataInteraction.isNotClickable() = UltronEspressoInteraction(this).isNotClickable()\nfun DataInteraction.isChecked() = UltronEspressoInteraction(this).isChecked()\nfun DataInteraction.isNotChecked() = UltronEspressoInteraction(this).isNotChecked()\nfun DataInteraction.isFocusable() = UltronEspressoInteraction(this).isFocusable()\nfun DataInteraction.isNotFocusable() = UltronEspressoInteraction(this).isNotFocusable()\nfun DataInteraction.hasFocus() = UltronEspressoInteraction(this).hasFocus()\nfun DataInteraction.isJavascriptEnabled() = UltronEspressoInteraction(this).isJavascriptEnabled()\nfun DataInteraction.hasText(text: String) = UltronEspressoInteraction(this).hasText(text)\nfun DataInteraction.hasText(resourceId: Int) = UltronEspressoInteraction(this).hasText(resourceId)\nfun DataInteraction.hasText(stringMatcher: Matcher<String>) =\n    UltronEspressoInteraction(this).hasText(stringMatcher)\n\nfun DataInteraction.textContains(text: String) = UltronEspressoInteraction(this).textContains(text)\nfun DataInteraction.hasContentDescription(text: String) =\n    UltronEspressoInteraction(this).hasContentDescription(text)\n\nfun DataInteraction.hasContentDescription(resourceId: Int) =\n    UltronEspressoInteraction(this).hasContentDescription(resourceId)\n\nfun DataInteraction.hasContentDescription(charSequenceMatcher: Matcher<CharSequence>) =\n    UltronEspressoInteraction(this).hasContentDescription(charSequenceMatcher)\n\nfun DataInteraction.contentDescriptionContains(text: String) =\n    UltronEspressoInteraction(this).contentDescriptionContains(text)\n\nfun DataInteraction.assertMatches(condition: Matcher<View>) =\n    UltronEspressoInteraction(this).assertMatches(condition)\n"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/extensions/DrawableExt.kt",
    "content": "package com.atiurin.ultron.extensions\n\nimport android.graphics.Bitmap\nimport android.graphics.Canvas\nimport android.graphics.drawable.BitmapDrawable\nimport android.graphics.drawable.Drawable\n\nfun Drawable.isSameAs(\n    expected: Drawable\n): Boolean {\n    val stateA = expected.constantState\n    val stateB = this.constantState\n    // If the constant state is identical, they are using the same drawable resource.\n    // However, the opposite is not necessarily true.\n    val areBitmapsSame = this.getBitmap().isSameAs(expected.getBitmap())\n    val areStatesSame = stateA != null && stateB != null && stateA == stateB\n    return (areStatesSame || areBitmapsSame)\n}\n\nfun Drawable.getBitmap(): Bitmap {\n    val result: Bitmap\n    if (this is BitmapDrawable) {\n        result = this.bitmap\n    } else {\n        var width = this.intrinsicWidth\n        var height = this.intrinsicHeight\n        // Some drawables have no intrinsic width - e.g. solid colours.\n        if (width <= 0) {\n            width = 1\n        }\n        if (height <= 0) {\n            height = 1\n        }\n        result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)\n        val canvas = Canvas(result)\n        this.setBounds(0, 0, canvas.width, canvas.height)\n        this.draw(canvas)\n    }\n    return result\n}\n"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/extensions/MatcherViewExt.kt",
    "content": "package com.atiurin.ultron.extensions\n\nimport android.view.View\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.UiController\nimport androidx.test.espresso.ViewAction\nimport androidx.test.espresso.action.EspressoKey\nimport com.atiurin.ultron.core.common.assertion.DefaultOperationAssertion\nimport com.atiurin.ultron.core.common.assertion.OperationAssertion\nimport com.atiurin.ultron.core.espresso.EspressoOperationResult\nimport com.atiurin.ultron.core.espresso.UltronEspressoInteraction\nimport com.atiurin.ultron.core.espresso.UltronEspressoOperation\nimport com.atiurin.ultron.core.espresso.action.UltronEspressoActionParams\nimport com.atiurin.ultron.listeners.setListenersState\nimport org.hamcrest.Matcher\nfun Matcher<View>.ultronInteraction() = UltronEspressoInteraction(onView(this))\nfun Matcher<View>.isSuccess(action: Matcher<View>.() -> Unit): Boolean =\n    onView(this).isSuccess { action() }\n\nfun Matcher<View>.withTimeout(timeoutMs: Long) =\n    UltronEspressoInteraction(onView(this)).withTimeout(timeoutMs)\n\nfun Matcher<View>.withResultHandler(resultHandler: (EspressoOperationResult<UltronEspressoOperation>) -> Unit) =\n    UltronEspressoInteraction(onView(this)).withResultHandler(resultHandler)\n\nfun Matcher<View>.withAssertion(assertion: OperationAssertion) =\n    UltronEspressoInteraction(onView(this)).withAssertion(assertion)\n\nfun Matcher<View>.withAssertion(name: String = \"\", isListened: Boolean = false, block: () -> Unit) =\n    UltronEspressoInteraction(onView(this)).withAssertion(\n        DefaultOperationAssertion(\n            name,\n            block.setListenersState(isListened)\n        )\n    )\n\nfun Matcher<View>.withName(name: String) = UltronEspressoInteraction(onView(this)).withName(name)\nfun Matcher<View>.withMetaInfo(meta: Any) =\n    UltronEspressoInteraction(onView(this)).withMetaInfo(meta)\n\n//actions\nfun Matcher<View>.click() = UltronEspressoInteraction(onView(this)).click()\nfun Matcher<View>.doubleClick() = UltronEspressoInteraction(onView(this)).doubleClick()\nfun Matcher<View>.longClick() = UltronEspressoInteraction(onView(this)).longClick()\n\nfun Matcher<View>.clickTopLeft(offsetX: Int = 0, offsetY: Int = 0) =\n    UltronEspressoInteraction(onView(this)).clickTopLeft(offsetX, offsetY)\n\nfun Matcher<View>.clickTopCenter(offsetY: Int) =\n    UltronEspressoInteraction(onView(this)).clickTopCenter(offsetY)\n\nfun Matcher<View>.clickTopRight(offsetX: Int = 0, offsetY: Int = 0) =\n    UltronEspressoInteraction(onView(this)).clickTopRight(offsetX, offsetY)\n\nfun Matcher<View>.clickCenterRight(offsetX: Int = 0) =\n    UltronEspressoInteraction(onView(this)).clickCenterRight(offsetX)\n\nfun Matcher<View>.clickBottomRight(offsetX: Int = 0, offsetY: Int = 0) =\n    UltronEspressoInteraction(onView(this)).clickBottomRight(offsetX, offsetY)\n\nfun Matcher<View>.clickBottomCenter(offsetY: Int = 0) =\n    UltronEspressoInteraction(onView(this)).clickBottomCenter(offsetY)\n\nfun Matcher<View>.clickBottomLeft(offsetX: Int = 0, offsetY: Int = 0) =\n    UltronEspressoInteraction(onView(this)).clickBottomLeft(offsetX, offsetY)\n\nfun Matcher<View>.clickCenterLeft(offsetX: Int = 0) =\n    UltronEspressoInteraction(onView(this)).clickCenterLeft(offsetX)\n\nfun Matcher<View>.typeText(text: String) = UltronEspressoInteraction(onView(this)).typeText(text)\nfun Matcher<View>.replaceText(text: String) =\n    UltronEspressoInteraction(onView(this)).replaceText(text)\n\nfun Matcher<View>.clearText() = UltronEspressoInteraction(onView(this)).clearText()\nfun Matcher<View>.pressKey(keyCode: Int) = UltronEspressoInteraction(onView(this)).pressKey(keyCode)\nfun Matcher<View>.pressKey(key: EspressoKey) = UltronEspressoInteraction(onView(this)).pressKey(key)\nfun Matcher<View>.closeSoftKeyboard() = UltronEspressoInteraction(onView(this)).closeSoftKeyboard()\nfun Matcher<View>.swipeLeft() = UltronEspressoInteraction(onView(this)).swipeLeft()\nfun Matcher<View>.swipeRight() = UltronEspressoInteraction(onView(this)).swipeRight()\nfun Matcher<View>.swipeUp() = UltronEspressoInteraction(onView(this)).swipeUp()\nfun Matcher<View>.swipeDown() = UltronEspressoInteraction(onView(this)).swipeDown()\nfun Matcher<View>.scrollTo() = UltronEspressoInteraction(onView(this)).scrollTo()\nfun Matcher<View>.perform(viewAction: ViewAction, description: String = \"\") =\n    UltronEspressoInteraction(onView(this)).perform(viewAction, description)\n\nfun Matcher<View>.perform(\n    params: UltronEspressoActionParams? = null,\n    block: (uiController: UiController, view: View) -> Unit\n) =\n    UltronEspressoInteraction(onView(this)).perform(params, block)\n\nfun <T> Matcher<View>.execute(\n    params: UltronEspressoActionParams? = null,\n    block: (uiController: UiController, view: View) -> T\n): T =\n    UltronEspressoInteraction(onView(this)).execute(params, block)\n\n//assertions\nfun Matcher<View>.isDisplayed() = UltronEspressoInteraction(onView(this)).isDisplayed()\nfun Matcher<View>.isNotDisplayed() = UltronEspressoInteraction(onView(this)).isNotDisplayed()\nfun Matcher<View>.exists() = UltronEspressoInteraction(onView(this)).exists()\nfun Matcher<View>.doesNotExist() = UltronEspressoInteraction(onView(this)).doesNotExist()\nfun Matcher<View>.isCompletelyDisplayed() =\n    UltronEspressoInteraction(onView(this)).isCompletelyDisplayed()\n\nfun Matcher<View>.isDisplayingAtLeast(percentage: Int) =\n    UltronEspressoInteraction(onView(this)).isDisplayingAtLeast(percentage)\n\nfun Matcher<View>.isEnabled() = UltronEspressoInteraction(onView(this)).isEnabled()\nfun Matcher<View>.isNotEnabled() = UltronEspressoInteraction(onView(this)).isNotEnabled()\nfun Matcher<View>.isSelected() = UltronEspressoInteraction(onView(this)).isSelected()\nfun Matcher<View>.isNotSelected() = UltronEspressoInteraction(onView(this)).isNotSelected()\nfun Matcher<View>.isClickable() = UltronEspressoInteraction(onView(this)).isClickable()\nfun Matcher<View>.isNotClickable() = UltronEspressoInteraction(onView(this)).isNotClickable()\nfun Matcher<View>.isChecked() = UltronEspressoInteraction(onView(this)).isChecked()\nfun Matcher<View>.isNotChecked() = UltronEspressoInteraction(onView(this)).isNotChecked()\nfun Matcher<View>.isFocusable() = UltronEspressoInteraction(onView(this)).isFocusable()\nfun Matcher<View>.isNotFocusable() = UltronEspressoInteraction(onView(this)).isNotFocusable()\nfun Matcher<View>.hasFocus() = UltronEspressoInteraction(onView(this)).hasFocus()\nfun Matcher<View>.isJavascriptEnabled() =\n    UltronEspressoInteraction(onView(this)).isJavascriptEnabled()\n\nfun Matcher<View>.hasText(text: String) = UltronEspressoInteraction(onView(this)).hasText(text)\nfun Matcher<View>.hasText(resourceId: Int) =\n    UltronEspressoInteraction(onView(this)).hasText(resourceId)\n\nfun Matcher<View>.hasText(stringMatcher: Matcher<String>) =\n    UltronEspressoInteraction(onView(this)).hasText(stringMatcher)\n\nfun Matcher<View>.textContains(text: String) =\n    UltronEspressoInteraction(onView(this)).textContains(text)\n\nfun Matcher<View>.hasContentDescription(text: String) =\n    UltronEspressoInteraction(onView(this)).hasContentDescription(text)\n\nfun Matcher<View>.hasContentDescription(resourceId: Int) =\n    UltronEspressoInteraction(onView(this)).hasContentDescription(resourceId)\n\nfun Matcher<View>.hasContentDescription(charSequenceMatcher: Matcher<CharSequence>) =\n    UltronEspressoInteraction(onView(this)).hasContentDescription(charSequenceMatcher)\n\nfun Matcher<View>.contentDescriptionContains(text: String) =\n    UltronEspressoInteraction(onView(this)).contentDescriptionContains(text)\n\nfun Matcher<View>.assertMatches(condition: Matcher<View>) =\n    UltronEspressoInteraction(onView(this)).assertMatches(condition)\n"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/extensions/PerfomOnViewExt.kt",
    "content": "package com.atiurin.ultron.extensions\n\nimport android.view.View\nimport androidx.test.espresso.DataInteraction\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.ViewInteraction\nimport com.atiurin.ultron.core.espresso.UltronEspressoInteraction\nimport com.atiurin.ultron.core.espresso.action.UltronEspressoActionParams\nimport com.atiurin.ultron.custom.espresso.action.CustomEspressoActionType.PERFORM_ON_VIEW\nimport com.atiurin.ultron.custom.espresso.action.CustomEspressoActionType.PERFORM_ON_VIEW_FORCIBLY\nimport com.atiurin.ultron.custom.espresso.action.getView\nimport com.atiurin.ultron.custom.espresso.base.UltronViewFinder\nimport com.atiurin.ultron.utils.runOnUiThread\nimport org.hamcrest.Matcher\n\n/**\n * Ultron is not responsible for the outcome of these actions.\n * If you use this approach, you clearly understand what you are doing.\n */\nfun View.performOnView(action: View.() -> Unit) {\n    runOnUiThread {\n        action(this)\n    }\n}\n\n/**\n * Perform action on view in ui thread with Espresso Idling Mechanism\n */\n@Deprecated(\"Use perform(params: UltronEspressoActionParams? = null, block: (uiController: UiController, view: View) -> Unit)\", ReplaceWith(\"perform(params, block\"))\nfun <T> UltronEspressoInteraction<T>.performOnView(action: View.() -> Unit) = perform(\n    UltronEspressoActionParams(\n        operationName =\"Perform on view with '${getInteractionMatcher()}' in root '${getInteractionRootMatcher()}'\",\n        operationDescription =\"${interaction.simpleClassName()} perform $action on view '$PERFORM_ON_VIEW' of '${getInteractionMatcher()}' with root '${getInteractionRootMatcher()}' during ${getActionTimeout()} ms\",\n        operationType = PERFORM_ON_VIEW\n    )\n) { _, view ->\n    view.performOnView(action)\n}\n\nfun <T> UltronEspressoInteraction<T>.performOnViewForcibly(action: View.() -> Unit) {\n    executeAction(\n        operationBlock = { UltronViewFinder(interaction).view.performOnView(action) },\n        name = \"Perform forcibly on view with '${getInteractionMatcher()}' in root '${getInteractionRootMatcher()}'\",\n        type = PERFORM_ON_VIEW_FORCIBLY,\n        description = \"${interaction.simpleClassName()} perform $action forcibly on view '$PERFORM_ON_VIEW_FORCIBLY' of '${getInteractionMatcher()}' with root '${getInteractionRootMatcher()}' during ${getActionTimeout()} ms\",\n    )\n}\n\n/**\n * Performs the given [action] on the view matched by this matcher on the MainThread.\n *\n * Obtaining of the view is based on the common espresso idle state mechanism.\n */\n@Deprecated(\"Use perform(params: UltronEspressoActionParams? = null, block: (uiController: UiController, view: View) -> Unit)\", ReplaceWith(\"perform(params, block\"))\nfun Matcher<View>.performOnView(action: View.() -> Unit) = UltronEspressoInteraction(onView(this)).performOnView(action)\n\n@Deprecated(\"Use perform(params: UltronEspressoActionParams? = null, block: (uiController: UiController, view: View) -> Unit)\", ReplaceWith(\"perform(params, block\"))\nfun ViewInteraction.performOnView(action: View.() -> Unit) = UltronEspressoInteraction(this).performOnView(action)\n\n@Deprecated(\"Use perform(params: UltronEspressoActionParams? = null, block: (uiController: UiController, view: View) -> Unit)\", ReplaceWith(\"perform(params, block\"))\nfun DataInteraction.performOnView(action: View.() -> Unit) = UltronEspressoInteraction(this).performOnView(action)\n\n/**\n * Performs the given [action] on the view matched by this matcher on the MainThread.\n *\n * The difference between `performOnViewForcibly()` and `performOnView()` is that in the first method,\n * obtaining of the view is not bound to the common espresso idle state mechanism.\n */\nfun Matcher<View>.performOnViewForcibly(action: View.() -> Unit) = UltronEspressoInteraction(onView(this)).performOnViewForcibly(action)\nfun ViewInteraction.performOnViewForcibly(action: View.() -> Unit) = UltronEspressoInteraction(this).performOnViewForcibly(action)\nfun DataInteraction.performOnViewForcibly(action: View.() -> Unit) = UltronEspressoInteraction(this).performOnViewForcibly(action)"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/extensions/RecyclerViewExt.kt",
    "content": "package com.atiurin.ultron.extensions\n\nimport androidx.recyclerview.widget.RecyclerView\nimport com.atiurin.ultron.utils.runOnUiThread\n\ninternal fun RecyclerView.instantScrollToPosition(\n    position: Int,\n    snapPreferredSide: Float = 0f\n) {\n    val adjustedPos = position.coerceIn(0, (adapter?.itemCount ?: 1) - 1)\n\n    runOnUiThread {\n        layoutManager?.scrollToPosition(adjustedPos)\n\n        post {\n            layoutManager?.apply {\n                findViewByPosition(adjustedPos)?.let { targetView ->\n                    val parentExtent = if (canScrollVertically()) height else width\n                    val childExtent =\n                        if (canScrollVertically()) targetView.height else targetView.width\n\n                    when {\n                        childExtent > parentExtent -> {\n                            if (canScrollVertically()) scrollBy(0, targetView.top)\n                            else scrollBy(targetView.left, 0)\n                        }\n\n                        else -> {\n                            val snapOffset = (parentExtent - childExtent) * snapPreferredSide\n                            if (canScrollVertically()) {\n                                scrollBy(0, targetView.top - snapOffset.toInt())\n                            } else {\n                                scrollBy(targetView.left - snapOffset.toInt(), 0)\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/extensions/ReflectionExt.kt",
    "content": "package com.atiurin.ultron.extensions\n\nimport android.view.View\nimport androidx.test.espresso.DataInteraction\nimport androidx.test.espresso.Root\nimport androidx.test.espresso.ViewInteraction\nimport androidx.test.espresso.base.ViewFinderImpl\nimport androidx.test.espresso.web.model.ElementReference\nimport androidx.test.espresso.web.sugar.Web\nimport androidx.test.uiautomator.BySelector\nimport androidx.test.uiautomator.UiObject2\nimport org.hamcrest.Matcher\nimport java.util.concurrent.atomic.AtomicReference\n\ninternal fun DataInteraction.getMatcher(propertyName: String): Matcher<View>? {\n    return this.getProperty(propertyName)\n}\n\ninternal fun DataInteraction.getRootMatcher(): Matcher<Root>? {\n    return this.getProperty(\"rootMatcher\")\n}\n\ninternal fun DataInteraction.getDataMatcher(): Matcher<View>? {\n    return this.getProperty(\"dataMatcher\")\n}\n\ninternal fun DataInteraction.getTargetMatcher(): Matcher<View>? {\n    return this.getMethodResult(\"makeTargetMatcher\")\n}\n\ninternal fun ViewInteraction.getMatcher(propertyName: String): Matcher<View>? {\n    return this.getProperty(propertyName)\n}\n\ninternal fun ViewInteraction.getViewMatcher(): Matcher<View>? {\n    return this.getProperty(\"viewMatcher\")\n}\n\ninternal fun ViewInteraction.getRootMatcherRef(): AtomicReference<Matcher<Root>>? {\n    return this.getProperty(\"rootMatcherRef\")\n}\n\ninternal fun ViewInteraction.getRootMatcher(): Matcher<Root>? {\n    return this.getRootMatcherRef()?.get()\n}\n\ninternal fun ViewInteraction.getViewFinder(): ViewFinderImpl? {\n    return this.getProperty(\"viewFinder\")\n}\n\ninternal fun <T> Web.WebInteraction<T>.getViewMatcher(): Matcher<View>? {\n    return this.getProperty(\"viewMatcher\")\n}\ninternal fun <T> Web.WebInteraction<T>.getElementReference(): ElementReference? {\n    return this.getProperty(\"element\")\n}\n\nfun UiObject2.getBySelector(): BySelector? = this.getProperty<BySelector>(\"mSelector\")\n\n"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/extensions/ViewExt.kt",
    "content": "package com.atiurin.ultron.extensions\n\nimport android.view.View\nimport androidx.test.espresso.util.TreeIterables\nimport org.hamcrest.Matcher\n\ninternal fun View.findChildView(matcher: Matcher<View>): View? {\n    var childView: View? = null\n    for (child in TreeIterables.breadthFirstViewTraversal(this)) {\n        if (matcher.matches(child)) {\n            childView = child\n            break\n        }\n    }\n    return childView\n}"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/extensions/ViewInteractionExt.kt",
    "content": "package com.atiurin.ultron.extensions\n\nimport android.view.View\nimport androidx.test.espresso.UiController\nimport androidx.test.espresso.ViewAction\nimport androidx.test.espresso.ViewInteraction\nimport androidx.test.espresso.action.EspressoKey\nimport androidx.test.espresso.matcher.RootMatchers.withDecorView\nimport com.atiurin.ultron.core.common.assertion.DefaultOperationAssertion\nimport com.atiurin.ultron.core.common.assertion.OperationAssertion\nimport com.atiurin.ultron.core.espresso.EspressoOperationResult\nimport com.atiurin.ultron.core.espresso.UltronEspressoInteraction\nimport com.atiurin.ultron.core.espresso.UltronEspressoOperation\nimport com.atiurin.ultron.core.espresso.action.UltronEspressoActionParams\nimport com.atiurin.ultron.custom.espresso.base.createRootViewPicker\nimport com.atiurin.ultron.listeners.setListenersState\nimport com.atiurin.ultron.utils.runOnUiThread\nimport org.hamcrest.CoreMatchers.`is`\nimport org.hamcrest.Matcher\n\nfun ViewInteraction.isSuccess(action: ViewInteraction.() -> Unit): Boolean =\n    runCatching { action() }.isSuccess\n\nfun ViewInteraction.withTimeout(timeoutMs: Long) =\n    UltronEspressoInteraction(this).withTimeout(timeoutMs)\n\nfun ViewInteraction.withResultHandler(resultHandler: (EspressoOperationResult<UltronEspressoOperation>) -> Unit) =\n    UltronEspressoInteraction(this).withResultHandler(resultHandler)\n\nfun ViewInteraction.withAssertion(assertion: OperationAssertion) =\n    UltronEspressoInteraction(this).withAssertion(assertion)\n\nfun ViewInteraction.withAssertion(\n    name: String = \"\",\n    isListened: Boolean = false,\n    block: () -> Unit\n) =\n    UltronEspressoInteraction(this).withAssertion(\n        DefaultOperationAssertion(\n            name,\n            block.setListenersState(isListened)\n        )\n    )\n\nfun ViewInteraction.withName(name: String) = UltronEspressoInteraction(this).withName(name)\nfun ViewInteraction.withMetaInfo(meta: Any) = UltronEspressoInteraction(this).withMetaInfo(meta)\n\n//root view searching\nfun ViewInteraction.withSuitableRoot(): ViewInteraction {\n    val viewMatcher: Matcher<View>? = this.getViewMatcher()\n    var decorView: View? = null\n    runOnUiThread { decorView = viewMatcher?.let { createRootViewPicker(it).get() } }\n    return when {\n        decorView != null -> {\n            this.inRoot(withDecorView(`is`(decorView)))\n        }\n\n        else -> {\n            this\n        }\n    }\n}\n\n//actions\nfun ViewInteraction.click() = UltronEspressoInteraction(this).click()\nfun ViewInteraction.doubleClick() = UltronEspressoInteraction(this).doubleClick()\nfun ViewInteraction.longClick() = UltronEspressoInteraction(this).longClick()\n\nfun ViewInteraction.clickTopLeft(offsetX: Int = 0, offsetY: Int = 0) =\n    UltronEspressoInteraction(this).clickTopLeft(offsetX, offsetY)\n\nfun ViewInteraction.clickTopCenter(offsetY: Int) =\n    UltronEspressoInteraction(this).clickTopCenter(offsetY)\n\nfun ViewInteraction.clickTopRight(offsetX: Int = 0, offsetY: Int = 0) =\n    UltronEspressoInteraction(this).clickTopRight(offsetX, offsetY)\n\nfun ViewInteraction.clickCenterRight(offsetX: Int = 0) =\n    UltronEspressoInteraction(this).clickCenterRight(offsetX)\n\nfun ViewInteraction.clickBottomRight(offsetX: Int = 0, offsetY: Int = 0) =\n    UltronEspressoInteraction(this).clickBottomRight(offsetX, offsetY)\n\nfun ViewInteraction.clickBottomCenter(offsetY: Int = 0) =\n    UltronEspressoInteraction(this).clickBottomCenter(offsetY)\n\nfun ViewInteraction.clickBottomLeft(offsetX: Int = 0, offsetY: Int = 0) =\n    UltronEspressoInteraction(this).clickBottomLeft(offsetX, offsetY)\n\nfun ViewInteraction.clickCenterLeft(offsetX: Int = 0) =\n    UltronEspressoInteraction(this).clickCenterLeft(offsetX)\n\nfun ViewInteraction.typeText(text: String) = UltronEspressoInteraction(this).typeText(text)\nfun ViewInteraction.replaceText(text: String) = UltronEspressoInteraction(this).replaceText(text)\nfun ViewInteraction.clearText() = UltronEspressoInteraction(this).clearText()\nfun ViewInteraction.pressKey(keyCode: Int) = UltronEspressoInteraction(this).pressKey(keyCode)\nfun ViewInteraction.pressKey(key: EspressoKey) = UltronEspressoInteraction(this).pressKey(key)\nfun ViewInteraction.closeSoftKeyboard() = UltronEspressoInteraction(this).closeSoftKeyboard()\nfun ViewInteraction.swipeLeft() = UltronEspressoInteraction(this).swipeLeft()\nfun ViewInteraction.swipeRight() = UltronEspressoInteraction(this).swipeRight()\nfun ViewInteraction.swipeUp() = UltronEspressoInteraction(this).swipeUp()\nfun ViewInteraction.swipeDown() = UltronEspressoInteraction(this).swipeDown()\nfun ViewInteraction.scrollTo() = UltronEspressoInteraction(this).scrollTo()\nfun ViewInteraction.perform(viewAction: ViewAction, description: String = \"\") =\n    UltronEspressoInteraction(this).perform(viewAction, description)\n\nfun ViewInteraction.perform(\n    params: UltronEspressoActionParams? = null,\n    block: (uiController: UiController, view: View) -> Unit\n) =\n    UltronEspressoInteraction(this).perform(params, block)\n\nfun <T> ViewInteraction.execute(\n    params: UltronEspressoActionParams? = null,\n    block: (uiController: UiController, view: View) -> T\n): T =\n    UltronEspressoInteraction(this).execute(params, block)\n\n//assertions\nfun ViewInteraction.isDisplayed() = UltronEspressoInteraction(this).isDisplayed()\nfun ViewInteraction.isNotDisplayed() = UltronEspressoInteraction(this).isNotDisplayed()\nfun ViewInteraction.exists() = UltronEspressoInteraction(this).exists()\nfun ViewInteraction.doesNotExist() = UltronEspressoInteraction(this).doesNotExist()\nfun ViewInteraction.isCompletelyDisplayed() =\n    UltronEspressoInteraction(this).isCompletelyDisplayed()\n\nfun ViewInteraction.isDisplayingAtLeast(percentage: Int) =\n    UltronEspressoInteraction(this).isDisplayingAtLeast(percentage)\n\nfun ViewInteraction.isEnabled() = UltronEspressoInteraction(this).isEnabled()\nfun ViewInteraction.isNotEnabled() = UltronEspressoInteraction(this).isNotEnabled()\nfun ViewInteraction.isSelected() = UltronEspressoInteraction(this).isSelected()\nfun ViewInteraction.isNotSelected() = UltronEspressoInteraction(this).isNotSelected()\nfun ViewInteraction.isClickable() = UltronEspressoInteraction(this).isClickable()\nfun ViewInteraction.isNotClickable() = UltronEspressoInteraction(this).isNotClickable()\nfun ViewInteraction.isChecked() = UltronEspressoInteraction(this).isChecked()\nfun ViewInteraction.isNotChecked() = UltronEspressoInteraction(this).isNotChecked()\nfun ViewInteraction.isFocusable() = UltronEspressoInteraction(this).isFocusable()\nfun ViewInteraction.isNotFocusable() = UltronEspressoInteraction(this).isNotFocusable()\nfun ViewInteraction.hasFocus() = UltronEspressoInteraction(this).hasFocus()\nfun ViewInteraction.isJavascriptEnabled() = UltronEspressoInteraction(this).isJavascriptEnabled()\nfun ViewInteraction.hasText(text: String) = UltronEspressoInteraction(this).hasText(text)\nfun ViewInteraction.hasText(resourceId: Int) = UltronEspressoInteraction(this).hasText(resourceId)\nfun ViewInteraction.hasText(stringMatcher: Matcher<String>) =\n    UltronEspressoInteraction(this).hasText(stringMatcher)\n\nfun ViewInteraction.textContains(text: String) = UltronEspressoInteraction(this).textContains(text)\nfun ViewInteraction.hasContentDescription(text: String) =\n    UltronEspressoInteraction(this).hasContentDescription(text)\n\nfun ViewInteraction.hasContentDescription(resourceId: Int) =\n    UltronEspressoInteraction(this).hasContentDescription(resourceId)\n\nfun ViewInteraction.hasContentDescription(charSequenceMatcher: Matcher<CharSequence>) =\n    UltronEspressoInteraction(this).hasContentDescription(charSequenceMatcher)\n\nfun ViewInteraction.contentDescriptionContains(text: String) =\n    UltronEspressoInteraction(this).contentDescriptionContains(text)\n\nfun ViewInteraction.assertMatches(condition: Matcher<View>) =\n    UltronEspressoInteraction(this).assertMatches(condition)\n"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/utils/ViewGroupUtils.kt",
    "content": "package com.atiurin.ultron.utils\n\nimport android.view.View\nimport android.view.ViewGroup\n\n/** Performs the given action on each view in this view group. */\ninline fun ViewGroup.forEach(action: (view: View) -> Unit) {\n    for (index in 0 until childCount) {\n        action(getChildAt(index))\n    }\n}"
  },
  {
    "path": "ultron-android/src/main/kotlin/com/atiurin/ultron/utils/ViewUtils.kt",
    "content": "package com.atiurin.ultron.utils\n\nimport android.view.View\nimport android.view.ViewGroup\n\n/**\n * Returns a [Sequence] over this view and its descendants recursively.\n * This is a depth-first traversal similar to [View.findViewById].\n * A view with no children will return a single-element sequence of itself.\n */\nval View.allViews: Sequence<View>\n    get() = sequence {\n        yield(this@allViews)\n        if (this@allViews is ViewGroup) {\n            yieldAll(this@allViews.descendants)\n        }\n    }\n\n/**\n * Returns a [Sequence] over the child views in this view group recursively.\n * This performs a depth-first traversal.\n * A view with no children will return a zero-element sequence.\n */\nval ViewGroup.descendants: Sequence<View>\n    get() = sequence {\n        forEach { child ->\n            yield(child)\n            if (child is ViewGroup) {\n                yieldAll(child.descendants)\n            }\n        }\n    }\n\n/**\n * Returns true when this view's visibility is [View.VISIBLE], false otherwise.\n *\n * ```\n * if (view.isVisible) {\n *     // Behavior...\n * }\n * ```\n *\n * Setting this property to true sets the visibility to [View.VISIBLE], false to [View.GONE].\n *\n * ```\n * view.isVisible = true\n * ```\n */\ninline var View.isVisible: Boolean\n    get() = visibility == View.VISIBLE\n    set(value) {\n        visibility = if (value) View.VISIBLE else View.GONE\n    }\n"
  },
  {
    "path": "ultron-android/src/main/res/values/strings.xml",
    "content": "<resources>\n    <string name=\"app_name\">EspressoPageObject</string>\n</resources>\n"
  },
  {
    "path": "ultron-android/src/test/java/com/atiurin/ultron/ExampleUnitTest.java",
    "content": "package com.atiurin.ultron;\n\nimport org.junit.Test;\n\nimport static org.junit.Assert.*;\n\n/**\n * Example local unit test, which will execute on the development machine (host).\n *\n * @see <a href=\"http://d.android.com/tools/testing\">Testing documentation</a>\n */\npublic class ExampleUnitTest {\n    @Test\n    public void addition_isCorrect() {\n        assertEquals(4, 2 + 2);\n    }\n}"
  },
  {
    "path": "ultron-common/.gitignore",
    "content": "/build"
  },
  {
    "path": "ultron-common/build.gradle.kts",
    "content": "import com.vanniktech.maven.publish.SonatypeHost\nimport org.jetbrains.kotlin.gradle.dsl.JvmTarget\nimport org.jetbrains.kotlin.gradle.ExperimentalWasmDsl\n\nplugins {\n    alias(libs.plugins.kotlinMultiplatform)\n    alias(libs.plugins.androidLibrary)\n    alias(libs.plugins.vanniktech.mavenPublish)\n    id(\"org.jetbrains.dokka\")\n}\n\ngroup = project.findProperty(\"GROUP\")!!\nversion = project.findProperty(\"VERSION_NAME\")!!\n\nkotlin {\n    compilerOptions {\n        apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_1)\n    }\n    androidTarget {\n        publishLibraryVariants(\"release\")\n        compilerOptions {\n            jvmTarget.set(JvmTarget.JVM_11)\n        }\n    }\n    jvm(\"desktop\")\n    macosX64()\n    macosArm64()\n    iosX64()\n    iosArm64()\n    iosSimulatorArm64()\n    @OptIn(ExperimentalWasmDsl::class)\n    wasmJs() {\n        browser()\n        nodejs()\n    }\n    js(IR) {\n        browser()\n        nodejs()\n    }\n    sourceSets {\n        applyDefaultHierarchyTemplate()\n        val commonMain by getting {\n            dependencies {\n                implementation(libs.okio)\n                implementation(libs.kotlinx.datetime)\n                implementation(libs.atomicfu)\n                implementation(libs.kotlin.test)\n                implementation(libs.kotlinx.coroutines.core)\n            }\n        }\n        val androidMain by getting {\n            dependencies {\n                implementation(kotlin(\"stdlib-jdk8\"))\n                implementation(libs.androidx.monitor)\n                implementation(libs.androidx.lifecycle.common.jvm)\n                implementation(libs.androidx.core.ktx)\n                api(Libs.uiautomator)\n                api(Libs.junit)\n            }\n        }\n        val shared by creating {\n            dependsOn(commonMain)\n            dependencies {\n                api(libs.kotlinx.coroutines.core)\n            }\n        }\n        // desktop\n        jvmMain {\n            dependsOn(shared)\n            dependencies {\n                api(kotlin(\"stdlib\"))\n            }\n        }\n        val desktopMain by getting {\n            dependsOn(shared)\n            dependsOn(jvmMain.get())\n        }\n        // native\n        val nativeMain by getting { dependsOn(shared) }\n        // js\n        val jsWasmMain by creating {\n            dependsOn(shared)\n        }\n        val jsMain by getting {\n            dependsOn(jsWasmMain)\n            dependencies {\n                implementation(kotlin(\"stdlib-js\"))\n            }\n        }\n        val wasmJsMain by getting {\n            dependsOn(jsWasmMain)\n            dependencies {\n                implementation(kotlin(\"stdlib\"))\n            }\n        }\n    }\n}\n\nandroid {\n    namespace = \"com.atiurin.ultron.common\"\n    compileSdk = 35\n    defaultConfig {\n        minSdk = 21\n    }\n    compileOptions {\n        sourceCompatibility = JavaVersion.VERSION_11\n        targetCompatibility = JavaVersion.VERSION_11\n    }\n}\n\nval dokkaOutputDir = layout.buildDirectory.dir(\"dokka\").get().asFile\n\ntasks.dokkaHtml.configure {\n    outputDirectory.set(file(dokkaOutputDir))\n}\n\nval deleteDokkaOutputDir by tasks.register<Delete>(\"deleteDokkaOutputDirectory\") {\n    delete(dokkaOutputDir)\n}\n\nval ultronComposeJavadocJar by tasks.registering(Jar::class) {\n    archiveClassifier.set(\"javadoc\")\n    duplicatesStrategy = DuplicatesStrategy.EXCLUDE\n    dependsOn(deleteDokkaOutputDir, tasks.dokkaHtml)\n    from(dokkaOutputDir)\n}\n\nmavenPublishing {\n    publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)\n    signAllPublications()\n    coordinates(artifactId = \"ultron-common\")\n\n    pom {\n        name = \"Ultron Common\"\n        description = \"Android & Compose Multiplatform UI testing framework\"\n        url = \"https://github.com/open-tool/ultron\"\n        inceptionYear = \"2021\"\n\n        licenses {\n            license {\n                name.set(\"The Apache License, Version 2.0\")\n                url.set(\"http://www.apache.org/licenses/LICENSE-2.0.txt\")\n            }\n        }\n\n        issueManagement {\n            system = \"GitHub Issues\"\n            url = \"https://github.com/open-tool/ultron/issues\"\n        }\n\n        developers {\n            developer {\n                id = \"alex-tiurin\"\n                name = \"Aleksei Tiurin\"\n                url = \"https://github.com/open-tool\"\n            }\n        }\n\n        scm {\n            url = \"https://github.com/open-tool/ultron\"\n            connection = \"scm:git@github.com:open-tool/ultron.git\"\n            developerConnection = \"scm:git@github.com:open-tool/ultron.git\"\n        }\n    }\n}\n\n\n"
  },
  {
    "path": "ultron-common/gradle.properties",
    "content": "GROUP=com.atiurin\nPOM_ARTIFACT_ID=ultron-common\n\nPOM_NAME=ultron-common\nPOM_PACKAGING=aar\n\nPOM_DESCRIPTION=Ultron UI testing framework core library\nPOM_INCEPTION_YEAR=2024\n"
  },
  {
    "path": "ultron-common/src/androidMain/kotlin/com/atiurin/ultron/core/config/UltronAndroidCommonConfig.kt",
    "content": "package com.atiurin.ultron.core.config\n\nimport com.atiurin.ultron.testlifecycle.setupteardown.ConditionExecutorWrapper\nimport com.atiurin.ultron.testlifecycle.setupteardown.ConditionsExecutor\nimport com.atiurin.ultron.testlifecycle.setupteardown.DefaultConditionExecutorWrapper\nimport com.atiurin.ultron.testlifecycle.setupteardown.DefaultConditionsExecutor\n\nobject UltronAndroidCommonConfig {\n    class Conditions {\n        companion object {\n            var conditionExecutorWrapper: ConditionExecutorWrapper = DefaultConditionExecutorWrapper()\n            var conditionsExecutor: ConditionsExecutor = DefaultConditionsExecutor()\n        }\n    }\n}"
  },
  {
    "path": "ultron-common/src/androidMain/kotlin/com/atiurin/ultron/extensions/AnyExt.android.kt",
    "content": "package com.atiurin.ultron.extensions\n\ninline fun <reified T> Any.getProperty(propertyName: String): T? {\n    return try {\n        val property = this.javaClass.getDeclaredField(propertyName)\n        property.isAccessible = true\n        property.get(this) as T\n    } catch (ex: Throwable) { null }\n}\n\ninline fun <reified T> Any.getMethodResult(methodName: String, vararg args: Any?): T? {\n    return try {\n        val method = this.javaClass.getDeclaredMethod(methodName)\n        method.isAccessible = true\n        method.invoke(this, *args) as T\n    } catch (ex: Throwable) { null }\n}\n\nfun Class<*>.isAssignedFrom(klasses: List<Class<*>>): Boolean{\n    klasses.forEach {\n        if (it.isAssignableFrom(this)) return true\n    }\n    return false\n}"
  },
  {
    "path": "ultron-common/src/androidMain/kotlin/com/atiurin/ultron/extensions/BundleExt.kt",
    "content": "package com.atiurin.ultron.extensions\n\nimport android.os.Bundle\n\nfun Bundle.putArguments(key: String, vararg values: CharSequence) {\n    val arguments: String = listOfNotNull(\n        getCharSequence(key),\n        *values\n    ).joinToString(separator = \",\")\n    putCharSequence(key, arguments)\n}"
  },
  {
    "path": "ultron-common/src/androidMain/kotlin/com/atiurin/ultron/extensions/DescriptionExt.kt",
    "content": "package com.atiurin.ultron.extensions\n\nimport org.junit.runner.Description\n\nfun Description.fullTestName() = \"'${this.className}.${this.methodName}'\""
  },
  {
    "path": "ultron-common/src/androidMain/kotlin/com/atiurin/ultron/extensions/FileExt.android.kt",
    "content": "package com.atiurin.ultron.extensions\n\nimport com.atiurin.ultron.log.UltronLog\nimport java.io.File\nimport java.io.PrintWriter\n\nfun File.clearContent() {\n    PrintWriter(this).apply {\n        print(\"\")\n        close()\n    }\n}\n\nfun File.createDirectoryIfNotExists() {\n    if (!exists()) {\n        val result = mkdirs()\n        if (!result) UltronLog.error(\"Unable to create directory '${this.absolutePath}'\")\n    }\n}"
  },
  {
    "path": "ultron-common/src/androidMain/kotlin/com/atiurin/ultron/hierarchy/HierarchyDumpResult.kt",
    "content": "package com.atiurin.ultron.hierarchy\n\nimport com.atiurin.ultron.file.MimeType\nimport java.io.File\n\ndata class HierarchyDumpResult(\n    val isSuccess: Boolean,\n    val file: File,\n    val mimeType: MimeType = MimeType.XML\n)\n"
  },
  {
    "path": "ultron-common/src/androidMain/kotlin/com/atiurin/ultron/hierarchy/HierarchyDumper.kt",
    "content": "package com.atiurin.ultron.hierarchy\n\nimport java.io.File\n\ninterface HierarchyDumper {\n    fun dumpFullWindowHierarchy(file: File): HierarchyDumpResult\n}"
  },
  {
    "path": "ultron-common/src/androidMain/kotlin/com/atiurin/ultron/hierarchy/UiDeviceHierarchyDumper.kt",
    "content": "package com.atiurin.ultron.hierarchy\n\nimport androidx.test.platform.app.InstrumentationRegistry\nimport androidx.test.uiautomator.UiDevice\nimport com.atiurin.ultron.log.UltronLog\nimport java.io.File\n\nclass UiDeviceHierarchyDumper : HierarchyDumper {\n    private val uiDevice: UiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())\n    override fun dumpFullWindowHierarchy(file: File): HierarchyDumpResult {\n        var isSuccess = false\n        runCatching {\n            uiDevice.dumpWindowHierarchy(file)\n        }.onFailure {\n            UltronLog.error(\"Couldn't dump window hierarchy. ${it.message}\")\n        }.onSuccess {\n            UltronLog.debug(\"Window hierarchy is dumped to ${file.absolutePath}\")\n            isSuccess = true\n        }\n        return HierarchyDumpResult(isSuccess, file)\n    }\n}"
  },
  {
    "path": "ultron-common/src/androidMain/kotlin/com/atiurin/ultron/log/UltronFileLoggerImpl.android.kt",
    "content": "package com.atiurin.ultron.log\n\nimport android.os.Build\nimport com.atiurin.ultron.core.config.UltronCommonConfig\nimport com.atiurin.ultron.extensions.clearContent\nimport com.atiurin.ultron.utils.createCacheFile\nimport java.io.File\nimport java.text.SimpleDateFormat\nimport java.time.LocalDateTime\nimport java.time.format.DateTimeFormatter\nimport java.util.Date\nimport java.util.Locale\n\nclass UltronFileLoggerImpl: UltronFileLogger() {\n    private var file: File = createCacheFile(\"ultron_\", \".log\")\n\n    override fun getLogFilePath(): String = file.absolutePath\n    override fun clearFile() = file.clearContent()\n\n    override fun info(message: String) = append(LogLevel.I, message)\n    override fun info(message: String, throwable: Throwable) = append(LogLevel.I, message)\n    override fun debug(message: String) = append(LogLevel.D, message)\n    override fun debug(message: String, throwable: Throwable) = append(LogLevel.D, message)\n    override fun warn(message: String) = append(LogLevel.W, message)\n    override fun warn(message: String, throwable: Throwable) = append(LogLevel.W, message)\n    override fun error(message: String) = append(LogLevel.E, message)\n    override fun error(message: String, throwable: Throwable) = append(LogLevel.E, message)\n\n    private fun append(level: LogLevel, text: String, throwable: Throwable? = null) {\n        val throwableText = if (throwable == null) \"\" else \"${throwable.message}, cause ${throwable.cause}\\n\"\n        file.appendLine(\"${getTime()} $level: $text\\n$throwableText\")\n    }\n\n    private fun File.appendLine(text: String) = this.appendText(text)\n\n    private fun getTime(): String {\n        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n            val current = LocalDateTime.now()\n            current.format(DateTimeFormatter.ofPattern(UltronCommonConfig.logDateFormat))\n        } else {\n            SimpleDateFormat(UltronCommonConfig.logDateFormat, Locale.getDefault()).format(Date())\n        }\n    }\n\n}"
  },
  {
    "path": "ultron-common/src/androidMain/kotlin/com/atiurin/ultron/log/UltronLog.android.kt",
    "content": "package com.atiurin.ultron.log\n\nactual fun getFileLogger(): UltronFileLogger = UltronFileLoggerImpl()"
  },
  {
    "path": "ultron-common/src/androidMain/kotlin/com/atiurin/ultron/log/UltronLogcatLogger.android.kt",
    "content": "package com.atiurin.ultron.log\n\nimport android.util.Log\n\nclass UltronLogcatLogger : ULogger(){\n    companion object {\n        const val LOG_TAG = \"Ultron\"\n    }\n    override fun info(message: String) = Log.i(LOG_TAG, message)\n    override fun info(message: String, throwable: Throwable) = Log.i(LOG_TAG, message, throwable)\n    override fun debug(message: String) = Log.d(LOG_TAG, message)\n    override fun debug(message: String, throwable: Throwable) = Log.d(LOG_TAG, message, throwable)\n    override fun warn(message: String) = Log.w(LOG_TAG, message)\n    override fun warn(message: String, throwable: Throwable) = Log.w(LOG_TAG, message, throwable)\n    override fun error(message: String) = Log.e(LOG_TAG, message)\n    override fun error(message: String, throwable: Throwable) = Log.e(LOG_TAG, message, throwable)\n}"
  },
  {
    "path": "ultron-common/src/androidMain/kotlin/com/atiurin/ultron/runner/RunListener.kt",
    "content": "package com.atiurin.ultron.runner\n\nimport org.junit.runner.Description\nimport org.junit.runner.Result\nimport org.junit.runner.notification.Failure\n\ninterface RunListener {\n    /**\n     * Called before any tests have been run. This may be called on an\n     * arbitrary thread.\n     *\n     * @param description describes the tests to be run\n     */\n    fun testRunStarted(description: Description) = Unit\n\n    /**\n     * Called when all tests have finished. This may be called on an\n     * arbitrary thread.\n     *\n     * @param result the summary of the test run, including all the tests that failed\n     */\n    fun testRunFinished(result: Result) = Unit\n\n    /**\n     * Called when a test suite is about to be started. If this method is\n     * called for a given [Description], then [.testSuiteFinished]\n     * will also be called for the same `Description`.\n     *\n     *\n     * Note that not all runners will call this method, so runners should\n     * be prepared to handle [.testStarted] calls for tests\n     * where there was no corresponding `testSuiteStarted()` call for\n     * the parent `Description`.\n     *\n     * @param description the description of the test suite that is about to be run\n     * (generally a class name)\n     */\n    fun testSuiteStarted(description: Description) = Unit\n\n    /**\n     * Called when a test suite has finished, whether the test suite succeeds or fails.\n     * This method will not be called for a given [Description] unless\n     * [.testSuiteStarted] was called for the same @code Description}.\n     *\n     * @param description the description of the test suite that just ran\n     */\n    fun testSuiteFinished(description: Description) = Unit\n\n    /**\n     * Called when an atomic test is about to be started.\n     *\n     * @param description the description of the test that is about to be run\n     * (generally a class and method name)\n     */\n    fun testStarted(description: Description) = Unit\n\n    /**\n     * Called when an atomic test has finished, whether the test succeeds or fails.\n     *\n     * @param description the description of the test that just ran\n     */\n    fun testFinished(description: Description) = Unit\n\n    /**\n     * Called when an atomic test fails, or when a listener throws an exception.\n     *\n     *\n     * In the case of a failure of an atomic test, this method will be called\n     * with the same `Description` passed to\n     * [.testStarted], from the same thread that called\n     * [.testStarted].\n     *\n     *\n     * In the case of a listener throwing an exception, this will be called with\n     * a `Description` of [Description.TEST_MECHANISM], and may be called\n     * on an arbitrary thread.\n     *\n     * @param failure describes the test that failed and the exception that was thrown\n     */\n    fun testFailure(failure: Failure) = Unit\n\n    /**\n     * Called when an atomic test flags that it assumes a condition that is\n     * false\n     *\n     * @param failure describes the test that failed and the\n     * [org.junit.AssumptionViolatedException] that was thrown\n     */\n    fun testAssumptionFailure(failure: Failure) = Unit\n\n    /**\n     * Called when a test will not be run, generally because a test method is annotated\n     * with [org.junit.Ignore].\n     *\n     * @param description describes the test that will not be run\n     */\n    fun testIgnored(description: Description) = Unit\n}"
  },
  {
    "path": "ultron-common/src/androidMain/kotlin/com/atiurin/ultron/runner/UltronLogRunListener.kt",
    "content": "package com.atiurin.ultron.runner\n\nimport com.atiurin.ultron.extensions.fullTestName\nimport com.atiurin.ultron.log.LogLevel\nimport com.atiurin.ultron.log.UltronLogUtil.logTextBlock\nimport org.junit.runner.Description\nimport org.junit.runner.Result\nimport org.junit.runner.notification.Failure\n\nclass UltronLogRunListener : UltronRunListener() {\n    override fun testRunStarted(description: Description) {\n        logTextBlock(\"TEST RUN STARTED\")\n    }\n\n    override fun testStarted(description: Description) {\n        logTextBlock(\"TEST ${description.fullTestName()} STARTED\")\n    }\n\n    override fun testFinished(description: Description) {\n        logTextBlock(\"TEST ${description.fullTestName()} FINISHED\")\n    }\n\n    override fun testFailure(failure: Failure) {\n        logTextBlock(\n            logLevel = LogLevel.E,\n            text = \"\"\" |TEST ${failure.description.fullTestName()} FAILED with exception:\n            |Message: ${failure.exception.message},\n            |Cause:  ${failure.exception.cause}\n            |Stacktrace: ${failure.exception.stackTrace.joinToString(\"\\n\")}\n            \"\"\".trimMargin()\n        )\n    }\n\n    override fun testAssumptionFailure(failure: Failure) {\n        logTextBlock(logLevel = LogLevel.E, text = \"TEST ${failure.description.fullTestName()} ASSUMPTION FAILURE\")\n    }\n\n    override fun testIgnored(description: Description) {\n        logTextBlock(\"TEST ${description.fullTestName()} IGNORED\")\n    }\n\n    override fun testRunFinished(result: Result) {\n        logTextBlock(\n            \"\"\"\n            |TEST RUN FINISHED ${if (result.wasSuccessful()) \"SUCCESSFULLY\" else \"with FAILURE\"}\n            |Duration: ${result.runTime} ms\n            |Tests count: ${result.runCount}\n            |Ignored: ${result.ignoreCount}\n            |Failed: ${result.failureCount}\n            \"\"\".trimMargin()\n        )\n    }\n}"
  },
  {
    "path": "ultron-common/src/androidMain/kotlin/com/atiurin/ultron/runner/UltronRunInformer.kt",
    "content": "package com.atiurin.ultron.runner\n\nimport com.atiurin.ultron.listeners.AbstractListenersContainer\nimport org.junit.runner.Description\nimport org.junit.runner.Result\nimport org.junit.runner.notification.Failure\n\nabstract class UltronRunInformer : AbstractListenersContainer<UltronRunListener>(), RunListener {\n    override fun testRunStarted(description: Description) = getListeners().forEach { it.testRunStarted(description) }\n    override fun testStarted(description: Description) = getListeners().forEach { it.testStarted(description) }\n    override fun testFinished(description: Description) = getListeners().forEach { it.testFinished(description) }\n    override fun testFailure(failure: Failure) {\n        getListeners().forEach { it.testFailure(failure) }\n    }\n    override fun testAssumptionFailure(failure: Failure) = getListeners().forEach { it.testAssumptionFailure(failure) }\n    override fun testIgnored(description: Description) = getListeners().forEach { it.testIgnored(description) }\n    override fun testRunFinished(result: Result) = getListeners().forEach { it.testRunFinished(result) }\n}"
  },
  {
    "path": "ultron-common/src/androidMain/kotlin/com/atiurin/ultron/runner/UltronRunListener.kt",
    "content": "package com.atiurin.ultron.runner\n\nimport com.atiurin.ultron.listeners.AbstractListener\n\nabstract class UltronRunListener: RunListener, AbstractListener()"
  },
  {
    "path": "ultron-common/src/androidMain/kotlin/com/atiurin/ultron/screenshot/ScreenshotResult.kt",
    "content": "package com.atiurin.ultron.screenshot\n\nimport com.atiurin.ultron.file.MimeType\nimport java.io.File\n\ndata class ScreenshotResult(\n    val isSuccess: Boolean,\n    val file: File,\n    val mimeType: MimeType = MimeType.JPEG\n)\n"
  },
  {
    "path": "ultron-common/src/androidMain/kotlin/com/atiurin/ultron/screenshot/Screenshoter.kt",
    "content": "package com.atiurin.ultron.screenshot\n\nimport java.io.File\n\ninterface Screenshoter {\n    fun takeFullScreenShot(file: File): ScreenshotResult\n}"
  },
  {
    "path": "ultron-common/src/androidMain/kotlin/com/atiurin/ultron/screenshot/UiAutomationScreenshoter.kt",
    "content": "package com.atiurin.ultron.screenshot\n\nimport android.annotation.SuppressLint\nimport android.app.Instrumentation\nimport android.app.UiAutomation\nimport android.graphics.Bitmap\nimport android.os.Build\nimport androidx.test.platform.app.InstrumentationRegistry\nimport androidx.test.uiautomator.Configurator\nimport com.atiurin.ultron.exceptions.UltronException\nimport com.atiurin.ultron.file.MimeType\nimport com.atiurin.ultron.log.UltronLog\nimport com.atiurin.ultron.log.UltronLog.debug\nimport java.io.BufferedOutputStream\nimport java.io.File\nimport java.io.FileOutputStream\nimport java.io.IOException\n\nclass UiAutomationScreenshoter(private val quality: Int): Screenshoter {\n    val mimeType = MimeType.JPEG\n\n    @SuppressLint(\"NewApi\")\n    override fun takeFullScreenShot(file: File): ScreenshotResult {\n        val screenshot: Bitmap =\n            getUiAutomation(InstrumentationRegistry.getInstrumentation()).takeScreenshot()\n                ?: return ScreenshotResult(false, file)\n        var bos: BufferedOutputStream? = null\n        try {\n            bos = BufferedOutputStream(FileOutputStream(file))\n            screenshot.compress(Bitmap.CompressFormat.JPEG, quality, bos)\n            bos.flush()\n        } catch (ioe: IOException) {\n            UltronLog.error(\"failed to save screen shot to file\")\n            return ScreenshotResult(false, file, mimeType)\n        } finally {\n            if (bos != null) {\n                try {\n                    bos.close()\n                } catch (ioe: IOException) {\n                    // Ignore\n                }\n            }\n            screenshot.recycle()\n        }\n        return ScreenshotResult(true, file, mimeType)\n    }\n\n    /**\n     * Code from the source code of UiDevice + improvements\n     */\n    fun getUiAutomation(instrumentation: Instrumentation): UiAutomation {\n        val flags = Configurator.getInstance().uiAutomationFlags\n        return if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N && !isReleaseBuild) {\n            instrumentation.getUiAutomation(flags)\n        } else {\n            // Custom flags not supported prior to N.\n            if (flags != DEFAULT_UIAUTOMATION_FLAGS) {\n                debug(\"UiAutomation flags not supported prior to N - ignoring.\")\n            }\n            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {\n                instrumentation.uiAutomation\n            } else {\n                throw UltronException(\"Screenshot not supported for SDK lower ${Build.VERSION_CODES.JELLY_BEAN_MR2}\")\n            }\n        }\n    }\n\n    companion object {\n        const val DEFAULT_UIAUTOMATION_FLAGS = 0\n        val isReleaseBuild = \"REL\" == Build.VERSION.CODENAME\n    }\n\n}"
  },
  {
    "path": "ultron-common/src/androidMain/kotlin/com/atiurin/ultron/screenshot/ViewScreenshoter.kt",
    "content": "package com.atiurin.ultron.screenshot\n\nimport android.annotation.SuppressLint\nimport android.app.Activity\nimport android.graphics.Bitmap\nimport android.graphics.Canvas\nimport android.view.View\nimport com.atiurin.ultron.exceptions.UltronException\nimport com.atiurin.ultron.file.MimeType\nimport com.atiurin.ultron.utils.ActivityUtil.getResumedActivity\nimport com.atiurin.ultron.utils.createCacheFile\nimport com.atiurin.ultron.utils.runOnUiThread\nimport java.io.BufferedOutputStream\nimport java.io.File\nimport java.io.FileOutputStream\n\nclass ViewScreenshoter(private val quality: Int = 90) : Screenshoter {\n    val mimeType = MimeType.JPEG\n\n    @SuppressLint(\"SetWorldReadable\")\n    override fun takeFullScreenShot(file: File): ScreenshotResult {\n        val activity = getResumedActivity() ?: throw UltronException(\"There is no resumed activity.\")\n        val bitmap = drawActivityBitmap(activity)\n        writeBitmapToFile(file, bitmap)\n        return ScreenshotResult(true, file, mimeType)\n    }\n\n    fun takeViewScreenshot(view: View): ScreenshotResult {\n        val tempFile = createCacheFile()\n        val bitmap = drawViewBitmap(view)\n        writeBitmapToFile(tempFile, bitmap)\n        return ScreenshotResult(true, tempFile, mimeType)\n    }\n\n    @SuppressLint(\"SetWorldReadable\")\n    private fun writeBitmapToFile(file: File, bitmap: Bitmap){\n        BufferedOutputStream(FileOutputStream(file)).use { outputStream ->\n            bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)\n            file.setReadable(true, false)\n        }\n        bitmap.recycle()\n    }\n\n    private fun drawActivityBitmap(activity: Activity): Bitmap = drawBitmap { activity.drawToBitmap() }\n    private fun drawViewBitmap(view: View): Bitmap = drawBitmap { view.drawToBitmap() }\n\n    private fun drawBitmap(block: () -> Bitmap): Bitmap{\n        val result = runCatching {\n            runOnUiThread {\n                block()\n            }\n        }.onFailure { ex -> throw UltronException(\"Take screenshot failed due to ${ex.message}\") }\n        return result.getOrThrow()\n    }\n\n    private fun View.drawToBitmap(): Bitmap {\n        if (this.width == 0 || this.height == 0) {\n            throw UltronException(\n                \"Invalid ${this.javaClass.simpleName} size. It has zero height or width.\"\n            )\n        }\n        val bitmap = Bitmap.createBitmap(this.width, this.height, Bitmap.Config.ARGB_8888)\n        val bitmapHolder = Canvas(bitmap)\n        this.draw(bitmapHolder)\n        return bitmap\n    }\n\n    private fun Activity.drawToBitmap(): Bitmap = window.decorView.drawToBitmap()\n}\n\n"
  },
  {
    "path": "ultron-common/src/androidMain/kotlin/com/atiurin/ultron/testlifecycle/activity/UltronActivityRule.kt",
    "content": "package com.atiurin.ultron.testlifecycle.activity\n\nimport android.app.Activity\nimport androidx.annotation.RestrictTo\nimport com.atiurin.ultron.core.config.UltronAndroidCommonConfig\nimport org.junit.rules.ExternalResource\nimport com.atiurin.ultron.testlifecycle.setupteardown.Condition\n\n\n/**\n * reduced copy of the [androidx.test.ext.junit.rules.ActivityScenarioRule]\n * @param activityClass an activity class to launch\n * Differences:\n * 1. has only one constructor (launch by activityClass)\n * 2. has allure step for setup and teardown\n * 3. finish activity does not await idle state\n * 4. finish all activities in RESUMED, PAUSED and STOPPED stage\n */\nopen class UltronActivityRule<A : Activity?>(\n    activityClass: Class<A>\n) : ExternalResource() {\n    private var scenarioSupplier: Supplier<UltronActivityScenario<A>>\n    private var scenario: UltronActivityScenario<A>? = null\n\n    init {\n        scenarioSupplier = Supplier {\n            UltronActivityScenario.launch(activityClass)\n        }\n    }\n\n    /**\n     * Same as [java.util.function.Supplier] which requires API level 24.\n     *\n     * @hide\n     */\n    @RestrictTo(RestrictTo.Scope.LIBRARY)\n    internal fun interface Supplier<T> {\n        fun get(): T\n    }\n\n    fun launchActivity() {\n        scenario = scenarioSupplier.get()\n    }\n\n    @Throws(Throwable::class)\n    override fun before() {\n        val condition = Condition(\n            counter = -1,\n            key = \"LAUNCH ACTIVITY\",\n            name = \"SetUp 'launch activity'\",\n            actions = { launchActivity() }\n        )\n        UltronAndroidCommonConfig.Conditions.conditionExecutorWrapper.execute(condition)\n    }\n\n    override fun after() {\n        val condition = Condition(\n            counter = -1,\n            key = \"FINISH ACTIVITY\",\n            name = \"TearDown 'finish activity'\",\n            actions = { scenario!!.close() }\n        )\n        UltronAndroidCommonConfig.Conditions.conditionExecutorWrapper.execute(condition)\n    }\n\n    /**\n     * Returns [UltronActivityScenario] of the given activity class.\n     *\n     * @throws NullPointerException if you call this method while test is not running\n     * @return a non-null [UltronActivityScenario] instance\n     */\n    fun getScenario(): UltronActivityScenario<A> {\n        return scenario ?: throw IllegalStateException(\"scenario not initialized\")\n    }\n}"
  },
  {
    "path": "ultron-common/src/androidMain/kotlin/com/atiurin/ultron/testlifecycle/activity/UltronActivityScenario.kt",
    "content": "package com.atiurin.ultron.testlifecycle.activity\n\nimport android.annotation.SuppressLint\nimport android.app.Activity\nimport android.app.Instrumentation\nimport android.content.Intent\nimport android.os.Build\nimport android.os.Bundle\nimport android.os.Looper\nimport android.os.SystemClock\nimport android.provider.Settings\nimport android.util.Log\nimport androidx.annotation.GuardedBy\nimport androidx.lifecycle.Lifecycle.Event\nimport androidx.lifecycle.Lifecycle.State\nimport androidx.test.internal.platform.ServiceLoaderWrapper\nimport androidx.test.internal.platform.app.ActivityInvoker\nimport androidx.test.internal.platform.os.ControlledLooper\nimport androidx.test.internal.util.Checks\nimport androidx.test.platform.app.InstrumentationRegistry\nimport androidx.test.runner.lifecycle.ActivityLifecycleCallback\nimport androidx.test.runner.lifecycle.ActivityLifecycleMonitor\nimport androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry\nimport androidx.test.runner.lifecycle.Stage\nimport androidx.tracing.Trace\nimport java.io.Closeable\nimport java.util.Arrays\nimport java.util.EnumMap\nimport java.util.concurrent.TimeUnit\nimport java.util.concurrent.locks.Condition\nimport java.util.concurrent.locks.ReentrantLock\n\n\n/**\n * ActivityScenario provides APIs to start and drive an Activity's lifecycle state for testing. It\n * works with arbitrary activities and works consistently across different versions of the Android\n * framework.\n *\n *\n * The ActivityScenario API uses [State] extensively. If you are unfamiliar with [ ] components, please read [lifecycle](https://developer.android.com/topic/libraries/architecture/lifecycle#lc)\n * before starting.\n *\n *\n * It is crucial to understand the difference between [State] and [Event].\n *\n *\n * [SimpleActivityScenario.moveToState] allows you to transition your Activity's state to\n * [State.CREATED], [State.STARTED], [State.RESUMED], or [State.DESTROYED].\n * There are two paths for an Activity to reach [State.CREATED]: after [Event.ON_CREATE]\n * happens but before [Event.ON_START], or after [Event.ON_STOP]. ActivityScenario\n * always moves the Activity's state using the second path. The same applies to [ ][State.STARTED].\n *\n *\n * [State.DESTROYED] is the terminal state. You cannot move your Activity to other state\n * once it reaches to that state. If you want to test recreation of Activity instance, use [ ][.recreate].\n *\n *\n * ActivityScenario does't clean up device state automatically and may leave the activity keep\n * running after the test finishes. Call [.close] in your test to clean up the state or use\n * try-with-resources statement. This is optional but highly recommended to improve the stability of\n * your tests. Also, consider using [androidx.test.ext.junit.rules.ActivityScenarioRule].\n *\n *\n * This class is a replacement of ActivityController in Robolectric and ActivityTestRule in ATSL.\n *\n *\n * Following are the example of common use cases.\n *\n * <pre>`Before:\n * MyActivity activity = Robolectric.setupActivity(MyActivity.class);\n * assertThat(activity.getSomething()).isEqualTo(\"something\");\n *\n * After:\n * try(ActivityScenario<MyActivity> scenario = ActivityScenario.launch(MyActivity.class)) {\n * scenario.onActivity(activity -> {\n * assertThat(activity.getSomething()).isEqualTo(\"something\");\n * });\n * }\n *\n * Before:\n * ActivityController<MyActivity> controller = Robolectric.buildActivity(MyActivity.class);\n * controller.create().start().resume();  // Moves the activity state to State.RESUMED.\n * controller.pause();    // Moves the activity state to State.STARTED. (ON_PAUSE is an event).\n * controller.stop();     // Moves the activity state to State.CREATED. (ON_STOP is an event).\n * controller.destroy();  // Moves the activity state to State.DESTROYED.\n *\n * After:\n * try(ActivityScenario<MyActivity> scenario = ActivityScenario.launch(MyActivity.class)) {\n * scenario.moveToState(State.RESUMED);    // Moves the activity state to State.RESUMED.\n * scenario.moveToState(State.STARTED);    // Moves the activity state to State.STARTED.\n * scenario.moveToState(State.CREATED);    // Moves the activity state to State.CREATED.\n * scenario.moveToState(State.DESTROYED);  // Moves the activity state to State.DESTROYED.\n * }\n`</pre> *\n */\n// suppress AutoCloseable usage error\nclass UltronActivityScenario<A : Activity?> : AutoCloseable, Closeable {\n    /** A lock that is used to block the main thread until the Activity becomes a requested state.  */\n    private val lock = ReentrantLock()\n\n    /** A condition object to be notified when the activity state changes.  */\n    private val stateChangedCondition: Condition = lock.newCondition()\n\n    /** An intent to start a testing Activity.  */\n    private lateinit var startActivityIntent: Intent\n\n    /** An ActivityInvoker to use. Implementation class can be configured by service provider.  */\n    @SuppressLint(\"RestrictedApi\")\n    private val activityInvoker: ActivityInvoker = ServiceLoaderWrapper.loadSingleService(\n        ActivityInvoker::class.java\n    ) { UltronInstrumentationActivityInvoker() }\n\n    @SuppressLint(\"RestrictedApi\")\n    private val controlledLooper: ControlledLooper = ServiceLoaderWrapper.loadSingleService(\n        ControlledLooper::class.java\n    ) { ControlledLooper.NO_OP_CONTROLLED_LOOPER }\n\n    /**\n     * A current activity stage. This variable is updated by [ActivityLifecycleMonitor] from the\n     * main thread.\n     */\n    @GuardedBy(\"lock\")\n    private var currentActivityStage = Stage.PRE_ON_CREATE\n\n    /**\n     * A current activity. This variable is updated by [ActivityLifecycleMonitor] from the main\n     * thread.\n     */\n    @GuardedBy(\"lock\")\n    private var currentActivity: A? = null\n\n    /** Private constructor. Use [.launch] to instantiate this class.  */\n    @SuppressLint(\"RestrictedApi\")\n    private constructor(activityClass: Class<A>) {\n        this.startActivityIntent =\n            Checks.checkNotNull(\n                activityInvoker.getIntentForActivity(\n                    Checks.checkNotNull(\n                        activityClass\n                    )\n                )\n            )\n    }\n\n    /**\n     * An internal helper method to perform initial launch operation for the given scenario instance\n     * along with preconditions checks around device's configuration.\n     *\n     * @param activityOptions activity options bundle to be passed when launching this activity\n     * @param launchActivityForResult whether or not activity result code and data is needed\n     */\n    @SuppressLint(\"RestrictedApi\")\n    private fun launchInternal(activityOptions: Bundle?, launchActivityForResult: Boolean) {\n        Checks.checkState(\n            Settings.System.getInt(\n                InstrumentationRegistry.getInstrumentation().targetContext.contentResolver,\n                Settings.Global.ALWAYS_FINISH_ACTIVITIES,\n                0\n            )\n                    == 0,\n            \"\\\"Don't keep activities\\\" developer options must be disabled for ActivityScenario\"\n        )\n\n        Checks.checkNotMainThread()\n\n        Trace.beginSection(\"ActivityScenario launch\")\n        try {\n            InstrumentationRegistry.getInstrumentation().waitForIdleSync()\n\n            ActivityLifecycleMonitorRegistry.getInstance()\n                .addLifecycleCallback(activityLifecycleObserver)\n\n            // prefer the single argument variant for startActivity for backwards compatibility with older\n            // Robolectric versions\n            if (activityOptions == null) {\n                if (launchActivityForResult) {\n                    activityInvoker.startActivityForResult(startActivityIntent)\n                } else {\n                    activityInvoker.startActivity(startActivityIntent)\n                }\n            } else {\n                if (launchActivityForResult) {\n                    activityInvoker.startActivityForResult(startActivityIntent, activityOptions)\n                } else {\n                    activityInvoker.startActivity(startActivityIntent, activityOptions)\n                }\n            }\n\n            // Accept any steady states. An activity may start another activity in its onCreate method.\n            // Such\n            // an activity goes back to created or started state immediately after it is resumed.\n            waitForActivityToBecomeAnyOf(*STEADY_STATES.values.toTypedArray<State>())\n        } finally {\n            Trace.endSection()\n        }\n    }\n\n    /**\n     * Finishes the managed activity and cleans up device's state. This method blocks execution until\n     * the activity becomes [State.DESTROYED].\n     *\n     *\n     * It is highly recommended to call this method after you test is done to keep the device state\n     * clean although this is optional.\n     *\n     *\n     * You may call this method more than once. If the activity has been finished already, this\n     * method does nothing.\n     *\n     *\n     * Avoid calling this method directly. Consider one of the following options instead:\n     *\n     * <pre>`Option 1, use try-with-resources:\n     *\n     * try (ActivityScenario<MyActivity> scenario = ActivityScenario.launch(MyActivity.class)) {\n     * // Your test code goes here.\n     * }\n     *\n     * Option 2, use ActivityScenarioRule:\n     *\n    ` * @Rule `public ActivityScenarioRule<MyActivity> rule = new ActivityScenarioRule<>(MyActivity.class);\n     *\n    ` * @Test`public void myTest() {\n     * ActivityScenario<MyActivity> scenario = rule.getScenario();\n     * // Your test code goes here.\n     * }\n    `</pre> *\n     */\n    override fun close() {\n        Trace.beginSection(\"ActivityScenario close\")\n        try {\n            moveToState(State.DESTROYED)\n            ActivityLifecycleMonitorRegistry.getInstance()\n                .removeLifecycleCallback(activityLifecycleObserver)\n        } finally {\n            Trace.endSection()\n        }\n    }\n\n    /**\n     * Blocks the current thread until activity transition completes and its state becomes one of a\n     * given state.\n     */\n    private fun waitForActivityToBecomeAnyOf(vararg expectedStates: State) {\n        // Wait for idle sync otherwise we might hit transient state.\n        InstrumentationRegistry.getInstrumentation().waitForIdleSync()\n\n        val expectedStateSet: Set<State?> = HashSet(Arrays.asList(*expectedStates))\n        lock.lock()\n        try {\n            if (expectedStateSet.contains(STEADY_STATES[currentActivityStage])) {\n                return\n            }\n\n            var now = SystemClock.elapsedRealtime()\n            val deadline = now + TIMEOUT_MILLISECONDS\n            while (now < deadline\n                && !expectedStateSet.contains(STEADY_STATES[currentActivityStage])\n            ) {\n                stateChangedCondition.await(deadline - now, TimeUnit.MILLISECONDS)\n                now = SystemClock.elapsedRealtime()\n            }\n\n            if (!expectedStateSet.contains(STEADY_STATES[currentActivityStage])) {\n                throw AssertionError(\n                    String.format(\n                        \"Activity never becomes requested state \\\"%s\\\" (last lifecycle transition =\"\n                                + \" \\\"%s\\\")\",\n                        expectedStateSet, currentActivityStage\n                    )\n                )\n            }\n        } catch (e: InterruptedException) {\n            throw AssertionError(\n                String.format(\n                    \"Activity never becomes requested state \\\"%s\\\" (last lifecycle transition = \\\"%s\\\")\",\n                    expectedStateSet, currentActivityStage\n                ),\n                e\n            )\n        } finally {\n            lock.unlock()\n        }\n    }\n\n    /** Observes an Activity lifecycle change events and updates ActivityScenario's internal state.  */\n    private val activityLifecycleObserver =\n        ActivityLifecycleCallback { activity, stage ->\n            if (!activityMatchesIntent(startActivityIntent, activity)) {\n                Log.v(\n                    TAG,\n                    String.format(\n                        (\"Activity lifecycle changed event received but ignored because the intent does\"\n                                + \" not match. startActivityIntent=%s, activity.getIntent()=%s,\"\n                                + \" activity=%s\"),\n                        startActivityIntent, activity.intent, activity\n                    )\n                )\n                return@ActivityLifecycleCallback\n            }\n            lock.lock()\n            try {\n                when (currentActivityStage) {\n                    Stage.PRE_ON_CREATE, Stage.DESTROYED ->\n                        // The initial state (or after destroyed when the activity is being recreated)\n                        // transition must be to CREATED. Ignore events with non-created stage, which are\n                        // likely come from activities that the previous test starts and doesn't clean up.\n                        if (stage != Stage.CREATED) {\n                            Log.v(\n                                TAG,\n                                String.format(\n                                    (\"Activity lifecycle changed event received but ignored because the\"\n                                            + \" reported transition was not ON_CREATE while the last known\"\n                                            + \" transition was %s\"),\n                                    currentActivityStage\n                                )\n                            )\n                            return@ActivityLifecycleCallback\n                        }\n\n                    else ->\n                        // Make sure the received event is about the activity which this ActivityScenario\n                        // is monitoring. The Android framework may start multiple instances of a same\n                        // activity class and intent at a time. Also, there can be a race condition between\n                        // an activity that is used by the previous test and being destroyed and an activity\n                        // that is being resumed.\n                        if (currentActivity !== activity) {\n                            Log.v(\n                                TAG,\n                                String.format(\n                                    (\"Activity lifecycle changed event received but ignored because the\"\n                                            + \" activity instance does not match. currentActivity=%s,\"\n                                            + \" receivedActivity=%s\"),\n                                    currentActivity, activity\n                                )\n                            )\n                            return@ActivityLifecycleCallback\n                        }\n                }\n\n                // Update the internal state to be synced with the Android system. Don't hold activity\n                // reference if the new state is destroyed. It's not good idea to access to destroyed\n                // activity since the system may reuse the instance or want to garbage collect.\n                currentActivityStage = stage\n                currentActivity = (if (stage != Stage.DESTROYED) activity else null) as A?\n\n                Log.v(\n                    TAG,\n                    String.format(\n                        \"Update currentActivityStage to %s, currentActivity=%s\",\n                        currentActivityStage, currentActivity\n                    )\n                )\n\n                stateChangedCondition.signal()\n            } finally {\n                lock.unlock()\n            }\n        }\n\n    /**\n     * ActivityState is a state class that holds a snapshot of an Activity's current state and a\n     * reference to the Activity.\n     */\n    private class ActivityState<A : Activity?>(\n        val activity: A?,\n        val state: State?,\n        val stage: Stage\n    )\n\n    private val currentActivityState: ActivityState<A?>\n        get() {\n            lock.lock()\n            try {\n                val finishTime = SystemClock.elapsedRealtime() + 10000L\n                do {\n                    try {\n                        if (STEADY_STATES[currentActivityStage] != null) break\n                    } catch (_: Exception) {\n                    }\n                } while (SystemClock.elapsedRealtime() < finishTime)\n                return ActivityState(\n                    currentActivity,\n                    STEADY_STATES[currentActivityStage], currentActivityStage\n                )\n            } finally {\n                lock.unlock()\n            }\n        }\n\n    /**\n     * Moves Activity state to a new state.\n     *\n     *\n     * If a new state and current state are the same, it does nothing. It accepts [ ][State.CREATED], [State.STARTED], [State.RESUMED], and [State.DESTROYED].\n     *\n     *\n     * [State.DESTROYED] is the terminal state. You cannot move the state to other state\n     * after the activity reaches that state.\n     *\n     *\n     * The activity must be at the top of the back stack (excluding internal facilitator activities\n     * started by this library), otherwise [AssertionError] may be thrown. If the activity\n     * starts another activity (such as DialogActivity), make sure you close these activities and\n     * bring back the original activity foreground before you call this method.\n     *\n     *\n     * This method cannot be called from the main thread except in Robolectric tests.\n     *\n     * @throws IllegalArgumentException if unsupported `newState` is given\n     * @throws IllegalStateException if Activity is destroyed, finished or finishing\n     * @throws AssertionError if Activity never becomes requested state\n     */\n    @SuppressLint(\"RestrictedApi\")\n    fun moveToState(newState: State): UltronActivityScenario<A> {\n        Checks.checkNotMainThread()\n        if (newState != State.DESTROYED) {\n            InstrumentationRegistry.getInstrumentation().waitForIdleSync()\n        }\n\n        val currentState = currentActivityState\n\n        if (currentState.state == newState) {\n            return this\n        }\n        Checks.checkState(\n            currentState.state != State.DESTROYED && currentState.activity != null,\n            String.format(\n                \"Cannot move to state \\\"%s\\\" since the Activity has been destroyed already\",\n                newState\n            )\n        )\n\n        when (newState) {\n            State.CREATED -> activityInvoker.stopActivity(currentState.activity)\n            State.STARTED -> {\n                // ActivityInvoker#pauseActivity only accepts resumed or paused activity. Move the state to\n                // resumed first.\n                moveToState(State.RESUMED)\n                activityInvoker.pauseActivity(currentState.activity)\n            }\n\n            State.RESUMED -> activityInvoker.resumeActivity(currentState.activity)\n            State.DESTROYED -> {\n                repeat(3) {\n                    InstrumentationRegistry.getInstrumentation().runOnMainSync {\n                        listOf(Stage.RESUMED, Stage.PAUSED, Stage.STOPPED).forEach {\n                            ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(it).forEach { activity ->\n                                if (activity != currentState.activity) {\n                                    activity.finish()\n                                }\n                            }\n                        }\n                    }\n                }\n                activityInvoker.finishActivity(currentState.activity)\n            }\n            else -> throw IllegalArgumentException(\n                String.format(\"A requested state \\\"%s\\\" is not supported\", newState)\n            )\n        }\n\n        waitForActivityToBecomeAnyOf(newState)\n        return this\n    }\n\n    /**\n     * ActivityAction interface should be implemented by any class whose instances are intended to be\n     * executed by the main thread. An Activity that is instrumented by the ActivityScenario is passed\n     * to [ActivityAction.perform] method.\n     *\n     * <pre>`Example:\n     * ActivityScenario<MyActivity> scenario = ActivityScenario.launch(MyActivity.class);\n     * scenario.onActivity(activity -> {\n     * assertThat(activity.getSomething()).isEqualTo(\"something\");\n     * });\n    `</pre> *\n     *\n     *\n     * You should never keep the Activity reference. It should only be accessed in [ ][ActivityAction.perform] scope for two reasons: 1) Android framework may re-create the Activity\n     * during lifecycle changes, your holding reference might be stale. 2) It increases the reference\n     * counter and it may affect to the framework behavior, especially after you finish the Activity.\n     *\n     * <pre>`Bad Example:\n     * ActivityScenario<MyActivity> scenario = ActivityScenario.launch(MyActivity.class);\n     * final MyActivity[] myActivityHolder = new MyActivity[1];\n     * scenario.onActivity(activity -> {\n     * myActivityHolder[0] = activity;\n     * });\n     * assertThat(myActivityHolder[0].getSomething()).isEqualTo(\"something\");\n    `</pre> *\n     */\n    fun interface ActivityAction<A : Activity?> {\n        /**\n         * This method is invoked on the main thread with the reference to the Activity.\n         *\n         * @param activity an Activity instrumented by the [SimpleActivityScenario]. It never be null.\n         */\n        fun perform(activity: A)\n    }\n\n    /**\n     * Runs a given `action` on the current Activity's main thread.\n     *\n     *\n     * Note that you should never keep Activity reference passed into your `action` because\n     * it can be recreated at anytime during state transitions.\n     *\n     * @throws IllegalStateException if Activity is destroyed, finished or finishing\n     */\n    @SuppressLint(\"RestrictedApi\")\n    fun onActivity(action: ActivityAction<A?>): UltronActivityScenario<A> {\n        // A runnable to perform given ActivityAction. This runnable should be invoked from the\n        // application main thread.\n        val runnableAction =\n            Runnable {\n                Checks.checkMainThread()\n                lock.lock()\n                try {\n                    Checks.checkNotNull(\n                        currentActivity,\n                        \"Cannot run onActivity since Activity has been destroyed already\"\n                    )\n                    action.perform(currentActivity)\n                } finally {\n                    lock.unlock()\n                }\n            }\n\n        if (Looper.myLooper() == Looper.getMainLooper()) {\n            // execute any queued work on main looper, to make behavior consistent between running\n            // on Robolectric with paused main looper and instrumentation\n            controlledLooper.drainMainThreadUntilIdle()\n            runnableAction.run()\n        } else {\n            InstrumentationRegistry.getInstrumentation().waitForIdleSync()\n            InstrumentationRegistry.getInstrumentation().runOnMainSync(runnableAction)\n        }\n\n        return this\n    }\n\n    val result: Instrumentation.ActivityResult\n        /**\n         * Waits for the activity to be finished and returns the activity result.\n         *\n         *\n         * ActivityScenario.launchActivityForResult() must be used to launch an Activity before this\n         * method is called.\n         *\n         *\n         * Note: This method doesn't call [Activity.finish]. The activity must be finishing or\n         * finished otherwise this method will throws runtime exception after the timeout.\n         *\n         * <pre>`Example:\n         * ActivityScenario<MyActivity> scenario =\n         * ActivityScenario.launchActivityForResult(MyActivity.class);\n         * // Let's say MyActivity has a button that finishes itself.\n         * onView(withId(R.id.finish_button)).perform(click());\n         * assertThat(scenario.getResult().getResultCode()).isEqualTo(Activity.RESULT_OK);\n        `</pre> *\n         *\n         * @return activity result of the activity that managed by this scenario class.\n         * @throws IllegalStateException when you call this method with an Activity that was not started\n         * by [.launchActivityForResult]\n         */\n        @SuppressLint(\"RestrictedApi\")\n        get() = activityInvoker.activityResult\n\n    val state: State\n        /**\n         * Returns the current activity state. The possible states are [State.CREATED], [ ][State.STARTED], [State.RESUMED], and [State.DESTROYED].\n         *\n         *\n         * This method cannot be called from the main thread except in Robolectric tests.\n         */\n        @SuppressLint(\"RestrictedApi\")\n        get() {\n            val currentActivityState =\n                currentActivityState\n            return Checks.checkNotNull(\n                currentActivityState.state,\n                \"Could not get current state of activity %s due to the transition is incomplete. Current\"\n                        + \" stage = %s\",\n                currentActivityState.activity,\n                currentActivityState.stage\n            )\n        }\n\n    companion object {\n        private val TAG: String = UltronActivityScenario::class.java.simpleName\n\n        /**\n         * The timeout for [.waitForActivityToBecomeAnyOf] method. If an Activity doesn't become\n         * requested state after the timeout, we will throw [AssertionError] to fail tests.\n         */\n        private const val TIMEOUT_MILLISECONDS: Long = 45000\n\n        /**\n         * A map to lookup steady [State] by [Stage]. Transient stages such as [ ][Stage.CREATED], [Stage.STARTED] and [Stage.RESTARTED] are not included in the map.\n         */\n        private val STEADY_STATES: MutableMap<Stage, State> = EnumMap(\n            Stage::class.java\n        )\n\n        init {\n            STEADY_STATES[Stage.RESUMED] = State.RESUMED\n            STEADY_STATES[Stage.PAUSED] = State.STARTED\n            STEADY_STATES[Stage.STOPPED] = State.CREATED\n            STEADY_STATES[Stage.DESTROYED] = State.DESTROYED\n        }\n\n        /**\n         * Launches an activity of a given class and constructs ActivityScenario with the activity. Waits\n         * for the lifecycle state transitions to be complete. Typically the initial state of the activity\n         * is [State.RESUMED] but can be in another state. For instance, if your activity calls\n         * [Activity.finish] from your [Activity.onCreate], the state is [ ][State.DESTROYED] when this method returns.\n         *\n         *\n         * If you need to supply parameters to the start activity intent, use [.launch].\n         *\n         *\n         * If you need to get the activity result, use [.launchActivityForResult].\n         *\n         *\n         * This method cannot be called from the main thread except in Robolectric tests.\n         *\n         * @param activityClass an activity class to launch\n         * @throws AssertionError if the lifecycle state transition never completes within the timeout\n         * @return ActivityScenario which you can use to make further state transitions\n         */\n        @SuppressLint(\"RestrictedApi\")\n        fun <A : Activity?> launch(activityClass: Class<A>): UltronActivityScenario<A> {\n            val scenario = UltronActivityScenario(Checks.checkNotNull(activityClass))\n            scenario.launchInternal( /*activityOptions=*/null,  /*launchActivityForResult=*/false)\n            return scenario\n        }\n\n        /** Determine if the intent matches the given activity.  */\n        private fun activityMatchesIntent(\n            startActivityIntent: Intent,\n            launchedActivity: Activity\n        ): Boolean {\n            // The logic here is almost the same as Intent.filterEquals\n            // but we need to handle case where startActivityIntent does not have component specified\n            // (aka is implicit intent). The launchedActivity intent will always have component specified,\n            // since\n            // the framework populates it\n\n            val activityIntent = launchedActivity.intent\n            if (!equals(startActivityIntent.action, activityIntent.action) ||\n                !equals(startActivityIntent.data, activityIntent.data) ||\n                !equals(startActivityIntent.type, activityIntent.type)\n            ) {\n                return false\n            }\n\n            val isActivityInSamePackage =\n                hasPackageEquivalentComponent(startActivityIntent)\n                        && hasPackageEquivalentComponent(activityIntent)\n\n            return if (!isActivityInSamePackage\n                && !equals(startActivityIntent.getPackage(), activityIntent.getPackage())\n            ) {\n                false\n            } else if (startActivityIntent.component != null\n                && !equals(startActivityIntent.component, activityIntent.component)\n            ) {\n                false\n            } else if (!equals(startActivityIntent.categories, activityIntent.categories)) {\n                false\n            } else if (Build.VERSION.SDK_INT >= 29\n                && !equals(startActivityIntent.identifier, activityIntent.identifier)\n            ) {\n                false\n            } else {\n                true\n            }\n        }\n\n        /**\n         * Return `true` if the component name is not null and is in the same package that this\n         * intent limited to. otherwise return `false`. Note: this code is copied from `Intent#hasPackageEquivalentComponent`.\n         */\n        private fun hasPackageEquivalentComponent(intent: Intent): Boolean {\n            val componentName = intent.component\n            val packageName = intent.getPackage()\n            // packageName may be null when the resolved Activity is in the same package to this\n            // running process.\n            return componentName != null\n                    && (packageName == null || packageName == componentName.packageName)\n        }\n\n        // reimplementation of Objects.equals since it is only available on APIs >= 19\n        private fun equals(a: Any?, b: Any?): Boolean {\n            return (a === b) || (a != null && a == b)\n        }\n    }\n}"
  },
  {
    "path": "ultron-common/src/androidMain/kotlin/com/atiurin/ultron/testlifecycle/activity/UltronInstrumentationActivityInvoker.kt",
    "content": "package com.atiurin.ultron.testlifecycle.activity\n\nimport android.annotation.SuppressLint\nimport android.app.Activity\nimport android.app.ActivityOptions\nimport android.app.Instrumentation\nimport android.app.PendingIntent\nimport android.content.BroadcastReceiver\nimport android.content.Context\nimport android.content.Intent\nimport android.content.IntentFilter\nimport android.content.IntentSender\nimport android.os.Build\nimport android.os.Bundle\nimport android.util.Log\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.internal.platform.app.ActivityInvoker\nimport androidx.test.internal.platform.app.ActivityLifecycleTimeout\nimport androidx.test.internal.util.Checks\nimport androidx.test.platform.app.InstrumentationRegistry\nimport androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry\nimport androidx.test.runner.lifecycle.Stage\nimport java.util.Arrays\nimport java.util.concurrent.CountDownLatch\nimport java.util.concurrent.TimeUnit\n\n\n/**\n * On-device [ActivityInvoker] implementation that drives Activity lifecycles using [ ] indirectly via [Context.startActivity] and [ ][Activity.recreate].\n *\n *\n * Some methods in this class are non-blocking API. It's caller's responsibility to wait for\n * activity state to be desired state.\n */\n@SuppressLint(\"RestrictedApi\")\ninternal class UltronInstrumentationActivityInvoker : ActivityInvoker {\n    /**\n     * BootstrapActivity starts a test target activity specified by the extras bundle with key [ ][.TARGET_ACTIVITY_INTENT_KEY] in the intent that starts this bootstrap activity. The target\n     * activity is started by [Activity.startActivityForResult] when the bootstrap activity is\n     * created. Upon an arrival of the activity result, the bootstrap activity forwards the result to\n     * the instrumentation process by broadcasting the result and finishes itself. This activity also\n     * finishes itself when it receives [.FINISH_BOOTSTRAP_ACTIVITY] action.\n     */\n    class BootstrapActivity : Activity() {\n        private val receiver: BroadcastReceiver = object : BroadcastReceiver() {\n            override fun onReceive(context: Context, intent: Intent) {\n                finishActivity( /*requestCode=*/0)\n                finish()\n            }\n        }\n\n        private var isTargetActivityStarted = false\n\n        override fun onCreate(savedInstanceState: Bundle?) {\n            super.onCreate(savedInstanceState)\n            registerBroadcastReceiver(\n                this, receiver, IntentFilter(\n                    FINISH_BOOTSTRAP_ACTIVITY\n                )\n            )\n\n            isTargetActivityStarted =\n                (savedInstanceState != null\n                        && savedInstanceState.getBoolean(IS_TARGET_ACTIVITY_STARTED_KEY, false))\n\n            // disable starting animations\n            overridePendingTransition(0, 0)\n        }\n\n        override fun finish() {\n            super.finish()\n            // disable closing animations\n            overridePendingTransition(0, 0)\n        }\n\n        override fun onResume() {\n            super.onResume()\n\n            if (!isTargetActivityStarted) {\n                isTargetActivityStarted = true\n                val startTargetActivityIntent =\n                    Checks.checkNotNull(\n                        intent.getParcelableExtra<PendingIntent>(\n                            TARGET_ACTIVITY_INTENT_KEY\n                        )\n                    )\n                val options =\n                    optInToGrantBalPrivileges(\n                        intent.getBundleExtra(TARGET_ACTIVITY_OPTIONS_BUNDLE_KEY)\n                    )\n                try {\n                    if (options == null) {\n                        // Override and disable FLAG_ACTIVITY_NEW_TASK flag by flagsMask and flagsValue.\n                        // PendingIntentRecord#sendInner() will mask the original intent flag with the flagsMask\n                        // then override those bits with the new flagsValue specified here. This override is\n                        // necessary because if the activity is started as a new task ActivityStarter disposes\n                        // the originator information and the result is never be delivered. Instead you will get\n                        // an error \"Activity is launching as a new task, so cancelling activity result.\" and\n                        // #onActivityResult() will be invoked immediately with result code\n                        // Activity#RESULT_CANCELED.\n                        startIntentSenderForResult(\n                            startTargetActivityIntent.intentSender,  /*requestCode=*/\n                            0,  /*fillInIntent=*/\n                            null,  /*flagsMask=*/\n                            Intent.FLAG_ACTIVITY_NEW_TASK,  /*flagsValues=*/\n                            0,  /*extraFlags=*/\n                            0\n                        )\n                    } else {\n                        startIntentSenderForResult(\n                            startTargetActivityIntent.intentSender,  /*requestCode=*/\n                            0,  /*fillInIntent=*/\n                            null,  /*flagsMask=*/\n                            Intent.FLAG_ACTIVITY_NEW_TASK,  /*flagsValues=*/\n                            0,  /*extraFlags=*/\n                            0,\n                            options\n                        )\n                    }\n                } catch (e: IntentSender.SendIntentException) {\n                    Log.e(TAG, \"Failed to start target activity.\", e)\n                    throw RuntimeException(e)\n                }\n            }\n        }\n\n        override fun onSaveInstanceState(outState: Bundle) {\n            super.onSaveInstanceState(outState)\n            outState.putBoolean(IS_TARGET_ACTIVITY_STARTED_KEY, isTargetActivityStarted)\n        }\n\n        override fun onDestroy() {\n            super.onDestroy()\n            unregisterReceiver(receiver)\n        }\n\n        override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {\n            if (requestCode == 0) {\n                val activityResultReceivedActionIntent = Intent(\n                    BOOTSTRAP_ACTIVITY_RESULT_RECEIVED\n                )\n                activityResultReceivedActionIntent.putExtra(\n                    BOOTSTRAP_ACTIVITY_RESULT_CODE_KEY,\n                    resultCode\n                )\n                if (data != null) {\n                    activityResultReceivedActionIntent.putExtra(\n                        BOOTSTRAP_ACTIVITY_RESULT_DATA_KEY,\n                        data\n                    )\n                }\n                sendBroadcast(activityResultReceivedActionIntent)\n                finish()\n            }\n        }\n\n        companion object {\n            private val TAG: String = BootstrapActivity::class.java.name\n            private const val IS_TARGET_ACTIVITY_STARTED_KEY = \"IS_TARGET_ACTIVITY_STARTED_KEY\"\n        }\n    }\n\n    /**\n     * ActivityResultWaiter listens broadcast messages and waits for [ ][.BOOTSTRAP_ACTIVITY_RESULT_RECEIVED] action. Upon the reception of that action, it retrieves\n     * result code and data from the action and makes a local copy. Clients can access to the result\n     * by [.getActivityResult].\n     */\n    private class ActivityResultWaiter(context: Context) {\n        private val latch = CountDownLatch(1)\n        private var activityResult: Instrumentation.ActivityResult? = null\n\n        /**\n         * Constructs ActivityResultWaiter and starts listening to broadcast with the given context. It\n         * keeps subscribing the event until it receives [.BOOTSTRAP_ACTIVITY_RESULT_RECEIVED]\n         * action.\n         */\n        init {\n            val receiver: BroadcastReceiver =\n                object : BroadcastReceiver() {\n                    override fun onReceive(context: Context, intent: Intent) {\n                        // Stop listening to the broadcast once we get the result.\n                        context.unregisterReceiver(this)\n\n                        if (BOOTSTRAP_ACTIVITY_RESULT_RECEIVED == intent.action) {\n                            val resultCode =\n                                intent.getIntExtra(\n                                    BOOTSTRAP_ACTIVITY_RESULT_CODE_KEY, Activity.RESULT_CANCELED\n                                )\n                            var resultData = intent.getParcelableExtra<Intent>(\n                                BOOTSTRAP_ACTIVITY_RESULT_DATA_KEY\n                            )\n                            if (resultData != null) {\n                                // Make a copy of resultData since the lifetime of the given intent is unknown.\n                                resultData = Intent(resultData)\n                            }\n                            activityResult = Instrumentation.ActivityResult(resultCode, resultData)\n                            latch.countDown()\n                        }\n                    }\n                }\n            val intentFilter = IntentFilter(BOOTSTRAP_ACTIVITY_RESULT_RECEIVED)\n            intentFilter.addAction(CANCEL_ACTIVITY_RESULT_WAITER)\n            registerBroadcastReceiver(context, receiver, intentFilter)\n        }\n\n        /**\n         * Waits for the activity result to be available until the timeout and returns the result.\n         *\n         * @throws NullPointerException if the result doesn't become available after the timeout\n         * @return activity result of which [.startActivity] starts\n         */\n        fun getActivityResult(): Instrumentation.ActivityResult? {\n            try {\n                latch.await(ActivityLifecycleTimeout.getMillis(), TimeUnit.MILLISECONDS)\n            } catch (e: InterruptedException) {\n                Log.i(TAG, \"Waiting activity result was interrupted\", e)\n            }\n            Checks.checkNotNull(\n                activityResult,\n                \"onActivityResult never be called after %d milliseconds\",\n                ActivityLifecycleTimeout.getMillis()\n            )\n            return activityResult\n        }\n\n        companion object {\n            private val TAG: String = ActivityResultWaiter::class.java.name\n        }\n    }\n\n    /**\n     * An empty activity with style \"android:windowIsFloating = false\". The style is set by\n     * AndroidManifest.xml via \"android:theme\".\n     *\n     *\n     * When this activity is resumed, it broadcasts [.EMPTY_ACTIVITY_RESUMED] action to\n     * notify the state.\n     *\n     *\n     * This activity finishes itself when it receives [.FINISH_EMPTY_ACTIVITIES] action.\n     *\n     *\n     * This activity is used to send an arbitrary resumed Activity to stopped.\n     */\n    class EmptyActivity : Activity() {\n        private val receiver: BroadcastReceiver = object : BroadcastReceiver() {\n            override fun onReceive(context: Context, intent: Intent) {\n                finish()\n            }\n        }\n\n        override fun onCreate(savedInstanceState: Bundle?) {\n            super.onCreate(savedInstanceState)\n            registerBroadcastReceiver(\n                this, receiver, IntentFilter(\n                    FINISH_EMPTY_ACTIVITIES\n                )\n            )\n\n            // disable starting animations\n            overridePendingTransition(0, 0)\n        }\n\n        override fun finish() {\n            super.finish()\n            // disable closing animations\n            overridePendingTransition(0, 0)\n        }\n\n        override fun onResume() {\n            super.onResume()\n            sendBroadcast(Intent(EMPTY_ACTIVITY_RESUMED))\n        }\n\n        override fun onDestroy() {\n            super.onDestroy()\n            unregisterReceiver(receiver)\n        }\n    }\n\n    /**\n     * An empty activity with style \"android:windowIsFloating = true\". The style is set by\n     * AndroidManifest.xml via \"android:theme\".\n     *\n     *\n     * When this activity is resumed, it broadcasts [.EMPTY_FLOATING_ACTIVITY_RESUMED] action\n     * to notify the state.\n     *\n     *\n     * This activity finishes itself when it receives [.FINISH_EMPTY_ACTIVITIES] action.\n     *\n     *\n     * This activity is used to send an arbitrary resumed Activity to paused.\n     */\n    class EmptyFloatingActivity : Activity() {\n        private val receiver: BroadcastReceiver = object : BroadcastReceiver() {\n            override fun onReceive(context: Context, intent: Intent) {\n                finish()\n            }\n        }\n\n        override fun onCreate(savedInstanceState: Bundle?) {\n            super.onCreate(savedInstanceState)\n            registerBroadcastReceiver(\n                this, receiver, IntentFilter(\n                    FINISH_EMPTY_ACTIVITIES\n                )\n            )\n\n            // disable starting animations\n            overridePendingTransition(0, 0)\n        }\n\n        override fun finish() {\n            super.finish()\n            // disable closing animations\n            overridePendingTransition(0, 0)\n        }\n\n        override fun onResume() {\n            super.onResume()\n            sendBroadcast(Intent(EMPTY_FLOATING_ACTIVITY_RESUMED))\n        }\n\n        override fun onDestroy() {\n            super.onDestroy()\n            unregisterReceiver(receiver)\n        }\n    }\n\n    /** A waiter to observe activity result that is started by [.startActivity].  */\n    private var activityResultWaiter: ActivityResultWaiter? = null\n\n    /** Starts an Activity using the given intent.  */\n    override fun startActivity(intent: Intent, activityOptions: Bundle?) {\n        // make sure the intent can resolve an activity\n        val ai = intent.resolveActivityInfo(\n            ApplicationProvider.getApplicationContext<Context>().packageManager,\n            0\n        )\n            ?: throw RuntimeException(\"Unable to resolve activity for: $intent\")\n        // Close empty activities and bootstrap activity if it's running. This might happen if the\n        // previous test crashes before it cleans up the state.\n        ApplicationProvider.getApplicationContext<Context>().sendBroadcast(\n            Intent(\n                FINISH_BOOTSTRAP_ACTIVITY\n            )\n        )\n        ApplicationProvider.getApplicationContext<Context>().sendBroadcast(\n            Intent(\n                FINISH_EMPTY_ACTIVITIES\n            )\n        )\n\n        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)\n\n        if (Build.VERSION.SDK_INT < 28) {\n            check(activityOptions == null) { \"Starting an activity with activityOptions is not supported on APIs below 28.\" }\n            InstrumentationRegistry.getInstrumentation().startActivitySync(intent)\n        } else {\n            InstrumentationRegistry.getInstrumentation().startActivitySync(intent, activityOptions)\n        }\n    }\n\n    override fun startActivity(intent: Intent) {\n        startActivity(intent, null)\n    }\n\n    /** Starts an Activity using the given intent.  */\n    override fun startActivityForResult(intent: Intent, activityOptionsBundle: Bundle?) {\n        // make sure the intent can resolve an activity\n        var activityOptionsBundle = activityOptionsBundle\n        val ai = intent.resolveActivityInfo(\n            ApplicationProvider.getApplicationContext<Context>().packageManager,\n            0\n        )\n        checkNotNull(ai) { \"Unable to resolve activity for: $intent\" }\n        // Close empty activities and bootstrap activity if it's running. This might happen if the\n        // previous test crashes before it cleans up the state.\n        ApplicationProvider.getApplicationContext<Context>().sendBroadcast(\n            Intent(\n                FINISH_BOOTSTRAP_ACTIVITY\n            )\n        )\n        ApplicationProvider.getApplicationContext<Context>().sendBroadcast(\n            Intent(\n                FINISH_EMPTY_ACTIVITIES\n            )\n        )\n\n        activityResultWaiter = ActivityResultWaiter(ApplicationProvider.getApplicationContext())\n\n        activityOptionsBundle = optInToGrantBalPrivileges(activityOptionsBundle)\n\n        // make an immutable intent if its implicit\n        val intentMutability =\n            if (intent.getPackage() == null && intent.component == null)\n                PendingIntent.FLAG_IMMUTABLE\n            else\n                PendingIntent.FLAG_MUTABLE\n\n        // Note: Instrumentation.startActivitySync(Intent) cannot be used here because BootstrapActivity\n        // may start in different process. Also, we use PendingIntent because the target activity may\n        // set \"exported\" attribute to false so that it prohibits starting the activity outside of their\n        // package. With PendingIntent we delegate the authority to BootstrapActivity.\n        val bootstrapIntent =\n            getIntentForActivity(BootstrapActivity::class.java)\n                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)\n                .putExtra(\n                    TARGET_ACTIVITY_INTENT_KEY,\n                    PendingIntent.getActivity(\n                        ApplicationProvider.getApplicationContext(),  /* requestCode= */\n                        0,\n                        intent,  /* flags= */\n                        PendingIntent.FLAG_UPDATE_CURRENT or intentMutability\n                    )\n                )\n                .putExtra(TARGET_ACTIVITY_OPTIONS_BUNDLE_KEY, activityOptionsBundle)\n\n        ApplicationProvider.getApplicationContext<Context>()\n            .startActivity(bootstrapIntent, activityOptionsBundle)\n    }\n\n    override fun startActivityForResult(intent: Intent) {\n        startActivityForResult(intent, null)\n    }\n\n    override fun getActivityResult(): Instrumentation.ActivityResult {\n        checkNotNull(activityResultWaiter) {\n            (\"You must start Activity first. Make sure you are using launchActivityForResult() to\"\n                    + \" launch an Activity.\")\n        }\n        return activityResultWaiter!!.getActivityResult()!!\n    }\n\n    /** Resumes the tested activity by finishing empty activities.  */\n    override fun resumeActivity(activity: Activity) {\n        checkActivityStageIsIn(activity, Stage.RESUMED, Stage.PAUSED, Stage.STOPPED)\n        ApplicationProvider.getApplicationContext<Context>().sendBroadcast(\n            Intent(\n                FINISH_EMPTY_ACTIVITIES\n            )\n        )\n    }\n\n    /**\n     * Pauses the tested activity by starting [EmptyFloatingActivity] on top of the tested\n     * activity.\n     */\n    override fun pauseActivity(activity: Activity) {\n        checkActivityStageIsIn(activity, Stage.RESUMED, Stage.PAUSED)\n        startFloatingEmptyActivitySync()\n    }\n\n    private fun startFloatingEmptyActivitySync() {\n        val latch = CountDownLatch(1)\n        val receiver: BroadcastReceiver =\n            object : BroadcastReceiver() {\n                override fun onReceive(context: Context, intent: Intent) {\n                    latch.countDown()\n                }\n            }\n        registerBroadcastReceiver(\n            ApplicationProvider.getApplicationContext(), receiver, IntentFilter(\n                EMPTY_FLOATING_ACTIVITY_RESUMED\n            )\n        )\n\n        // Starting an arbitrary Activity (android:windowIsFloating = true) forces the tested Activity\n        // to the paused state.\n        ApplicationProvider.getApplicationContext<Context>()\n            .startActivity(\n                getIntentForActivity(EmptyFloatingActivity::class.java)\n                    .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n            )\n\n        try {\n            latch.await(ActivityLifecycleTimeout.getMillis(), TimeUnit.MILLISECONDS)\n        } catch (e: InterruptedException) {\n            throw RuntimeException(\"Failed to pause activity\", e)\n        } finally {\n            ApplicationProvider.getApplicationContext<Context>().unregisterReceiver(receiver)\n        }\n    }\n\n    /** Stops the tested activity by starting [EmptyActivity] on top of the tested activity.  */\n    override fun stopActivity(activity: Activity) {\n        checkActivityStageIsIn(activity, Stage.RESUMED, Stage.PAUSED, Stage.STOPPED)\n        startEmptyActivitySync()\n    }\n\n    private fun startEmptyActivitySync() {\n        val latch = CountDownLatch(1)\n        val receiver: BroadcastReceiver =\n            object : BroadcastReceiver() {\n                override fun onReceive(context: Context, intent: Intent) {\n                    latch.countDown()\n                }\n            }\n        registerBroadcastReceiver(\n            ApplicationProvider.getApplicationContext(), receiver, IntentFilter(\n                EMPTY_ACTIVITY_RESUMED\n            )\n        )\n\n        // Starting an arbitrary Activity (android:windowIsFloating = false) forces the tested Activity\n        // to the stopped state.\n        ApplicationProvider.getApplicationContext<Context>()\n            .startActivity(\n                getIntentForActivity(EmptyActivity::class.java).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n            )\n\n        try {\n            latch.await(ActivityLifecycleTimeout.getMillis(), TimeUnit.MILLISECONDS)\n        } catch (e: InterruptedException) {\n            throw RuntimeException(\"Failed to stop activity\", e)\n        } finally {\n            ApplicationProvider.getApplicationContext<Context>().unregisterReceiver(receiver)\n        }\n    }\n\n    /**\n     * Recreates the Activity by [Activity.recreate].\n     *\n     *\n     * Note that [Activity.recreate]'s behavior differs by Android framework version. For\n     * example, the version P brings Activity's lifecycle state to the original state after the\n     * re-creation. A stopped Activity goes to stopped state after the re-creation in concrete.\n     * Whereas the version O ignores [Activity.recreate] method call when the activity is in\n     * stopped state. The version N re-creates stopped Activity but brings back to paused state\n     * instead of stopped.\n     *\n     *\n     * In short, make sure to set Activity's state to resumed before calling this method otherwise\n     * the behavior is the framework version dependent.\n     */\n    override fun recreateActivity(activity: Activity) {\n        checkActivityStageIsIn(activity, Stage.RESUMED, Stage.PAUSED, Stage.STOPPED)\n        InstrumentationRegistry.getInstrumentation().runOnMainSync { activity.recreate() }\n    }\n\n    override fun finishActivity(activity: Activity) {\n        // Stop the activity before calling Activity#finish() as a workaround for the framework bug in\n        // API level 15 to 19 where the framework may not call #onStop and #onDestroy if you call\n        // Activity#finish() while it is resumed. The exact root cause is unknown but moving the\n        // activity back-and-forth between foreground and background helps the finish operation to be\n        // executed so here we try finishing the activity by several means. This hack is not necessary\n        // for the API level above 19.\n        InstrumentationRegistry.getInstrumentation().runOnMainSync { activity.finish() }\n        if (activityResultWaiter != null) {\n            ApplicationProvider.getApplicationContext<Context>().sendBroadcast(\n                Intent(\n                    FINISH_BOOTSTRAP_ACTIVITY\n                )\n            )\n            InstrumentationRegistry.getInstrumentation().runOnMainSync { activity.finish() }\n        }\n        ApplicationProvider.getApplicationContext<Context>().sendBroadcast(\n            Intent(\n                FINISH_EMPTY_ACTIVITIES\n            )\n        )\n        if (activityResultWaiter != null) {\n            ApplicationProvider.getApplicationContext<Context>().sendBroadcast(\n                Intent(\n                    CANCEL_ACTIVITY_RESULT_WAITER\n                )\n            )\n        }\n    }\n\n    companion object {\n        /** A bundle key to retrieve an intent to start test target activity in extras bundle.  */\n        private const val TARGET_ACTIVITY_INTENT_KEY =\n            \"test.junit_custom.rules.activity.InstrumentationActivityInvoker.START_TARGET_ACTIVITY_INTENT_KEY\"\n\n        /** A bundle key to retrieve an options bundle to start test target activity in extras bundle.  */\n        private const val TARGET_ACTIVITY_OPTIONS_BUNDLE_KEY =\n            \"test.junit_custom.rules.activity.InstrumentationActivityInvoker.TARGET_ACTIVITY_OPTIONS_BUNDLE_KEY\"\n\n        /**\n         * An intent action broadcasted by [BootstrapActivity] notifying the activity receives\n         * activity result and passes payload back to the instrumentation process.\n         */\n        private const val BOOTSTRAP_ACTIVITY_RESULT_RECEIVED =\n            \"test.junit_custom.rules.activity.InstrumentationActivityInvoker.BOOTSTRAP_ACTIVITY_RESULT_RECEIVED\"\n\n        /**\n         * A bundle key to retrieve an activity result code from the extras bundle of [ ][.BOOTSTRAP_ACTIVITY_RESULT_RECEIVED] action.\n         */\n        private const val BOOTSTRAP_ACTIVITY_RESULT_CODE_KEY =\n            \"test.junit_custom.rules.activity.InstrumentationActivityInvoker.BOOTSTRAP_ACTIVITY_RESULT_CODE_KEY\"\n\n        /**\n         * A bundle key to retrieve an activity result data intent from the extras bundle of [ ][.BOOTSTRAP_ACTIVITY_RESULT_RECEIVED] action.\n         */\n        private const val BOOTSTRAP_ACTIVITY_RESULT_DATA_KEY =\n            \"test.junit_custom.rules.activity.InstrumentationActivityInvoker.BOOTSTRAP_ACTIVITY_RESULT_DATA_KEY\"\n\n        /**\n         * An intent action broadcasted by InstrumentActivityInvoker to clean up any [ ]s that are still registered at the end\n         */\n        private const val CANCEL_ACTIVITY_RESULT_WAITER =\n            \"test.junit_custom.rules.activity.InstrumentationActivityInvoker.CANCEL_ACTIVITY_RESULT_WAITER\"\n\n        /**\n         * An intent action broadcasted by [EmptyActivity] notifying the activity becomes resumed\n         * state.\n         */\n        private const val EMPTY_ACTIVITY_RESUMED =\n            \"test.junit_custom.rules.activity.InstrumentationActivityInvoker.EMPTY_ACTIVITY_RESUMED\"\n\n        /**\n         * An intent action broadcasted by [EmptyFloatingActivity] notifying the activity becomes\n         * resumed state.\n         */\n        private const val EMPTY_FLOATING_ACTIVITY_RESUMED =\n            \"test.junit_custom.rules.activity.InstrumentationActivityInvoker.EMPTY_FLOATING_ACTIVITY_RESUMED\"\n\n        /** An intent action to notify [BootstrapActivity] to be finished.  */\n        private const val FINISH_BOOTSTRAP_ACTIVITY =\n            \"test.junit_custom.rules.activity.InstrumentationActivityInvoker.FINISH_BOOTSTRAP_ACTIVITY\"\n\n        /**\n         * An intent action to notify [EmptyActivity] and [EmptyFloatingActivity] to be\n         * finished.\n         */\n        private const val FINISH_EMPTY_ACTIVITIES =\n            \"test.junit_custom.rules.activity.InstrumentationActivityInvoker.FINISH_EMPTY_ACTIVITIES\"\n\n        private fun optInToGrantBalPrivileges(activityOptionsBundle: Bundle?): Bundle? {\n            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {\n                return activityOptionsBundle\n            }\n            // Initialize a bundle to grant this activities start privilege.\n            val updatedActivityOptions =\n                ActivityOptions.makeBasic()\n                    .setPendingIntentBackgroundActivityStartMode(ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED)\n                    .toBundle()\n            // Merge the bundle with the one passed in. This allows overriding the start mode if desired.\n            if (activityOptionsBundle != null) {\n                updatedActivityOptions.putAll(activityOptionsBundle)\n            }\n            return updatedActivityOptions\n        }\n\n        private fun checkActivityStageIsIn(activity: Activity, vararg expected: Stage) {\n            checkActivityStageIsIn(activity, HashSet(Arrays.asList(*expected)))\n        }\n\n        private fun checkActivityStageIsIn(activity: Activity, expected: Set<Stage>) {\n            InstrumentationRegistry.getInstrumentation()\n                .runOnMainSync {\n                    val stage =\n                        ActivityLifecycleMonitorRegistry.getInstance()\n                            .getLifecycleStageOf(activity)\n                    Checks.checkState(\n                        expected.contains(stage),\n                        \"Activity's stage must be %s but was %s\",\n                        expected,\n                        stage\n                    )\n                }\n        }\n\n        private fun registerBroadcastReceiver(\n            context: Context, broadcastReceiver: BroadcastReceiver, intentFilter: IntentFilter\n        ) {\n            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {\n                context.registerReceiver(broadcastReceiver, intentFilter)\n            } else {\n                context.registerReceiver(broadcastReceiver, intentFilter, Context.RECEIVER_EXPORTED)\n            }\n        }\n    }\n}"
  },
  {
    "path": "ultron-common/src/androidMain/kotlin/com/atiurin/ultron/testlifecycle/rulesequence/RuleSequence.kt",
    "content": "package com.atiurin.ultron.testlifecycle.rulesequence\n\nimport com.atiurin.ultron.testlifecycle.setupteardown.RuleSequenceTearDown\nimport org.junit.rules.TestRule\nimport org.junit.runner.Description\nimport org.junit.runners.model.Statement\nimport java.util.concurrent.atomic.AtomicInteger\n\n\n/**\n * The replacement of standart JUnit4 RuleChain class.\n * It provides 3 lists of rules:\n * 1. [firstRules] - will be executed firstly\n * 2. [rules] - will be executed normally after [firstRules]\n * 3. [lastRules] - will be executed last\n *\n * The rules provided in constructor will be added to normal [rules]\n */\nclass RuleSequence : TestRule {\n    private val firstRules: MutableList<RuleContainer> = mutableListOf()\n    private val lastRules: MutableList<RuleContainer> = mutableListOf()\n    private val rules: MutableList<RuleContainer> = mutableListOf()\n\n    private val firstRulesCounter = AtomicInteger(0)\n    private val rulesCounter = AtomicInteger(0)\n    private val lastRulesCounter = AtomicInteger(0)\n\n\n    constructor(rule: TestRule) {\n        this.add(rule)\n    }\n\n    constructor(vararg rules: TestRule) {\n        rules.forEach { this.add(it) }\n    }\n\n\n    fun addFirst(vararg rules: TestRule) = apply {\n        rules.forEach { rule ->\n            if (rule is RuleSequenceTearDown) this.lastRules.add(RuleContainer(-firstRulesCounter.getAndIncrement(), rule))\n            else this.firstRules.add(RuleContainer(firstRulesCounter.getAndIncrement(), rule))\n        }\n    }\n\n    fun add(vararg rules: TestRule) = apply {\n        rules.forEach { rule ->\n            if (rule is RuleSequenceTearDown) this.rules.add(RuleContainer(-rulesCounter.getAndIncrement(), rule))\n            else this.rules.add(RuleContainer(rulesCounter.getAndIncrement(), rule))\n        }\n    }\n\n    fun addLast(vararg rules: TestRule) = apply {\n        rules.forEach { rule ->\n            if (rule is RuleSequenceTearDown) this.firstRules.add(RuleContainer(-lastRulesCounter.getAndIncrement(), rule))\n            else this.lastRules.add(RuleContainer(lastRulesCounter.getAndIncrement(), rule))\n        }\n    }\n\n    override fun apply(base: Statement?, description: Description?): Statement? {\n        var statement = base\n        // we do it in such manner because statement is matreshka\n        // it works with LIFO principle\n        lastRules.sortedByDescending { it.counter }.forEach {\n            statement = it.rule.apply(statement, description)\n        }\n        rules.sortedByDescending { it.counter }.forEach {\n            statement = it.rule.apply(statement, description)\n        }\n        firstRules.sortedByDescending { it.counter }.forEach {\n            statement = it.rule.apply(statement, description)\n        }\n        return statement\n    }\n\n    private class RuleContainer(val counter: Int, val rule: TestRule)\n}\n"
  },
  {
    "path": "ultron-common/src/androidMain/kotlin/com/atiurin/ultron/testlifecycle/setupteardown/Condition.kt",
    "content": "package com.atiurin.ultron.testlifecycle.setupteardown\n\ndata class Condition(val counter: Int, val key: String, val name: String = \"\", val actions: () -> Unit)"
  },
  {
    "path": "ultron-common/src/androidMain/kotlin/com/atiurin/ultron/testlifecycle/setupteardown/ConditionExecutorWrapper.kt",
    "content": "package com.atiurin.ultron.testlifecycle.setupteardown\n\ninterface ConditionExecutorWrapper {\n    fun execute(condition: Condition)\n}"
  },
  {
    "path": "ultron-common/src/androidMain/kotlin/com/atiurin/ultron/testlifecycle/setupteardown/ConditionRule.kt",
    "content": "package com.atiurin.ultron.testlifecycle.setupteardown\n\nimport com.atiurin.ultron.core.config.UltronAndroidCommonConfig\nimport org.junit.internal.AssumptionViolatedException\nimport org.junit.rules.TestRule\nimport org.junit.runner.Description\nimport org.junit.runners.model.MultipleFailureException\nimport org.junit.runners.model.Statement\nimport java.util.*\nimport java.util.concurrent.atomic.AtomicInteger\n\n/**\n * Class to execute setup and teardown methods before and after @Test\n *\n * How it works:\n * The execution order of all lambdas depends on it addition order\n * If lambda added without key it is executed for all @Tests in class\n * Supported the unlimited number of lambdas without key\n */\nabstract class ConditionRule(open val name: String) : TestRule {\n    companion object {\n        private const val COMMON_CONDITION_KEY = \"COMMON_CONDITION_KEY\"\n    }\n\n    private val commonConditionCounter = AtomicInteger(0)\n    internal val commonConditionKeys = mutableListOf<String>()\n    private val conditionCounter = AtomicInteger(0)\n    internal val conditions = mutableListOf<Condition>()\n\n    open var conditionsExecutor: ConditionsExecutor = UltronAndroidCommonConfig.Conditions.conditionsExecutor\n\n    open fun add(key: String = getCommonKey(), name: String = \"\", actions: () -> Unit) = apply {\n        if (key.contains(COMMON_CONDITION_KEY)) {\n            commonConditionKeys.add(key)\n        }\n        conditions.add(Condition(conditionCounter.getAndIncrement(), key, name, actions))\n    }\n\n    private fun getCommonKey(): String {\n        return \"${COMMON_CONDITION_KEY}_${commonConditionCounter.getAndIncrement()}\"\n    }\n\n    override fun apply(base: Statement, description: Description): Statement {\n        return object : Statement() {\n            override fun evaluate() {\n                val errors: MutableList<Throwable> = ArrayList()\n                startingQuietly(description, errors)\n                try {\n                    if (errors.isEmpty()) {\n                        base.evaluate()\n                        succeededQuietly(description, errors)\n                    }\n                } catch (e: AssumptionViolatedException) {\n                    errors.add(e)\n                    skippedQuietly(e, description, errors)\n                } catch (e: Throwable) {\n                    errors.add(e)\n                    failedQuietly(e, description, errors)\n                } finally {\n                    finishedQuietly(description, errors)\n                }\n                MultipleFailureException.assertEmpty(errors)\n            }\n        }\n    }\n\n    open fun succeededQuietly(\n        description: Description, errors: MutableList<Throwable>\n    ) {\n        try {\n            succeeded(description)\n        } catch (e: Throwable) {\n            errors.add(e)\n        }\n    }\n\n    open fun failedQuietly(\n        e: Throwable, description: Description, errors: MutableList<Throwable>\n    ) {\n        try {\n            failed(e, description)\n        } catch (e1: Throwable) {\n            errors.add(e1)\n        }\n    }\n\n    open fun skippedQuietly(\n        e: AssumptionViolatedException, description: Description, errors: MutableList<Throwable>\n    ) {\n        try {\n            if (e is org.junit.AssumptionViolatedException) {\n                skipped(e, description)\n            } else {\n                skipped(e, description)\n            }\n        } catch (e1: Throwable) {\n            errors.add(e1)\n        }\n    }\n\n    open fun startingQuietly(\n        description: Description, errors: MutableList<Throwable>\n    ) {\n        try {\n            starting(description)\n        } catch (e: Throwable) {\n            errors.add(e)\n        }\n    }\n\n    open fun finishedQuietly(\n        description: Description, errors: MutableList<Throwable>\n    ) {\n        try {\n            finished(description)\n        } catch (e: Throwable) {\n            errors.add(e)\n        }\n    }\n\n\n    /**\n     * Invoked when a test succeeds\n     */\n    protected open fun succeeded(description: Description) {}\n\n    /**\n     * Invoked when a test fails\n     */\n    protected open fun failed(e: Throwable?, description: Description) {}\n\n    /**\n     * Invoked when a test is skipped due to a failed assumption.\n     */\n    protected open fun skipped(e: org.junit.AssumptionViolatedException, description: Description) {\n        // For backwards compatibility with JUnit 4.11 and earlier, call the legacy version\n        val asInternalException: AssumptionViolatedException = e\n        skipped(asInternalException, description)\n    }\n\n    /**\n     * Invoked when a test is skipped due to a failed assumption.\n     *\n     */\n    @Deprecated(\"use {@link #skipped(AssumptionViolatedException, Description)}\")\n    protected open fun skipped(e: AssumptionViolatedException?, description: Description) {\n    }\n\n    /**\n     * Invoked when a test is about to start\n     */\n    open fun starting(description: Description) {}\n\n    /**\n     * Invoked when a test method finishes (whether passing or failing)\n     */\n    open fun finished(description: Description) {}\n\n    /**\n     * Required due to parametrized test name\n     */\n    internal fun getMethodName(rawName: String): String {\n        val bracketIndex = rawName.indexOf('[')\n        return if(bracketIndex != -1) {\n            rawName.substring(0, bracketIndex)\n        } else {\n            rawName\n        }\n    }\n}"
  },
  {
    "path": "ultron-common/src/androidMain/kotlin/com/atiurin/ultron/testlifecycle/setupteardown/ConditionsExecutor.kt",
    "content": "package com.atiurin.ultron.testlifecycle.setupteardown\n\nimport kotlin.reflect.KClass\n\ninterface ConditionsExecutor {\n    val conditionExecutor: ConditionExecutorWrapper\n    fun before(name: String, ruleClass: KClass<*>)\n    fun execute(conditions: List<Condition>, keys: List<String>, description: String = \"\")\n}"
  },
  {
    "path": "ultron-common/src/androidMain/kotlin/com/atiurin/ultron/testlifecycle/setupteardown/DefaultConditionExecutorWrapper.kt",
    "content": "package com.atiurin.ultron.testlifecycle.setupteardown\n\nimport com.atiurin.ultron.log.UltronLog\n\nclass DefaultConditionExecutorWrapper : ConditionExecutorWrapper {\n    override fun execute(condition: Condition) {\n        UltronLog.info(\"Execute condition '${condition.name}' with key '${condition.key}'\")\n        condition.actions()\n    }\n}"
  },
  {
    "path": "ultron-common/src/androidMain/kotlin/com/atiurin/ultron/testlifecycle/setupteardown/DefaultConditionsExecutor.kt",
    "content": "package com.atiurin.ultron.testlifecycle.setupteardown\n\nimport com.atiurin.ultron.core.config.UltronAndroidCommonConfig\nimport com.atiurin.ultron.log.UltronLog\nimport kotlin.reflect.KClass\n\nopen class DefaultConditionsExecutor : ConditionsExecutor {\n    override val conditionExecutor: ConditionExecutorWrapper by lazy { UltronAndroidCommonConfig.Conditions.conditionExecutorWrapper }\n    override fun before(name: String, ruleClass: KClass<*>) {\n        UltronLog.info(\"Execute ${ruleClass.simpleName} '$name' conditions\")\n    }\n    override fun execute(conditions: List<Condition>, keys: List<String>, description: String) {\n        conditions\n            .sortedBy { it.counter }\n            .filter { it.key in keys }\n            .forEach { condition ->\n                conditionExecutor.execute(condition)\n            }\n    }\n}"
  },
  {
    "path": "ultron-common/src/androidMain/kotlin/com/atiurin/ultron/testlifecycle/setupteardown/RuleSequenceTearDown.kt",
    "content": "package com.atiurin.ultron.testlifecycle.setupteardown\n\ninterface RuleSequenceTearDown"
  },
  {
    "path": "ultron-common/src/androidMain/kotlin/com/atiurin/ultron/testlifecycle/setupteardown/SetUp.kt",
    "content": "package com.atiurin.ultron.testlifecycle.setupteardown\n\nimport kotlin.annotation.Retention\n\n@Retention(AnnotationRetention.RUNTIME)\n@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)\nannotation class SetUp(vararg val value: String)"
  },
  {
    "path": "ultron-common/src/androidMain/kotlin/com/atiurin/ultron/testlifecycle/setupteardown/SetUpRule.kt",
    "content": "package com.atiurin.ultron.testlifecycle.setupteardown\n\nimport org.junit.runner.Description\n\nopen class SetUpRule(override val name: String = \"\") : ConditionRule(name) {\n    override fun starting(description: Description) {\n        val keys = mutableListOf<String>().apply { this.addAll(commonConditionKeys) }\n        val method = description.testClass.getMethod(getMethodName(description.methodName))\n        if (method.isAnnotationPresent(SetUp::class.java)) {\n            val setUpAnnotation = method.getAnnotation(SetUp::class.java)\n            if (setUpAnnotation != null) {\n                keys.addAll(setUpAnnotation.value.toList()) //get the list of keys in annotation SetUp\n            }\n            conditionsExecutor.before(name, this::class)\n            conditionsExecutor.execute(conditions, keys, name)\n        } else {\n            conditionsExecutor.before(name, this::class)\n            conditionsExecutor.execute(conditions, commonConditionKeys, name)\n        }\n        super.starting(description)\n    }\n}"
  },
  {
    "path": "ultron-common/src/androidMain/kotlin/com/atiurin/ultron/testlifecycle/setupteardown/TearDown.kt",
    "content": "package com.atiurin.ultron.testlifecycle.setupteardown\n\n@Retention(AnnotationRetention.RUNTIME)\n@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)\nannotation class TearDown(vararg val value: String)"
  },
  {
    "path": "ultron-common/src/androidMain/kotlin/com/atiurin/ultron/testlifecycle/setupteardown/TearDownRule.kt",
    "content": "package com.atiurin.ultron.testlifecycle.setupteardown\n\nimport org.junit.runner.Description\n\nopen class TearDownRule(override val name: String = \"\") : ConditionRule(name), RuleSequenceTearDown {\n    override fun finished(description: Description) {\n        val keys = mutableListOf<String>().apply { this.addAll(commonConditionKeys) }\n        val method = description.testClass.getMethod(getMethodName(description.methodName))\n        if (method.isAnnotationPresent(TearDown::class.java)) {\n            val tearDownAnnotation = method.getAnnotation(TearDown::class.java)\n            if (tearDownAnnotation != null) {\n                keys.addAll(tearDownAnnotation.value.toList()) //get the list of keys in annotation TearDown\n            }\n            conditionsExecutor.before(name, this::class)\n            conditionsExecutor.execute(conditions, keys, name)\n        } else {\n            conditionsExecutor.before(name, this::class)\n            conditionsExecutor.execute(conditions, commonConditionKeys, name)\n        }\n        super.finished(description)\n    }\n}"
  },
  {
    "path": "ultron-common/src/androidMain/kotlin/com/atiurin/ultron/utils/ActivityUtil.android.kt.kt",
    "content": "package com.atiurin.ultron.utils\n\nimport android.app.Activity\nimport androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry\nimport androidx.test.runner.lifecycle.Stage\nimport com.atiurin.ultron.log.UltronLog\n\nobject ActivityUtil {\n\n    fun getResumedActivity(): Activity? {\n        var resumedActivity: Activity? = null\n\n        val findResumedActivity = {\n            val resumedActivities = ActivityLifecycleMonitorRegistry.getInstance()\n                .getActivitiesInStage(Stage.RESUMED)\n            if (resumedActivities.iterator().hasNext()) {\n                resumedActivity = resumedActivities.iterator().next()\n            }\n        }\n\n        runOnUiThread { findResumedActivity() }\n\n        resumedActivity ?: UltronLog.error(\"No resumed activity found\")\n        return resumedActivity\n    }\n}"
  },
  {
    "path": "ultron-common/src/androidMain/kotlin/com/atiurin/ultron/utils/InstrumentationUtil.android.kt",
    "content": "package com.atiurin.ultron.utils\n\nimport android.content.Context\nimport android.graphics.Color\nimport android.graphics.drawable.Drawable\nimport android.os.Build\nimport android.os.Looper\nimport androidx.annotation.*\nimport androidx.test.platform.app.InstrumentationRegistry\nimport com.atiurin.ultron.exceptions.UltronException\nimport java.io.File\nimport java.util.*\nimport java.util.concurrent.ExecutionException\nimport java.util.concurrent.FutureTask\n\nfun getResourceName(@AnyRes resourceId: Int, context: Context): String {\n    return context.resources.getResourceName(resourceId)\n}\n\nfun getTargetResourceName(@AnyRes resourceId: Int): String {\n    return getResourceName(resourceId, InstrumentationRegistry.getInstrumentation().targetContext)\n}\n\nfun getTestResourceName(@AnyRes resourceId: Int): String {\n    return getResourceName(resourceId, InstrumentationRegistry.getInstrumentation().context)\n}\n\nfun getString(@StringRes resourceId: Int, context: Context): String {\n    return context.getString(resourceId)\n}\n\nfun getTargetString(@StringRes resourceId: Int): String {\n    return getString(resourceId, InstrumentationRegistry.getInstrumentation().targetContext)\n}\n\nfun getTestString(@StringRes resourceId: Int): String {\n    return getString(resourceId, InstrumentationRegistry.getInstrumentation().context)\n}\n\nfun getDrawable(@DrawableRes resourceId: Int, context: Context): Drawable? {\n    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {\n        context.getDrawable(resourceId)\n    } else {\n        throw UltronException(\"getDrawable is not supported for SDKs lower ${Build.VERSION_CODES.LOLLIPOP}\")\n    }\n}\n\nfun getTargetDrawable(@DrawableRes resourceId: Int): Drawable? {\n    return getDrawable(resourceId, InstrumentationRegistry.getInstrumentation().targetContext)\n}\n\nfun getTestDrawable(@DrawableRes resourceId: Int): Drawable? {\n    return getDrawable(resourceId, InstrumentationRegistry.getInstrumentation().context)\n}\n\nfun getColor(@ColorRes colorResId: Int, context: Context): Int {\n    return if (Build.VERSION.SDK_INT <= 22) context.resources.getColor(colorResId) else context.getColor(colorResId)\n}\n\nfun getTargetColor(@ColorRes colorResId: Int): Int {\n    return getColor(colorResId, InstrumentationRegistry.getInstrumentation().targetContext)\n}\n\nfun getTestColor(@ColorRes colorResId: Int): Int {\n    return getColor(colorResId, InstrumentationRegistry.getInstrumentation().context)\n}\n\nfun getColorHex(color: Int): String {\n    return String.format(\n        Locale.ROOT, \"#%02X%06X\", 0xFF and Color.alpha(color), 0xFFFFFF and color\n    )\n}\n\nfun <T> runOnUiThread(action: () -> T): T {\n    if (Looper.myLooper() == Looper.getMainLooper()) {\n        return action()\n    }\n\n    // Note: This implementation is directly taken from ActivityTestRule\n    val task: FutureTask<T> = FutureTask(action)\n    InstrumentationRegistry.getInstrumentation().runOnMainSync(task)\n    try {\n        return task.get()\n    } catch (e: ExecutionException) {\n        throw e.cause!!\n    }\n}\n\nfun createCacheFile(prefix: String = \"temp\", suffix: String? = null): File {\n    val cacheDir = InstrumentationRegistry.getInstrumentation().targetContext.cacheDir\n    return File.createTempFile(prefix, suffix, cacheDir)\n}"
  },
  {
    "path": "ultron-common/src/androidMain/kotlin/com/atiurin/ultron/utils/ThreadUtil.android.kt",
    "content": "package com.atiurin.ultron.utils\n\nimport android.os.SystemClock\n\nactual fun sleep(timeMs: Long) {\n    SystemClock.sleep(timeMs)\n}"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/annotations/ExperimentalUltronApi.kt",
    "content": "package com.atiurin.ultron.annotations\n\nimport kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS\nimport kotlin.annotation.AnnotationTarget.CLASS\nimport kotlin.annotation.AnnotationTarget.CONSTRUCTOR\nimport kotlin.annotation.AnnotationTarget.FIELD\nimport kotlin.annotation.AnnotationTarget.FUNCTION\nimport kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE\nimport kotlin.annotation.AnnotationTarget.PROPERTY\nimport kotlin.annotation.AnnotationTarget.PROPERTY_GETTER\nimport kotlin.annotation.AnnotationTarget.PROPERTY_SETTER\nimport kotlin.annotation.AnnotationTarget.TYPEALIAS\nimport kotlin.annotation.AnnotationTarget.VALUE_PARAMETER\n\n@RequiresOptIn\n@MustBeDocumented\n@Target(\n    CLASS,\n    ANNOTATION_CLASS,\n    PROPERTY,\n    FIELD,\n    LOCAL_VARIABLE,\n    VALUE_PARAMETER,\n    CONSTRUCTOR,\n    FUNCTION,\n    PROPERTY_GETTER,\n    PROPERTY_SETTER,\n    TYPEALIAS\n)\n@Retention(AnnotationRetention.BINARY)\npublic annotation class ExperimentalUltronApi"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/AbstractOperationLifecycle.kt",
    "content": "package com.atiurin.ultron.core.common\n\nimport com.atiurin.ultron.core.config.UltronCommonConfig\nimport com.atiurin.ultron.listeners.AbstractListenersContainer\nimport com.atiurin.ultron.listeners.UltronLifecycleListener\n\nabstract class AbstractOperationLifecycle : AbstractListenersContainer<UltronLifecycleListener>() {\n\n    //set your own implementation in case you would like to customise the behaviour\n    open var operationProcessor: OperationProcessor = object : OperationProcessor {\n        override fun <Op : Operation, OpRes : OperationResult<Op>> process(executor: OperationExecutor<Op, OpRes>): OpRes {\n            return executor.execute()\n        }\n    }\n\n    /**\n     * @param executor contains all info for operation execution\n     * @param resultHandler the point to access operation result by external analyzers\n     * @return [OperationResult] of operation execution\n     */\n    inline fun <Op : Operation, OpRes : OperationResult<Op>> execute(\n        executor: OperationExecutor<Op, OpRes>,\n        resultHandler: (OpRes) -> Unit = {}\n    ): OpRes {\n        val lifecycleListeners = getListeners() + UltronCommonConfig.getListeners()\n        val isListen = executor.operation.type !in UltronCommonConfig.operationsExcludedFromListeners\n                && UltronCommonConfig.isListenersOn\n        if (isListen) lifecycleListeners.forEach { it.before(executor.operation) }\n        val operationResult = operationProcessor.process(executor)\n        if (isListen) {\n            if (operationResult.success) {\n                lifecycleListeners.forEach { it.afterSuccess(operationResult as OperationResult<Operation>) }\n            } else {\n                lifecycleListeners.forEach { it.afterFailure(operationResult as OperationResult<Operation>) }\n            }\n            lifecycleListeners.forEach { it.after(operationResult as OperationResult<Operation>) }\n        }\n        resultHandler(operationResult)\n        return operationResult\n    }\n\n}"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/DefaultElementInfo.kt",
    "content": "package com.atiurin.ultron.core.common\n\ndata class DefaultElementInfo(override var name: String = \"\", override var meta: Any? = null) : ElementInfo {\n    override fun copy(): DefaultElementInfo {\n        return DefaultElementInfo(name, meta)\n    }\n}\n\n"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/DefaultOperationIterationResult.kt",
    "content": "package com.atiurin.ultron.core.common\n\ndata class DefaultOperationIterationResult(\n    override val success: Boolean,\n    override val exception: Throwable?\n) : OperationIterationResult"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/ElementInfo.kt",
    "content": "package com.atiurin.ultron.core.common\n\ninterface ElementInfo {\n    var name: String\n    var meta: Any?\n\n    fun copy(): ElementInfo\n}\n\n"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/Operation.kt",
    "content": "package com.atiurin.ultron.core.common\n\nimport com.atiurin.ultron.core.common.assertion.OperationAssertion\n\ninterface Operation {\n    val name: String\n    val description: String\n    val type: UltronOperationType\n    val timeoutMs: Long\n    val assertion: OperationAssertion\n    val elementInfo: ElementInfo\n    fun execute(): OperationIterationResult\n}\n"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/OperationExecutor.kt",
    "content": "package com.atiurin.ultron.core.common\n\nimport com.atiurin.ultron.exceptions.UltronAssertionBlockException\nimport com.atiurin.ultron.exceptions.UltronException\nimport com.atiurin.ultron.extensions.simpleClassName\nimport com.atiurin.ultron.utils.measureTimeMillis\nimport com.atiurin.ultron.utils.nowMs\nimport com.atiurin.ultron.utils.sleep\nimport kotlin.reflect.KClass\n\ninterface OperationExecutor<Op : Operation, OpRes : OperationResult<Op>> {\n    val operation: Op\n    val pollingTimeout: Long\n    val descriptor: ResultDescriptor\n    var doBetweenOperationRetry: () -> Unit\n\n    fun getAllowedExceptions(operation: Operation): List<KClass<out Throwable>>\n\n    fun generateResult(\n        success: Boolean,\n        exceptions: List<Throwable>,\n        description: String,\n        lastOperationIterationResult: OperationIterationResult?,\n        executionTimeMs: Long\n    ): OpRes\n\n    fun getWrapperException(originalException: Throwable): Throwable\n\n    fun execute(): OpRes {\n        return descriptor.nestedOperation { execWithAssertion(operation.timeoutMs, null, true) }\n    }\n\n    private tailrec fun execWithAssertion(\n        operationDuration: Long,\n        previousResult: OpRes?,\n        isFirstIteration: Boolean = false\n    ): OpRes {\n        var result = execOperation(operationDuration, previousResult)\n        if (result.success) {\n            result = execAssertion(result)\n            if (!result.success && (operationDuration > result.executionTimeMs || isFirstIteration)) {\n                return execWithAssertion(operationDuration, result)\n            }\n        }\n        return result\n    }\n\n    fun isExceptionInList(\n        exception: Throwable,\n        exceptionClasses: List<KClass<out Throwable>>\n    ): Boolean {\n        val exceptionClass = exception::class\n        return exceptionClasses.any { it == exceptionClass }\n    }\n\n    fun execOperation(operationDuration: Long, previousResult: OpRes?): OpRes {\n        var isSuccess: Boolean = false\n        val description = StringBuilder()\n        descriptor.appendLine(description, \"------ Operation ${operation.name} ------\")\n        descriptor.increaseLevel()\n        val exceptions = mutableListOf<Throwable>()\n        val endTime = nowMs() + operationDuration\n        var lastIteration: OperationIterationResult? = null\n        val execTime = measureTimeMillis {\n            try {\n                do {\n                    val result = operation.execute()\n                    isSuccess = result.success\n                    if (!isSuccess) {\n                        val error =\n                            result.exception ?: UltronException(\"Create an issue to ULTRON project\")\n                        if (isExceptionInList(error, getAllowedExceptions(operation))) {\n                            if (!exceptions.any { it.simpleClassName() == error.simpleClassName() && it.message == error.message }) {\n                                exceptions.add(error)\n                            }\n                        } else {\n                            descriptor.appendLine(\n                                description,\n                                \"Not allowed exception occurs - ${error.simpleClassName()}, cause - ${error.cause}\"\n                            )\n                            exceptions.add(error)\n                            throw error\n                        }\n                    }\n                    lastIteration = result\n                    if (!isSuccess) {\n                        doBetweenOperationRetry()\n                        if (pollingTimeout > 0) sleep(pollingTimeout)\n                    }\n\n                } while (nowMs() < endTime && !isSuccess)\n            } catch (th: Throwable) {\n                isSuccess = false // just make sure we will have correct action status\n            }\n        }\n        val operationExceptions = exceptions.map { getWrapperException(it) }\n            .distinctBy { \"${it.simpleClassName()} + ${it.message}\" }\n        descriptor.describeResult(description, isSuccess, execTime, operationExceptions)\n        descriptor.decreaseLevel()\n        descriptor.append(description, \"------ End of operation '${operation.name}' ------\")\n        val operationResult = generateResult(\n            success = isSuccess,\n            exceptions = operationExceptions,\n            description = description.toString(),\n            lastOperationIterationResult = lastIteration,\n            executionTimeMs = execTime\n        )\n        return mergeResults(previousResult, operationResult)\n    }\n\n    fun execAssertion(previousResult: OpRes): OpRes {\n        var isSuccess = true\n        val description = StringBuilder()\n        descriptor.appendLine(\n            description,\n            \"\\n------ Assertion block '${operation.assertion.name}' ------\"\n        )\n        descriptor.increaseLevel()\n        val exceptions: MutableList<Throwable> = mutableListOf()\n        val assertionExecTime = measureTimeMillis {\n            try {\n                operation.assertion.block.invoke()\n                descriptor.appendLine(description, \"Result = PASSED!\")\n            } catch (ex: Throwable) {\n                isSuccess = false\n                val originalException = getWrapperException(ex)\n                exceptions.add(\n                    UltronAssertionBlockException(\n                        \"\"\"\n                        |Exception in assertion block '${operation.assertion.name}' of operation '${operation.name}'. \n                        |${\"exception: ${originalException::class.simpleName}\".prefixTab()}\n                        |${\"message: ${originalException.message}\".prefixTabForAllLines()}\n                    \"\"\".trimMargin()\n                    )\n                )\n            }\n        }\n        descriptor.describeResult(description, isSuccess, assertionExecTime, exceptions)\n        descriptor.decreaseLevel()\n        descriptor.appendLine(\n            description,\n            \"------ End of assertion block '${operation.assertion.name}' ------\"\n        )\n        val assertionResult = generateResult(\n            success = isSuccess,\n            exceptions = exceptions,\n            description = description.toString(),\n            lastOperationIterationResult = null,\n            executionTimeMs = assertionExecTime\n        )\n        return mergeResults(previousResult, assertionResult)\n    }\n\n    private fun mergeResults(previousResult: OpRes?, currentResult: OpRes): OpRes {\n        return generateResult(\n            success = currentResult.success,\n            exceptions = mutableListOf<Throwable>().apply {\n                previousResult?.let { addAll(it.exceptions) }\n                addAll(currentResult.exceptions)\n            },\n            description = previousResult?.let { it.description + \"\\n\" + currentResult.description }\n                ?: currentResult.description,\n            lastOperationIterationResult = currentResult.operationIterationResult\n                ?: previousResult?.operationIterationResult,\n            executionTimeMs = (previousResult?.executionTimeMs ?: 0) + currentResult.executionTimeMs\n        )\n    }\n}\n\n\n"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/OperationIterationResult.kt",
    "content": "package com.atiurin.ultron.core.common\n\ninterface OperationIterationResult {\n    val success: Boolean\n    val exception: Throwable?\n}"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/OperationProcessor.kt",
    "content": "package com.atiurin.ultron.core.common\n\ninterface OperationProcessor {\n    fun <Op : Operation, OpRes : OperationResult<Op>> process(executor: OperationExecutor<Op, OpRes>): OpRes\n}"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/OperationResult.kt",
    "content": "package com.atiurin.ultron.core.common\n\n/**\n * Interface contains references to [Operation]\n */\ninterface OperationResult<T : Operation> {\n    val operation: T\n    val success: Boolean\n    val exceptions: List<Throwable>\n    var description: String\n    var operationIterationResult: OperationIterationResult?\n    val executionTimeMs: Long\n}"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/ResultDescriptor.kt",
    "content": "package com.atiurin.ultron.core.common\n\nclass ResultDescriptor {\n    private var nestingLevel = 1\n\n    fun <T> nestedOperation(block: () -> T): T {\n        nestingLevel++\n        val result = block()\n        nestingLevel--\n        return result\n    }\n\n    fun increaseLevel() {\n        nestingLevel += 1\n    }\n\n    fun decreaseLevel() {\n        nestingLevel -= 1\n    }\n\n    fun describeResult(\n        descBuilder: StringBuilder,\n        isSuccess: Boolean,\n        execTimeMs: Long,\n        exceptions: List<Throwable>\n    ) = apply {\n        if (!isSuccess && exceptions.isNotEmpty()) {\n            descBuilder\n                .prefixAllLinesWithTab(nestingLevel, \"Result = FAILED (${execTimeMs} ms) \")\n                .prefixAllLinesWithTab(\n                    nestingLevel,\n                    if (exceptions.size > 1) {\n                        \"\"\"\n                    |Errors were caught: \n                    |${exceptions.map { \"- '${it::class.simpleName}', message: '${it.message}' cause: '${it.cause}'\\n\" }}\n                    |Last error is ${exceptions.last()::class.simpleName}\n                    |message: ${exceptions.last().message}\n                    \"\"\".trimMargin()\n                    } else {\n                        \"\"\"\n                    |exception: ${exceptions.last()::class.simpleName} \n                    |message: ${exceptions.last().message}\n                    \"\"\".trimMargin()\n                    }\n                )\n        } else {\n            descBuilder.prefixAllLinesWithTab(nestingLevel, \"Result = PASSED ($execTimeMs ms)\")\n        }\n    }\n\n    fun append(descBuilder: StringBuilder, text: String): StringBuilder {\n        return descBuilder.prefixWithTab(nestingLevel, text)\n    }\n\n    fun appendLine(descBuilder: StringBuilder, text: String): StringBuilder {\n        return descBuilder.prefixAllLinesWithTab(nestingLevel, text)\n    }\n\n\n    private fun StringBuilder.prefixAllLinesWithTab(tabCount: Int, text: String) = apply {\n        val prefix = StringBuilder(\"\")\n        repeat(tabCount) { prefix.append(\"\\t\") }\n        text.lines().forEach {\n            this.appendLine(\"$prefix$it\")\n        }\n    }\n\n    private fun StringBuilder.prefixWithTab(tabCount: Int, text: String) = apply {\n        val prefix = StringBuilder(\"\")\n        repeat(tabCount) { prefix.append(\"\\t\") }\n        this.append(\"$prefix$text\")\n    }\n}\n\nfun String.prefixTab() = \"\\t$this\"\n\nfun String.prefixTabForAllLines(): String {\n    val result = StringBuilder()\n    val lines = this.lines()\n    lines.forEachIndexed { index, line ->\n        if (lines.lastIndex == index) result.append(\"\\t$line\")\n        else result.appendLine(\"\\t$line\")\n    }\n    return result.toString()\n}\n\n"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/UltronOperationType.kt",
    "content": "package com.atiurin.ultron.core.common\n\ninterface UltronOperationType\n\nenum class CommonOperationType : UltronOperationType { DEFAULT }\n"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/assertion/DefaultOperationAssertion.kt",
    "content": "package com.atiurin.ultron.core.common.assertion\n\nopen class DefaultOperationAssertion(override val name: String, override val block: () -> Unit) : OperationAssertion"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/assertion/EmptyOperationAssertion.kt",
    "content": "package com.atiurin.ultron.core.common.assertion\n\nclass EmptyOperationAssertion : OperationAssertion {\n    override val name: String\n        get() = \"\"\n    override val block: () -> Unit\n        get() = {}\n}\n\nfun OperationAssertion.isEmptyAssertion(): Boolean = this is EmptyOperationAssertion"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/assertion/NoListenersOperationAssertion.kt",
    "content": "package com.atiurin.ultron.core.common.assertion\n\nimport com.atiurin.ultron.listeners.setListenersState\n\nclass NoListenersOperationAssertion(override val name: String, override val block: () -> Unit) :\n    DefaultOperationAssertion(name, block.setListenersState(false))"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/assertion/OperationAssertion.kt",
    "content": "package com.atiurin.ultron.core.common.assertion\n\ninterface OperationAssertion {\n    val name: String\n    val block: () -> Unit\n}\n\n"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/assertion/SoftAssertion.kt",
    "content": "package com.atiurin.ultron.core.common.assertion\n\nimport com.atiurin.ultron.core.config.UltronCommonConfig\nimport com.atiurin.ultron.log.UltronLog\n\nfun softAssertion(failOnException: Boolean = true, block: () -> Unit){\n    UltronLog.info(\"Start soft assertion context\")\n    with(UltronCommonConfig.testContext){\n        softAssertion = true\n        block()\n        softAssertion = false\n        if (failOnException){\n            softAnalyzer.verify()\n        }\n    }\n    UltronLog.info(\"Finish soft assertion context\")\n}\n\nfun verifySoftAssertions(){\n    UltronCommonConfig.testContext.softAnalyzer.verify()\n}"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/options/ClickOption.kt",
    "content": "package com.atiurin.ultron.core.common.options\n\ndata class ClickOption(val xOffset: Long = 0, val yOffset: Long = 0)\n"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/options/ContentDescriptionContainsOption.kt",
    "content": "package com.atiurin.ultron.core.common.options\n\ndata class ContentDescriptionContainsOption(\n    val substring: Boolean = false,\n    val ignoreCase: Boolean = false\n)\n"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/options/DoubleClickOption.kt",
    "content": "package com.atiurin.ultron.core.common.options\n\ndata class DoubleClickOption(\n    val xOffset: Long = 0,\n    val yOffset: Long = 0,\n    val delayMs: Long? = null\n)\n"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/options/LongClickOption.kt",
    "content": "package com.atiurin.ultron.core.common.options\n\ndata class LongClickOption(\n    val xOffset: Long = 0,\n    val yOffset: Long = 0,\n    val durationMs: Long? = null\n)"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/options/PerformCustomBlockOption.kt",
    "content": "package com.atiurin.ultron.core.common.options\n\nimport com.atiurin.ultron.core.common.UltronOperationType\n\ndata class PerformCustomBlockOption(\n    val operationType: UltronOperationType,\n    val description: String = \"\"\n)"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/options/TextContainsOption.kt",
    "content": "package com.atiurin.ultron.core.common.options\n\ndata class TextContainsOption(\n    val substring: Boolean = false,\n    val ignoreCase: Boolean = false\n)\n"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/options/TextEqualsOption.kt",
    "content": "package com.atiurin.ultron.core.common.options\n\ndata class TextEqualsOption(val includeEditableText: Boolean = true)"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/resultanalyzer/CheckOperationResultAnalyzer.kt",
    "content": "package com.atiurin.ultron.core.common.resultanalyzer\n\nimport com.atiurin.ultron.core.common.Operation\nimport com.atiurin.ultron.core.common.OperationResult\n\nclass CheckOperationResultAnalyzer : OperationResultAnalyzer {\n    override fun <Op : Operation, OpRes : OperationResult<Op>> analyze(operationResult: OpRes): Boolean {\n        return operationResult.success\n    }\n}"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/resultanalyzer/DefaultSoftAssertionOperationResultAnalyzer.kt",
    "content": "package com.atiurin.ultron.core.common.resultanalyzer\n\nimport com.atiurin.ultron.core.common.Operation\nimport com.atiurin.ultron.core.common.OperationResult\nimport com.atiurin.ultron.exceptions.UltronOperationException\n\nclass DefaultSoftAssertionOperationResultAnalyzer : SoftAssertionOperationResultAnalyzer {\n    private val caughtExceptions = mutableListOf<Throwable>()\n    lateinit var originalAnalyzer: OperationResultAnalyzer\n\n    override fun <Op : Operation, OpRes : OperationResult<Op>> analyze(operationResult: OpRes): Boolean {\n        return kotlin.runCatching {\n            originalAnalyzer.analyze(operationResult)\n        }.onFailure { ex ->\n            caughtExceptions.add(ex)\n        }.isSuccess\n    }\n\n    override fun clear(){\n        caughtExceptions.clear()\n    }\n\n    override fun verify(){\n        val message = StringBuilder()\n        if (caughtExceptions.isNotEmpty()){\n            val delimiter = \"========================================================================================\"\n            caughtExceptions.forEach { ex ->\n                message.appendLine(ex.message)\n                message.appendLine(delimiter)\n            }\n            clear()\n            throw UltronOperationException(\"SOFT ASSERTION FAILED. Details:\\n$message\")\n        }\n    }\n}"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/resultanalyzer/OperationResultAnalyzer.kt",
    "content": "package com.atiurin.ultron.core.common.resultanalyzer\n\nimport com.atiurin.ultron.core.common.Operation\nimport com.atiurin.ultron.core.common.OperationResult\n\ninterface OperationResultAnalyzer {\n    /**\n     * @return success status of operation execution\n     */\n    fun <Op : Operation, OpRes : OperationResult<Op>> analyze(operationResult: OpRes): Boolean\n}"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/resultanalyzer/SoftAssertionOperationResultAnalyzer.kt",
    "content": "package com.atiurin.ultron.core.common.resultanalyzer\n\ninterface SoftAssertionOperationResultAnalyzer : OperationResultAnalyzer {\n    /**\n     * Clears all previously caught exceptions, effectively resetting the internal state.\n     * Use this method when starting a new set of assertions to ensure\n     * that previous exceptions do not affect the current verification process.\n     */\n    fun clear()\n\n    /**\n     * Verifies whether any exceptions were caught during previous operations.\n     * If there were caught exceptions, this method throws a general exception summarizing them.\n     * Use this method at the end of your test or operation to ensure that all assertions passed.\n     *\n     * @throws Exception if one or more exceptions were previously caught.\n     */\n    fun verify()\n}"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/common/resultanalyzer/UltronDefaultOperationResultAnalyzer.kt",
    "content": "package com.atiurin.ultron.core.common.resultanalyzer\n\nimport com.atiurin.ultron.core.common.Operation\nimport com.atiurin.ultron.core.common.OperationResult\nimport com.atiurin.ultron.core.common.assertion.isEmptyAssertion\nimport com.atiurin.ultron.exceptions.UltronException\nimport com.atiurin.ultron.exceptions.UltronOperationException\n\nopen class UltronDefaultOperationResultAnalyzer : OperationResultAnalyzer {\n    override fun <Op : Operation, OpRes : OperationResult<Op>> analyze(operationResult: OpRes): Boolean {\n        if (!operationResult.success) {\n            val exceptionToThrow = operationResult.exceptions.lastOrNull()\n                ?: UltronException(\n                    \"\"\"Unknown exception occurs during operation '${operationResult.operation.name}'. \n                | ! Operation result is '${operationResult.success}'.\n                | ! Operation result description: ${operationResult.description}\n                |\"\"\"\".trimMargin()\n                )\n            val delimiter = \"----------------------------------------------------------------------------------------\"\n            val assertion = if (!operationResult.operation.assertion.isEmptyAssertion()) \"\\nwith assertion block '${operationResult.operation.assertion.name}'\" else \"\"\n            throw UltronOperationException(\n                message = \"\"\"Failed '${operationResult.operation.name}' during ${operationResult.executionTimeMs}\n                | > $delimiter\n                | ! Begin of operation '${operationResult.operation.name}'$assertion\n                | ! $delimiter\n                | ! ${operationResult.description.lines().joinToString(\"\\n !\")}\n                | ! $delimiter\n                | ! End of operation '${operationResult.operation.name}'$assertion      \n                | > $delimiter\n                \"\"\".trimMargin(), exceptionToThrow\n            )\n        }\n        return operationResult.success\n    }\n}"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/config/UltronCommonConfig.kt",
    "content": "package com.atiurin.ultron.core.config\n\nimport com.atiurin.ultron.core.common.UltronOperationType\nimport com.atiurin.ultron.core.test.context.DefaultUltronTestContext\nimport com.atiurin.ultron.core.test.context.UltronTestContext\nimport com.atiurin.ultron.core.common.resultanalyzer.OperationResultAnalyzer\nimport com.atiurin.ultron.core.common.resultanalyzer.UltronDefaultOperationResultAnalyzer\nimport com.atiurin.ultron.listeners.AbstractListenersContainer\nimport com.atiurin.ultron.listeners.UltronLifecycleListener\n\nobject UltronCommonConfig : AbstractListenersContainer<UltronLifecycleListener>() {\n    val operationsExcludedFromListeners: MutableList<UltronOperationType> = mutableListOf()\n    var operationTimeoutMs: Long = 5_000\n    var isListenersOn = true\n    var logDateFormat = \"MM-dd HH:mm:ss.SSS\"\n    var logToFile: Boolean = true\n    var resultAnalyzer: OperationResultAnalyzer = UltronDefaultOperationResultAnalyzer()\n    var testContext: UltronTestContext = DefaultUltronTestContext()\n\n    class Defaults {\n        companion object {\n            const val OPERATION_TIMEOUT_MS = 5_000L\n            const val POLLING_TIMEOUT_MS = 0L\n        }\n    }\n}"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/test/TestMethod.kt",
    "content": "package com.atiurin.ultron.core.test\n\nimport com.atiurin.ultron.core.config.UltronCommonConfig\nimport com.atiurin.ultron.core.test.context.UltronTestContext\n\nclass TestMethod(testContext: UltronTestContext) {\n    init {\n        UltronCommonConfig.testContext = testContext\n    }\n\n    private var beforeTest: TestMethod.() -> Unit = {}\n    private var afterTest: TestMethod.() -> Unit = {}\n    private var test: TestMethod.() -> Unit = {}\n\n    internal fun attack() {\n        var throwable: Throwable? = null\n        beforeTest()\n        runCatching(test).onFailure { ex ->\n            throwable = ex\n        }\n        runCatching(afterTest).onFailure { ex ->\n            throwable?.let { throw it }\n            throw ex\n        }\n        throwable?.let { throw it }\n    }\n\n    fun before(block: TestMethod.() -> Unit) = apply {\n        beforeTest = block\n    }\n\n    fun after(block: TestMethod.() -> Unit) = apply {\n        afterTest = block\n    }\n\n    fun go(block: TestMethod.() -> Unit) = apply {\n        test = block\n    }\n}\n"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/test/UltronTest.kt",
    "content": "package com.atiurin.ultron.core.test\n\nimport com.atiurin.ultron.annotations.ExperimentalUltronApi\nimport com.atiurin.ultron.core.test.context.DefaultUltronTestContextProvider\nimport com.atiurin.ultron.core.test.context.UltronTestContextProvider\nimport com.atiurin.ultron.exceptions.UltronException\n\n/**\n * Base class for tests in the Ultron framework. Provides mechanisms for managing test context,\n * common pre-test and post-test actions, and handling test lifecycle events.\n *\n * @param testContextProvider Provides the context for the test. Defaults to [DefaultUltronTestContextProvider].\n */\nopen class UltronTest(\n    private val testContextProvider: UltronTestContextProvider = DefaultUltronTestContextProvider()\n) {\n    companion object {\n        /**\n         * A map to track whether `beforeAllTests` has been executed for a given class name.\n         */\n        private val beforeAllTestsExecutionMap: MutableMap<String, Boolean> = mutableMapOf()\n    }\n\n    /**\n     * The fully qualified name of the current test class.\n     *\n     * @throws UltronException if the test class is anonymous.\n     */\n    private val className = this::class.simpleName\n        ?: throw UltronException(\"Don't use anonymous class for UltronTest\")\n\n    /**\n     * Function to be executed once before all tests in the class.\n     * Can be overridden in subclasses.\n     */\n    @ExperimentalUltronApi\n    open val beforeFirstTest: () -> Unit = {}\n\n    /**\n     * Function to be executed before each test.\n     * Can be overridden in subclasses.\n     */\n    open val beforeTest: () -> Unit = {}\n\n    /**\n     * Function to be executed after each test.\n     * Can be overridden in subclasses.\n     */\n    open val afterTest: () -> Unit = {}\n\n    /**\n     * Executes a test with the provided configuration.\n     *\n     * @param suppressCommonBefore If `true`, the `beforeTest` function will not be executed. Defaults to `false`.\n     * @param suppressCommonAfter If `true`, the `afterTest` function will not be executed. Defaults to `false`.\n     * @param configureTestBlock The block of test logic to execute. Implemented as an extension of [TestMethod].\n     */\n    @OptIn(ExperimentalUltronApi::class)\n    fun test(\n        suppressCommonBefore: Boolean = false,\n        suppressCommonAfter: Boolean = false,\n        configureTestBlock: TestMethod.() -> Unit\n    ) {\n        TestMethod(testContextProvider.provide()).apply {\n            // Ensure `beforeAllTests` is executed only once per class\n            if (beforeAllTestsExecutionMap[className] != true) {\n                beforeFirstTest()\n                beforeAllTestsExecutionMap[className] = true\n            }\n\n            // Execute common `beforeTest` logic if not suppressed\n            if (!suppressCommonBefore) {\n                beforeTest()\n            }\n            var throwable: Throwable? = null\n            // Configure and execute the test block\n            runCatching {\n                configureTestBlock()\n                attack()\n            }.onFailure { ex ->\n                throwable = ex\n            }\n\n            // Execute common `afterTest` logic if not suppressed\n            if (!suppressCommonAfter) {\n                runCatching(afterTest).onFailure { ex ->\n                    throwable?.let { throw it }\n                    throw ex\n                }\n            }\n            throwable?.let { throw it }\n        }\n    }\n}\n"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/test/context/DefaultUltronTestContext.kt",
    "content": "package com.atiurin.ultron.core.test.context\n\nimport com.atiurin.ultron.core.common.resultanalyzer.DefaultSoftAssertionOperationResultAnalyzer\nimport com.atiurin.ultron.core.common.resultanalyzer.OperationResultAnalyzer\n\nopen class DefaultUltronTestContext : UltronTestContext {\n    override var softAssertion: Boolean = false\n    override val softAnalyzer = DefaultSoftAssertionOperationResultAnalyzer()\n\n    override fun wrapAnalyzerIfSoftAssertion(analyzer: OperationResultAnalyzer): OperationResultAnalyzer {\n        return if (softAssertion) softAnalyzer.apply {\n            originalAnalyzer = analyzer\n        } else analyzer\n    }\n}"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/test/context/DefaultUltronTestContextProvider.kt",
    "content": "package com.atiurin.ultron.core.test.context\n\nopen class DefaultUltronTestContextProvider : UltronTestContextProvider {\n    override fun provide(): UltronTestContext {\n        return DefaultUltronTestContext()\n    }\n}"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/test/context/UltronTestContext.kt",
    "content": "package com.atiurin.ultron.core.test.context\n\nimport com.atiurin.ultron.core.common.resultanalyzer.OperationResultAnalyzer\nimport com.atiurin.ultron.core.common.resultanalyzer.SoftAssertionOperationResultAnalyzer\n\ninterface UltronTestContext {\n    var softAssertion: Boolean\n    val softAnalyzer: SoftAssertionOperationResultAnalyzer\n\n    fun wrapAnalyzerIfSoftAssertion(analyzer: OperationResultAnalyzer): OperationResultAnalyzer\n}\n"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/core/test/context/UltronTestContextProvider.kt",
    "content": "package com.atiurin.ultron.core.test.context\n\ninterface UltronTestContextProvider {\n    fun provide(): UltronTestContext\n}"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/exceptions/UltronAssertionBlockException.kt",
    "content": "package com.atiurin.ultron.exceptions\n\nclass UltronAssertionBlockException(override val message: String) : AssertionError(message)"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/exceptions/UltronAssertionException.kt",
    "content": "package com.atiurin.ultron.exceptions\n\nclass UltronAssertionException(override val message: String) : AssertionError(message)"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/exceptions/UltronException.kt",
    "content": "package com.atiurin.ultron.exceptions\n\nclass UltronException(override val message: String) : RuntimeException(message)"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/exceptions/UltronOperationException.kt",
    "content": "package com.atiurin.ultron.exceptions\n\n\nclass UltronOperationException : RuntimeException {\n    constructor(message: String) : super(message)\n    constructor(message: String, cause: Throwable) : super(message, cause)\n}"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/exceptions/UltronUiAutomatorException.kt",
    "content": "package com.atiurin.ultron.exceptions\n\nclass UltronUiAutomatorException(override val message: String) : AssertionError(message)"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/exceptions/UltronWrapperException.kt",
    "content": "package com.atiurin.ultron.exceptions\n\nclass UltronWrapperException : AssertionError {\n    constructor(message: String) : super(message)\n    constructor(message: String, cause: Throwable)\n            : super(\n        \"$message${\n            if (cause is UltronWrapperException || cause is UltronOperationException) \"\"\n            else \"\\nOriginal error - ${cause::class.simpleName}: ${cause.message}\"\n        }\"\n    )\n}"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/extensions/AnyCommonExt.kt",
    "content": "package com.atiurin.ultron.extensions\n\nfun Any?.simpleClassName() = this?.let { it::class.simpleName }\n"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/file/MimeType.kt",
    "content": "package com.atiurin.ultron.file\n\nenum class MimeType(val extension: String, val value: String) {\n    /** JPEG images */\n    JPEG(\".jpeg\", \"image/jpeg\"),\n\n    /** Portable Network Graphics */\n    PNG(\".png\", \"image/png\"),\n\n    /** WEBP image */\n    WEBP(\".webp\", \"image/webp\"),\n\n    JSON(\".json\", \"application/json\"),\n\n    /** Adobe Portable Document Format (PDF) */\n    PDF(\".pdf\", \"application/pdf\"),\n\n    /** MP4 audio */\n    MP4(\".mp4\", \"audio/mp4\"),\n\n    /** AVI: Audio Video Interleave */\n    AVI(\".avi\", \"video/x-msvideo\"),\n\n    /** MPEG Video */\n    MPEG(\".mpeg\", \"video/mpeg\"),\n\n    /** Cascading Style Sheets (CSS) */\n    CSS(\".css\", \"text/css\"),\n\n    /** Comma-separated values (CSV) */\n    CSV(\".csv\", \"text/csv\"),\n\n    /** HyperText Markup Language (HTML) */\n    HTML(\".html\", \"text/html\"),\n\n    /** Text, (generally ASCII or ISO 8859-n) */\n    PLAIN_TEXT(\".txt\", \"text/plain\"),\n\n    /** YAML */\n    YAML(\".yaml\", \"text/yaml\"),\n\n    XML(\".xml\", \"text/xml\")\n}\n"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/listeners/AbstractListener.kt",
    "content": "package com.atiurin.ultron.listeners\n\nabstract class AbstractListener {\n    var id: String\n    constructor(id: String){\n        this.id = id\n    }\n    constructor(){\n        this.id = this::class.simpleName.orEmpty()\n    }\n}"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/listeners/AbstractListenersContainer.kt",
    "content": "package com.atiurin.ultron.listeners\n\nimport kotlin.reflect.KClass\n\nabstract class AbstractListenersContainer<T: AbstractListener> {\n    private var listeners: MutableList<T> = mutableListOf()\n\n    open fun getListeners(): List<T> {\n        return listeners\n    }\n\n    fun addListener(listener: T) {\n        val exist = listeners.find { it.id == listener.id }\n        exist?.let { listeners.remove(it) }\n        listeners.add(listener)\n    }\n\n    fun clearListeners() {\n        listeners.clear()\n    }\n\n    fun removeListener(listenerId: String) {\n        val exist = listeners.find { it.id == listenerId }\n        if (exist != null) {\n            listeners.remove(exist)\n        }\n    }\n\n    fun <T : AbstractListener> removeListener(listenerClass: KClass<T>) {\n        val exist = listeners.find { it.id == listenerClass.simpleName }\n        if (exist != null) {\n            listeners.remove(exist)\n        }\n    }\n}"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/listeners/LifecycleListener.kt",
    "content": "package com.atiurin.ultron.listeners\n\nimport com.atiurin.ultron.core.common.Operation\nimport com.atiurin.ultron.core.common.OperationResult\n\ninternal interface LifecycleListener{\n    /**\n     * executed before any action or assertion\n     */\n    fun before(operation: Operation)\n    /**\n     * called when action or assertion has been executed successfully\n     */\n    fun afterSuccess(operationResult: OperationResult<Operation>)\n\n    /**\n     * called when action or assertion failed\n     */\n    fun afterFailure(operationResult: OperationResult<Operation>)\n\n    /**\n     * called in any case of action or assertion result\n     */\n    fun after(operationResult: OperationResult<Operation>)\n}"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/listeners/LogLifecycleListener.kt",
    "content": "package com.atiurin.ultron.listeners\n\nimport com.atiurin.ultron.core.common.Operation\nimport com.atiurin.ultron.core.common.OperationResult\nimport com.atiurin.ultron.log.UltronLog\n\nclass LogLifecycleListener : UltronLifecycleListener() {\n    override fun before(operation: Operation) {\n        UltronLog.info(\"Start execution of ${operation.name}\")\n        UltronLog.info(\"Element info: ${operation.elementInfo}\")\n    }\n\n    override fun afterSuccess(operationResult: OperationResult<Operation>) {\n        UltronLog.info(\"Successfully executed ${operationResult.operation.name}\")\n    }\n\n    override fun afterFailure(operationResult: OperationResult<Operation>) {\n        UltronLog.error(\"Failed ${operationResult.operation.name}.  with description: \\n\" +\n                \"${operationResult.description} \")\n    }\n}"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/listeners/UltronLifecycleListener.kt",
    "content": "package com.atiurin.ultron.listeners\n\nimport com.atiurin.ultron.core.common.Operation\nimport com.atiurin.ultron.core.common.OperationResult\n\nabstract class UltronLifecycleListener : LifecycleListener, AbstractListener(){\n    override fun after(operationResult: OperationResult<Operation>) = Unit\n    override fun afterFailure(operationResult: OperationResult<Operation>) = Unit\n    override fun afterSuccess(operationResult: OperationResult<Operation>) = Unit\n    override fun before(operation: Operation) = Unit\n}\n"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/listeners/UltronListenerUtil.kt",
    "content": "package com.atiurin.ultron.listeners\n\nimport com.atiurin.ultron.core.config.UltronCommonConfig\n\n\nfun <T> executeWithoutListeners(block: () -> T): T {\n    UltronCommonConfig.isListenersOn = false\n    val result = block.invoke()\n    UltronCommonConfig.isListenersOn = true\n    return result\n}\n\nfun <T> executeWithListeners(block: () -> T): T {\n    UltronCommonConfig.isListenersOn = true\n    return block.invoke()\n}\n\nfun <T> executableWithoutListeners(block: () -> T): () -> T  = { executeWithoutListeners(block) }\nfun <T> executableWithListeners(block: () -> T): () -> T  = { executeWithListeners(block) }\n\nfun <T> (() -> T).setListenersState(value: Boolean): () -> T {\n    return if(value) executableWithListeners(this)\n    else executableWithoutListeners(this)\n}\n"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/log/LogLevel.kt",
    "content": "package com.atiurin.ultron.log\n\nenum class LogLevel {\n    I, D, W, E\n}"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/log/ULogger.kt",
    "content": "package com.atiurin.ultron.log\n\nabstract class ULogger {\n    var id: String\n\n    constructor(id: String){\n        this.id = id\n    }\n    constructor(){\n        this.id = this::class.simpleName.orEmpty()\n    }\n\n    abstract fun info(message: String): Any\n    abstract fun info(message: String, throwable: Throwable): Any\n    abstract fun debug(message: String): Any\n    abstract fun debug(message: String, throwable: Throwable): Any\n    abstract fun warn(message: String): Any\n    abstract fun warn(message: String, throwable: Throwable): Any\n    abstract fun error(message: String): Any\n    abstract fun error(message: String, throwable: Throwable): Any\n}"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/log/UltronFileLogger.kt",
    "content": "package com.atiurin.ultron.log\n\n\nabstract class UltronFileLogger : ULogger() {\n    abstract fun getLogFilePath(): String\n    abstract fun clearFile()\n}"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/log/UltronLog.kt",
    "content": "package com.atiurin.ultron.log\n\nexpect fun getFileLogger(): UltronFileLogger\n\nobject UltronLog {\n    val fileLogger by lazy { getFileLogger() }\n\n    private val loggers = mutableSetOf<ULogger>()\n\n    fun addLogger(logger: ULogger) {\n        removeLogger(logger)\n        loggers.add(logger)\n    }\n\n    fun removeLogger(logger: ULogger){\n        removeLogger(logger.id)\n    }\n\n    fun removeLogger(id: String){\n        val exist = loggers.find { it.id == id }\n        exist?.let { loggers.remove(it) }\n    }\n\n    fun clearLoggers() = loggers.clear()\n\n    fun info(message: String) = loggers.forEach { it.info(message) }\n    fun info(message: String, throwable: Throwable) = loggers.forEach { it.info(message, throwable) }\n    fun debug(message: String) = loggers.forEach { it.debug(message) }\n    fun debug(message: String, throwable: Throwable) = loggers.forEach { it.debug(message, throwable) }\n    fun warn(message: String) = loggers.forEach { it.warn(message) }\n    fun warn(message: String, throwable: Throwable) = loggers.forEach { it.warn(message, throwable) }\n    fun error(message: String) = loggers.forEach { it.error(message) }\n    fun error(message: String, throwable: Throwable) = loggers.forEach { it.error(message, throwable) }\n\n    fun log(level: LogLevel, message: String) = when (level) {\n        LogLevel.I -> info(message)\n        LogLevel.D -> debug(message)\n        LogLevel.W -> warn(message)\n        LogLevel.E -> error(message)\n    }\n}"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/log/UltronLogUtil.kt",
    "content": "package com.atiurin.ultron.log\n\nobject UltronLogUtil {\n    const val testStageDelimiter    = \"========================================================================================================\"\n    const val stepDelimiter         = \"********************************************************************\"\n\n    fun logTextBlock(text: String, logLevel: LogLevel = LogLevel.I, delimiter: String = testStageDelimiter) {\n        UltronLog.log(logLevel, delimiter)\n        UltronLog.log(logLevel, text)\n        UltronLog.log(logLevel, delimiter)\n    }\n}"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/page/Page.kt",
    "content": "package com.atiurin.ultron.page\n\nabstract class Page<T>{\n    inline operator fun <R> invoke(block: T.() -> R): R {\n        return block.invoke(this as T)\n    }\n}"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/page/Screen.kt",
    "content": "package com.atiurin.ultron.page\n\nabstract class Screen<T> {\n    inline operator fun <R> invoke(block: T.() -> R): R {\n        return block.invoke(this as T)\n    }\n}"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/utils/AssertUtils.kt",
    "content": "package com.atiurin.ultron.utils\n\nimport com.atiurin.ultron.exceptions.UltronAssertionException\nimport kotlinx.atomicfu.AtomicRef\n\nobject AssertUtils {\n    fun assertTrue(block: () -> Boolean, timeoutMs: Long = 5_000, desc: String = \"\" ) =\n        assertTrue(block, timeoutMs) { desc }\n\n    fun assertTrue(block: () -> Boolean, timeoutMs: Long = 5_000, desc: () -> String = { \"\" }) {\n        val startTime = nowMs()\n        while (nowMs() < startTime + timeoutMs){\n            if (block()) return\n        }\n        throw UltronAssertionException(\"Assertion '${desc.invoke()}' failed during $timeoutMs ms\")\n    }\n    fun <R> assertTrueAndValueInDesc(valueBlock: () -> R, assertionBlock: (R) -> Boolean, timeoutMs: Long = 5_000, desc: (R) -> String = { \"\" }) {\n        val finishTime = nowMs() + timeoutMs\n        var result = valueBlock()\n        while (nowMs() < finishTime){\n            if (assertionBlock(result)) return\n            result = valueBlock()\n        }\n        throw UltronAssertionException(\"Assertion '${desc.invoke(result)}' failed during $timeoutMs ms\")\n    }\n\n    fun <R> assertTrueAndReturn(resultContainer: R, block: (R) -> Boolean, timeoutMs: Long = 5_000, desc: String = \"\"): R {\n        val finishTime = nowMs() + timeoutMs\n        while (nowMs() < finishTime){\n            if (block(resultContainer)) return resultContainer\n        }\n        throw UltronAssertionException(\"Assertion '$desc' failed during $timeoutMs ms\")\n    }\n\n    fun <R> assertTrueAndReturnValue(valueBlock: () -> R, assertionBlock: (R) -> Boolean, timeoutMs: Long = 5_000, desc: String = \"\"): R {\n        val finishTime = nowMs() + timeoutMs\n        while (nowMs() < finishTime){\n            val result = valueBlock()\n            if (assertionBlock(result)) return result\n        }\n        throw UltronAssertionException(\"Assertion '$desc' failed during $timeoutMs ms\")\n    }\n\n    /**\n     * Immediately throws an exception if block returns false\n     */\n    fun assertTrueWhileTime(block: () -> Boolean, timeoutMs: Long = 5_000, desc: String = \"\"){\n        val startTime = nowMs()\n        while (nowMs() < startTime + timeoutMs){\n            if (!block()) throw UltronAssertionException(\"Assertion '$desc' failed\")\n        }\n    }\n\n}"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/utils/ThreadUtil.kt",
    "content": "package com.atiurin.ultron.utils\n\nexpect fun sleep(timeMs: Long)"
  },
  {
    "path": "ultron-common/src/commonMain/kotlin/com/atiurin/ultron/utils/TimeUtil.kt",
    "content": "package com.atiurin.ultron.utils\n\nimport kotlin.time.ExperimentalTime\nimport kotlin.time.Clock\n\n@OptIn(ExperimentalTime::class)\nfun now() = Clock.System.now()\n@OptIn(ExperimentalTime::class)\nfun nowMs() = now().toEpochMilliseconds()\n\n@OptIn(ExperimentalTime::class)\nfun measureTimeMillis(function: () -> Any): Long {\n    val start = now()\n    function()\n    return now().minus(start).inWholeMilliseconds\n}"
  },
  {
    "path": "ultron-common/src/jsWasmMain/kotlin/com/atiurin/ultron/utils/ThreadUtil.jsWasm.kt",
    "content": "package com.atiurin.ultron.utils\n\nactual fun sleep(timeMs: Long) {}"
  },
  {
    "path": "ultron-common/src/jvmMain/kotlin/com/atiurin/ultron/utils/ThreadUtil.jvm.kt",
    "content": "package com.atiurin.ultron.utils\n\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.runBlocking\n\nactual fun sleep(timeMs: Long) {\n    runBlocking {\n        delay(timeMs)\n    }\n}"
  },
  {
    "path": "ultron-common/src/nativeMain/kotlin/com/atiurin/ultron/utils/ThreadUtil.native.kt",
    "content": "package com.atiurin.ultron.utils\n\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.runBlocking\n\nactual fun sleep(timeMs: Long) {\n    runBlocking {\n        delay(timeMs)\n    }\n}"
  },
  {
    "path": "ultron-common/src/shared/kotlin/com/atiurin/ultron/log/UltronLog.shared.kt",
    "content": "package com.atiurin.ultron.log\n\n/**\n * Not implemented yet\n */\nactual fun getFileLogger(): UltronFileLogger {\n    return object : UltronFileLogger() {\n        override fun getLogFilePath(): String = \"\"\n        override fun clearFile() = Unit\n        override fun info(message: String) = Unit\n        override fun info(message: String, throwable: Throwable) = Unit\n        override fun debug(message: String) = Unit\n        override fun debug(message: String, throwable: Throwable) = Unit\n        override fun warn(message: String) = Unit\n        override fun warn(message: String, throwable: Throwable) = Unit\n        override fun error(message: String) = Unit\n        override fun error(message: String, throwable: Throwable) = Unit\n    }\n}"
  },
  {
    "path": "ultron-compose/build.gradle.kts",
    "content": "import com.vanniktech.maven.publish.SonatypeHost\nimport org.jetbrains.compose.ExperimentalComposeLibrary\nimport org.jetbrains.kotlin.gradle.dsl.JvmTarget\n\nplugins {\n    alias(libs.plugins.kotlinMultiplatform)\n    alias(libs.plugins.androidLibrary)\n    alias(libs.plugins.jetbrainsCompose)\n    alias(libs.plugins.compose.compiler)\n    alias(libs.plugins.vanniktech.mavenPublish)\n    id(\"org.jetbrains.dokka\")\n}\n\ngroup = project.findProperty(\"GROUP\")!!\nversion = project.findProperty(\"VERSION_NAME\")!!\n\nkotlin {\n    compilerOptions {\n        apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_1)\n    }\n    androidTarget {\n        publishLibraryVariants(\"release\")\n        compilerOptions {\n            jvmTarget.set(JvmTarget.JVM_11)\n        }\n    }\n    jvm(\"desktop\")\n    macosX64()\n    macosArm64()\n    iosX64()\n    iosArm64()\n    iosSimulatorArm64()\n    wasmJs(){\n        browser()\n        nodejs()\n    }\n    js(IR){\n        browser()\n        nodejs()\n    }\n\n    @OptIn(ExperimentalComposeLibrary::class)\n    sourceSets {\n        applyDefaultHierarchyTemplate()\n        val commonMain by getting {\n            dependencies {\n                api(project(\":ultron-common\"))\n                implementation(kotlin(\"reflect\"))\n                api(libs.kotlin.test)\n                api(compose.uiTest)\n                implementation(libs.atomicfu)\n            }\n        }\n        val androidMain by getting {\n            dependencies {\n                api(project(\":ultron-common\"))\n                implementation(Libs.androidXRunner)\n                api(Libs.composeUiTest)\n            }\n        }\n        val shared by creating {\n            dependsOn(commonMain)\n        }\n        jvmMain {\n            dependsOn(shared)\n            dependencies {\n                api(kotlin(\"stdlib\"))\n            }\n        }\n        val desktopMain by getting {\n            dependsOn(shared)\n            dependsOn(jvmMain.get())\n            dependencies {\n                api(project(\":ultron-common\"))\n                implementation(compose.uiTest)\n            }\n        }\n        val nativeMain by getting { dependsOn(shared) }\n\n        val jsWasmMain by creating {\n            dependsOn(shared)\n        }\n        val jsMain by getting {\n            dependsOn(jsWasmMain)\n            dependencies {\n                implementation(kotlin(\"stdlib-js\"))\n            }\n        }\n        val wasmJsMain by getting {\n            dependsOn(jsWasmMain)\n            dependencies {\n                implementation(kotlin(\"stdlib\"))\n            }\n        }\n    }\n}\n\nandroid {\n    compileSdk = 35\n    namespace = \"com.atiurin.ultron.compose\"\n    defaultConfig {\n        minSdk = 21\n        multiDexEnabled = true\n    }\n    compileOptions {\n        targetCompatibility = JavaVersion.VERSION_11\n        sourceCompatibility = JavaVersion.VERSION_11\n    }\n}\n\nval dokkaOutputDir = buildDir.resolve(\"dokka\")\ntasks.dokkaHtml { outputDirectory.set(file(dokkaOutputDir)) }\nval deleteDokkaOutputDir by tasks.register<Delete>(\"deleteDokkaOutputDirectory\") { delete(dokkaOutputDir) }\n\nval ultronComposeJavadocJar by tasks.registering(Jar::class) {\n    archiveClassifier.set(\"javadoc\")\n    duplicatesStrategy = DuplicatesStrategy.EXCLUDE\n    dependsOn(deleteDokkaOutputDir, tasks.dokkaHtml)\n    from(dokkaOutputDir)\n}\nmavenPublishing {\n    publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)\n    signAllPublications()\n    coordinates(artifactId = \"ultron-compose\")\n\n    pom {\n        name = \"Ultron Common\"\n        description = \"Android & Compose Multiplatform UI testing framework\"\n        url = \"https://github.com/open-tool/ultron\"\n        inceptionYear = \"2021\"\n\n        licenses {\n            license {\n                name.set(\"The Apache License, Version 2.0\")\n                url.set(\"http://www.apache.org/licenses/LICENSE-2.0.txt\")\n            }\n        }\n\n        issueManagement {\n            system = \"GitHub Issues\"\n            url = \"https://github.com/open-tool/ultron/issues\"\n        }\n\n        developers {\n            developer {\n                id = \"alex-tiurin\"\n                name = \"Aleksei Tiurin\"\n                url = \"https://github.com/open-tool\"\n            }\n        }\n\n        scm {\n            url = \"https://github.com/open-tool/ultron\"\n            connection = \"scm:git@github.com:open-tool/ultron.git\"\n            developerConnection = \"scm:git@github.com:open-tool/ultron.git\"\n        }\n    }\n}"
  },
  {
    "path": "ultron-compose/gradle.properties",
    "content": "GROUP=com.atiurin\nPOM_ARTIFACT_ID=ultron-compose\n\nPOM_NAME=ultron-compose\nPOM_PACKAGING=aar\n\nPOM_DESCRIPTION=UI testing framework for Compose\nPOM_INCEPTION_YEAR=2024\n"
  },
  {
    "path": "ultron-compose/src/androidMain/kotlin/com/atiurin/ultron/core/compose/ComposeRuleContainer.android.kt",
    "content": "package com.atiurin.ultron.core.compose\n\nimport android.annotation.SuppressLint\nimport androidx.activity.ComponentActivity\nimport androidx.compose.ui.test.TestContext\nimport androidx.compose.ui.test.junit4.AndroidComposeTestRule\nimport androidx.compose.ui.test.junit4.ComposeContentTestRule\nimport androidx.compose.ui.test.junit4.ComposeTestRule\nimport androidx.compose.ui.test.junit4.createAndroidComposeRule\nimport androidx.compose.ui.test.junit4.createComposeRule\nimport androidx.compose.ui.test.junit4.createEmptyComposeRule\nimport androidx.test.ext.junit.rules.ActivityScenarioRule\nimport com.atiurin.ultron.exceptions.UltronException\nimport com.atiurin.ultron.extensions.getProperty\nimport com.atiurin.ultron.testlifecycle.activity.UltronActivityRule\n\nobject ComposeRuleContainer {\n    var rule: ComposeTestRule? = null\n    var testContext: TestContext? = null\n\n    fun init(composeRule: ComposeTestRule) {\n        rule = composeRule\n        ComposeTestContainer.init(\n            UltronComposeTestEnvironment(\n                provider = composeRule,\n                mainClock = composeRule.mainClock,\n                density = composeRule.density\n            )\n        )\n        testContext = composeRule.getTestContext()\n    }\n\n    fun getComposeRule(): ComposeTestRule {\n        return rule ?: throw UltronException(\"Initialise ComposeTestRule using createUltronComposeRule<A>() factory method.\")\n    }\n\n    fun getComposeTestContext(): TestContext {\n        return testContext ?: throw UltronException(\"Initialise ComposeTestRule using createUltronComposeRule<A>() factory method.\")\n    }\n}\n\n/**\n * Factory method to provide android specific implementation of [createComposeRule], for a given\n * activity class type [A].\n *\n * This method is useful for tests that require a custom Activity. This is usually the case for\n * tests where the compose content is set by that Activity, instead of via the test rule's\n * [setContent][ComposeContentTestRule.setContent]. Make sure that you add the provided activity\n * into your app's manifest file (usually in main/AndroidManifest.xml).\n *\n * This creates a test rule that is using [ActivityScenarioRule] as the activity launcher. If you\n * would like to use a different one you can create [AndroidComposeTestRule] directly and supply\n * it with your own launcher.\n *\n * If your test doesn't require a specific Activity, use [createDefaultUltronComposeRule] instead.\n */\ninline fun <reified A : ComponentActivity> createUltronComposeRule(): AndroidComposeTestRule<ActivityScenarioRule<A>, A> {\n    val rule = createAndroidComposeRule<A>()\n    ComposeRuleContainer.init(rule)\n    return rule\n}\n\n@SuppressLint(\"SuspiciousIndentation\")\ninline fun <reified A : ComponentActivity> createSimpleUltronComposeRule(): com.atiurin.ultron.core.compose.activity.AndroidComposeTestRule<UltronActivityRule<A>, A> {\n    val rule = com.atiurin.ultron.core.compose.activity.createAndroidComposeRule(A::class.java)\n    ComposeRuleContainer.init(rule)\n    return rule\n}\n\n/**\n * Factory method to provide implementation of [ComposeContentTestRule]\n *\n * This method is useful for tests that doesn't require a custom Activity.\n *\n * It's expected that compose content is set via the test rule's\n * [setContent][ComposeContentTestRule.setContent]\n */\nfun createDefaultUltronComposeRule(): ComposeContentTestRule {\n    val rule = createComposeRule()\n    ComposeRuleContainer.init(rule)\n    return rule\n}\n\n/**\n * Factory method to provide an implementation of [ComposeTestRule] that doesn't create a compose\n * host for you in which you can set content.\n *\n * This method is useful for tests that need to create their own compose host during the test.\n * The returned test rule will not create a host, and consequently does not provide a\n * `setContent` method. To set content in tests using this rule, use the appropriate `setContent`\n * methods from your compose host.\n *\n * A typical use case on Android is when the test needs to launch an Activity (the compose host)\n * after one or more dependencies have been injected.\n */\nfun createEmptyUltronComposeRule(): ComposeTestRule {\n    val rule = createEmptyComposeRule()\n    ComposeRuleContainer.init(rule)\n    return rule\n}\n\nfun ComposeTestRule.getTestContext() = this.getProperty<TestContext>(\"testContext\")\n\n\n\n"
  },
  {
    "path": "ultron-compose/src/androidMain/kotlin/com/atiurin/ultron/core/compose/UltronComposeUiBlockExt.kt",
    "content": "package com.atiurin.ultron.core.compose\n\nimport androidx.compose.ui.test.SemanticsMatcher\nimport com.atiurin.ultron.core.compose.page.UltronComposeUiBlock\nimport com.atiurin.ultron.exceptions.UltronException\nimport com.atiurin.ultron.log.UltronLog\nimport kotlin.reflect.typeOf\n\n/**\n * Creates a child UI block of a specified type using a matcher modified based on the given search type.\n *\n * This extension function simplifies the creation of a new instance of a child UI block by automatically invoking\n * the appropriate constructor of the specified type [T]. The matcher of the current block is transformed\n * to locate the desired child block based on the specified `descendantSearch` parameter.\n *\n * @param T The type of the child UI block to be created. It must extend [UltronComposeUiBlock].\n * @param uiBlock The existing instance of the child UI block to use as a template for the new block.\n *                The `blockMatcher` and `blockDescription` properties of this block are used to create the new instance.\n * @param descendantSearch Specifies the type of search:\n *                         - `true` (default): Performs a descendant search.\n *                         - `false`: Performs a direct child search.\n * @return A new instance of the specified type [T], initialized with the updated matcher.\n *\n * @throws UltronException If the specified class [T] does not have an appropriate constructor or cannot be instantiated.\n *\n * ### Requirements:\n * The class [T] must meet the following conditions to be instantiated:\n * 1. It must not be a nested or inner class. It should be defined at the top level or as a file-level class.\n * 2. It must have one of the following constructors:\n *    - A constructor with one parameter of type [SemanticsMatcher]:\n *      `class CustomBlock(blockMatcher: SemanticsMatcher)`\n *    - A constructor with two parameters: `blockMatcher` of type [SemanticsMatcher] and `blockDescription` of type [String]:\n *      `class CustomBlock(blockMatcher: SemanticsMatcher, blockDescription: String)`\n *\n * If neither constructor is available, consider using an alternative method for child creation,\n * such as `child(childMatcher, descendantSearch, uiBlockFactory)`.\n *\n * ### Usage Example:\n * ```kotlin\n * // ui elements declaration\n * class CustomElementBlock(blockMatcher: SemanticsMatcher, blockDescription: String = \"\") : UltronComposeUiBlock(blockMatcher, blockDescription)\n *\n * class CustomComplexBlock(blockMatcher: SemanticsMatcher, blockDescription: String = \"\") : UltronComposeUiBlock(blockMatcher, blockDescription){\n *      val custom = child(CustomElementBlock(hasTestTag(elementTag), \"custom element child of '$blockDescription'\"))\n * }\n * // screen object\n * object MyScreen : Screen<MyScreen>(){\n *      val complexBlock = CustomComplexBlock(hasTestTag(\"bigUiBlockTag\"), \"Presentable description of UI block\")\n * }\n *\n * // somewhere in test\n * MyScreen {\n *     complexBlock.custom.assertIsDisplayed()\n * }\n * ```\n */\ninline fun <reified T : UltronComposeUiBlock> UltronComposeUiBlock.child(\n    uiBlock: T,\n    descendantSearch: Boolean = true\n): T {\n    val newMatcher = when (descendantSearch) {\n        true -> _descendantSearch(uiBlock.blockMatcher)\n        false -> _childSearch(uiBlock.blockMatcher)\n    }\n    val updateBlock = runCatching {\n        T::class.constructors.forEach { constructor ->\n            UltronLog.info(\"Constructor: $constructor, Parameters: ${constructor.parameters.map { it.type }}\")\n        }\n        T::class.constructors.firstOrNull {\n            it.parameters.size == 2 && it.parameters.first().type == typeOf<SemanticsMatcher>()\n                    && it.parameters[1].type == typeOf<String>()\n        }?.let { constructor ->\n            return@runCatching constructor.call(newMatcher, uiBlock.blockDescription)\n        }\n        T::class.constructors.firstOrNull {\n            it.parameters.size == 1 && it.parameters.first().type == typeOf<SemanticsMatcher>()\n        }?.let {\n            return@runCatching it.call(newMatcher)\n        }\n        null\n    }.onFailure {\n        if (it is IllegalArgumentException) {\n            throw UltronException(\n                \"${T::class.simpleName} has hidden java constructor parameters. ${T::class.simpleName} must be defined as a top-level class (not nested inside any other class).\"\n            )\n        } else throw UltronException(\"Unable to create updated ${T::class.simpleName}. Message: ${it.message}\")\n    }.getOrNull()\n    updateBlock?.let {\n        return it\n    } ?: throw UltronException(\n        \"\"\" |${T::class.simpleName} doesn't have an appropriate constructor with arguments: (SemanticsMatcher, String) or (SemanticsMatcher)  \n            |Ensure that the class meets the following conditions:\n            |1. ${T::class.simpleName} must not be defined inside another class. It should be a top-level or file-level class.\n            |2. ${T::class.simpleName} must have one of the following constructors:\n            |- A constructor with one parameter of type SemanticsMatcher:\n            |class ${T::class.simpleName}(blockMatcher: SemanticsMatcher) : UltronComposeUiBlock(blockMatcher)\n            |- A constructor with two parameters: blockMatcher of type SemanticsMatcher and blockDescription of type String:\n            |class ${T::class.simpleName}(blockMatcher: SemanticsMatcher, blockDescription: String) : UltronComposeUiBlock(blockMatcher, blockDescription)\n            |If neither constructor is available, consider using another method for child declaration:\n            |fun <B : UltronComposeUiBlock> child(childMatcher: SemanticsMatcher, descendantSearch: Boolean = true, uiBlockFactory: (SemanticsMatcher) -> B): B\n        \"\"\".trimIndent()\n    )\n}"
  },
  {
    "path": "ultron-compose/src/androidMain/kotlin/com/atiurin/ultron/core/compose/activity/AndroidComposeTestRule.kt",
    "content": "package com.atiurin.ultron.core.compose.activity\n\nimport androidx.activity.ComponentActivity\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.test.AndroidComposeUiTestEnvironment\nimport androidx.compose.ui.test.ExperimentalTestApi\nimport androidx.compose.ui.test.IdlingResource\nimport androidx.compose.ui.test.MainTestClock\nimport androidx.compose.ui.test.SemanticsMatcher\nimport androidx.compose.ui.test.SemanticsNodeInteraction\nimport androidx.compose.ui.test.SemanticsNodeInteractionCollection\nimport androidx.compose.ui.test.junit4.ComposeContentTestRule\nimport androidx.compose.ui.test.waitUntilAtLeastOneExists\nimport androidx.compose.ui.test.waitUntilDoesNotExist\nimport androidx.compose.ui.test.waitUntilExactlyOneExists\nimport androidx.compose.ui.test.waitUntilNodeCount\nimport androidx.compose.ui.unit.Density\nimport com.atiurin.ultron.testlifecycle.activity.UltronActivityRule\nimport org.junit.rules.TestRule\nimport org.junit.runner.Description\nimport org.junit.runners.model.Statement\nimport kotlin.coroutines.CoroutineContext\nimport kotlin.coroutines.EmptyCoroutineContext\n\n@OptIn(ExperimentalTestApi::class)\nclass AndroidComposeTestRule<R : TestRule, A : ComponentActivity> private constructor(\n    val activityRule: R,\n    private val environment: AndroidComposeUiTestEnvironment<A>\n) : ComposeContentTestRule {\n    private val composeTest = environment.test\n\n    /**\n     * Android specific implementation of [ComposeContentTestRule], where compose content is hosted\n     * by an Activity.\n     *\n     * The Activity is normally launched by the given [activityRule] before the test starts, but it\n     * is possible to pass a test rule that chooses to launch an Activity on a later time. The\n     * Activity is retrieved from the [activityRule] by means of the [activityProvider], which can be\n     * thought of as a getter for the Activity on the [activityRule]. If you use an [activityRule]\n     * that launches an Activity on a later time, you should make sure that the Activity is launched\n     * by the time or while the [activityProvider] is called.\n     *\n     * The [AndroidComposeTestRule] wraps around the given [activityRule] to make sure the Activity\n     * is launched _after_ the [AndroidComposeTestRule] has completed all necessary steps to control\n     * and monitor the compose content.\n     *\n     * @param activityRule Test rule to use to launch the Activity.\n     * @param activityProvider Function to retrieve the Activity from the given [activityRule].\n     */\n    constructor(\n        activityRule: R,\n        activityProvider: (R) -> A\n    ) : this(\n        activityRule = activityRule,\n        effectContext = EmptyCoroutineContext,\n        activityProvider = activityProvider,\n    )\n\n    /**\n     * Android specific implementation of [ComposeContentTestRule], where compose content is hosted\n     * by an Activity.\n     *\n     * The Activity is normally launched by the given [activityRule] before the test starts, but it\n     * is possible to pass a test rule that chooses to launch an Activity on a later time. The\n     * Activity is retrieved from the [activityRule] by means of the [activityProvider], which can be\n     * thought of as a getter for the Activity on the [activityRule]. If you use an [activityRule]\n     * that launches an Activity on a later time, you should make sure that the Activity is launched\n     * by the time or while the [activityProvider] is called.\n     *\n     * The [AndroidComposeTestRule] wraps around the given [activityRule] to make sure the Activity\n     * is launched _after_ the [AndroidComposeTestRule] has completed all necessary steps to control\n     * and monitor the compose content.\n     *\n     * @param activityRule Test rule to use to launch the Activity.\n     * @param effectContext The [CoroutineContext] used to run the composition. The context for\n     * `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context.\n     * @param activityProvider Function to retrieve the Activity from the given [activityRule].\n     */\n    @ExperimentalTestApi\n    constructor(\n        activityRule: R,\n        effectContext: CoroutineContext = EmptyCoroutineContext,\n        activityProvider: (R) -> A,\n    ) : this(\n        activityRule,\n        AndroidComposeUiTestEnvironment(effectContext) { activityProvider(activityRule) },\n    )\n\n    /**\n     * Provides the current activity.\n     *\n     * Avoid calling often as it can involve synchronization and can be slow.\n     */\n    val activity: A get() = checkNotNull(composeTest.activity) { \"Host activity not found\" }\n\n    override fun apply(base: Statement, description: Description): Statement {\n        val testStatement = activityRule.apply(base, description)\n        return object : Statement() {\n            override fun evaluate() {\n                environment.runTest {\n                    testStatement.evaluate()\n                }\n            }\n        }\n    }\n\n    @Deprecated(\n        message = \"Do not instantiate this Statement, use AndroidComposeTestRule instead\",\n        level = DeprecationLevel.ERROR\n    )\n    inner class AndroidComposeStatement(private val base: Statement) : Statement() {\n        override fun evaluate() {\n            base.evaluate()\n        }\n    }\n\n    /*\n     * WHEN THE NAME AND SHAPE OF THE NEW COMMON INTERFACES HAS BEEN DECIDED,\n     * REPLACE ALL OVERRIDES BELOW WITH DELEGATION: ComposeTest by composeTest\n     */\n\n    override val density: Density get() = composeTest.density\n\n    override val mainClock: MainTestClock get() = composeTest.mainClock\n\n    override fun <T> runOnUiThread(action: () -> T): T = composeTest.runOnUiThread(action)\n\n    override fun <T> runOnIdle(action: () -> T): T = composeTest.runOnIdle(action)\n\n    override fun waitForIdle() = composeTest.waitForIdle()\n\n    override suspend fun awaitIdle() = composeTest.awaitIdle()\n\n    override fun waitUntil(timeoutMillis: Long, condition: () -> Boolean) =\n        composeTest.waitUntil(conditionDescription = null, timeoutMillis, condition)\n\n    override fun waitUntil(\n        conditionDescription: String,\n        timeoutMillis: Long,\n        condition: () -> Boolean\n    ) {\n        composeTest.waitUntil(conditionDescription, timeoutMillis, condition)\n    }\n\n    @ExperimentalTestApi\n    override fun waitUntilNodeCount(matcher: SemanticsMatcher, count: Int, timeoutMillis: Long) =\n        composeTest.waitUntilNodeCount(matcher, count, timeoutMillis)\n\n    @ExperimentalTestApi\n    override fun waitUntilAtLeastOneExists(matcher: SemanticsMatcher, timeoutMillis: Long) =\n        composeTest.waitUntilAtLeastOneExists(matcher, timeoutMillis)\n\n    @ExperimentalTestApi\n    override fun waitUntilExactlyOneExists(matcher: SemanticsMatcher, timeoutMillis: Long) =\n        composeTest.waitUntilExactlyOneExists(matcher, timeoutMillis)\n\n    @ExperimentalTestApi\n    override fun waitUntilDoesNotExist(matcher: SemanticsMatcher, timeoutMillis: Long) =\n        composeTest.waitUntilDoesNotExist(matcher, timeoutMillis)\n\n    override fun registerIdlingResource(idlingResource: IdlingResource) =\n        composeTest.registerIdlingResource(idlingResource)\n\n    override fun unregisterIdlingResource(idlingResource: IdlingResource) =\n        composeTest.unregisterIdlingResource(idlingResource)\n\n    override fun onNode(\n        matcher: SemanticsMatcher,\n        useUnmergedTree: Boolean\n    ): SemanticsNodeInteraction = composeTest.onNode(matcher, useUnmergedTree)\n\n    override fun onAllNodes(\n        matcher: SemanticsMatcher,\n        useUnmergedTree: Boolean\n    ): SemanticsNodeInteractionCollection = composeTest.onAllNodes(matcher, useUnmergedTree)\n\n    override fun setContent(composable: @Composable () -> Unit) = composeTest.setContent(composable)\n\n    fun cancelAndRecreateRecomposer() {\n        environment.cancelAndRecreateRecomposer()\n    }\n}\n\nprivate fun <A : ComponentActivity> getActivityFromTestRule(rule: UltronActivityRule<A>): A {\n    var activity: A? = null\n    rule.getScenario().onActivity { activity = it }\n    if (activity == null) {\n        throw IllegalStateException(\"Activity was not set in the ActivityScenarioRule!\")\n    }\n    return activity!!\n}\n\nfun <A : ComponentActivity> createAndroidComposeRule(\n    activityClass: Class<A>\n): AndroidComposeTestRule<UltronActivityRule<A>, A> = AndroidComposeTestRule(\n    activityRule = UltronActivityRule(activityClass),\n    activityProvider = ::getActivityFromTestRule\n)"
  },
  {
    "path": "ultron-compose/src/androidMain/kotlin/com/atiurin/ultron/core/compose/config/UltronComposeConfig.android.kt",
    "content": "package com.atiurin.ultron.core.compose.config\n\nimport com.atiurin.ultron.log.ULogger\nimport com.atiurin.ultron.log.UltronLogcatLogger\n\nactual fun getPlatformLoggers(): List<ULogger> {\n    return listOf(UltronLogcatLogger())\n}"
  },
  {
    "path": "ultron-compose/src/androidMain/kotlin/com/atiurin/ultron/core/compose/list/ItemChildInteractionProvider.android.kt",
    "content": "package com.atiurin.ultron.core.compose.list\n\nimport androidx.compose.ui.test.SemanticsMatcher\nimport androidx.compose.ui.test.SemanticsNodeInteraction\nimport androidx.compose.ui.test.filterToOne\nimport androidx.compose.ui.test.onChildAt\nimport androidx.compose.ui.test.onChildren\nimport androidx.compose.ui.test.performScrollToNode\nimport com.atiurin.ultron.core.compose.ComposeTestContainer.withComposeTestEnvironment\nimport com.atiurin.ultron.extensions.findNodeInTree\n\nactual fun getItemChildInteractionProvider(): ItemChildInteractionProvider {\n    return AndroidItemChildInteractionProvider()\n}\n\nclass AndroidItemChildInteractionProvider : ItemChildInteractionProvider {\n    override fun onItemChild(\n        listMatcher: SemanticsMatcher,\n        itemMatcher: SemanticsMatcher,\n        childMatcher: SemanticsMatcher,\n        useUnmergedTree: Boolean\n    ): () -> SemanticsNodeInteraction = {\n        withComposeTestEnvironment { testEnvironment ->\n            testEnvironment.provider.onNode(listMatcher, useUnmergedTree)\n                .performScrollToNode(itemMatcher)\n                .onChildren().filterToOne(itemMatcher)\n                .findNodeInTree(childMatcher, useUnmergedTree)\n        }\n    }\n\n    override fun onVisibleItemChild(\n        listMatcher: SemanticsMatcher,\n        index: Int,\n        childMatcher: SemanticsMatcher,\n        useUnmergedTree: Boolean\n    ): () -> SemanticsNodeInteraction = {\n        withComposeTestEnvironment { testEnvironment ->\n            testEnvironment.provider.onNode(listMatcher, useUnmergedTree)\n                .onChildAt(index)\n                .findNodeInTree(childMatcher, useUnmergedTree)\n        }\n    }\n}"
  },
  {
    "path": "ultron-compose/src/androidMain/kotlin/com/atiurin/ultron/core/compose/list/UltronComposeListItem.android.kt",
    "content": "package com.atiurin.ultron.core.compose.list\n\nimport androidx.compose.ui.test.SemanticsMatcher\nimport com.atiurin.ultron.exceptions.UltronException\n\nactual inline fun <reified T : UltronComposeListItem> getComposeListItemInstance(\n    ultronComposeList: UltronComposeList,\n    itemMatcher: SemanticsMatcher\n): T {\n    val item = createUltronComposeListItemInstance<T>()\n    item.setExecutor(ultronComposeList, itemMatcher)\n    return item\n}\n\nactual inline fun <reified T : UltronComposeListItem> getComposeListItemInstance(\n    ultronComposeList: UltronComposeList,\n    position: Int,\n    isPositionPropertyConfigured: Boolean\n): T {\n    val item = createUltronComposeListItemInstance<T>()\n    item.setExecutor(ultronComposeList, position, isPositionPropertyConfigured)\n    return item\n}\n\ninline fun <reified T : UltronComposeListItem> createUltronComposeListItemInstance(): T {\n    return try {\n        T::class.java.newInstance()\n    } catch (ex: Exception) {\n        val desc = when {\n            T::class.isInner -> {\n                \"${T::class.simpleName} is an inner class so you have to delete inner modifier (It is often when kotlin throws 'has no zero argument constructor' but real reason is an inner modifier)\"\n            }\n\n            T::class.constructors.find { it.parameters.isEmpty() } == null -> {\n                \"${T::class.simpleName} doesn't have a constructor without params (create an empty constructor)\"\n            }\n\n            else -> ex.message\n        }\n        throw UltronException(\n            \"\"\"\n                    |Couldn't create an instance of ${T::class.simpleName}. \n                    |Possible reason: $desc \n                    |Original exception: ${ex.message}, cause ${ex.cause}\n                \"\"\".trimMargin()\n        )\n    }\n}"
  },
  {
    "path": "ultron-compose/src/androidMain/kotlin/com/atiurin/ultron/core/compose/listeners/ComposDebugListener.kt",
    "content": "package com.atiurin.ultron.core.compose.listeners\n\nimport com.atiurin.ultron.core.common.Operation\nimport com.atiurin.ultron.core.common.OperationResult\nimport com.atiurin.ultron.core.compose.ComposeTestContainer\nimport com.atiurin.ultron.core.compose.ComposeTestContainer.withComposeTestEnvironment\nimport com.atiurin.ultron.listeners.UltronLifecycleListener\n\nclass ComposDebugListener(private val advanceFrameAmount: Int = 10) : UltronLifecycleListener() {\n    override fun after(operationResult: OperationResult<Operation>) {\n        super.after(operationResult)\n        if (android.os.Debug.isDebuggerConnected() && ComposeTestContainer.isInitialized){\n            withComposeTestEnvironment { env ->\n                repeat(advanceFrameAmount) {\n                    env.mainClock.advanceTimeByFrame()\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "ultron-compose/src/androidMain/kotlin/com/atiurin/ultron/core/compose/nodeinteraction/UltronComposeSemanticsNodeInteraction.android.kt",
    "content": "package com.atiurin.ultron.core.compose.nodeinteraction\n\nimport android.os.Build\nimport androidx.annotation.RequiresApi\nimport androidx.compose.ui.graphics.ImageBitmap\nimport androidx.compose.ui.semantics.AccessibilityAction\nimport androidx.compose.ui.semantics.SemanticsPropertyKey\nimport androidx.compose.ui.test.SemanticsNodeInteraction\nimport androidx.compose.ui.test.captureToImage\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType\nimport com.atiurin.ultron.extensions.requireSemantics\nimport com.atiurin.ultron.utils.runOnUiThread\nimport java.util.concurrent.atomic.AtomicReference\n\n@RequiresApi(Build.VERSION_CODES.O)\nfun UltronComposeSemanticsNodeInteraction.captureToImage(): ImageBitmap {\n    val image = AtomicReference<ImageBitmap>()\n    executeOperation(\n        operationBlock = { image.set(semanticsNodeInteraction.captureToImage()) },\n        name = \"CaptureImage for '${elementInfo.name}'\",\n        type = ComposeOperationType.CAPTURE_IMAGE,\n        description = \"Compose captureToImage for '${elementInfo.name}' during $timeoutMs ms\",\n    )\n    return image.get()\n}\n\nfun <T : Function<Boolean>, R> SemanticsNodeInteraction.performSemanticsActionWithResult(\n    key: SemanticsPropertyKey<AccessibilityAction<T>>, invocation: (T) -> R?\n): R? {\n    val node = fetchSemanticsNode(\"Failed to perform ${key.name} action.\")\n    requireSemantics(node, key) {\n        \"Failed to perform action ${key.name}\"\n    }\n    return runOnUiThread {\n        node.config[key].action?.let(invocation)\n    }\n}"
  },
  {
    "path": "ultron-compose/src/androidMain/kotlin/com/atiurin/ultron/extensions/ReflectionComposeExt.android.kt",
    "content": "package com.atiurin.ultron.extensions\n\nimport androidx.compose.ui.test.SemanticsNodeInteraction\nimport androidx.compose.ui.test.SemanticsNodeInteractionCollection\n\ninternal fun SemanticsNodeInteraction.getUseMergedTree(): Boolean? {\n    return this.getProperty(\"useUnmergedTree\")\n}\ninternal fun SemanticsNodeInteractionCollection.getUseMergedTree(): Boolean? {\n    return this.getProperty(\"useUnmergedTree\")\n}"
  },
  {
    "path": "ultron-compose/src/androidMain/kotlin/com/atiurin/ultron/extensions/SemanticsMatcherExt.android.kt",
    "content": "package com.atiurin.ultron.extensions\n\nimport android.os.Build\nimport androidx.annotation.RequiresApi\nimport androidx.compose.ui.graphics.ImageBitmap\nimport androidx.compose.ui.test.SemanticsMatcher\nimport com.atiurin.ultron.core.compose.nodeinteraction.UltronComposeSemanticsNodeInteraction\nimport com.atiurin.ultron.core.compose.nodeinteraction.captureToImage\n\n@RequiresApi(Build.VERSION_CODES.O)\nfun SemanticsMatcher.captureToImage(): ImageBitmap = UltronComposeSemanticsNodeInteraction(this).captureToImage()"
  },
  {
    "path": "ultron-compose/src/androidMain/kotlin/com/atiurin/ultron/extensions/SemanticsNodeInteractionExt.android.kt",
    "content": "package com.atiurin.ultron.extensions\n\nimport androidx.compose.ui.test.SemanticsNodeInteraction\nimport androidx.compose.ui.test.SemanticsMatcher\nimport androidx.compose.ui.test.SemanticsNodeInteractionCollection\nimport androidx.compose.ui.test.SemanticsSelector\nimport androidx.compose.ui.test.TestContext\nimport com.atiurin.ultron.exceptions.UltronException\n\nactual fun SemanticsNodeInteraction.getSelectorDescription(): String  = this.getSemanticsSelector().description\n\nfun SemanticsNodeInteractionCollection.getTestContext() = this.getProperty<TestContext>(\"testContext\")\n    ?: throw UltronException(\"Couldn't get testContext from $this\")\n\nfun SemanticsNodeInteractionCollection.getSemanticsSelector() = this.getProperty<SemanticsSelector>(\"selector\")\n    ?: throw UltronException(\"Couldn't get selector from $this\")\n\nfun SemanticsNodeInteraction.getTestContext() = this.getProperty<TestContext>(\"testContext\")\n    ?: throw UltronException(\"Couldn't get testContext from $this\")\n\nfun SemanticsNodeInteraction.getSemanticsSelector() = this.getProperty<SemanticsSelector>(\"selector\")\n    ?: throw UltronException(\"Couldn't get selector from $this\")\n\nfun SemanticsNodeInteraction.findNodeInTree(\n    matcher: SemanticsMatcher,\n    useUnmergedTree: Boolean,\n): SemanticsNodeInteraction {\n    return SemanticsNodeInteraction(\n        testContext = this.getTestContext(),\n        useUnmergedTree = useUnmergedTree,\n        selector = this.getSemanticsSelector().addFindNodeInTreeSelector(\"findNodeInTree\", matcher)\n    )\n}"
  },
  {
    "path": "ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/core/compose/ComposeTestContainer.kt",
    "content": "package com.atiurin.ultron.core.compose\n\nimport androidx.compose.ui.test.SemanticsNodeInteractionsProvider\nimport com.atiurin.ultron.exceptions.UltronException\n\nobject ComposeTestContainer {\n    private lateinit var testEnvironment: ComposeTestEnvironment\n\n    fun init(testEnvironment: ComposeTestEnvironment) {\n        this.testEnvironment = testEnvironment\n    }\n\n    val isInitialized : Boolean\n        get() = ::testEnvironment.isInitialized\n\n    fun getProvider(): SemanticsNodeInteractionsProvider = this.testEnvironment.provider\n\n    fun <T> withComposeTestEnvironment(block: (ComposeTestEnvironment) -> T): T {\n        if (!isInitialized) throw UltronException(\"ComposeTestContainer isn't initialized!\")\n        return block(testEnvironment)\n    }\n}\n"
  },
  {
    "path": "ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/core/compose/ComposeTestEnvironment.kt",
    "content": "package com.atiurin.ultron.core.compose\n\nimport androidx.compose.ui.test.MainTestClock\nimport androidx.compose.ui.test.SemanticsNodeInteractionsProvider\nimport androidx.compose.ui.unit.Density\n\ninterface ComposeTestEnvironment {\n    val provider: SemanticsNodeInteractionsProvider\n    /**\n     * Current device screen's density.\n     */\n    val density: Density\n\n    /**\n     * Clock that drives frames and recompositions in compose tests.\n     */\n    val mainClock: MainTestClock\n}\n\ndata class UltronComposeTestEnvironment(\n    override val provider: SemanticsNodeInteractionsProvider,\n    override val density: Density,\n    override val mainClock: MainTestClock\n): ComposeTestEnvironment"
  },
  {
    "path": "ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/core/compose/UltronUiTest.kt",
    "content": "package com.atiurin.ultron.core.compose\n\nimport androidx.compose.ui.test.ComposeUiTest\nimport androidx.compose.ui.test.ExperimentalTestApi\nimport androidx.compose.ui.test.runComposeUiTest\nimport kotlin.coroutines.CoroutineContext\nimport kotlin.coroutines.EmptyCoroutineContext\n\n@OptIn(ExperimentalTestApi::class)\nfun runUltronUiTest(\n    effectContext: CoroutineContext = EmptyCoroutineContext,\n    block: ComposeUiTest.() -> Unit\n) {\n    runComposeUiTest(effectContext){\n        ComposeTestContainer.init(\n            UltronComposeTestEnvironment(\n                provider = this,\n                mainClock = this.mainClock,\n                density = this.density\n            )\n        )\n        block()\n    }\n}"
  },
  {
    "path": "ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/core/compose/config/UltronComposeConfig.kt",
    "content": "package com.atiurin.ultron.core.compose.config\n\nimport com.atiurin.ultron.core.common.Operation\nimport com.atiurin.ultron.core.common.OperationResult\nimport com.atiurin.ultron.core.common.resultanalyzer.OperationResultAnalyzer\nimport com.atiurin.ultron.core.compose.ComposeTestEnvironment\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationResult\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType\nimport com.atiurin.ultron.core.compose.operation.UltronComposeOperation\nimport com.atiurin.ultron.exceptions.UltronAssertionException\nimport com.atiurin.ultron.exceptions.UltronException\nimport com.atiurin.ultron.exceptions.UltronWrapperException\nimport com.atiurin.ultron.extensions.simpleClassName\nimport com.atiurin.ultron.listeners.LogLifecycleListener\nimport com.atiurin.ultron.listeners.UltronLifecycleListener\nimport com.atiurin.ultron.log.ULogger\nimport com.atiurin.ultron.log.UltronLog\nimport com.atiurin.ultron.core.config.UltronCommonConfig as config\n\nobject UltronComposeConfig {\n    init {\n        config.operationsExcludedFromListeners.addAll(\n            listOf(ComposeOperationType.GET_LIST_ITEM, ComposeOperationType.GET_LIST_ITEM_CHILD)\n        )\n        config.addListener(LogLifecycleListener())\n    }\n\n    const val DEFAULT_LAZY_COLUMN_OPERATIONS_TIMEOUT = 10_000L\n\n    @Deprecated(\n        message = \"Default moved to UltronCommonConfig.Defaults\",\n        replaceWith = ReplaceWith(expression = \"UltronCommonConfig.Defaults.OPERATION_TIMEOUT_MS\")\n    )\n    const val DEFAULT_OPERATION_TIMEOUT = config.Defaults.OPERATION_TIMEOUT_MS\n\n    var params: UltronComposeConfigParams = UltronComposeConfigParams()\n\n    @Deprecated(\"Use [UltronComposeConfig.params.operationPollingTimeoutMs]\")\n    var COMPOSE_OPERATION_POLLING_TIMEOUT = params.operationPollingTimeoutMs\n\n    @Deprecated(\"Use [UltronComposeConfig.params.lazyColumnOperationTimeoutMs]\")\n    var LAZY_COLUMN_OPERATIONS_TIMEOUT = params.lazyColumnOperationTimeoutMs\n\n    @Deprecated(\"Use [UltronComposeConfig.params.lazyColumnItemSearchLimit]\")\n    var LAZY_COLUMN_ITEM_SEARCH_LIMIT = params.lazyColumnItemSearchLimit\n\n    @Deprecated(\"Use [UltronComposeConfig.params.operationTimeoutMs]\")\n    var OPERATION_TIMEOUT = params.operationTimeoutMs\n\n    var resultAnalyzer: OperationResultAnalyzer = config.resultAnalyzer\n\n    inline fun setResultAnalyzer(crossinline block: (OperationResult<Operation>) -> Boolean) {\n        resultAnalyzer = object : OperationResultAnalyzer {\n            override fun <Op : Operation, OpRes : OperationResult<Op>> analyze(\n                operationResult: OpRes,\n            ): Boolean {\n                return block(operationResult as OperationResult<Operation>)\n            }\n        }\n    }\n\n    var doBetweenOperationRetry: (Operation, ComposeTestEnvironment) -> Unit = { operation, testEnvironment ->\n        if (testEnvironment.mainClock.autoAdvance){\n            testEnvironment.mainClock.advanceTimeByFrame()\n        }\n    }\n\n    val resultHandler: (ComposeOperationResult<UltronComposeOperation>) -> Unit = {\n        config.testContext.wrapAnalyzerIfSoftAssertion(resultAnalyzer).analyze(it)\n    }\n\n    var allowedExceptions = mutableListOf(\n        AssertionError::class,\n        UltronWrapperException::class,\n        UltronAssertionException::class,\n        UltronException::class,\n    )\n\n    @Deprecated(\n        message = \"Listeners storage moved to UltronCommonConfig\",\n        replaceWith = ReplaceWith(expression = \"UltronCommonConfig.addListener(Listener)\")\n    )\n    fun addListener(listener: UltronLifecycleListener) {\n        UltronLog.info(\"Add UltronComposeOperationLifecycle listener ${listener.simpleClassName()}\")\n        config.addListener(listener)\n    }\n\n    fun applyRecommended() {\n        params = UltronComposeConfigParams()\n        modify()\n    }\n\n    fun apply(block: UltronComposeConfigParams.() -> Unit) {\n        params.block()\n        modify()\n    }\n\n    private fun modify() {\n        getPlatformLoggers().forEach {\n            UltronLog.addLogger(it)\n        }\n        config.addListener(LogLifecycleListener())\n        if (config.logToFile) {\n            UltronLog.addLogger(UltronLog.fileLogger)\n        } else {\n            UltronLog.removeLogger(UltronLog.fileLogger.id)\n        }\n        UltronLog.info(\"UltronComposeConfig applied with params $params}\")\n    }\n}\n\nexpect fun getPlatformLoggers(): List<ULogger>\n"
  },
  {
    "path": "ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/core/compose/config/UltronComposeConfigParams.kt",
    "content": "package com.atiurin.ultron.core.compose.config\n\ndata class UltronComposeConfigParams(\n    var operationTimeoutMs: Long = UltronComposeConfig.DEFAULT_OPERATION_TIMEOUT,\n    var operationPollingTimeoutMs: Long = 0,\n    var lazyColumnOperationTimeoutMs: Long = UltronComposeConfig.DEFAULT_LAZY_COLUMN_OPERATIONS_TIMEOUT,\n    var lazyColumnItemSearchLimit: Int = -1,\n    var useUnmergedTree: Boolean = false\n)\n"
  },
  {
    "path": "ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/core/compose/list/ComposeItemExecutor.kt",
    "content": "package com.atiurin.ultron.core.compose.list\n\nimport androidx.compose.ui.test.SemanticsMatcher\nimport com.atiurin.ultron.core.compose.nodeinteraction.UltronComposeSemanticsNodeInteraction\n\ninterface ComposeItemExecutor {\n    fun scrollToItem(offset: Int = 0)\n    fun getItemInteraction(): UltronComposeSemanticsNodeInteraction\n    fun getItemChildInteraction(childMatcher: SemanticsMatcher): UltronComposeSemanticsNodeInteraction\n}"
  },
  {
    "path": "ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/core/compose/list/IndexComposeItemExecutor.kt",
    "content": "package com.atiurin.ultron.core.compose.list\n\nimport androidx.compose.ui.test.SemanticsMatcher\nimport com.atiurin.ultron.core.compose.nodeinteraction.UltronComposeSemanticsNodeInteraction\n\nclass IndexComposeItemExecutor(\n    val ultronComposeList: UltronComposeList,\n    val index: Int\n) : ComposeItemExecutor {\n    override fun scrollToItem(offset: Int) {\n        ultronComposeList.scrollToIndex(index)\n    }\n    override fun getItemInteraction() : UltronComposeSemanticsNodeInteraction = ultronComposeList.onVisibleItem(index)\n    override fun getItemChildInteraction(childMatcher: SemanticsMatcher): UltronComposeSemanticsNodeInteraction =\n        ultronComposeList.onVisibleItemChild(index, childMatcher)\n}"
  },
  {
    "path": "ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/core/compose/list/ItemChildInteractionProvider.kt",
    "content": "package com.atiurin.ultron.core.compose.list\n\nimport androidx.compose.ui.test.SemanticsMatcher\nimport androidx.compose.ui.test.SemanticsNodeInteraction\nimport androidx.compose.ui.test.filterToOne\nimport androidx.compose.ui.test.hasAnyAncestor\nimport androidx.compose.ui.test.hasAnyDescendant\nimport androidx.compose.ui.test.onChildAt\nimport androidx.compose.ui.test.onChildren\nimport androidx.compose.ui.test.onParent\nimport androidx.compose.ui.test.performScrollToNode\nimport androidx.compose.ui.test.printToLog\nimport com.atiurin.ultron.core.compose.ComposeTestContainer\nimport com.atiurin.ultron.exceptions.UltronAssertionException\nimport com.atiurin.ultron.log.UltronLog\n\ninterface ItemChildInteractionProvider {\n    fun onItemChild(\n        listMatcher: SemanticsMatcher,\n        itemMatcher: SemanticsMatcher,\n        childMatcher: SemanticsMatcher,\n        useUnmergedTree: Boolean\n    ): () -> SemanticsNodeInteraction = {\n        ComposeTestContainer.withComposeTestEnvironment { testEnvironment ->\n            val snip = testEnvironment.provider\n            snip.onNode(listMatcher).performScrollToNode(itemMatcher)\n            snip.onNode(\n                hasAnyAncestor(listMatcher)\n                    .and(hasAnyAncestor(itemMatcher))\n                    .and(childMatcher),\n                useUnmergedTree\n            )\n        }\n    }\n\n    fun onVisibleItemChild(listMatcher: SemanticsMatcher, index: Int, childMatcher: SemanticsMatcher, useUnmergedTree: Boolean): () -> SemanticsNodeInteraction = {\n        ComposeTestContainer.withComposeTestEnvironment { testEnvironment ->\n            val snip = testEnvironment.provider\n            val itemInteraction = snip.onNode(listMatcher, useUnmergedTree).onChildAt(index)\n            if (childMatcher.matches(itemInteraction.fetchSemanticsNode())) {\n                UltronLog.warn(\"Child matcher matches item itself. Ultron will return item interaction instead of child interaction\")\n                itemInteraction.onParent().printToLog(\"Ultron\")\n            }\n            getItemChildInteraction(itemInteraction, childMatcher)\n        }\n    }\n}\n\ninternal fun getItemChildInteraction(nodeInteraction: SemanticsNodeInteraction, childMatcher: SemanticsMatcher): SemanticsNodeInteraction {\n    val node = nodeInteraction.fetchSemanticsNode()\n    if (childMatcher.matches(node)) return nodeInteraction\n    if (node.children.any { childMatcher.matches(it) }) {\n        return nodeInteraction.onChildren().filterToOne(childMatcher)\n    }\n    node.children.forEachIndexed { index, semanticsNode ->\n        if (hasAnyDescendant(childMatcher).matches(semanticsNode)) {\n            return getItemChildInteraction(nodeInteraction.onChildAt(index), childMatcher)\n        }\n    }\n    throw UltronAssertionException(\"Can't find child with matcher $childMatcher\")\n}\n\n\nexpect fun getItemChildInteractionProvider(): ItemChildInteractionProvider"
  },
  {
    "path": "ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/core/compose/list/MatcherComposeItemExecutor.kt",
    "content": "package com.atiurin.ultron.core.compose.list\n\nimport androidx.compose.ui.test.SemanticsMatcher\nimport com.atiurin.ultron.core.compose.nodeinteraction.UltronComposeSemanticsNodeInteraction\n\nclass MatcherComposeItemExecutor(\n    val ultronComposeList: UltronComposeList,\n    val itemMatcher: SemanticsMatcher\n) : ComposeItemExecutor {\n    override fun scrollToItem(offset: Int) {\n        ultronComposeList.scrollToNode(itemMatcher)\n    }\n\n    override fun getItemInteraction(): UltronComposeSemanticsNodeInteraction {\n        scrollToItem()\n        return ultronComposeList.onItem(itemMatcher)\n    }\n\n    override fun getItemChildInteraction(childMatcher: SemanticsMatcher): UltronComposeSemanticsNodeInteraction {\n        scrollToItem()\n        return ultronComposeList.onItemChild(itemMatcher, childMatcher)\n    }\n}"
  },
  {
    "path": "ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/core/compose/list/PositionComposeItemExecutor.kt",
    "content": "package com.atiurin.ultron.core.compose.list\n\nimport androidx.compose.ui.test.SemanticsMatcher\nimport androidx.compose.ui.test.hasAnyDescendant\nimport com.atiurin.ultron.core.compose.nodeinteraction.UltronComposeSemanticsNodeInteraction\nimport com.atiurin.ultron.exceptions.UltronException\n\nclass PositionComposeItemExecutor(\n    val ultronComposeList: UltronComposeList,\n    val position: Int\n) : ComposeItemExecutor {\n    private val positionKey = ultronComposeList.positionPropertyKey\n        ?: throw UltronException(\"[positionPropertyKey] parameter is not specified for Compose List\")\n    private val positionMatcher = SemanticsMatcher.expectValue(\n        positionKey,\n        position\n    ) or hasAnyDescendant(SemanticsMatcher.expectValue(positionKey, position))\n\n    override fun scrollToItem(offset: Int) {\n        ultronComposeList.scrollToNode(positionMatcher)\n    }\n\n    override fun getItemInteraction(): UltronComposeSemanticsNodeInteraction {\n        return ultronComposeList.onItem(positionMatcher)\n    }\n\n    override fun getItemChildInteraction(childMatcher: SemanticsMatcher): UltronComposeSemanticsNodeInteraction {\n        return ultronComposeList.onItemChild(positionMatcher, childMatcher)\n    }\n}"
  },
  {
    "path": "ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/core/compose/list/UltronComposeList.kt",
    "content": "package com.atiurin.ultron.core.compose.list\n\nimport androidx.compose.ui.semantics.SemanticsNode\nimport androidx.compose.ui.semantics.SemanticsPropertyKey\nimport androidx.compose.ui.test.SemanticsMatcher\nimport androidx.compose.ui.test.SemanticsNodeInteraction\nimport androidx.compose.ui.test.filterToOne\nimport androidx.compose.ui.test.onChildAt\nimport androidx.compose.ui.test.onChildren\nimport androidx.compose.ui.test.performScrollToNode\nimport com.atiurin.ultron.core.common.options.ContentDescriptionContainsOption\nimport com.atiurin.ultron.core.compose.config.UltronComposeConfig\nimport com.atiurin.ultron.core.compose.nodeinteraction.UltronComposeSemanticsNodeInteraction\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType\nimport com.atiurin.ultron.core.compose.operation.UltronComposeOperationParams\nimport com.atiurin.ultron.exceptions.UltronAssertionException\nimport com.atiurin.ultron.exceptions.UltronException\nimport com.atiurin.ultron.utils.AssertUtils\nimport kotlin.reflect.KClass\n\nclass UltronComposeList(\n    val listMatcher: SemanticsMatcher,\n    var useUnmergedTree: Boolean = true,\n    var positionPropertyKey: SemanticsPropertyKey<Int>? = null,\n    val initBlock: UltronComposeList.() -> Unit = {},\n    private val itemSearchLimit: Int = UltronComposeConfig.params.lazyColumnItemSearchLimit,\n    private var operationTimeoutMs: Long = UltronComposeConfig.params.lazyColumnOperationTimeoutMs\n) {\n    private val itemChildInteractionProvider = getItemChildInteractionProvider()\n    private var listName: String = listMatcher.description\n    val itemInstancesMap = mutableMapOf<KClass<*>, () -> UltronComposeListItem>()\n    inline fun <reified T : UltronComposeListItem> registerItem(noinline creator: () -> T){\n        itemInstancesMap[T::class] = creator\n    }\n    init {\n        initBlock()\n    }\n\n    fun withDescription(description: String) = apply { this.listName = description }\n\n    open fun withTimeout(timeoutMs: Long) =\n        UltronComposeList(\n            listMatcher = listMatcher,\n            useUnmergedTree = useUnmergedTree,\n            positionPropertyKey = positionPropertyKey,\n            itemSearchLimit = itemSearchLimit,\n            operationTimeoutMs = timeoutMs\n        ).withDescription(listName)\n\n    /**\n     * @return current [UltronComposeList] operations timeout\n     */\n    fun getOperationTimeout() = operationTimeoutMs\n\n    fun item(matcher: SemanticsMatcher) = UltronComposeListItem(this, matcher)\n    fun item(position: Int): UltronComposeListItem {\n        if (positionPropertyKey == null) {\n            throw UltronException(\n                \"\"\"\n                    |[positionPropertyKey] parameter is not specified for UltronComposeList\n                    |Configure it like\n                    |```\n                    |composeList(.., positionPropertyKey = ListItemPositionPropertyKey)\n                    |```\n                \"\"\".trimMargin()\n            )\n        }\n        return UltronComposeListItem(this, position, true)\n    }\n\n    fun firstItem(): UltronComposeListItem = item(0)\n\n    /**\n     * This method works properly before any scroll of target list\n     * After scroll the positions of children inside the list are changed.\n     */\n    fun visibleItem(index: Int) = UltronComposeListItem(this, index)\n\n    /**\n     * This method works properly before any scroll of target list\n     * After scroll the positions of children inside the list are changed.\n     */\n    fun firstVisibleItem() = visibleItem(0)\n\n    /**\n     * This method works properly before any scroll of target list\n     * After scroll the positions of children inside the list are changed.\n     */\n    fun lastVisibleItem() = visibleItem(getInteraction().execute { it.fetchSemanticsNode().children.lastIndex })\n\n    fun onItemChild(itemMatcher: SemanticsMatcher, childMatcher: SemanticsMatcher): UltronComposeSemanticsNodeInteraction =\n        UltronComposeSemanticsNodeInteraction(\n            getInteraction()\n                .execute(\n                    UltronComposeOperationParams(\n                        operationName = \"Get item '${itemMatcher.description}' child '${childMatcher.description}' in list '${getInteraction().elementInfo.name}'\",\n                        operationDescription = \"Get Compose list item '${itemMatcher.description}' child '${childMatcher.description}' in list '${getInteraction().elementInfo.name}'\",\n                        operationType = ComposeOperationType.GET_LIST_ITEM_CHILD\n                    )\n                ) { listInteraction ->\n                    itemChildInteractionProvider.onItemChild(listMatcher, itemMatcher, childMatcher, useUnmergedTree).invoke()\n                }\n        ).withName(\"Child: '${childMatcher.description}' on item with: '${itemMatcher.description}' in list: '$listName'\")\n\n\n    fun onVisibleItemChild(index: Int, childMatcher: SemanticsMatcher) = UltronComposeSemanticsNodeInteraction(\n        getInteraction().execute(\n            UltronComposeOperationParams(\n                operationName = \"Get child '${childMatcher.description}' of visible item at index $index in list '${getInteraction().elementInfo.name}'\",\n                operationDescription = \"Get Compose list child '${childMatcher.description}' of visible item at index $index in list '${getInteraction().elementInfo.name}'\",\n                operationType = ComposeOperationType.GET_LIST_ITEM_CHILD\n            )\n        ) { listInteraction ->\n            itemChildInteractionProvider.onVisibleItemChild(listMatcher, index, childMatcher, useUnmergedTree).invoke()\n        }\n    ).withName(\"Child: '$childMatcher' on item at position: '$index' in list: '$listName'\")\n\n    inline fun <reified T : UltronComposeListItem> getItem(matcher: SemanticsMatcher): T {\n        return getComposeListItemInstance(this, matcher)\n    }\n\n    inline fun <reified T : UltronComposeListItem> getItem(position: Int): T {\n        if (positionPropertyKey == null) {\n            throw UltronException(\n                \"\"\"\n                    |[positionPropertyKey] parameter is not specified for UltronComposeList\n                    |Configure it like \n                    |```\n                    |composeList(.., positionPropertyKey = ListItemPositionPropertyKey)\n                    |```\n                \"\"\".trimMargin()\n            )\n        }\n        return getComposeListItemInstance(this, position, true)\n    }\n\n    inline fun <reified T : UltronComposeListItem> getFirstItem(): T = getItem(0)\n\n    /**\n     * This method works properly before any scroll of target list\n     * After scroll the positions of children inside the list are changed.\n     */\n    inline fun <reified T : UltronComposeListItem> getVisibleItem(index: Int): T {\n        return getComposeListItemInstance(this, index)\n    }\n\n    /**\n     * This method works properly before any scroll of target list\n     * After scroll the positions of children inside the list are changed.\n     * */\n    inline fun <reified T : UltronComposeListItem> getFirstVisibleItem(): T = getVisibleItem(0)\n\n    /**\n     * This method works properly before any scroll of target list\n     * After scroll the positions of children inside the list are changed.\n     * */\n    inline fun <reified T : UltronComposeListItem> getLastVisibleItem(): T = getVisibleItem(getInteraction().execute { it.fetchSemanticsNode().children.lastIndex })\n\n    /**\n     * Provide a scope with references to list SemanticsNode and SemanticsNodeInteraction.\n     * It is possible to evaluate any action or assertion on this node.\n     */\n    fun <T> performOnList(block: (SemanticsNode, SemanticsNodeInteraction) -> T): T =\n        UltronComposeSemanticsNodeInteraction(listMatcher, useUnmergedTree).withName(listName).execute { listSemanticsNodeInteraction ->\n            val listSemanticsNode = listSemanticsNodeInteraction.fetchSemanticsNode()\n            block(listSemanticsNode, listSemanticsNodeInteraction)\n        }\n\n    /**\n     * @return SemanticsNodeInteraction for list item\n     */\n    fun onItem(matcher: SemanticsMatcher) = UltronComposeSemanticsNodeInteraction(\n        getInteraction().execute(\n            UltronComposeOperationParams(\n                operationName = \"Get item '${matcher.description}' in list '${getInteraction().elementInfo.name}'\",\n                operationDescription = \"Get Compose list item with matcher '${matcher.description}' in list '${getInteraction().elementInfo.name}'\",\n                operationType = ComposeOperationType.GET_LIST_ITEM\n            )\n        ) { listInteraction ->\n            listInteraction.performScrollToNode(matcher).onChildren().filterToOne(matcher)\n        }\n    ).withName(\"Item with: '${matcher.description}' on '$listName'\")\n\n    fun visibleChild(childMatcher: SemanticsMatcher) = UltronComposeSemanticsNodeInteraction(\n        getInteraction().execute(\n            UltronComposeOperationParams(\n                operationName = \"Get child '${childMatcher.description}' of list '${getInteraction().elementInfo.name}'\",\n                operationDescription = \"Get Compose list child '${childMatcher.description}' of list '${getInteraction().elementInfo.name}'\",\n                operationType = ComposeOperationType.GET_LIST_ITEM\n            )\n        ) { listInteraction ->\n            listInteraction.onChildren().filterToOne(childMatcher)\n        }\n    ).withName(\"Child: '${childMatcher.description}' on '$listName\")\n\n    fun onVisibleItem(index: Int) = UltronComposeSemanticsNodeInteraction(\n        getInteraction().execute(\n            UltronComposeOperationParams(\n                operationName = \"Get visible item at index $index in list '${getInteraction().elementInfo.name}'\",\n                operationDescription = \"Get Compose list visible item at index $index in list '${getInteraction().elementInfo.name}'\",\n                operationType = ComposeOperationType.GET_LIST_ITEM\n            )\n        ) { listInteraction ->\n            val visibleItemsList = listInteraction.fetchSemanticsNode().children\n            if (index > visibleItemsList.size) {\n                throw UltronException(\n                    \"\"\"\n                |Item index ($index) is out of visible items (${visibleItemsList.size}). \n                |It's impossible to get the reference to item by index after scroll. You have 3 variants:\n                |1. [Preferred one] Use another method to receive list item with matcher UltronComposeList.item(matcher: SemanticsMatcher) \n                |   In case you still wanna scroll to item by position in list:\n                |   - Add testTag for items in LazyColumn definition like 'itemsIndexed(items){ index, index -> .. Modifier.testTag(\"position=`$`index\") }'\n                |   - Use matcher in test to get item 'list.item(hasTestTag(\"position=`$`index\"))'\n                |2. [A good way also] Use another method to receive list item with position UltronComposeList.item(position: Int) \n                |   To use this method you have to:\n                |   - Configure custom Position SemanticsProperty for compose list items in application code\n                |   - Setup [positionPropertyKey] parameter for composeList(..) in test code\n                |   - Read documentation for details.  \n                |3. Scroll to index by using UltronComposeList.scrollToIndex(index: Int) and use SemanticsMatcher to find item. \n            \"\"\".trimMargin()\n                )\n            }\n            listInteraction.onChildAt(index)\n        }\n    ).withName(\"item at index $index on '$listName\")\n\n    fun scrollToNode(itemMatcher: SemanticsMatcher) = apply { getInteraction().scrollToNode(itemMatcher) }\n    fun scrollToIndex(index: Int) = apply { getInteraction().scrollToIndex(index) }\n    fun scrollToKey(key: Any) = apply { getInteraction().scrollToKey(key) }\n    fun assertIsDisplayed() = apply { getInteraction().assertIsDisplayed() }\n    fun assertIsNotDisplayed() = apply { getInteraction().assertIsNotDisplayed() }\n    fun assertExists() = apply { getInteraction().assertExists() }\n    fun assertDoesNotExist() = apply { getInteraction().assertDoesNotExist() }\n    fun assertContentDescriptionEquals(vararg expected: String) = apply { getInteraction().assertContentDescriptionEquals(*expected) }\n    fun assertContentDescriptionContains(expected: String, option: ContentDescriptionContainsOption? = null) =\n        apply { getInteraction().assertContentDescriptionContains(expected, option) }\n\n    fun assertMatches(matcher: SemanticsMatcher) = apply { getInteraction().assertMatches(matcher) }\n    fun assertNotEmpty() = apply {\n        AssertUtils.assertTrue(\n            { getVisibleItemsCount() > 0 }, getOperationTimeout(),\n            { \"Compose list (${listName}) is NOT empty\" }\n        )\n    }\n\n    fun assertEmpty() = apply {\n        AssertUtils.assertTrue(\n            { getVisibleItemsCount() == 0 }, getOperationTimeout(),\n            { \"Compose list (${listName}) has no items (visible items count = ${getVisibleItemsCount()})\" }\n        )\n    }\n\n    fun assertVisibleItemsCount(expected: Int) = apply {\n        AssertUtils.assertTrue(\n            { getVisibleItemsCount() == expected }, getOperationTimeout(),\n            { \"Compose list (${listName}) has visible items count = $expected (actual visible items count = ${getVisibleItemsCount()})\" }\n        )\n    }\n\n    /**\n     * Asserts whether an item exists in the list or not.\n     * If the item doesn't exist, the operation is considered successful immediately.\n     * If the item exists, the operation waits for a specified timeout ([operationTimeoutMs]) for the item to disappear.\n     * Otherwise, an exception will be thrown.\n     */\n    fun assertItemDoesNotExist(itemMatcher: SemanticsMatcher) {\n        getInteraction().perform(\n            params = UltronComposeOperationParams(\n                operationName = \"Assert item ${itemMatcher.description} doesn't exist in list ${getInteraction().elementInfo.name}\",\n                operationDescription = \"Assert item ${itemMatcher.description} doesn't exist in list ${getInteraction().elementInfo.name} during ${getOperationTimeout()}\",\n                operationType = ComposeOperationType.ASSERT_LIST_ITEM_DOES_NOT_EXIST\n            )\n        ) {\n            runCatching { it.performScrollToNode(itemMatcher) }\n                .onSuccess { throw UltronAssertionException(\"Item '${itemMatcher.description}' exists in list '${listName}'\") }\n                .onFailure { e ->\n                    e.message?.let { message ->\n                        if (!message.contains(\"No node found that matches\")) {\n                            throw AssertionError(message)\n                        }\n                    }\n                }\n        }\n    }\n\n    fun getVisibleItemsCount(): Int = getInteraction().execute { it.fetchSemanticsNode().children.size }\n    fun getInteraction() = UltronComposeSemanticsNodeInteraction(listMatcher, useUnmergedTree, operationTimeoutMs).withName(listName)\n\n    @Deprecated(\"Use getInteraction() instead\", ReplaceWith(\"getInteraction()\"))\n    fun getMatcher() = getInteraction()\n}\n\n\n\nfun composeList(\n    listMatcher: SemanticsMatcher,\n    useUnmergedTree: Boolean = true,\n    positionPropertyKey: SemanticsPropertyKey<Int>? = null,\n    initBlock: UltronComposeList.() -> Unit = {}\n) = UltronComposeList(listMatcher, useUnmergedTree, positionPropertyKey, initBlock)\n"
  },
  {
    "path": "ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/core/compose/list/UltronComposeListItem.kt",
    "content": "package com.atiurin.ultron.core.compose.list\n\nimport androidx.compose.ui.input.key.KeyEvent\nimport androidx.compose.ui.semantics.AccessibilityAction\nimport androidx.compose.ui.semantics.ProgressBarRangeInfo\nimport androidx.compose.ui.semantics.SemanticsPropertyKey\nimport androidx.compose.ui.test.ExperimentalTestApi\nimport androidx.compose.ui.test.MouseInjectionScope\nimport androidx.compose.ui.test.SemanticsMatcher\nimport androidx.compose.ui.test.SemanticsNodeInteraction\nimport androidx.compose.ui.text.TextRange\nimport androidx.compose.ui.unit.Dp\nimport com.atiurin.ultron.core.common.options.ClickOption\nimport com.atiurin.ultron.core.common.options.ContentDescriptionContainsOption\nimport com.atiurin.ultron.core.common.options.DoubleClickOption\nimport com.atiurin.ultron.core.common.options.LongClickOption\nimport com.atiurin.ultron.core.common.options.PerformCustomBlockOption\nimport com.atiurin.ultron.core.common.options.TextContainsOption\nimport com.atiurin.ultron.core.common.options.TextEqualsOption\nimport com.atiurin.ultron.core.compose.nodeinteraction.UltronComposeSemanticsNodeInteraction\nimport com.atiurin.ultron.core.compose.nodeinteraction.click\nimport com.atiurin.ultron.core.compose.nodeinteraction.clickBottomCenter\nimport com.atiurin.ultron.core.compose.nodeinteraction.clickBottomLeft\nimport com.atiurin.ultron.core.compose.nodeinteraction.clickBottomRight\nimport com.atiurin.ultron.core.compose.nodeinteraction.clickCenterLeft\nimport com.atiurin.ultron.core.compose.nodeinteraction.clickCenterRight\nimport com.atiurin.ultron.core.compose.nodeinteraction.clickTopCenter\nimport com.atiurin.ultron.core.compose.nodeinteraction.clickTopLeft\nimport com.atiurin.ultron.core.compose.nodeinteraction.clickTopRight\nimport com.atiurin.ultron.core.compose.nodeinteraction.doubleClick\nimport com.atiurin.ultron.core.compose.nodeinteraction.doubleClickBottomCenter\nimport com.atiurin.ultron.core.compose.nodeinteraction.doubleClickBottomLeft\nimport com.atiurin.ultron.core.compose.nodeinteraction.doubleClickBottomRight\nimport com.atiurin.ultron.core.compose.nodeinteraction.doubleClickCenterLeft\nimport com.atiurin.ultron.core.compose.nodeinteraction.doubleClickCenterRight\nimport com.atiurin.ultron.core.compose.nodeinteraction.doubleClickTopCenter\nimport com.atiurin.ultron.core.compose.nodeinteraction.doubleClickTopLeft\nimport com.atiurin.ultron.core.compose.nodeinteraction.doubleClickTopRight\nimport com.atiurin.ultron.core.compose.nodeinteraction.longClick\nimport com.atiurin.ultron.core.compose.nodeinteraction.longClickBottomCenter\nimport com.atiurin.ultron.core.compose.nodeinteraction.longClickBottomLeft\nimport com.atiurin.ultron.core.compose.nodeinteraction.longClickBottomRight\nimport com.atiurin.ultron.core.compose.nodeinteraction.longClickCenterLeft\nimport com.atiurin.ultron.core.compose.nodeinteraction.longClickCenterRight\nimport com.atiurin.ultron.core.compose.nodeinteraction.longClickTopCenter\nimport com.atiurin.ultron.core.compose.nodeinteraction.longClickTopLeft\nimport com.atiurin.ultron.core.compose.nodeinteraction.longClickTopRight\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationResult\nimport com.atiurin.ultron.core.compose.operation.UltronComposeOperation\nimport com.atiurin.ultron.core.compose.operation.UltronComposeOperationParams\nimport com.atiurin.ultron.core.compose.option.ComposeSwipeOption\n\nopen class UltronComposeListItem {\n    lateinit var executor: ComposeItemExecutor\n\n    constructor(ultronComposeList: UltronComposeList, itemMatcher: SemanticsMatcher) {\n        setExecutor(ultronComposeList, itemMatcher)\n    }\n\n    constructor(ultronComposeList: UltronComposeList, index: Int, isPositionPropertyConfigured: Boolean = false) {\n        setExecutor(ultronComposeList, index, isPositionPropertyConfigured)\n    }\n\n    /**\n     * Use this constructor to inherit from [UltronComposeListItem]\n     * Don't create an instance of subclass. Use [UltronComposeList.item] instead\n     */\n    protected constructor()\n\n    fun child(block: () -> SemanticsMatcher): Lazy<UltronComposeSemanticsNodeInteraction> = lazy {\n        getChild(block())\n    }\n\n    fun setExecutor(ultronComposeList: UltronComposeList, itemMatcher: SemanticsMatcher) {\n        this.executor = MatcherComposeItemExecutor(ultronComposeList, itemMatcher)\n    }\n\n    fun setExecutor(ultronComposeList: UltronComposeList, index: Int, isPositionPropertyConfigured: Boolean = false) {\n        if (isPositionPropertyConfigured) {\n            this.executor = PositionComposeItemExecutor(ultronComposeList, index)\n        } else {\n            this.executor = IndexComposeItemExecutor(ultronComposeList, index)\n        }\n    }\n\n    fun withTimeout(timeoutMs: Long) = getItemUltronComposeInteraction().withTimeout(timeoutMs)\n    fun withResultHandler(resultHandler: (ComposeOperationResult<UltronComposeOperation>) -> Unit) =\n        getItemUltronComposeInteraction().withResultHandler(resultHandler)\n\n    fun getChild(childMatcher: SemanticsMatcher): UltronComposeSemanticsNodeInteraction = executor.getItemChildInteraction(childMatcher)\n    fun getItemUltronComposeInteraction(): UltronComposeSemanticsNodeInteraction = executor.getItemInteraction()\n    fun scrollToItem(offset: Int = 0): UltronComposeListItem = apply { executor.scrollToItem(offset) }\n\n    fun click(option: ClickOption? = null) = apply { getItemUltronComposeInteraction().click(option) }\n    fun clickCenterLeft(option: ClickOption? = null) = apply { getItemUltronComposeInteraction().clickCenterLeft(option) }\n    fun clickCenterRight(option: ClickOption? = null) = apply { getItemUltronComposeInteraction().clickCenterRight(option) }\n    fun clickTopCenter(option: ClickOption? = null) = apply { getItemUltronComposeInteraction().clickTopCenter(option) }\n    fun clickTopLeft(option: ClickOption? = null) = apply { getItemUltronComposeInteraction().clickTopLeft(option) }\n    fun clickTopRight(option: ClickOption? = null) = apply { getItemUltronComposeInteraction().clickTopRight(option) }\n    fun clickBottomCenter(option: ClickOption? = null) = apply { getItemUltronComposeInteraction().clickBottomCenter(option) }\n    fun clickBottomLeft(option: ClickOption? = null) = apply { getItemUltronComposeInteraction().clickBottomLeft(option) }\n    fun clickBottomRight(option: ClickOption? = null) = apply { getItemUltronComposeInteraction().clickBottomRight(option) }\n\n    fun longClick(option: LongClickOption? = null) = apply { getItemUltronComposeInteraction().longClick(option) }\n    fun longClickCenterLeft(option: LongClickOption? = null) = apply { getItemUltronComposeInteraction().longClickCenterLeft(option) }\n    fun longClickCenterRight(option: LongClickOption? = null) = apply { getItemUltronComposeInteraction().longClickCenterRight(option) }\n    fun longClickTopCenter(option: LongClickOption? = null) = apply { getItemUltronComposeInteraction().longClickTopCenter(option) }\n    fun longClickTopLeft(option: LongClickOption? = null) = apply { getItemUltronComposeInteraction().longClickTopLeft(option) }\n    fun longClickTopRight(option: LongClickOption? = null) = apply { getItemUltronComposeInteraction().longClickTopRight(option) }\n    fun longClickBottomCenter(option: LongClickOption? = null) = apply { getItemUltronComposeInteraction().longClickBottomCenter(option) }\n    fun longClickBottomLeft(option: LongClickOption? = null) = apply { getItemUltronComposeInteraction().longClickBottomLeft(option) }\n    fun longClickBottomRight(option: LongClickOption? = null) = apply { getItemUltronComposeInteraction().longClickBottomRight(option) }\n\n    fun doubleClick(option: DoubleClickOption? = null) = apply { getItemUltronComposeInteraction().doubleClick(option) }\n    fun doubleClickCenterLeft(option: DoubleClickOption? = null) = apply { getItemUltronComposeInteraction().doubleClickCenterLeft(option) }\n    fun doubleClickCenterRight(option: DoubleClickOption? = null) = apply { getItemUltronComposeInteraction().doubleClickCenterRight(option) }\n    fun doubleClickTopCenter(option: DoubleClickOption? = null) = apply { getItemUltronComposeInteraction().doubleClickTopCenter(option) }\n    fun doubleClickTopLeft(option: DoubleClickOption? = null) = apply { getItemUltronComposeInteraction().doubleClickTopLeft(option) }\n    fun doubleClickTopRight(option: DoubleClickOption? = null) = apply { getItemUltronComposeInteraction().doubleClickTopRight(option) }\n    fun doubleClickBottomCenter(option: DoubleClickOption? = null) = apply { getItemUltronComposeInteraction().doubleClickBottomCenter(option) }\n    fun doubleClickBottomLeft(option: DoubleClickOption? = null) = apply { getItemUltronComposeInteraction().doubleClickBottomLeft(option) }\n    fun doubleClickBottomRight(option: DoubleClickOption? = null) = apply { getItemUltronComposeInteraction().doubleClickBottomRight(option) }\n    fun swipeDown(option: ComposeSwipeOption? = null) = apply { getItemUltronComposeInteraction().swipeDown(option) }\n    fun swipeUp(option: ComposeSwipeOption? = null) = apply { getItemUltronComposeInteraction().swipeUp(option) }\n    fun swipeLeft(option: ComposeSwipeOption? = null) = apply { getItemUltronComposeInteraction().swipeLeft(option) }\n    fun swipeRight(option: ComposeSwipeOption? = null) = apply { getItemUltronComposeInteraction().swipeRight(option) }\n    fun swipe(option: ComposeSwipeOption) = apply { getItemUltronComposeInteraction().swipe(option) }\n\n    fun imeAction() = apply { getItemUltronComposeInteraction().imeAction() }\n    fun pressKey(keyEvent: KeyEvent) = apply { getItemUltronComposeInteraction().pressKey(keyEvent) }\n    fun getText(): String? = getItemUltronComposeInteraction().getText()\n    fun inputText(text: String) = apply { getItemUltronComposeInteraction().inputText(text) }\n    fun inputTextSelection(selection: TextRange) = apply { getItemUltronComposeInteraction().inputTextSelection(selection) }\n    fun clearText() = apply { getItemUltronComposeInteraction().clearText() }\n    fun replaceText(text: String) = apply { getItemUltronComposeInteraction().replaceText(text) }\n    fun printToLog(tag: String, maxDepth: Int = Int.MAX_VALUE) = apply { getItemUltronComposeInteraction().printToLog(tag, maxDepth) }\n\n    @OptIn(ExperimentalTestApi::class)\n    fun performMouseInput(block: MouseInjectionScope.() -> Unit) = apply { getItemUltronComposeInteraction().performMouseInput(block) }\n    fun performSemanticsAction(key: SemanticsPropertyKey<AccessibilityAction<() -> Boolean>>) = apply { getItemUltronComposeInteraction().performSemanticsAction(key) }\n\n    @Deprecated(\n        \"Use the execute(params: UltronComposeOperationParams?, block: (SemanticsNodeInteraction) -> T) method instead.\",\n        ReplaceWith(\"execute(params, block)\")\n    )\n    fun <T> perform(\n        option: PerformCustomBlockOption, block: (SemanticsNodeInteraction) -> T\n    ) = getItemUltronComposeInteraction().perform(option, block)\n\n    @Deprecated(\n        \"Use the execute(params: UltronComposeOperationParams?, block: (SemanticsNodeInteraction) -> T) method instead.\",\n        ReplaceWith(\"execute(params, block)\")\n    )\n    fun <T> perform(\n        block: (SemanticsNodeInteraction) -> T\n    ): T = getItemUltronComposeInteraction().execute(null, block)\n\n    fun <T> execute(\n        params: UltronComposeOperationParams? = null, block: (SemanticsNodeInteraction) -> T\n    ) = getItemUltronComposeInteraction().execute(params, block)\n\n    fun assertIsDisplayed() = apply { getItemUltronComposeInteraction().assertIsDisplayed() }\n    fun assertIsNotDisplayed() = apply { getItemUltronComposeInteraction().assertIsNotDisplayed() }\n    fun assertExists() = apply { getItemUltronComposeInteraction().assertExists() }\n    fun assertIsEnabled() = apply { getItemUltronComposeInteraction().assertIsEnabled() }\n    fun assertIsNotEnabled() = apply { getItemUltronComposeInteraction().assertIsNotEnabled() }\n    fun assertIsFocused() = apply { getItemUltronComposeInteraction().assertIsFocused() }\n    fun assertIsNotFocused() = apply { getItemUltronComposeInteraction().assertIsNotFocused() }\n    fun assertIsSelected() = apply { getItemUltronComposeInteraction().assertIsSelected() }\n    fun assertIsNotSelected() = apply { getItemUltronComposeInteraction().assertIsNotSelected() }\n    fun assertIsSelectable() = apply { getItemUltronComposeInteraction().assertIsSelectable() }\n    fun assertIsOn() = apply { getItemUltronComposeInteraction().assertIsOn() }\n    fun assertIsOff() = apply { getItemUltronComposeInteraction().assertIsOff() }\n    fun assertIsToggleable() = apply { getItemUltronComposeInteraction().assertIsToggleable() }\n    fun assertHasClickAction() = apply { getItemUltronComposeInteraction().assertHasClickAction() }\n    fun assertHasNoClickAction() = apply { getItemUltronComposeInteraction().assertHasNoClickAction() }\n    fun assertTextEquals(vararg expected: String, option: TextEqualsOption? = null) = apply { getItemUltronComposeInteraction().assertTextEquals(*expected, option = option) }\n    fun assertTextContains(expected: String, option: TextContainsOption? = null) = apply { getItemUltronComposeInteraction().assertTextContains(expected, option) }\n    fun assertContentDescriptionEquals(vararg expected: String) = apply { getItemUltronComposeInteraction().assertContentDescriptionEquals(*expected) }\n    fun assertContentDescriptionContains(expected: String, option: ContentDescriptionContainsOption? = null) =\n        apply { getItemUltronComposeInteraction().assertContentDescriptionContains(expected, option) }\n\n    fun assertValueEquals(expected: String) = apply { getItemUltronComposeInteraction().assertValueEquals(expected) }\n    fun assertRangeInfoEquals(range: ProgressBarRangeInfo) = apply { getItemUltronComposeInteraction().assertRangeInfoEquals(range) }\n    fun assertHeightIsAtLeast(minHeight: Dp) = apply { getItemUltronComposeInteraction().assertHeightIsAtLeast(minHeight) }\n    fun assertHeightIsEqualTo(expectedHeight: Dp) = apply { getItemUltronComposeInteraction().assertHeightIsEqualTo(expectedHeight) }\n    fun assertWidthIsAtLeast(minWidth: Dp) = apply { getItemUltronComposeInteraction().assertWidthIsAtLeast(minWidth) }\n    fun assertWidthIsEqualTo(expectedWidth: Dp) = apply { getItemUltronComposeInteraction().assertWidthIsEqualTo(expectedWidth) }\n    fun assertMatches(matcher: SemanticsMatcher, messagePrefixOnError: (() -> String)? = null) =\n        apply { getItemUltronComposeInteraction().assertMatches(matcher, messagePrefixOnError) }\n\n    fun assertTextEquals(expected: String) = apply { getItemUltronComposeInteraction().assertTextEquals(expected) }\n    fun assertTextContains(expected: String) = apply { getItemUltronComposeInteraction().assertTextContains(expected) }\n}\n\nexpect inline fun <reified T : UltronComposeListItem> getComposeListItemInstance(\n    ultronComposeList: UltronComposeList,\n    itemMatcher: SemanticsMatcher\n): T\n\nexpect inline fun <reified T : UltronComposeListItem> getComposeListItemInstance(\n    ultronComposeList: UltronComposeList,\n    position: Int,\n    isPositionPropertyConfigured: Boolean = false\n): T"
  },
  {
    "path": "ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/core/compose/nodeinteraction/SwipePosition.kt",
    "content": "package com.atiurin.ultron.core.compose.nodeinteraction\n\nimport androidx.compose.ui.geometry.Offset\n\ndata class SwipePosition(val start: Offset, val end: Offset)"
  },
  {
    "path": "ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/core/compose/nodeinteraction/UltronComposeOffsets.kt",
    "content": "package com.atiurin.ultron.core.compose.nodeinteraction\n\nenum class UltronComposeOffsets {\n    CENTER, CENTER_LEFT, CENTER_RIGHT,\n    TOP_CENTER, TOP_LEFT, TOP_RIGHT,\n    BOTTOM_CENTER, BOTTOM_LEFT, BOTTOM_RIGHT\n}\n"
  },
  {
    "path": "ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/core/compose/nodeinteraction/UltronComposeSemanticsNodeInteraction.kt",
    "content": "package com.atiurin.ultron.core.compose.nodeinteraction\n\n\nimport androidx.compose.ui.input.key.KeyEvent\nimport androidx.compose.ui.semantics.AccessibilityAction\nimport androidx.compose.ui.semantics.ProgressBarRangeInfo\nimport androidx.compose.ui.semantics.SemanticsActions\nimport androidx.compose.ui.semantics.SemanticsNode\nimport androidx.compose.ui.semantics.SemanticsPropertyKey\nimport androidx.compose.ui.test.ExperimentalTestApi\nimport androidx.compose.ui.test.MouseInjectionScope\nimport androidx.compose.ui.test.SemanticsMatcher\nimport androidx.compose.ui.test.SemanticsNodeInteraction\nimport androidx.compose.ui.test.assert\nimport androidx.compose.ui.test.assertContentDescriptionContains\nimport androidx.compose.ui.test.assertContentDescriptionEquals\nimport androidx.compose.ui.test.assertHasClickAction\nimport androidx.compose.ui.test.assertHasNoClickAction\nimport androidx.compose.ui.test.assertHeightIsAtLeast\nimport androidx.compose.ui.test.assertHeightIsEqualTo\nimport androidx.compose.ui.test.assertIsDisplayed\nimport androidx.compose.ui.test.assertIsEnabled\nimport androidx.compose.ui.test.assertIsFocused\nimport androidx.compose.ui.test.assertIsNotDisplayed\nimport androidx.compose.ui.test.assertIsNotEnabled\nimport androidx.compose.ui.test.assertIsNotFocused\nimport androidx.compose.ui.test.assertIsNotSelected\nimport androidx.compose.ui.test.assertIsOff\nimport androidx.compose.ui.test.assertIsOn\nimport androidx.compose.ui.test.assertIsSelectable\nimport androidx.compose.ui.test.assertIsSelected\nimport androidx.compose.ui.test.assertIsToggleable\nimport androidx.compose.ui.test.assertRangeInfoEquals\nimport androidx.compose.ui.test.assertTextContains\nimport androidx.compose.ui.test.assertTextEquals\nimport androidx.compose.ui.test.assertValueEquals\nimport androidx.compose.ui.test.assertWidthIsAtLeast\nimport androidx.compose.ui.test.assertWidthIsEqualTo\nimport androidx.compose.ui.test.click\nimport androidx.compose.ui.test.doubleClick\nimport androidx.compose.ui.test.longClick\nimport androidx.compose.ui.test.performCustomAccessibilityActionWithLabel\nimport androidx.compose.ui.test.performCustomAccessibilityActionWithLabelMatching\nimport androidx.compose.ui.test.performImeAction\nimport androidx.compose.ui.test.performKeyPress\nimport androidx.compose.ui.test.performMouseInput\nimport androidx.compose.ui.test.performScrollTo\nimport androidx.compose.ui.test.performScrollToIndex\nimport androidx.compose.ui.test.performScrollToKey\nimport androidx.compose.ui.test.performScrollToNode\nimport androidx.compose.ui.test.performSemanticsAction\nimport androidx.compose.ui.test.performTextClearance\nimport androidx.compose.ui.test.performTextInput\nimport androidx.compose.ui.test.performTextInputSelection\nimport androidx.compose.ui.test.performTextReplacement\nimport androidx.compose.ui.test.performTouchInput\nimport androidx.compose.ui.test.printToLog\nimport androidx.compose.ui.test.swipe\nimport androidx.compose.ui.text.AnnotatedString\nimport androidx.compose.ui.text.TextRange\nimport androidx.compose.ui.unit.Dp\nimport com.atiurin.ultron.core.common.CommonOperationType\nimport com.atiurin.ultron.core.common.DefaultElementInfo\nimport com.atiurin.ultron.core.common.ElementInfo\nimport com.atiurin.ultron.core.common.UltronOperationType\nimport com.atiurin.ultron.core.common.assertion.DefaultOperationAssertion\nimport com.atiurin.ultron.core.common.assertion.EmptyOperationAssertion\nimport com.atiurin.ultron.core.common.assertion.OperationAssertion\nimport com.atiurin.ultron.core.common.options.ClickOption\nimport com.atiurin.ultron.core.common.options.ContentDescriptionContainsOption\nimport com.atiurin.ultron.core.common.options.DoubleClickOption\nimport com.atiurin.ultron.core.common.options.LongClickOption\nimport com.atiurin.ultron.core.common.options.PerformCustomBlockOption\nimport com.atiurin.ultron.core.common.options.TextContainsOption\nimport com.atiurin.ultron.core.common.options.TextEqualsOption\nimport com.atiurin.ultron.core.compose.ComposeTestContainer\nimport com.atiurin.ultron.core.compose.config.UltronComposeConfig\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationExecutor\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationResult\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.ASSERT_MATCHES\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.CLEAR_TEXT\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.CLICK\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.COLLAPSE\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.CONTAINS_TEXT\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.CONTENT_DESCRIPTION_CONTAINS_TEXT\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.COPY_TEXT\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.CUT_TEXT\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.DISMISS\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.DOES_NOT_EXIST\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.DOUBLE_CLICK\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.EXISTS\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.EXPAND\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.GET_SEMANTICS_NODE\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.GET_TEXT\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.HAS_CLICK_ACTION\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.HAS_CONTENT_DESCRIPTION\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.HAS_NO_CLICK_ACTION\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.HEIGHT_IS_AT_LEAST\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.HEIGHT_IS_EQUAL_TO\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.IME_ACTION\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.IS_DISPLAYED\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.IS_ENABLED\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.IS_FOCUSED\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.IS_INDETERMINATE\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.IS_NOT_DISPLAYED\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.IS_NOT_ENABLED\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.IS_NOT_FOCUSED\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.IS_NOT_SELECTED\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.IS_OFF\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.IS_ON\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.IS_SELECTABLE\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.IS_SELECTED\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.IS_TOGGLEABLE\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.LONG_CLICK\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.MOUSE_INPUT\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.PASTE_TEXT\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.PRESS_KEY\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.PRINT_TO_LOG\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.PROGRESS_BAR_RANGE_EQUALS\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.REPLACE_TEXT\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.SCROLL_TO\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.SCROLL_TO_INDEX\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.SCROLL_TO_KEY\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.SCROLL_TO_NODE\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.SEMANTIC_ACTION\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.SET_PROGRESS\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.SET_SELECTION\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.SET_TEXT\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.SWIPE_DOWN\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.SWIPE_LEFT\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.SWIPE_RIGHT\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.SWIPE_UP\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.TEXT_EQUALS\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.TEXT_INPUT\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.TEXT_INPUT_SELECTION\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.VALUE_EQUALS\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.WIDTH_IS_AT_LEAST\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationType.WIDTH_IS_EQUAL_TO\nimport com.atiurin.ultron.core.compose.operation.UltronComposeOperation\nimport com.atiurin.ultron.core.compose.operation.UltronComposeOperationLifecycle\nimport com.atiurin.ultron.core.compose.operation.UltronComposeOperationParams\nimport com.atiurin.ultron.core.compose.option.ComposeSwipeOption\nimport com.atiurin.ultron.exceptions.UltronOperationException\nimport com.atiurin.ultron.extensions.assertIsIndeterminate\nimport com.atiurin.ultron.extensions.getDefaultDoubleClickDelay\nimport com.atiurin.ultron.extensions.getDefaultLongClickDuration\nimport com.atiurin.ultron.extensions.getOneOfConfigFields\nimport com.atiurin.ultron.extensions.getSelectorDescription\nimport com.atiurin.ultron.extensions.getUltronComposeOffset\nimport com.atiurin.ultron.extensions.provideSwipeDownPosition\nimport com.atiurin.ultron.extensions.provideSwipeLeftPosition\nimport com.atiurin.ultron.extensions.provideSwipeRightPosition\nimport com.atiurin.ultron.extensions.provideSwipeUpPosition\nimport com.atiurin.ultron.listeners.setListenersState\nimport kotlinx.atomicfu.AtomicRef\nimport kotlinx.atomicfu.atomic\nimport kotlinx.atomicfu.update\n\nopen class UltronComposeSemanticsNodeInteraction constructor(\n    private val semanticsNodeInteractionProvider: () -> SemanticsNodeInteraction,\n    val timeoutMs: Long = UltronComposeConfig.params.operationTimeoutMs,\n    val resultHandler: ((ComposeOperationResult<UltronComposeOperation>) -> Unit) = UltronComposeConfig.resultHandler,\n    val assertion: OperationAssertion = EmptyOperationAssertion(),\n    val elementInfo: ElementInfo = DefaultElementInfo()\n) {\n    val semanticsNodeInteraction: SemanticsNodeInteraction\n        get() = semanticsNodeInteractionProvider()\n\n    constructor(\n        semanticsNodeInteraction: SemanticsNodeInteraction,\n        timeoutMs: Long = UltronComposeConfig.params.operationTimeoutMs,\n        resultHandler: ((ComposeOperationResult<UltronComposeOperation>) -> Unit) = UltronComposeConfig.resultHandler,\n        assertion: OperationAssertion = EmptyOperationAssertion(),\n        elementInfo: ElementInfo = DefaultElementInfo()\n    ) : this(\n        semanticsNodeInteractionProvider = { semanticsNodeInteraction },\n        timeoutMs, resultHandler, assertion, elementInfo\n    )\n\n    constructor(\n        matcher: SemanticsMatcher,\n        useUnmergedTree: Boolean = UltronComposeConfig.params.useUnmergedTree,\n        timeoutMs: Long = UltronComposeConfig.params.operationTimeoutMs,\n        resultHandler: ((ComposeOperationResult<UltronComposeOperation>) -> Unit) = UltronComposeConfig.resultHandler,\n        assertion: OperationAssertion = EmptyOperationAssertion(),\n        elementInfo: ElementInfo = DefaultElementInfo()\n    ) : this(\n        semanticsNodeInteractionProvider = { ComposeTestContainer.getProvider().onNode(matcher, useUnmergedTree) },\n        timeoutMs, resultHandler, assertion, elementInfo\n    )\n\n    init {\n        if (elementInfo.name.isEmpty()) elementInfo.name = semanticsNodeInteraction.getSelectorDescription()\n    }\n\n    fun <T> isSuccess(action: UltronComposeSemanticsNodeInteraction.() -> T): Boolean = runCatching { action() }.isSuccess\n\n    fun withResultHandler(resultHandler: (ComposeOperationResult<UltronComposeOperation>) -> Unit) = UltronComposeSemanticsNodeInteraction(\n        semanticsNodeInteractionProvider, this.timeoutMs, resultHandler, this.assertion, this.elementInfo\n    )\n\n    fun withTimeout(timeoutMs: Long) = UltronComposeSemanticsNodeInteraction(\n        semanticsNodeInteractionProvider, timeoutMs, this.resultHandler, this.assertion, this.elementInfo\n    )\n\n\n    fun withAssertion(assertion: OperationAssertion) =\n        UltronComposeSemanticsNodeInteraction(semanticsNodeInteractionProvider, timeoutMs, resultHandler, assertion, this.elementInfo)\n\n    fun withAssertion(name: String = \"\", isListened: Boolean = false, block: () -> Unit) =\n        UltronComposeSemanticsNodeInteraction(\n            semanticsNodeInteractionProvider,\n            timeoutMs,\n            resultHandler,\n            DefaultOperationAssertion(name, block.setListenersState(isListened)),\n            this.elementInfo\n        )\n\n    fun withName(name: String) = apply { elementInfo.name = name }\n\n    fun withMetaInfo(meta: Any) = apply { elementInfo.meta = meta }\n\n    internal fun click(position: UltronComposeOffsets, option: ClickOption? = null) = apply {\n        val _option = option ?: ClickOption(0, 0)\n        executeOperation(\n            operationBlock = {\n                semanticsNodeInteraction.performTouchInput {\n                    val offset = getUltronComposeOffset(position).let { it.copy(x = it.x + _option.xOffset, y = it.y + _option.yOffset) }\n                    this.click(offset)\n                }\n            },\n            name = \"Click on '${elementInfo.name}' ${position.name}\",\n            type = CLICK,\n            description = \"Compose click on '${elementInfo.name}' ${position.name} with option = '$_option' during $timeoutMs ms\",\n        )\n    }\n\n    internal fun longClick(position: UltronComposeOffsets, option: LongClickOption? = null) = apply {\n        var _option = option\n        executeOperation(\n            operationBlock = {\n                semanticsNodeInteraction.performTouchInput {\n                    _option = _option ?: LongClickOption(0, 0, getDefaultLongClickDuration())\n                    val offset = getUltronComposeOffset(position).let { it.copy(x = it.x + (_option?.xOffset ?: 0L), y = it.y + (_option?.yOffset ?: 0)) }\n                    this.longClick(offset, _option?.durationMs ?: getDefaultLongClickDuration())\n                }\n            },\n            name = \"LongClick on '${elementInfo.name}' ${position.name}\",\n            type = LONG_CLICK,\n            description = \"Compose longClick on '${elementInfo.name}' ${position.name} with option = '$_option' during $timeoutMs ms\",\n        )\n    }\n\n    internal fun doubleClick(position: UltronComposeOffsets, option: DoubleClickOption? = null) = apply {\n        var _option = option\n        executeOperation(\n            operationBlock = {\n                semanticsNodeInteraction.performTouchInput {\n                    _option = _option ?: DoubleClickOption(0, 0, getDefaultDoubleClickDelay())\n                    val offset = getUltronComposeOffset(position).let { it.copy(x = it.x + (_option?.xOffset ?: 0L), y = it.y + (_option?.yOffset ?: 0)) }\n                    this.doubleClick(offset, delayMillis = _option?.delayMs ?: getDefaultDoubleClickDelay())\n                }\n            },\n            name = \"DoubleClick on '${elementInfo.name}' ${position.name}\",\n            type = DOUBLE_CLICK,\n            description = \"Compose doubleClick on '${elementInfo.name}' ${position.name} with option = '$_option' during $timeoutMs ms\",\n        )\n    }\n\n    fun swipeDown(option: ComposeSwipeOption? = null) = apply {\n        val _option = option ?: ComposeSwipeOption(0f, 0f, 0f, 0f, DEFAULT_SWIPE_DURATION)\n        executeOperation(\n            operationBlock = {\n                semanticsNodeInteraction.performTouchInput {\n                    val position = provideSwipeDownPosition(_option)\n                    swipe(position.start, position.end, _option.durationMs)\n                }\n            },\n            name = \"SwipeDown '${elementInfo.name}'\",\n            type = SWIPE_DOWN,\n            description = \"Compose swipeDown '${elementInfo.name}' with option = '$_option' during $timeoutMs ms \",\n        )\n    }\n\n    fun swipeUp(option: ComposeSwipeOption? = null) = apply {\n        val _option = option ?: ComposeSwipeOption(0f, 0f, 0f, 0f, DEFAULT_SWIPE_DURATION)\n        executeOperation(\n            operationBlock = {\n                semanticsNodeInteraction.performTouchInput {\n                    val position = provideSwipeUpPosition(_option)\n                    swipe(position.start, position.end, _option.durationMs)\n                }\n            },\n            name = \"SwipeUp '${elementInfo.name}'\",\n            type = SWIPE_UP,\n            description = \"Compose swipeUp '${elementInfo.name}' with option = '$_option' during $timeoutMs ms \",\n        )\n    }\n\n    fun swipeLeft(option: ComposeSwipeOption? = null) = apply {\n        val _option = option ?: ComposeSwipeOption(0f, 0f, 0f, 0f, DEFAULT_SWIPE_DURATION)\n        executeOperation(\n            operationBlock = {\n                semanticsNodeInteraction.performTouchInput {\n                    val position = provideSwipeLeftPosition(_option)\n                    this.swipe(position.start, position.end, _option.durationMs)\n                }\n            },\n            name = \"SwipeLeft to '${elementInfo.name}'\",\n            type = SWIPE_LEFT,\n            description = \"Compose swipeLeft '${elementInfo.name}' with option = '$_option' during $timeoutMs ms \",\n        )\n    }\n\n    fun swipeRight(option: ComposeSwipeOption? = null) = apply {\n        val _option = option ?: ComposeSwipeOption(0f, 0f, 0f, 0f, DEFAULT_SWIPE_DURATION)\n        executeOperation(\n            operationBlock = {\n                semanticsNodeInteraction.performTouchInput {\n                    val position = provideSwipeRightPosition(_option)\n                    this.swipe(position.start, position.end, _option.durationMs)\n                }\n            },\n            name = \"SwipeRight to '${elementInfo.name}'\",\n            type = SWIPE_RIGHT,\n            description = \"Compose swipeRight '${elementInfo.name}' with option = '$_option' during $timeoutMs ms \",\n        )\n    }\n\n    fun swipe(option: ComposeSwipeOption) = apply {\n        executeOperation(\n            operationBlock = {\n                semanticsNodeInteraction.performTouchInput {\n                    val position = provideSwipeRightPosition(option)\n                    this.swipe(position.start, position.end, option.durationMs)\n                }\n            },\n            name = \"Swipe to '${elementInfo.name}'\",\n            type = ComposeOperationType.SWIPE,\n            description = \"Compose swipe '${elementInfo.name}' with option = '$option' during $timeoutMs ms \",\n        )\n    }\n\n    fun scrollTo() = apply {\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.performScrollTo() },\n            name = \"ScrollTo on '${elementInfo.name}'\",\n            type = SCROLL_TO,\n            description = \"Compose scrollTo on '${elementInfo.name} during $timeoutMs ms\",\n        )\n    }\n\n    fun scrollToIndex(index: Int) = apply {\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.performScrollToIndex(index) },\n            name = \"ScrollToIndex '$index' on '${elementInfo.name}'\",\n            type = SCROLL_TO_INDEX,\n            description = \"Compose scrollToIndex $index on '${elementInfo.name} during $timeoutMs ms\",\n        )\n    }\n\n    fun scrollToKey(key: Any) = apply {\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.performScrollToKey(key) },\n            name = \"ScrollToKey '$key' on '${elementInfo.name}'\",\n            type = SCROLL_TO_KEY,\n            description = \"Compose scrollToKey '$key' on '${elementInfo.name} during $timeoutMs ms\",\n        )\n    }\n\n    fun scrollToNode(matcher: SemanticsMatcher) = apply {\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.performScrollToNode(matcher) },\n            name = \"ScrollToNode with: '${matcher.description}' on '${elementInfo.name}'\",\n            type = SCROLL_TO_NODE,\n            description = \"Compose scrollToNode with matcher '${matcher.description}' on '${elementInfo.name} during $timeoutMs ms\",\n        )\n    }\n\n    fun inputText(text: String) = apply {\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.performTextInput(text) },\n            name = \"InputText '$text' to '${elementInfo.name}'\",\n            type = TEXT_INPUT,\n            description = \"Compose inputText '$text' to '${elementInfo.name} during $timeoutMs ms\",\n        )\n    }\n\n    fun typeText(text: String) = inputText(text)\n\n    @OptIn(ExperimentalTestApi::class)\n    fun inputTextSelection(range: TextRange) = apply {\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.performTextInputSelection(range) },\n            name = \"TextInputSelection '$range' to '${elementInfo.name}'\",\n            type = TEXT_INPUT_SELECTION,\n            description = \"Compose inputTextSelection range '$range' to '${elementInfo.name} during $timeoutMs ms\",\n        )\n    }\n\n    fun selectText(range: TextRange) = inputTextSelection(range)\n\n    /**\n     *  in traversal mode we get selection from the `textSelectionRange` semantics which is\n     *  selection in original text. In non-traversal mode selection comes from the Talkback\n     *  and indices are relative to the transformed text\n     */\n    fun setSelection(startIndex: Int = 0, endIndex: Int = 0, traversalMode: Boolean) = apply {\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.performSemanticsAction(SemanticsActions.SetSelection) { it.invoke(startIndex, endIndex, traversalMode) } },\n            name = \"SetSelection from $startIndex to $endIndex for '${elementInfo.name}'\",\n            type = SET_SELECTION,\n            description = \"Compose setSelection from $startIndex to $endIndex for  '${elementInfo.name} with traversalMode = $traversalMode during $timeoutMs ms\",\n        )\n    }\n\n\n    fun imeAction() = apply {\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.performImeAction() },\n            name = \"PerformImeAction to '${elementInfo.name}'\",\n            type = IME_ACTION,\n            description = \"Compose performImeAction to '${elementInfo.name} during $timeoutMs ms\",\n        )\n    }\n\n    fun pressKey(key: KeyEvent) = apply {\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.performKeyPress(key) },\n            name = \"PressKey '$key' to '${elementInfo.name}'\",\n            type = PRESS_KEY,\n            description = \"Compose pressKey event '$key' to '${elementInfo.name} during $timeoutMs ms\",\n        )\n    }\n\n    fun getText(): String? {\n        var text: String? = null\n        executeOperation(\n            operationBlock = {\n                val textValues = semanticsNodeInteraction.getOneOfConfigFields(CONFIG_TEXT_FIELDS_LIST)\n                text = (textValues as Collection<AnnotatedString>).firstOrNull().toString()\n            },\n            name = \"Get text from '${elementInfo.name}'\",\n            type = GET_TEXT,\n            description = \"Compose getText from '${elementInfo.name}' during $timeoutMs ms\",\n        )\n        return text\n    }\n\n    fun clearText() = apply {\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.performTextClearance() },\n            name = \"ClearText in '${elementInfo.name}'\",\n            type = CLEAR_TEXT,\n            description = \"Compose clearText in '${elementInfo.name} during $timeoutMs ms\",\n        )\n    }\n\n    fun replaceText(text: String) = apply {\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.performTextReplacement(text) },\n            name = \"ReplaceText to '$text' in '${elementInfo.name}'\",\n            type = REPLACE_TEXT,\n            description = \"Compose replaceText to '$text' in '${elementInfo.name} during $timeoutMs ms\",\n        )\n    }\n\n    /**\n     * Action could be executed only if there is a text selection at the moment.\n     */\n    fun copyText() = apply {\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.performSemanticsAction(SemanticsActions.CopyText) },\n            name = \"CopyText to clipboard from '${elementInfo.name}'\",\n            type = COPY_TEXT,\n            description = \"Compose copyText to clipboard from '${elementInfo.name} during $timeoutMs ms\",\n        )\n    }\n\n    fun pasteText() = apply {\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.performSemanticsAction(SemanticsActions.PasteText) },\n            name = \"PasteText from clipboard to '${elementInfo.name}'\",\n            type = PASTE_TEXT,\n            description = \"Compose pasteText from clipboard to '${elementInfo.name} during $timeoutMs ms\",\n        )\n    }\n\n    fun cutText() = apply {\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.performSemanticsAction(SemanticsActions.CutText) },\n            name = \"CutText to clipboard from '${elementInfo.name}'\",\n            type = CUT_TEXT,\n            description = \"Compose cutText to clipboard from '${elementInfo.name} during $timeoutMs ms\",\n        )\n    }\n\n    fun setText(text: AnnotatedString) = apply {\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.performSemanticsAction(SemanticsActions.SetText) { it.invoke(text) } },\n            name = \"SetText '${text.text}' to '${elementInfo.name}'\",\n            type = SET_TEXT,\n            description = \"Compose setText '${text.text}' to '${elementInfo.name} during $timeoutMs ms\",\n        )\n    }\n\n    fun setText(text: String) = setText(AnnotatedString(text))\n\n    fun collapse() = apply {\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.performSemanticsAction(SemanticsActions.Collapse) },\n            name = \"Collapse '${elementInfo.name}'\",\n            type = COLLAPSE,\n            description = \"Compose collapse '${elementInfo.name} during $timeoutMs ms\",\n        )\n    }\n\n    fun expand() = apply {\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.performSemanticsAction(SemanticsActions.Expand) },\n            name = \"Expand '${elementInfo.name}'\",\n            type = EXPAND,\n            description = \"Compose expand '${elementInfo.name} during $timeoutMs ms\",\n        )\n    }\n\n    fun dismiss() = apply {\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.performSemanticsAction(SemanticsActions.Dismiss) },\n            name = \"Dismiss '${elementInfo.name}'\",\n            type = DISMISS,\n            description = \"Compose dismiss '${elementInfo.name} during $timeoutMs ms\",\n        )\n    }\n\n    fun setProgress(value: Float) = apply {\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.performSemanticsAction(SemanticsActions.SetProgress) { it.invoke(value) } },\n            name = \"SetProgress $value to '${elementInfo.name}'\",\n            type = SET_PROGRESS,\n            description = \"Compose setProgress $value to '${elementInfo.name} during $timeoutMs ms\",\n        )\n    }\n\n    @OptIn(ExperimentalTestApi::class)\n    fun performMouseInput(block: MouseInjectionScope.() -> Unit) = apply {\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.performMouseInput(block) },\n            name = \"MouseInput to '${elementInfo.name}'\",\n            type = MOUSE_INPUT,\n            description = \"Compose performMouseInput to '${elementInfo.name} during $timeoutMs ms\",\n        )\n    }\n\n    fun performSemanticsAction(key: SemanticsPropertyKey<AccessibilityAction<() -> Boolean>>) = apply { performSemanticsAction(key) { it.invoke() } }\n\n    fun <T : Function<Boolean>> performSemanticsAction(key: SemanticsPropertyKey<AccessibilityAction<T>>, invocation: (T) -> Unit) = apply {\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.performSemanticsAction(key, invocation) },\n            name = \"SemanticAction '${key.name}' to '${elementInfo.name}'\",\n            type = SEMANTIC_ACTION,\n            description = \"Compose semanticAction '${key.name}' to '${elementInfo.name} during $timeoutMs ms\",\n        )\n    }\n\n    @OptIn(ExperimentalTestApi::class)\n    fun performCustomAccessibilityActionWithLabel(label: String) = apply {\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.performCustomAccessibilityActionWithLabel(label) },\n            name = \"PerformCustomAccessibilityActionWithLabel '${label}' to '${elementInfo.name}'\",\n            type = SEMANTIC_ACTION,\n            description = \"Compose PerformCustomAccessibilityActionWithLabel '${label}' to '${elementInfo.name} during $timeoutMs ms\",\n        )\n    }\n\n    @OptIn(ExperimentalTestApi::class)\n    fun performCustomAccessibilityActionWithLabelMatching(\n        predicateDescription: String? = null,\n        labelPredicate: (label: String) -> Boolean\n    ) = apply {\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.performCustomAccessibilityActionWithLabelMatching(predicateDescription, labelPredicate) },\n            name = \"PerformCustomAccessibilityActionWithLabelMatching '${predicateDescription}' to '${elementInfo.name}'\",\n            type = SEMANTIC_ACTION,\n            description = \"Compose PerformCustomAccessibilityActionWithLabelMatching '${predicateDescription}' to '${elementInfo.name} during $timeoutMs ms\",\n        )\n    }\n\n    /**\n     * @deprecated Use the method 'perform' with 'UltronComposeOperationParams' instead.\n     *\n     * This method has been deprecated in favor of the more generalized 'perform' method that\n     * allows you to perform a custom Compose operation using 'UltronComposeOperationParams'.\n     * The new method provides improved flexibility and consistency in handling operation parameters.\n     *\n     * @param option The deprecated option parameter.\n     * @param block The deprecated block parameter.\n     * @return The result of the deprecated operation.\n     */\n    @Deprecated(\"Use the method 'execute' instead\", ReplaceWith(\"execute(params, block)\"))\n    fun <T> perform(\n        option: PerformCustomBlockOption, block: (SemanticsNodeInteraction) -> T\n    ): T {\n        val container: AtomicRef<T?> = atomic(null)\n        executeOperation(\n            operationBlock = { container.update { block(semanticsNodeInteraction) } },\n            name = \"Perform '${option.description}''${elementInfo.name}'\",\n            type = option.operationType,\n            description = \"Compose operation '${option.operationType}' to '${elementInfo.name}' with option '$option' during $timeoutMs ms\",\n        )\n        return container.value ?: throw UltronOperationException(\"Unable to perform provided operation and get valuable result. Actual result of perform block is null.\")\n    }\n\n\n    @Deprecated(\"Use the method 'execute' instead\", ReplaceWith(\"execute(params, block)\"))\n    fun <T> perform(params: UltronComposeOperationParams? = null, block: (SemanticsNodeInteraction) -> T): T {\n        val _params = params ?: getDefaultOperationParams()\n        val container: AtomicRef<T?> = atomic(null)\n        executeOperation(\n            operationBlock = { container.update { block(semanticsNodeInteraction) } },\n            name = _params.operationName,\n            description = _params.operationDescription,\n            type = _params.operationType,\n        )\n        return container.value ?: throw UltronOperationException(\"Unable to perform provided operation and get valuable result. Actual result of perform block is null.\")\n    }\n\n    /**\n     * Performs a custom Compose operation using the provided parameters and operation block.\n     *\n     * This method allows you to execute a custom Compose operation by providing the parameters\n     * for the operation and a block of code that defines the operation's behavior. The operation\n     * block receives a SemanticsNodeInteraction as a parameter and is responsible for performing\n     * the desired interaction with the semantics node.\n     *\n     * @param params The optional parameters for the Compose operation. If null, default operation\n     *               parameters are used.\n     * @param block The block of code that defines the behavior of the custom operation. The block\n     *              receives a SemanticsNodeInteraction as a parameter.\n     * @return An updated instance of the class.\n     */\n    fun perform(params: UltronComposeOperationParams? = null, block: (SemanticsNodeInteraction) -> Unit) = apply {\n        val _params = params ?: getDefaultOperationParams()\n        executeOperation(\n            operationBlock = { block(semanticsNodeInteraction) },\n            name = _params.operationName,\n            description = _params.operationDescription,\n            type = _params.operationType,\n        )\n    }\n\n    /**\n     * This method allows you to execute a custom Compose operation by providing the parameters\n     * for the operation and a block of code that defines the operation's behavior. The operation\n     * block receives a SemanticsNodeInteraction as a parameter and is responsible for performing\n     * the desired interaction with the semantics node.\n     *\n     * @param <T> The type of the result returned by the operation block.\n     * @param params The optional parameters for the Compose operation. If null, default operation\n     *               parameters are used.\n     * @param block The block of code that defines the behavior of the custom operation. The block\n     *              receives a SemanticsNodeInteraction as a parameter and returns a value of type T.\n     * @return The result of the operation block.\n     */\n    fun <T> execute(params: UltronComposeOperationParams? = null, block: (SemanticsNodeInteraction) -> T): T {\n        val _params = params ?: getDefaultOperationParams()\n        val container: AtomicRef<T?> = atomic(null)\n        executeOperation(\n            operationBlock = { container.update { block(semanticsNodeInteraction) } },\n            name = _params.operationName,\n            description = _params.operationDescription,\n            type = _params.operationType,\n        )\n        return container.value ?: throw UltronOperationException(\"Unable to perform provided operation and get valuable result. Actual result of perform block is null.\")\n    }\n\n    fun getNode(): SemanticsNode = execute(\n        UltronComposeOperationParams(\n            operationName = \"Get SemanticsNode of '${elementInfo.name}'\",\n            operationDescription = \"Compose get SemanticsNode of '${elementInfo.name}' during $timeoutMs ms\",\n            operationType = GET_SEMANTICS_NODE\n        )\n    ) {\n        it.fetchSemanticsNode()\n    }\n\n    fun <T> getNodeConfigProperty(key: SemanticsPropertyKey<T>): T = execute(\n        UltronComposeOperationParams(\n            operationName = \"Get SemanticsNode config property '${key.name}' of '${elementInfo.name}'\",\n            operationDescription = \"Compose get SemanticsNode config property '${key.name}' of '${elementInfo.name}' during $timeoutMs ms\",\n            operationType = GET_SEMANTICS_NODE\n        )\n    ) {\n        it.fetchSemanticsNode().config[key]\n    }\n\n\n    fun printToLog(tag: String, maxDepth: Int = Int.MAX_VALUE) = apply {\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.printToLog(tag, maxDepth) },\n            name = \"Print Semantics info to log for '${elementInfo.name}'\",\n            type = PRINT_TO_LOG,\n            description = \"Compose printToLog for '${elementInfo.name}' during $timeoutMs ms\",\n        )\n    }\n\n    //assertions\n    fun assertIsDisplayed() = apply {\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.assertIsDisplayed() },\n            name = \"Assert '${elementInfo.name}' is displayed\",\n            type = IS_DISPLAYED,\n            description = \"Compose assertIsDisplayed '${elementInfo.name}' during $timeoutMs ms\",\n        )\n    }\n\n    fun assertIsNotDisplayed() = apply {\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.assertIsNotDisplayed() },\n            name = \"Assert '${elementInfo.name}' is not displayed\",\n            type = IS_NOT_DISPLAYED,\n            description = \"Compose assertIsNotDisplayed '${elementInfo.name}' during $timeoutMs ms\",\n        )\n    }\n\n    fun assertExists() = apply {\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.assertExists() },\n            name = \"Assert '${elementInfo.name}' exists\",\n            type = EXISTS,\n            description = \"Compose assertExists '${elementInfo.name}' during $timeoutMs ms\",\n        )\n    }\n\n    /**\n     * note: this assertion works immediately\n     */\n    fun assertDoesNotExist() = apply {\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.assertDoesNotExist() },\n            name = \"Assert '${elementInfo.name}' doesn't exist\",\n            type = DOES_NOT_EXIST,\n            description = \"Compose assertDoesNotExist '${elementInfo.name}' during $timeoutMs ms\",\n        )\n    }\n\n    fun assertIsEnabled() = apply {\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.assertIsEnabled() },\n            name = \"Assert '${elementInfo.name}' is enabled\",\n            type = IS_ENABLED,\n            description = \"Compose assertIsEnabled '${elementInfo.name}' during $timeoutMs ms\",\n        )\n    }\n\n    fun assertIsNotEnabled() = apply {\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.assertIsNotEnabled() },\n            name = \"Assert '${elementInfo.name}' is not enabled\",\n            type = IS_NOT_ENABLED,\n            description = \"Compose assertIsNotEnabled '${elementInfo.name}' during $timeoutMs ms\",\n        )\n    }\n\n    fun assertIsFocused() = apply {\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.assertIsFocused() },\n            name = \"Assert '${elementInfo.name}' is focused\",\n            type = IS_FOCUSED,\n            description = \"Compose assertIsFocused '${elementInfo.name}' during $timeoutMs ms\",\n        )\n    }\n\n    fun assertIsNotFocused() = apply {\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.assertIsNotFocused() },\n            name = \"Assert '${elementInfo.name}' is not focused\",\n            type = IS_NOT_FOCUSED,\n            description = \"Compose assertIsNotFocused'${elementInfo.name}' during $timeoutMs ms\",\n        )\n    }\n\n    fun assertIsSelected() = apply {\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.assertIsSelected() },\n            name = \"Assert '${elementInfo.name}' is selected\",\n            type = IS_SELECTED,\n            description = \"Compose assertIsSelected to '${elementInfo.name}' during $timeoutMs ms\",\n        )\n    }\n\n    fun assertIsNotSelected() = apply {\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.assertIsNotSelected() },\n            name = \"Assert '${elementInfo.name}'\",\n            type = IS_NOT_SELECTED,\n            description = \"Compose assertIsNotSelected to '${elementInfo.name}' during $timeoutMs ms\",\n        )\n    }\n\n    fun assertIsSelectable() = apply {\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.assertIsSelectable() },\n            name = \"Assert '${elementInfo.name}' is selectable\",\n            type = IS_SELECTABLE,\n            description = \"Compose assertIsSelectable '${elementInfo.name}' during $timeoutMs ms\",\n        )\n    }\n\n    fun assertIsIndeterminate() = apply {\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.assertIsIndeterminate() },\n            name = \"Assert '${elementInfo.name}' is indeterminate\",\n            type = IS_INDETERMINATE,\n            description = \"Compose assertIsIndeterminate '${elementInfo.name}' during $timeoutMs ms\",\n        )\n    }\n\n    fun assertIsOn() = apply {\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.assertIsOn() },\n            name = \"Assert '${elementInfo.name}' is on\",\n            type = IS_ON,\n            description = \"Compose assertIsOn '${elementInfo.name}' during $timeoutMs ms\",\n        )\n    }\n\n    fun assertIsOff() = apply {\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.assertIsOff() },\n            name = \"Assert '${elementInfo.name}' is off\",\n            type = IS_OFF,\n            description = \"Compose assertIsOff '${elementInfo.name}' during $timeoutMs ms\",\n        )\n    }\n\n    fun assertIsToggleable() = apply {\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.assertIsToggleable() },\n            name = \"Assert '${elementInfo.name}' Is Toggleable\",\n            type = IS_TOGGLEABLE,\n            description = \"Compose assertIsToggleable '${elementInfo.name}' during $timeoutMs ms\",\n        )\n    }\n\n    fun assertHasClickAction() = apply {\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.assertHasClickAction() },\n            name = \"Assert '${elementInfo.name}' has click action\",\n            type = HAS_CLICK_ACTION,\n            description = \"Compose assertHasClickAction '${elementInfo.name}' during $timeoutMs ms\",\n        )\n    }\n\n    fun assertHasNoClickAction() = apply {\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.assertHasNoClickAction() },\n            name = \"Assert '${elementInfo.name}' has no click action\",\n            type = HAS_NO_CLICK_ACTION,\n            description = \"Compose assertHasNoClickAction '${elementInfo.name}' during $timeoutMs ms\",\n        )\n    }\n\n    fun assertTextEquals(vararg expected: String, option: TextEqualsOption? = null) = apply {\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.assertTextEquals(*expected, includeEditableText = option?.includeEditableText ?: true) },\n            name = \"AssertTextEquals '${expected.toList()}' in '${elementInfo.name}'\",\n            type = TEXT_EQUALS,\n            description = \"Compose assertTextEquals '${expected.toList()}' with option = '$option' in '${elementInfo.name}' during $timeoutMs ms\",\n        )\n    }\n\n    fun assertTextContains(expected: String, option: TextContainsOption? = null) = apply {\n        val _option = option ?: TextContainsOption()\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.assertTextContains(expected, _option.substring, _option.ignoreCase) },\n            name = \"AssertTextContains '$expected' in '${elementInfo.name}'\",\n            type = CONTAINS_TEXT,\n            description = \"Compose assertTextContains '$expected' with option = '$_option' in '${elementInfo.name}' during $timeoutMs ms\",\n        )\n    }\n\n    fun assertContentDescriptionEquals(vararg expected: String) = apply {\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.assertContentDescriptionEquals(*expected) },\n            name = \"Assert ContentDescription equals '${expected.toList()}' in '${elementInfo.name}'\",\n            type = HAS_CONTENT_DESCRIPTION,\n            description = \"Compose assertContentDescriptionEquals '${expected.toList()}' in '${elementInfo.name}' during $timeoutMs ms\",\n        )\n    }\n\n    fun assertContentDescriptionContains(expected: String, option: ContentDescriptionContainsOption? = null) = apply {\n        val _option = option ?: ContentDescriptionContainsOption()\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.assertContentDescriptionContains(expected, _option.substring, _option.ignoreCase) },\n            name = \"Assert ContentDescription contains '$expected' in '${elementInfo.name}'\",\n            type = CONTENT_DESCRIPTION_CONTAINS_TEXT,\n            description = \"Compose assertContentDescriptionContains '$expected' with option = '$_option' in '${elementInfo.name}' during $timeoutMs ms\",\n        )\n    }\n\n    fun assertValueEquals(expected: String) = apply {\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.assertValueEquals(expected) },\n            name = \"AssertValueEquals '$expected' in '${elementInfo.name}'\",\n            type = VALUE_EQUALS,\n            description = \"Compose assertValueEquals '$expected' in '${elementInfo.name}' during $timeoutMs ms\",\n        )\n    }\n\n    fun assertRangeInfoEquals(range: ProgressBarRangeInfo) = apply {\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.assertRangeInfoEquals(range) },\n            name = \"AssertRangeInfoEquals '$range' in '${elementInfo.name}'\",\n            type = PROGRESS_BAR_RANGE_EQUALS,\n            description = \"Compose assertRangeInfoEquals '$range' in '${elementInfo.name}' during $timeoutMs ms\",\n        )\n    }\n\n    fun assertHeightIsAtLeast(minHeight: Dp) = apply {\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.assertHeightIsAtLeast(minHeight) },\n            name = \"AssertHeightIsAtLeast '$minHeight' of '${elementInfo.name}'\",\n            type = HEIGHT_IS_AT_LEAST,\n            description = \"Compose operation '$HEIGHT_IS_AT_LEAST' '$minHeight' of '${elementInfo.name}' during $timeoutMs ms\",\n        )\n    }\n\n    fun assertHeightIsEqualTo(expectedHeight: Dp) = apply {\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.assertHeightIsEqualTo(expectedHeight) },\n            name = \"AssertHeightIsEqualTo '$expectedHeight' of '${elementInfo.name}'\",\n            type = HEIGHT_IS_EQUAL_TO,\n            description = \"Compose assertHeightIsEqualTo '$expectedHeight' of '${elementInfo.name}' during $timeoutMs ms\",\n        )\n    }\n\n    fun assertWidthIsAtLeast(minWidth: Dp) = apply {\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.assertWidthIsAtLeast(minWidth) },\n            name = \"AssertWidthIsAtLeast '$minWidth' of '${elementInfo.name}'\",\n            type = WIDTH_IS_AT_LEAST,\n            description = \"Compose assertWidthIsAtLeast '$minWidth' of '${elementInfo.name}' during $timeoutMs ms\",\n        )\n    }\n\n    fun assertWidthIsEqualTo(expectedWidth: Dp) = apply {\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.assertWidthIsEqualTo(expectedWidth) },\n            name = \"AssertWidthIsEqualTo '$expectedWidth' of '${elementInfo.name}'\",\n            type = WIDTH_IS_EQUAL_TO,\n            description = \"Compose assertWidthIsEqualTo '$expectedWidth' of '${elementInfo.name}' during $timeoutMs ms\",\n        )\n    }\n\n    fun assertMatches(matcher: SemanticsMatcher, messagePrefixOnError: (() -> String)? = null) = apply {\n        executeOperation(\n            operationBlock = { semanticsNodeInteraction.assert(matcher, messagePrefixOnError) },\n            name = \"Assert '${elementInfo.name}' matches '${matcher.description}'\",\n            type = ASSERT_MATCHES,\n            description = \"Compose assertMatches '${matcher.description}' to '${elementInfo.name}' during $timeoutMs ms\",\n        )\n    }\n\n    fun executeOperation(operation: UltronComposeOperation) = UltronComposeOperationLifecycle.execute(\n        ComposeOperationExecutor(operation), resultHandler\n    )\n\n    fun executeOperation(\n        operationBlock: () -> Unit, name: String = \"empty name\", type: UltronOperationType = CommonOperationType.DEFAULT, description: String = \"empty description\"\n    ) = UltronComposeOperationLifecycle.execute(ComposeOperationExecutor(getComposeOperation(operationBlock, name, type, description)), resultHandler)\n\n    fun getComposeOperation(operationBlock: () -> Unit, name: String, type: UltronOperationType, description: String) = UltronComposeOperation(\n        operationBlock = operationBlock, name = name, type = type, description = description, timeoutMs = timeoutMs, assertion = assertion, elementInfo = elementInfo\n    )\n\n    private fun getDefaultOperationParams() = UltronComposeOperationParams(\n        operationName = \"Anonymous Compose operation on '${elementInfo.name}'\",\n        operationDescription = \"Anonymous Compose operation on '${elementInfo.name}' during $timeoutMs ms\",\n    )\n\n\n    companion object {\n        /**\n         * Executes any compose action inside Ultron lifecycle\n         */\n        @Deprecated(\n            \"It doesn't support custom assertion call provided by [withAssertion]\", ReplaceWith(\n                \"this.executeOperation(operation: UltronComposeOperation)\"\n            )\n        )\n        fun executeOperation(\n            operation: UltronComposeOperation, resultHandler: (ComposeOperationResult<UltronComposeOperation>) -> Unit = UltronComposeConfig.resultHandler\n        ) {\n            UltronComposeOperationLifecycle.execute(\n                ComposeOperationExecutor(operation), resultHandler\n            )\n        }\n\n        private const val CONFIG_TEXT_FIELD_NAME = \"Text\"\n        private const val CONFIG_EDITABLE_TEXT_FIELD_NAME = \"EditableText\"\n        val CONFIG_TEXT_FIELDS_LIST = listOf(CONFIG_TEXT_FIELD_NAME, CONFIG_EDITABLE_TEXT_FIELD_NAME)\n        internal const val DEFAULT_SWIPE_DURATION = 200L\n    }\n}\n"
  },
  {
    "path": "ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/core/compose/nodeinteraction/UltronComposeSemanticsNodeInteractionClicks.kt",
    "content": "package com.atiurin.ultron.core.compose.nodeinteraction\n\nimport com.atiurin.ultron.core.common.options.ClickOption\nimport com.atiurin.ultron.core.common.options.DoubleClickOption\nimport com.atiurin.ultron.core.common.options.LongClickOption\n\nfun UltronComposeSemanticsNodeInteraction.click(option: ClickOption? = null) = click(UltronComposeOffsets.CENTER, option)\nfun UltronComposeSemanticsNodeInteraction.clickCenterLeft(option: ClickOption? = null) = click(UltronComposeOffsets.CENTER_LEFT, option)\nfun UltronComposeSemanticsNodeInteraction.clickCenterRight(option: ClickOption? = null) = click(UltronComposeOffsets.CENTER_RIGHT, option)\nfun UltronComposeSemanticsNodeInteraction.clickTopCenter(option: ClickOption? = null) = click(UltronComposeOffsets.TOP_CENTER, option)\nfun UltronComposeSemanticsNodeInteraction.clickTopLeft(option: ClickOption? = null) = click(UltronComposeOffsets.TOP_LEFT, option)\nfun UltronComposeSemanticsNodeInteraction.clickTopRight(option: ClickOption? = null) = click(UltronComposeOffsets.TOP_RIGHT, option)\nfun UltronComposeSemanticsNodeInteraction.clickBottomCenter(option: ClickOption? = null) = click(UltronComposeOffsets.BOTTOM_CENTER, option)\nfun UltronComposeSemanticsNodeInteraction.clickBottomLeft(option: ClickOption? = null) = click(UltronComposeOffsets.BOTTOM_LEFT, option)\nfun UltronComposeSemanticsNodeInteraction.clickBottomRight(option: ClickOption? = null) = click(UltronComposeOffsets.BOTTOM_RIGHT, option)\n\nfun UltronComposeSemanticsNodeInteraction.longClick(option: LongClickOption? = null) = longClick(UltronComposeOffsets.CENTER, option)\nfun UltronComposeSemanticsNodeInteraction.longClickCenterLeft(option: LongClickOption? = null) = longClick(\n    UltronComposeOffsets.CENTER_LEFT, option)\nfun UltronComposeSemanticsNodeInteraction.longClickCenterRight(option: LongClickOption? = null) = longClick(\n    UltronComposeOffsets.CENTER_RIGHT, option)\nfun UltronComposeSemanticsNodeInteraction.longClickTopCenter(option: LongClickOption? = null) = longClick(\n    UltronComposeOffsets.TOP_CENTER, option)\nfun UltronComposeSemanticsNodeInteraction.longClickTopLeft(option: LongClickOption? = null) = longClick(\n    UltronComposeOffsets.TOP_LEFT, option)\nfun UltronComposeSemanticsNodeInteraction.longClickTopRight(option: LongClickOption? = null) = longClick(\n    UltronComposeOffsets.TOP_RIGHT, option)\nfun UltronComposeSemanticsNodeInteraction.longClickBottomCenter(option: LongClickOption? = null) = longClick(\n    UltronComposeOffsets.BOTTOM_CENTER, option)\nfun UltronComposeSemanticsNodeInteraction.longClickBottomLeft(option: LongClickOption? = null) = longClick(\n    UltronComposeOffsets.BOTTOM_LEFT, option)\nfun UltronComposeSemanticsNodeInteraction.longClickBottomRight(option: LongClickOption? = null) = longClick(\n    UltronComposeOffsets.BOTTOM_RIGHT, option)\n\nfun UltronComposeSemanticsNodeInteraction.doubleClick(option: DoubleClickOption? = null) = doubleClick(\n    UltronComposeOffsets.CENTER, option)\nfun UltronComposeSemanticsNodeInteraction.doubleClickCenterLeft(option: DoubleClickOption? = null) = doubleClick(\n    UltronComposeOffsets.CENTER_LEFT, option)\nfun UltronComposeSemanticsNodeInteraction.doubleClickCenterRight(option: DoubleClickOption? = null) = doubleClick(\n    UltronComposeOffsets.CENTER_RIGHT, option)\nfun UltronComposeSemanticsNodeInteraction.doubleClickTopCenter(option: DoubleClickOption? = null) = doubleClick(\n    UltronComposeOffsets.TOP_CENTER, option)\nfun UltronComposeSemanticsNodeInteraction.doubleClickTopLeft(option: DoubleClickOption? = null) = doubleClick(\n    UltronComposeOffsets.TOP_LEFT, option)\nfun UltronComposeSemanticsNodeInteraction.doubleClickTopRight(option: DoubleClickOption? = null) = doubleClick(\n    UltronComposeOffsets.TOP_RIGHT, option)\nfun UltronComposeSemanticsNodeInteraction.doubleClickBottomCenter(option: DoubleClickOption? = null) = doubleClick(\n    UltronComposeOffsets.BOTTOM_CENTER, option)\nfun UltronComposeSemanticsNodeInteraction.doubleClickBottomLeft(option: DoubleClickOption? = null) = doubleClick(\n    UltronComposeOffsets.BOTTOM_LEFT, option)\nfun UltronComposeSemanticsNodeInteraction.doubleClickBottomRight(option: DoubleClickOption? = null) = doubleClick(\n    UltronComposeOffsets.BOTTOM_RIGHT, option)\n\n"
  },
  {
    "path": "ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/core/compose/operation/ComposeOperationExecutor.kt",
    "content": "package com.atiurin.ultron.core.compose.operation\n\nimport com.atiurin.ultron.core.common.Operation\nimport com.atiurin.ultron.core.common.OperationExecutor\nimport com.atiurin.ultron.core.common.OperationIterationResult\nimport com.atiurin.ultron.core.common.ResultDescriptor\nimport com.atiurin.ultron.core.compose.ComposeTestContainer.withComposeTestEnvironment\nimport com.atiurin.ultron.core.compose.config.UltronComposeConfig\nimport com.atiurin.ultron.exceptions.UltronWrapperException\nimport kotlin.reflect.KClass\n\ninternal class ComposeOperationExecutor(\n    override val operation: UltronComposeOperation\n) : OperationExecutor<UltronComposeOperation, ComposeOperationResult<UltronComposeOperation>> {\n    override val descriptor: ResultDescriptor\n        get() = ResultDescriptor()\n    override val pollingTimeout: Long\n        get() = UltronComposeConfig.params.operationPollingTimeoutMs\n\n    override var doBetweenOperationRetry: () -> Unit = {\n        withComposeTestEnvironment { testEnvironment ->\n            UltronComposeConfig.doBetweenOperationRetry(operation, testEnvironment)\n        }\n    }\n    override fun generateResult(\n        success: Boolean,\n        exceptions: List<Throwable>,\n        description: String,\n        lastOperationIterationResult: OperationIterationResult?,\n        executionTimeMs: Long\n    ): ComposeOperationResult<UltronComposeOperation> {\n        return ComposeOperationResult(\n            operation = operation,\n            success = success,\n            exceptions = exceptions,\n            description = description,\n            operationIterationResult = lastOperationIterationResult,\n            executionTimeMs = executionTimeMs\n        )\n    }\n\n    override fun getAllowedExceptions(operation: Operation): List<KClass<out Throwable>> {\n        return UltronComposeConfig.allowedExceptions\n    }\n\n    override fun getWrapperException(originalException: Throwable): Throwable {\n        return if (originalException is AssertionError){\n            UltronWrapperException(\"[${operation.description}] failed\", originalException)\n        } else originalException\n    }\n}"
  },
  {
    "path": "ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/core/compose/operation/ComposeOperationResult.kt",
    "content": "package com.atiurin.ultron.core.compose.operation\n\nimport com.atiurin.ultron.core.common.Operation\nimport com.atiurin.ultron.core.common.OperationIterationResult\nimport com.atiurin.ultron.core.common.OperationResult\n\nclass ComposeOperationResult<T : Operation>(\n    override val operation: T,\n    override val success: Boolean,\n    override val exceptions: List<Throwable> = emptyList(),\n    override var description: String,\n    override var operationIterationResult: OperationIterationResult?,\n    override val executionTimeMs: Long\n) : OperationResult<T>"
  },
  {
    "path": "ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/core/compose/operation/ComposeOperationType.kt",
    "content": "package com.atiurin.ultron.core.compose.operation\n\nimport com.atiurin.ultron.core.common.UltronOperationType\n\nenum class ComposeOperationType : UltronOperationType {\n    CLICK, LONG_CLICK, DOUBLE_CLICK,\n    TEXT_INPUT, REPLACE_TEXT, CLEAR_TEXT, PRESS_KEY, TEXT_INPUT_SELECTION, COPY_TEXT, PASTE_TEXT, CUT_TEXT, SET_TEXT,\n    GET_TEXT, GET_SEMANTICS_NODE, GET_SEMANTICS_CONFIG_PROPERTY,\n    IME_ACTION, COLLAPSE, EXPAND, DISMISS, SET_PROGRESS, SET_SELECTION,\n    SWIPE_LEFT, SWIPE_RIGHT, SWIPE_UP, SWIPE_DOWN, SWIPE,\n    SCROLL_TO, SCROLL_TO_INDEX, SCROLL_TO_KEY, SCROLL_TO_NODE,\n    CUSTOM, MOUSE_INPUT, SEMANTIC_ACTION,\n    IS_DISPLAYED, IS_NOT_DISPLAYED,\n    DOES_NOT_EXIST, EXISTS,\n    IS_ENABLED, IS_NOT_ENABLED,\n    IS_SELECTED, IS_NOT_SELECTED, IS_SELECTABLE,\n    IS_TOGGLEABLE,\n    IS_FOCUSED, IS_NOT_FOCUSED,\n    IS_ON, IS_OFF, IS_INDETERMINATE,\n    HAS_CLICK_ACTION, HAS_NO_CLICK_ACTION,\n    HEIGHT_IS_AT_LEAST, HEIGHT_IS_EQUAL_TO, WIDTH_IS_AT_LEAST, WIDTH_IS_EQUAL_TO,\n    TEXT_EQUALS, CONTAINS_TEXT,\n    HAS_CONTENT_DESCRIPTION, CONTENT_DESCRIPTION_CONTAINS_TEXT,\n    VALUE_EQUALS,\n    PROGRESS_BAR_RANGE_EQUALS,\n    ASSERT_MATCHES,\n    CAPTURE_IMAGE,\n    GET_LIST_ITEM, GET_LIST_ITEM_CHILD, ASSERT_LIST_ITEM_DOES_NOT_EXIST,\n    PRINT_TO_LOG\n}"
  },
  {
    "path": "ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/core/compose/operation/UltronComposeCollectionInteraction.kt",
    "content": "package com.atiurin.ultron.core.compose.operation\n\nimport androidx.compose.ui.test.SemanticsMatcher\nimport com.atiurin.ultron.core.compose.ComposeTestContainer\nimport com.atiurin.ultron.core.compose.nodeinteraction.UltronComposeSemanticsNodeInteraction\nimport com.atiurin.ultron.utils.AssertUtils\n\nclass UltronComposeCollectionInteraction(\n    val matcher: SemanticsMatcher,\n    val useUnmergedTree: Boolean = false\n) {\n\n    companion object {\n        fun allNodes(matcher: SemanticsMatcher, useUnmergedTree: Boolean = false): UltronComposeCollectionInteraction {\n            return UltronComposeCollectionInteraction(matcher, useUnmergedTree)\n        }\n    }\n    private fun getCollection() = ComposeTestContainer.getProvider().onAllNodes(matcher, useUnmergedTree)\n\n    fun get(index: Int) = UltronComposeSemanticsNodeInteraction(\n        getCollection()[index]\n    )\n\n    fun fetchSemanticNodes() = getCollection().fetchSemanticsNodes()\n}\n\nfun UltronComposeCollectionInteraction.assertSize(expected: Int, operationTimeoutMs: Long = 5_000){\n    AssertUtils.assertTrueAndValueInDesc(\n        valueBlock = { fetchSemanticNodes().size },\n        assertionBlock = { value -> value == expected },\n        timeoutMs = operationTimeoutMs,\n        desc ={ value -> \"UltronComposeCollection($matcher) size expected to be $expected (actual size = $value)\" }\n    )\n}"
  },
  {
    "path": "ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/core/compose/operation/UltronComposeOperation.kt",
    "content": "package com.atiurin.ultron.core.compose.operation\n\nimport com.atiurin.ultron.core.common.*\nimport com.atiurin.ultron.core.common.assertion.DefaultOperationAssertion\nimport com.atiurin.ultron.core.common.assertion.OperationAssertion\n\nclass UltronComposeOperation(\n    val operationBlock: () -> Unit,\n    override val name: String,\n    override val type: UltronOperationType,\n    override val description: String,\n    override val timeoutMs: Long,\n    override val assertion: OperationAssertion = DefaultOperationAssertion(\"\"){},\n    override val elementInfo: ElementInfo = DefaultElementInfo()\n) : Operation {\n    override fun execute(): OperationIterationResult {\n        var success = true\n        var exception: Throwable? = null\n        try {\n            operationBlock()\n        }catch (error: Throwable){\n            success = false\n            exception = error\n        }\n        return DefaultOperationIterationResult(success, exception)\n    }\n}"
  },
  {
    "path": "ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/core/compose/operation/UltronComposeOperationLifecycle.kt",
    "content": "package com.atiurin.ultron.core.compose.operation\n\nimport com.atiurin.ultron.core.common.AbstractOperationLifecycle\n\nobject UltronComposeOperationLifecycle : AbstractOperationLifecycle()"
  },
  {
    "path": "ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/core/compose/operation/UltronComposeOperationParams.kt",
    "content": "package com.atiurin.ultron.core.compose.operation\n\nimport com.atiurin.ultron.core.common.UltronOperationType\n\ndata class UltronComposeOperationParams(\n    val operationName: String,\n    val operationDescription: String,\n    val operationType: UltronOperationType = ComposeOperationType.CUSTOM\n)\n"
  },
  {
    "path": "ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/core/compose/option/ComposeSwipeOption.kt",
    "content": "package com.atiurin.ultron.core.compose.option\n\nimport com.atiurin.ultron.core.compose.nodeinteraction.UltronComposeSemanticsNodeInteraction\n\ndata class ComposeSwipeOption(\n    val startXOffset: Float = 0f,\n    val endXOffset: Float = 0f,\n    val startYOffset: Float = 0f,\n    val endYOffset: Float = 0f,\n    val durationMs: Long = UltronComposeSemanticsNodeInteraction.DEFAULT_SWIPE_DURATION\n)"
  },
  {
    "path": "ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/core/compose/page/UltronComposeUiBlock.kt",
    "content": "package com.atiurin.ultron.core.compose.page\n\nimport androidx.compose.ui.test.SemanticsMatcher\nimport androidx.compose.ui.test.hasAnyAncestor\nimport androidx.compose.ui.test.hasParent\nimport com.atiurin.ultron.extensions.ultronInteraction\nimport com.atiurin.ultron.extensions.withName\n\n/**\n * Base class for creating and manipulating UI blocks in Compose testing.\n *\n * This class provides mechanisms for performing descendant and direct child searches within the Compose UI hierarchy,\n * allowing for efficient UI element matching and composition of UI blocks. It focuses on finding elements\n * within the scope of this block, not across the entire Compose hierarchy.\n *\n * @param blockMatcher The [SemanticsMatcher] used to identify this UI block.\n * @param blockDescription A textual description of the UI block, used for better logging and debugging.\n */\nopen class UltronComposeUiBlock(val blockMatcher: SemanticsMatcher, val blockDescription: String = \"\") {\n\n    /**\n     * Searches for a descendant element within the Compose UI block.\n     *\n     * Constructs a matcher that represents a descendant element of this block,\n     * based on the provided `childMatcher`.\n     *\n     * @param childMatcher Matcher to locate the descendant element within this block.\n     * @return A matcher for the descendant element of this block.\n     */\n    fun _descendantSearch(childMatcher: SemanticsMatcher): SemanticsMatcher {\n        return hasAnyAncestor(blockMatcher) and childMatcher\n    }\n\n    /**\n     * Searches for a direct child element of the current Compose UI block.\n     *\n     * Constructs a matcher that represents a direct child element of this block,\n     * based on the provided `childMatcher`.\n     *\n     * @param childMatcher Matcher to locate the direct child element within this block.\n     * @return A matcher for the direct child element of this block.\n     */\n    fun _childSearch(childMatcher: SemanticsMatcher): SemanticsMatcher {\n        return hasParent(blockMatcher) and childMatcher\n    }\n\n    /**\n     * A utility property for interaction with this UI block.\n     *\n     * Appends the block description to the matcher for better logging and debugging.\n     */\n    val uiBlock\n        get() = blockMatcher.let {\n            if (blockDescription.isNotBlank()) {\n                it.withName(blockDescription)\n            } else it.ultronInteraction()\n        }\n\n    /**\n     * Modifies the provided matcher based on the specified search type.\n     *\n     * Applies a descendant or direct child search transformation to the given `childMatcher`.\n     *\n     * @param childMatcher Matcher to be modified.\n     * @param descendantSearch Specifies the type of search:\n     *                         - `true` (default): Performs a descendant search.\n     *                         - `false`: Performs a direct child search.\n     * @return A new matcher modified according to the search type.\n     */\n    fun child(childMatcher: SemanticsMatcher, descendantSearch: Boolean = true): SemanticsMatcher = when (descendantSearch) {\n        true -> _descendantSearch(childMatcher)\n        false -> _childSearch(childMatcher)\n    }\n\n    /**\n     * Creates a child UI block using a matcher modified based on the specified search type.\n     *\n     * Applies a descendant or direct child search transformation to the provided `childMatcher`\n     * and constructs a new UI block instance using the `uiBlockFactory`.\n     *\n     * @param childMatcher Matcher to be modified for the child UI block.\n     * @param descendantSearch Specifies the type of search:\n     *                         - `true` (default): Performs a descendant search.\n     *                         - `false`: Performs a direct child search.\n     * @param uiBlockFactory A factory function to create a new instance of the child UI block.\n     *                       The function takes the modified matcher as input and returns a UI block instance.\n     * @return A new instance of the child UI block, constructed with the modified matcher.\n     */\n    fun <B : UltronComposeUiBlock> child(childMatcher: SemanticsMatcher, descendantSearch: Boolean = true, uiBlockFactory: (SemanticsMatcher) -> B): B {\n        val newMatcher = when (descendantSearch) {\n            true -> _descendantSearch(childMatcher)\n            false -> _childSearch(childMatcher)\n        }\n        return uiBlockFactory(newMatcher)\n    }\n}\n\n"
  },
  {
    "path": "ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/extensions/AssertionsExt.kt",
    "content": "package com.atiurin.ultron.extensions\n\nimport androidx.compose.ui.test.SemanticsNodeInteraction\nimport androidx.compose.ui.test.assert\n\nfun SemanticsNodeInteraction.assertIsIndeterminate(): SemanticsNodeInteraction = assert(isIndeterminate())"
  },
  {
    "path": "ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/extensions/FiltersExt.kt",
    "content": "package com.atiurin.ultron.extensions\n\nimport androidx.compose.ui.semantics.SemanticsProperties\nimport androidx.compose.ui.state.ToggleableState\nimport androidx.compose.ui.test.SemanticsMatcher\n\nfun isIndeterminate(): SemanticsMatcher = SemanticsMatcher.expectValue(\n    SemanticsProperties.ToggleableState, ToggleableState.Indeterminate\n)"
  },
  {
    "path": "ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/extensions/SemanticsMatcherExt.kt",
    "content": "package com.atiurin.ultron.extensions\n\nimport androidx.compose.ui.input.key.KeyEvent\nimport androidx.compose.ui.semantics.AccessibilityAction\nimport androidx.compose.ui.semantics.ProgressBarRangeInfo\nimport androidx.compose.ui.semantics.SemanticsNode\nimport androidx.compose.ui.semantics.SemanticsPropertyKey\nimport androidx.compose.ui.test.ExperimentalTestApi\nimport androidx.compose.ui.test.MouseInjectionScope\nimport androidx.compose.ui.test.SemanticsMatcher\nimport androidx.compose.ui.test.SemanticsNodeInteraction\nimport androidx.compose.ui.text.AnnotatedString\nimport androidx.compose.ui.text.TextRange\nimport androidx.compose.ui.unit.Dp\nimport com.atiurin.ultron.core.common.assertion.OperationAssertion\nimport com.atiurin.ultron.core.common.options.ClickOption\nimport com.atiurin.ultron.core.common.options.ContentDescriptionContainsOption\nimport com.atiurin.ultron.core.common.options.DoubleClickOption\nimport com.atiurin.ultron.core.common.options.LongClickOption\nimport com.atiurin.ultron.core.common.options.PerformCustomBlockOption\nimport com.atiurin.ultron.core.common.options.TextContainsOption\nimport com.atiurin.ultron.core.common.options.TextEqualsOption\nimport com.atiurin.ultron.core.compose.nodeinteraction.UltronComposeSemanticsNodeInteraction\nimport com.atiurin.ultron.core.compose.nodeinteraction.click\nimport com.atiurin.ultron.core.compose.nodeinteraction.clickBottomCenter\nimport com.atiurin.ultron.core.compose.nodeinteraction.clickBottomLeft\nimport com.atiurin.ultron.core.compose.nodeinteraction.clickBottomRight\nimport com.atiurin.ultron.core.compose.nodeinteraction.clickCenterLeft\nimport com.atiurin.ultron.core.compose.nodeinteraction.clickCenterRight\nimport com.atiurin.ultron.core.compose.nodeinteraction.clickTopCenter\nimport com.atiurin.ultron.core.compose.nodeinteraction.clickTopLeft\nimport com.atiurin.ultron.core.compose.nodeinteraction.clickTopRight\nimport com.atiurin.ultron.core.compose.nodeinteraction.doubleClick\nimport com.atiurin.ultron.core.compose.nodeinteraction.doubleClickBottomCenter\nimport com.atiurin.ultron.core.compose.nodeinteraction.doubleClickBottomLeft\nimport com.atiurin.ultron.core.compose.nodeinteraction.doubleClickBottomRight\nimport com.atiurin.ultron.core.compose.nodeinteraction.doubleClickCenterLeft\nimport com.atiurin.ultron.core.compose.nodeinteraction.doubleClickCenterRight\nimport com.atiurin.ultron.core.compose.nodeinteraction.doubleClickTopCenter\nimport com.atiurin.ultron.core.compose.nodeinteraction.doubleClickTopLeft\nimport com.atiurin.ultron.core.compose.nodeinteraction.doubleClickTopRight\nimport com.atiurin.ultron.core.compose.nodeinteraction.longClick\nimport com.atiurin.ultron.core.compose.nodeinteraction.longClickBottomCenter\nimport com.atiurin.ultron.core.compose.nodeinteraction.longClickBottomLeft\nimport com.atiurin.ultron.core.compose.nodeinteraction.longClickBottomRight\nimport com.atiurin.ultron.core.compose.nodeinteraction.longClickCenterLeft\nimport com.atiurin.ultron.core.compose.nodeinteraction.longClickCenterRight\nimport com.atiurin.ultron.core.compose.nodeinteraction.longClickTopCenter\nimport com.atiurin.ultron.core.compose.nodeinteraction.longClickTopLeft\nimport com.atiurin.ultron.core.compose.nodeinteraction.longClickTopRight\nimport com.atiurin.ultron.core.compose.operation.ComposeOperationResult\nimport com.atiurin.ultron.core.compose.operation.UltronComposeOperation\nimport com.atiurin.ultron.core.compose.operation.UltronComposeOperationParams\nimport com.atiurin.ultron.core.compose.option.ComposeSwipeOption\nfun SemanticsMatcher.ultronInteraction() = UltronComposeSemanticsNodeInteraction(this)\nfun SemanticsMatcher.isSuccess(action: SemanticsMatcher.() -> Unit) = UltronComposeSemanticsNodeInteraction(this).isSuccess { action() }\n\nfun SemanticsMatcher.withTimeout(timeoutMs: Long) = UltronComposeSemanticsNodeInteraction(this, timeoutMs = timeoutMs)\n\nfun SemanticsMatcher.withResultHandler(resultHandler: ((ComposeOperationResult<UltronComposeOperation>) -> Unit)) =\n    UltronComposeSemanticsNodeInteraction(this, resultHandler = resultHandler)\n\nfun SemanticsMatcher.withUseUnmergedTree(value: Boolean) = UltronComposeSemanticsNodeInteraction(this, useUnmergedTree = value)\n\nfun SemanticsMatcher.withAssertion(assertion: OperationAssertion) = UltronComposeSemanticsNodeInteraction(this, assertion = assertion)\n\nfun SemanticsMatcher.withAssertion(name: String = \"\", isListened: Boolean = false, block: () -> Unit) =\n    UltronComposeSemanticsNodeInteraction(this).withAssertion(name, isListened, block)\n\nfun SemanticsMatcher.withName(name: String) = UltronComposeSemanticsNodeInteraction(this).withName(name)\nfun SemanticsMatcher.withMetaInfo(meta: Any) = UltronComposeSemanticsNodeInteraction(this).withMetaInfo(meta)\n\nfun SemanticsMatcher.getText() = UltronComposeSemanticsNodeInteraction(this).getText()\nfun SemanticsMatcher.click(option: ClickOption? = null) = UltronComposeSemanticsNodeInteraction(this).click(option)\nfun SemanticsMatcher.clickCenterLeft(option: ClickOption? = null) = UltronComposeSemanticsNodeInteraction(this).clickCenterLeft(option)\n\nfun SemanticsMatcher.clickCenterRight(option: ClickOption? = null) = UltronComposeSemanticsNodeInteraction(this).clickCenterRight(option)\n\nfun SemanticsMatcher.clickTopCenter(option: ClickOption? = null) = UltronComposeSemanticsNodeInteraction(this).clickTopCenter(option)\n\nfun SemanticsMatcher.clickTopLeft(option: ClickOption? = null) = UltronComposeSemanticsNodeInteraction(this).clickTopLeft(option)\n\nfun SemanticsMatcher.clickTopRight(option: ClickOption? = null) = UltronComposeSemanticsNodeInteraction(this).clickTopRight(option)\n\nfun SemanticsMatcher.clickBottomCenter(option: ClickOption? = null) = UltronComposeSemanticsNodeInteraction(this).clickBottomCenter(option)\n\nfun SemanticsMatcher.clickBottomLeft(option: ClickOption? = null) = UltronComposeSemanticsNodeInteraction(this).clickBottomLeft(option)\n\nfun SemanticsMatcher.clickBottomRight(option: ClickOption? = null) = UltronComposeSemanticsNodeInteraction(this).clickBottomRight(option)\n\nfun SemanticsMatcher.longClick(option: LongClickOption? = null) = UltronComposeSemanticsNodeInteraction(this).longClick(option)\n\nfun SemanticsMatcher.longClickCenterLeft(option: LongClickOption? = null) = UltronComposeSemanticsNodeInteraction(this).longClickCenterLeft(option)\n\nfun SemanticsMatcher.longClickCenterRight(option: LongClickOption? = null) = UltronComposeSemanticsNodeInteraction(this).longClickCenterRight(option)\n\nfun SemanticsMatcher.longClickTopCenter(option: LongClickOption? = null) = UltronComposeSemanticsNodeInteraction(this).longClickTopCenter(option)\n\nfun SemanticsMatcher.longClickTopLeft(option: LongClickOption? = null) = UltronComposeSemanticsNodeInteraction(this).longClickTopLeft(option)\n\nfun SemanticsMatcher.longClickTopRight(option: LongClickOption? = null) = UltronComposeSemanticsNodeInteraction(this).longClickTopRight(option)\n\nfun SemanticsMatcher.longClickBottomCenter(option: LongClickOption? = null) = UltronComposeSemanticsNodeInteraction(this).longClickBottomCenter(option)\n\nfun SemanticsMatcher.longClickBottomLeft(option: LongClickOption? = null) = UltronComposeSemanticsNodeInteraction(this).longClickBottomLeft(option)\n\nfun SemanticsMatcher.longClickBottomRight(option: LongClickOption? = null) = UltronComposeSemanticsNodeInteraction(this).longClickBottomRight(option)\n\nfun SemanticsMatcher.doubleClick(option: DoubleClickOption? = null) = UltronComposeSemanticsNodeInteraction(this).doubleClick(option)\n\nfun SemanticsMatcher.doubleClickCenterLeft(option: DoubleClickOption? = null) = UltronComposeSemanticsNodeInteraction(this).doubleClickCenterLeft(option)\n\nfun SemanticsMatcher.doubleClickCenterRight(option: DoubleClickOption? = null) = UltronComposeSemanticsNodeInteraction(this).doubleClickCenterRight(option)\n\nfun SemanticsMatcher.doubleClickTopCenter(option: DoubleClickOption? = null) = UltronComposeSemanticsNodeInteraction(this).doubleClickTopCenter(option)\n\nfun SemanticsMatcher.doubleClickTopLeft(option: DoubleClickOption? = null) = UltronComposeSemanticsNodeInteraction(this).doubleClickTopLeft(option)\n\nfun SemanticsMatcher.doubleClickTopRight(option: DoubleClickOption? = null) = UltronComposeSemanticsNodeInteraction(this).doubleClickTopRight(option)\n\nfun SemanticsMatcher.doubleClickBottomCenter(option: DoubleClickOption? = null) = UltronComposeSemanticsNodeInteraction(this).doubleClickBottomCenter(option)\n\nfun SemanticsMatcher.doubleClickBottomLeft(option: DoubleClickOption? = null) = UltronComposeSemanticsNodeInteraction(this).doubleClickBottomLeft(option)\n\nfun SemanticsMatcher.doubleClickBottomRight(option: DoubleClickOption? = null) = UltronComposeSemanticsNodeInteraction(this).doubleClickBottomRight(option)\n\nfun SemanticsMatcher.swipeDown(option: ComposeSwipeOption? = null) = UltronComposeSemanticsNodeInteraction(this).swipeDown(option)\n\nfun SemanticsMatcher.swipeUp(option: ComposeSwipeOption? = null) = UltronComposeSemanticsNodeInteraction(this).swipeUp(option)\n\nfun SemanticsMatcher.swipeLeft(option: ComposeSwipeOption? = null) = UltronComposeSemanticsNodeInteraction(this).swipeLeft(option)\n\nfun SemanticsMatcher.swipeRight(option: ComposeSwipeOption? = null) = UltronComposeSemanticsNodeInteraction(this).swipeRight(option)\n\nfun SemanticsMatcher.swipe(option: ComposeSwipeOption) = UltronComposeSemanticsNodeInteraction(this).swipe(option)\n\nfun SemanticsMatcher.scrollTo() = UltronComposeSemanticsNodeInteraction(this).scrollTo()\nfun SemanticsMatcher.scrollToIndex(index: Int) = UltronComposeSemanticsNodeInteraction(this).scrollToIndex(index)\nfun SemanticsMatcher.scrollToKey(key: String) = UltronComposeSemanticsNodeInteraction(this).scrollToKey(key)\nfun SemanticsMatcher.scrollToNode(matcher: SemanticsMatcher) = UltronComposeSemanticsNodeInteraction(this).scrollToNode(matcher)\n\nfun SemanticsMatcher.imeAction() = UltronComposeSemanticsNodeInteraction(this).imeAction()\nfun SemanticsMatcher.pressKey(keyEvent: KeyEvent) = UltronComposeSemanticsNodeInteraction(this).pressKey(keyEvent)\nfun SemanticsMatcher.inputText(text: String) = UltronComposeSemanticsNodeInteraction(this).inputText(text)\nfun SemanticsMatcher.typeText(text: String) = UltronComposeSemanticsNodeInteraction(this).typeText(text)\nfun SemanticsMatcher.inputTextSelection(selection: TextRange) = UltronComposeSemanticsNodeInteraction(this).inputTextSelection(selection)\n\nfun SemanticsMatcher.setSelection(startIndex: Int = 0, endIndex: Int = 0, traversalMode: Boolean) =\n    UltronComposeSemanticsNodeInteraction(this).setSelection(startIndex, endIndex, traversalMode)\n\nfun SemanticsMatcher.selectText(range: TextRange) = UltronComposeSemanticsNodeInteraction(this).selectText(range)\nfun SemanticsMatcher.clearText() = UltronComposeSemanticsNodeInteraction(this).clearText()\nfun SemanticsMatcher.replaceText(text: String) = UltronComposeSemanticsNodeInteraction(this).replaceText(text)\nfun SemanticsMatcher.copyText() = UltronComposeSemanticsNodeInteraction(this).copyText()\nfun SemanticsMatcher.pasteText() = UltronComposeSemanticsNodeInteraction(this).pasteText()\nfun SemanticsMatcher.cutText() = UltronComposeSemanticsNodeInteraction(this).cutText()\nfun SemanticsMatcher.setText(text: String) = UltronComposeSemanticsNodeInteraction(this).setText(text)\nfun SemanticsMatcher.setText(text: AnnotatedString) = UltronComposeSemanticsNodeInteraction(this).setText(text)\n\nfun SemanticsMatcher.collapse() = UltronComposeSemanticsNodeInteraction(this).collapse()\nfun SemanticsMatcher.expand() = UltronComposeSemanticsNodeInteraction(this).expand()\nfun SemanticsMatcher.dismiss() = UltronComposeSemanticsNodeInteraction(this).dismiss()\nfun SemanticsMatcher.setProgress(value: Float) = UltronComposeSemanticsNodeInteraction(this).setProgress(value)\n\n@OptIn(ExperimentalTestApi::class)\nfun SemanticsMatcher.performMouseInput(block: MouseInjectionScope.() -> Unit) = UltronComposeSemanticsNodeInteraction(this).performMouseInput(block)\n\nfun SemanticsMatcher.performSemanticsAction(key: SemanticsPropertyKey<AccessibilityAction<() -> Boolean>>) = UltronComposeSemanticsNodeInteraction(this).performSemanticsAction(key)\nfun SemanticsMatcher.performCustomAccessibilityActionWithLabel(label: String) = UltronComposeSemanticsNodeInteraction(this).performCustomAccessibilityActionWithLabel(label)\nfun SemanticsMatcher.performCustomAccessibilityActionWithLabelMatching(\n    predicateDescription: String? = null,\n    labelPredicate: (label: String) -> Boolean\n) = UltronComposeSemanticsNodeInteraction(this).performCustomAccessibilityActionWithLabelMatching(predicateDescription, labelPredicate)\n\n@Deprecated(\n    \"Use execute(params: UltronComposeOperationParams? = null, block: (SemanticsNodeInteraction) -> T) instead\", ReplaceWith(\"execute(params, block)\")\n)\nfun <T> SemanticsMatcher.perform(block: (SemanticsNodeInteraction) -> T, option: PerformCustomBlockOption) = UltronComposeSemanticsNodeInteraction(this).perform(option, block)\n\nfun SemanticsMatcher.perform(params: UltronComposeOperationParams? = null, block: (SemanticsNodeInteraction) -> Unit) =\n    UltronComposeSemanticsNodeInteraction(this).perform(params, block)\n\nfun <T> SemanticsMatcher.execute(params: UltronComposeOperationParams? = null, block: (SemanticsNodeInteraction) -> T) =\n    UltronComposeSemanticsNodeInteraction(this).execute(params, block)\n\nfun SemanticsMatcher.getNode(): SemanticsNode = UltronComposeSemanticsNodeInteraction(this).getNode()\nfun <T> SemanticsMatcher.getNodeConfigProperty(key: SemanticsPropertyKey<T>): T = UltronComposeSemanticsNodeInteraction(this).getNodeConfigProperty(key)\nfun SemanticsMatcher.printToLog(tag: String, maxDepth: Int = Int.MAX_VALUE) = UltronComposeSemanticsNodeInteraction(this).printToLog(tag, maxDepth)\n\n//asserts\nfun SemanticsMatcher.assertIsDisplayed() = UltronComposeSemanticsNodeInteraction(this).assertIsDisplayed()\nfun SemanticsMatcher.assertIsNotDisplayed() = UltronComposeSemanticsNodeInteraction(this).assertIsNotDisplayed()\nfun SemanticsMatcher.assertExists() = UltronComposeSemanticsNodeInteraction(this).assertExists()\nfun SemanticsMatcher.assertDoesNotExist() = UltronComposeSemanticsNodeInteraction(this).assertDoesNotExist()\nfun SemanticsMatcher.assertIsEnabled() = UltronComposeSemanticsNodeInteraction(this).assertIsEnabled()\nfun SemanticsMatcher.assertIsNotEnabled() = UltronComposeSemanticsNodeInteraction(this).assertIsNotEnabled()\nfun SemanticsMatcher.assertIsFocused() = UltronComposeSemanticsNodeInteraction(this).assertIsFocused()\nfun SemanticsMatcher.assertIsNotFocused() = UltronComposeSemanticsNodeInteraction(this).assertIsNotFocused()\nfun SemanticsMatcher.assertIsSelected() = UltronComposeSemanticsNodeInteraction(this).assertIsSelected()\nfun SemanticsMatcher.assertIsNotSelected() = UltronComposeSemanticsNodeInteraction(this).assertIsNotSelected()\nfun SemanticsMatcher.assertIsSelectable() = UltronComposeSemanticsNodeInteraction(this).assertIsSelectable()\nfun SemanticsMatcher.assertIsIndeterminate() = UltronComposeSemanticsNodeInteraction(this).assertIsIndeterminate()\nfun SemanticsMatcher.assertIsOn() = UltronComposeSemanticsNodeInteraction(this).assertIsOn()\nfun SemanticsMatcher.assertIsOff() = UltronComposeSemanticsNodeInteraction(this).assertIsOff()\nfun SemanticsMatcher.assertIsToggleable() = UltronComposeSemanticsNodeInteraction(this).assertIsToggleable()\nfun SemanticsMatcher.assertHasClickAction() = UltronComposeSemanticsNodeInteraction(this).assertHasClickAction()\nfun SemanticsMatcher.assertHasNoClickAction() = UltronComposeSemanticsNodeInteraction(this).assertHasNoClickAction()\nfun SemanticsMatcher.assertTextEquals(vararg expected: String, option: TextEqualsOption? = null) =\n    UltronComposeSemanticsNodeInteraction(this).assertTextEquals(*expected, option = option)\n\nfun SemanticsMatcher.assertTextContains(expected: String, option: TextContainsOption? = null) = UltronComposeSemanticsNodeInteraction(this).assertTextContains(expected, option)\n\nfun SemanticsMatcher.assertContentDescriptionEquals(vararg expected: String) = UltronComposeSemanticsNodeInteraction(this).assertContentDescriptionEquals(*expected)\n\nfun SemanticsMatcher.assertContentDescriptionContains(expected: String, option: ContentDescriptionContainsOption? = null) =\n    UltronComposeSemanticsNodeInteraction(this).assertContentDescriptionContains(expected, option)\n\nfun SemanticsMatcher.assertValueEquals(expected: String) = UltronComposeSemanticsNodeInteraction(this).assertValueEquals(expected)\n\nfun SemanticsMatcher.assertRangeInfoEquals(range: ProgressBarRangeInfo) = UltronComposeSemanticsNodeInteraction(this).assertRangeInfoEquals(range)\n\nfun SemanticsMatcher.assertHeightIsAtLeast(minHeight: Dp) = UltronComposeSemanticsNodeInteraction(this).assertHeightIsAtLeast(minHeight)\n\nfun SemanticsMatcher.assertHeightIsEqualTo(expectedHeight: Dp) = UltronComposeSemanticsNodeInteraction(this).assertHeightIsEqualTo(expectedHeight)\n\nfun SemanticsMatcher.assertWidthIsAtLeast(minWidth: Dp) = UltronComposeSemanticsNodeInteraction(this).assertWidthIsAtLeast(minWidth)\n\nfun SemanticsMatcher.assertWidthIsEqualTo(expectedWidth: Dp) = UltronComposeSemanticsNodeInteraction(this).assertWidthIsEqualTo(expectedWidth)\n\nfun SemanticsMatcher.assertMatches(matcher: SemanticsMatcher, messagePrefixOnError: (() -> String)? = null) =\n    UltronComposeSemanticsNodeInteraction(this).assertMatches(matcher, messagePrefixOnError)\n\nfun SemanticsMatcher.withDescription(description: String): SemanticsMatcher {\n    return SemanticsMatcher(description) { this@withDescription.matches(it) }\n}"
  },
  {
    "path": "ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/extensions/SemanticsNodeExt.kt",
    "content": "package com.atiurin.ultron.extensions\n\nimport androidx.compose.ui.semantics.SemanticsNode\nimport androidx.compose.ui.test.SemanticsMatcher\n\nfun Iterable<SemanticsNode>.findNodeInTree(matcher: SemanticsMatcher): List<SemanticsNode> {\n    val targetNodes = mutableListOf<SemanticsNode>()\n    this.forEach { node ->\n        targetNodes.addAll(node.findNodeInTree(matcher))\n    }\n    return targetNodes\n}\n\nfun SemanticsNode.findNodeInTree(matcher: SemanticsMatcher): List<SemanticsNode> {\n    val targetNodes = mutableListOf<SemanticsNode>()\n    if (matcher.matches(this)) {\n        targetNodes.add(this)\n        return targetNodes\n    } else {\n        targetNodes.addAll(this.children.findNodeInTree(matcher))\n    }\n    return targetNodes\n}"
  },
  {
    "path": "ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/extensions/SemanticsNodeInteractionExt.kt",
    "content": "package com.atiurin.ultron.extensions\n\nimport androidx.compose.ui.semantics.SemanticsNode\nimport androidx.compose.ui.semantics.SemanticsPropertyKey\nimport androidx.compose.ui.test.SemanticsNodeInteraction\n\nfun SemanticsNodeInteraction.getConfigField(name: String): Any? {\n    for ((key, value) in this.fetchSemanticsNode().config) {\n        if (key.name == name) {\n            return value\n        }\n    }\n    return null\n}\n\nfun SemanticsNodeInteraction.getOneOfConfigFields(names: List<String>): Any? {\n    names.forEach { name ->\n        val value = getConfigField(name)\n        value?.let { return it }\n    }\n    return null\n}\n\nfun SemanticsNodeInteraction.requireSemantics(\n    node: SemanticsNode,\n    vararg properties: SemanticsPropertyKey<*>,\n    errorMessage: () -> String\n) {\n    val missingProperties = properties.filter { it !in node.config }\n    if (missingProperties.isNotEmpty()) {\n        val msg = \"${errorMessage()}, the node is missing [${missingProperties.joinToString()}]\"\n        throw AssertionError(msg)\n    }\n}\n\nexpect fun SemanticsNodeInteraction.getSelectorDescription(): String"
  },
  {
    "path": "ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/extensions/SemanticsSelectorExt.kt",
    "content": "package com.atiurin.ultron.extensions\n\nimport androidx.compose.ui.test.SelectionResult\nimport androidx.compose.ui.test.SemanticsMatcher\nimport androidx.compose.ui.test.SemanticsSelector\n\nfun SemanticsSelector.addFindNodeInTreeSelector(\n    selectorName: String,\n    matcher: SemanticsMatcher\n): SemanticsSelector {\n    return SemanticsSelector(\n        \"(${this.description}).$selectorName(${matcher.description})\",\n        requiresExactlyOneNode = false,\n        chainedInputSelector = this\n    ) { nodes ->\n        SelectionResult(nodes.findNodeInTree(matcher))\n    }\n}\n\n"
  },
  {
    "path": "ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/extensions/TouchInjectionScopeExt.kt",
    "content": "package com.atiurin.ultron.extensions\n\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.test.TouchInjectionScope\nimport com.atiurin.ultron.core.compose.nodeinteraction.SwipePosition\nimport com.atiurin.ultron.core.compose.nodeinteraction.UltronComposeOffsets\nimport com.atiurin.ultron.core.compose.option.ComposeSwipeOption\n\ninternal fun TouchInjectionScope.getUltronComposeOffset(position: UltronComposeOffsets) = when (position) {\n    UltronComposeOffsets.CENTER -> center\n    UltronComposeOffsets.CENTER_LEFT -> centerLeft.copy(x = 1f)\n    UltronComposeOffsets.CENTER_RIGHT -> centerRight\n    UltronComposeOffsets.TOP_CENTER -> topCenter.copy(y = 1f)\n    UltronComposeOffsets.TOP_LEFT -> topLeft.copy(x = 1f, y = 1f)\n    UltronComposeOffsets.TOP_RIGHT -> topRight.copy(y = 1f)\n    UltronComposeOffsets.BOTTOM_CENTER -> bottomCenter\n    UltronComposeOffsets.BOTTOM_LEFT -> bottomLeft.copy(x = 1f)\n    UltronComposeOffsets.BOTTOM_RIGHT -> bottomRight\n}\n\ninternal fun TouchInjectionScope.getDefaultLongClickDuration() = viewConfiguration.longPressTimeoutMillis + 100\ninternal fun TouchInjectionScope.getDefaultDoubleClickDelay() = (viewConfiguration.doubleTapMinTimeMillis + viewConfiguration.doubleTapTimeoutMillis) / 2\ninternal fun TouchInjectionScope.provideSwipeDownPosition(option: ComposeSwipeOption): SwipePosition {\n    val startY = top + option.startYOffset\n    val endY = bottom + option.endYOffset\n    val startX = centerX + option.startXOffset\n    val endX = centerX + option.endXOffset\n    require(startY <= endY) {\n        \"startY=$startY needs to be less than or equal to endY=$endY\"\n    }\n    return SwipePosition(start = Offset(startX, startY), end = Offset(endX, endY))\n}\ninternal fun TouchInjectionScope.provideSwipeUpPosition(option: ComposeSwipeOption): SwipePosition {\n    val startY = bottom + option.startYOffset\n    val endY = top + option.endYOffset\n    val startX = centerX + option.startXOffset\n    val endX = centerX + option.endXOffset\n    require(startY >= endY) {\n        \"startY=$startY needs to be greater than or equal to endY=$endY\"\n    }\n    return SwipePosition(start = Offset(startX, startY), end = Offset(endX, endY))\n}\ninternal fun TouchInjectionScope.provideSwipeLeftPosition(option: ComposeSwipeOption): SwipePosition {\n    val startY = centerY + option.startYOffset\n    val endY = centerY + option.endYOffset\n    val startX = right + option.startXOffset\n    val endX = left + option.endXOffset\n    require(startX >= endX) {\n        \"startX=$startX needs to be greater than or equal to endX=$endX\"\n    }\n    return SwipePosition(start = Offset(startX, startY), end = Offset(endX, endY))\n}\ninternal fun TouchInjectionScope.provideSwipeRightPosition(option: ComposeSwipeOption): SwipePosition {\n    val startY = centerY + option.startYOffset\n    val endY = centerY + option.endYOffset\n    val startX = left + option.startXOffset\n    val endX = right + option.endXOffset\n    require(startX <= endX) {\n        \"startX=$startX needs to be less than or equal to endX=$endX\"\n    }\n    return SwipePosition(start = Offset(startX, startY), end = Offset(endX, endY))\n}\n\n\n\n\n"
  },
  {
    "path": "ultron-compose/src/jvmMain/kotlin/com/atiurin/ultron/core/compose/UltronUiTest.jvm.kt",
    "content": "package com.atiurin.ultron.core.compose\n\nimport androidx.compose.ui.test.ExperimentalTestApi\nimport androidx.compose.ui.test.SkikoComposeUiTest\nimport kotlin.coroutines.CoroutineContext\nimport kotlin.coroutines.EmptyCoroutineContext\n\n@OptIn(ExperimentalTestApi::class)\nfun runDesktopUltronUiTest(\n    width: Int = 1024,\n    height: Int = 768,\n    effectContext: CoroutineContext = EmptyCoroutineContext,\n    block: SkikoComposeUiTest.() -> Unit\n) {\n    SkikoComposeUiTest(width, height, effectContext).runTest {\n        ComposeTestContainer.init(\n            UltronComposeTestEnvironment(\n                provider = this,\n                mainClock = this.mainClock,\n                density = this.density\n            )\n        )\n        block()\n    }\n}\n"
  },
  {
    "path": "ultron-compose/src/shared/kotlin/com/atiurin/ultron/core/compose/config/UltronComposeConfig.shared.kt",
    "content": "package com.atiurin.ultron.core.compose.config\n\nimport com.atiurin.ultron.log.ULogger\n\nactual fun getPlatformLoggers(): List<ULogger> {\n    return emptyList()\n}"
  },
  {
    "path": "ultron-compose/src/shared/kotlin/com/atiurin/ultron/core/compose/list/ItemChildInteractionProvider.shared.kt",
    "content": "package com.atiurin.ultron.core.compose.list\n\nactual fun getItemChildInteractionProvider(): ItemChildInteractionProvider {\n    return object : ItemChildInteractionProvider {}\n}"
  },
  {
    "path": "ultron-compose/src/shared/kotlin/com/atiurin/ultron/core/compose/list/UltronComposeListItem.shared.kt",
    "content": "package com.atiurin.ultron.core.compose.list\n\nimport androidx.compose.ui.test.SemanticsMatcher\nimport com.atiurin.ultron.core.compose.list.UltronComposeList\nimport com.atiurin.ultron.core.compose.list.UltronComposeListItem\nimport com.atiurin.ultron.exceptions.UltronException\n\nactual inline fun <reified T : UltronComposeListItem> getComposeListItemInstance(\n    ultronComposeList: UltronComposeList,\n    itemMatcher: SemanticsMatcher\n): T {\n    val item = ultronComposeList.itemInstancesMap[T::class]?.invoke()\n        ?: throw UltronException(\n            \"\"\"\n            |Item with ${T::class} not registered in compose list ${ultronComposeList.listMatcher.description}\n            |Configure it by using [initBlock] parameter\n            |```\n            |composeList(.., initBlock = {\n            |    registerItem { ListItem() }\n            |})\n            |```\n            \"\"\".trimMargin()\n        )\n    item.setExecutor(ultronComposeList, itemMatcher)\n    return item as T\n}\n\nactual inline fun <reified T : UltronComposeListItem> getComposeListItemInstance(\n    ultronComposeList: UltronComposeList,\n    position: Int,\n    isPositionPropertyConfigured: Boolean\n): T {\n    val item = ultronComposeList.itemInstancesMap[T::class]?.invoke()\n        ?: throw UltronException(\n            \"\"\"\n            |Item with ${T::class} not registered in compose list ${ultronComposeList.listMatcher.description}\n            |Configure it by using [initBlock] parameter\n            |```\n            |composeList(.., initBlock = {\n            |    registerItem { ListItem() }\n            |})\n            |```\n            \"\"\".trimMargin()\n        )\n    item.setExecutor(ultronComposeList, position, isPositionPropertyConfigured)\n    return item as T\n}"
  },
  {
    "path": "ultron-compose/src/shared/kotlin/com/atiurin/ultron/extensions/SemanticsNodeInteractionCommonExt.shared.kt",
    "content": "package com.atiurin.ultron.extensions\n\nimport androidx.compose.ui.test.SemanticsNodeInteraction\n\nactual fun SemanticsNodeInteraction.getSelectorDescription(): String =\n    \"[UI element description isn't implemented non Android platforms due to https://issuetracker.google.com/issues/342778294. Vote for this issue!]\""
  },
  {
    "path": "ultron-compose/src/test/java/com/atiurin/ultron/compose/ExampleUnitTest.kt",
    "content": "package com.atiurin.ultron.compose\n\nimport org.junit.Test\n\nimport org.junit.Assert.*\n\n/**\n * Example local unit test, which will execute on the development machine (host).\n *\n * See [testing documentation](http://d.android.com/tools/testing).\n */\nclass ExampleUnitTest {\n    @Test\n    fun addition_isCorrect() {\n        assertEquals(4, 2 + 2)\n    }\n}"
  }
]