Page Not Found | Ultron
-
+
diff --git a/assets/js/1e721490.a241dfb2.js b/assets/js/1e721490.4f7d5cce.js
similarity index 94%
rename from assets/js/1e721490.a241dfb2.js
rename to assets/js/1e721490.4f7d5cce.js
index feb08daa..c539336e 100644
--- a/assets/js/1e721490.a241dfb2.js
+++ b/assets/js/1e721490.4f7d5cce.js
@@ -1 +1 @@
-"use strict";(self.webpackChunkmy_website=self.webpackChunkmy_website||[]).push([[338],{8092:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>a,contentTitle:()=>i,default:()=>u,frontMatter:()=>l,metadata:()=>s,toc:()=>c});var r=n(4848),o=n(8453);const l={sidebar_position:1},i="Allure",s={id:"common/allure",title:"Allure",description:"Ultron can generate artifacts for Allure report.",source:"@site/docs/common/allure.md",sourceDirName:"common",slug:"/common/allure",permalink:"/ultron/docs/common/allure",draft:!1,unlisted:!1,tags:[],version:"current",sidebarPosition:1,frontMatter:{sidebar_position:1},sidebar:"tutorialSidebar",previous:{title:"withSuitableRoot",permalink:"/ultron/docs/android/rootview"},next:{title:"Ultron Extension",permalink:"/ultron/docs/common/extension"}},a={},c=[{value:"Custom results directory",id:"custom-results-directory",level:2},{value:"Ultron Allure report contains:",id:"ultron-allure-report-contains",level:2},{value:"Ultron step",id:"ultron-step",level:2},{value:"Best practice",id:"best-practice",level:3},{value:"Custom config",id:"custom-config",level:2},{value:"Add detailed info about your conditions to report",id:"add-detailed-info-about-your-conditions-to-report",level:2},{value:"How to add custom artifacts to Allure report?",id:"how-to-add-custom-artifacts-to-allure-report",level:2},{value:"Write artifact to report",id:"write-artifact-to-report",level:3},{value:"Manage artifact creation",id:"manage-artifact-creation",level:3}];function d(e){const t={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",hr:"hr",img:"img",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,o.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(t.h1,{id:"allure",children:"Allure"}),"\n",(0,r.jsx)(t.p,{children:"Ultron can generate artifacts for Allure report."}),"\n",(0,r.jsxs)(t.p,{children:["Just set Ultron ",(0,r.jsx)(t.code,{children:"testInstrumentationRunner"})," in your app build.gradle file (",(0,r.jsx)(t.a,{href:"https://github.com/open-tool/ultron/blob/master/sample-app/build.gradle.kts#L14",children:"example build.gradle.kts"}),")"]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-kotlin",children:'android {\n defaultConfig {\n testInstrumentationRunner = "com.atiurin.ultron.allure.UltronAllureTestRunner"\n ...\n }\n'})}),"\n",(0,r.jsxs)(t.p,{children:["and apply recommended config in your BaseTest class (",(0,r.jsx)(t.a,{href:"https://github.com/open-tool/ultron/blob/master/sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/BaseTest.kt#L31",children:"example BaseTest"}),")."]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-kotlin",children:"@BeforeClass @JvmStatic\nfun setConfig() {\n UltronConfig.applyRecommended()\n UltronAllureConfig.applyRecommended()\n}\n"})}),"\n",(0,r.jsx)(t.h2,{id:"custom-results-directory",children:"Custom results directory"}),"\n",(0,r.jsxs)(t.p,{children:["Ultron allows you to specify the directory where the Allure results will be stored.\nBy default, the results are stored in the ",(0,r.jsx)(t.code,{children:"/files/allure-results"})," directory in the root of the project.\nYou can change this directory by calling ",(0,r.jsx)(t.code,{children:"UltronAllureConfig.setAllureResultsDirectory()"})]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-kotlin",children:"@BeforeClass @JvmStatic\nfun setConfig() {\n ...\n UltronAllureConfig.applyRecommended()\n UltronAllureConfig.setAllureResultsDirectory(Environment.DIRECTORY_DOWNLOADS)\n}\n"})}),"\n",(0,r.jsx)(t.h2,{id:"ultron-allure-report-contains",children:"Ultron Allure report contains:"}),"\n",(0,r.jsxs)(t.ul,{children:["\n",(0,r.jsx)(t.li,{children:"Detailed report about all operations in your test"}),"\n",(0,r.jsx)(t.li,{children:"Logcat file (in case of failure)"}),"\n",(0,r.jsx)(t.li,{children:"Screenshot (in case of failure)"}),"\n",(0,r.jsx)(t.li,{children:"Ultron log file (in case of failure)"}),"\n"]}),"\n",(0,r.jsx)(t.p,{children:"You also can add any artifact you need. It will be described later."}),"\n",(0,r.jsx)(t.p,{children:(0,r.jsx)(t.img,{src:"https://github.com/open-tool/ultron/assets/12834123/c05c813a-ece6-45e6-a04f-e1c92b82ffb1",alt:"allure"})}),"\n",(0,r.jsx)(t.hr,{}),"\n",(0,r.jsxs)(t.h2,{id:"ultron-step",children:["Ultron ",(0,r.jsx)(t.code,{children:"step"})]}),"\n",(0,r.jsxs)(t.p,{children:["Ultron wraps Allure ",(0,r.jsx)(t.code,{children:"step"})," method into it's own one."]}),"\n",(0,r.jsx)(t.p,{children:"It's recommended to use Ultron method cause it will provide more info to report in future releases."}),"\n",(0,r.jsx)(t.h3,{id:"best-practice",children:"Best practice"}),"\n",(0,r.jsxs)(t.p,{children:["Wraps all steps with Ultron ",(0,r.jsx)(t.code,{children:"step"})," method e.g."]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-kotlin",children:'object ChatPage: Page(){\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",(0,r.jsx)(t.h2,{id:"custom-config",children:"Custom config"}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-kotlin",children:"UltronConfig.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",(0,r.jsx)(t.h2,{id:"add-detailed-info-about-your-conditions-to-report",children:"Add detailed info about your conditions to report"}),"\n",(0,r.jsxs)(t.p,{children:["Ultron provides cool feature called ",(0,r.jsx)(t.strong,{children:"Test condition management"})," (",(0,r.jsx)(t.a,{href:"https://github.com/open-tool/ultron/wiki/Full-control-of-your-tests",children:"https://github.com/open-tool/ultron/wiki/Full-control-of-your-tests"}),")"]}),"\n",(0,r.jsxs)(t.p,{children:["With recommended config all conditions will be added to Allure report automatically. The ",(0,r.jsx)(t.code,{children:"name"})," of rule and condition is used as Allure ",(0,r.jsx)(t.code,{children:"step"})," name."]}),"\n",(0,r.jsx)(t.p,{children:"For example this code"}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-kotlin",children:' 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",(0,r.jsx)(t.p,{children:"generate following marked steps"}),"\n",(0,r.jsx)(t.p,{children:(0,r.jsx)(t.img,{src:"https://user-images.githubusercontent.com/12834123/232789449-1b6a0bc8-5c68-4dd3-836c-8d39696ce8dd.png",alt:"conditions"})}),"\n",(0,r.jsx)(t.h2,{id:"how-to-add-custom-artifacts-to-allure-report",children:"How to add custom artifacts to Allure report?"}),"\n",(0,r.jsx)(t.h3,{id:"write-artifact-to-report",children:"Write artifact to report"}),"\n",(0,r.jsx)(t.p,{children:"The framework has special methods to write your artifacts into report."}),"\n",(0,r.jsxs)(t.p,{children:[(0,r.jsx)(t.code,{children:"createCacheFile"})," - creates temp file to write the content (",(0,r.jsx)(t.a,{href:"https://github.com/open-tool/ultron/blob/master/ultron/src/main/java/com/atiurin/ultron/utils/InstrumentationUtil.kt",children:"see InstrumentationUtil.kt"}),")\\"]}),"\n",(0,r.jsxs)(t.p,{children:[(0,r.jsx)(t.code,{children:"AttachUtil.attachFile(...)"})," - to attach file to report ",(0,r.jsx)(t.a,{href:"https://github.com/open-tool/ultron/blob/master/ultron-allure/src/main/java/com/atiurin/ultron/allure/attachment/AttachUtil.kt",children:"see AttachUtil"})]}),"\n",(0,r.jsx)(t.p,{children:"You method can looks like"}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-kotlin",children:'fun 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",(0,r.jsxs)(t.p,{children:[(0,r.jsx)(t.code,{children:"writeContentToFile(tempFile)"})," - you should implement it."]}),"\n",(0,r.jsx)(t.h3,{id:"manage-artifact-creation",children:"Manage artifact creation"}),"\n",(0,r.jsx)(t.p,{children:"You can attach artifact using 2 types of Ultron listeners:"}),"\n",(0,r.jsxs)(t.ul,{children:["\n",(0,r.jsxs)(t.li,{children:["\n",(0,r.jsxs)(t.p,{children:[(0,r.jsx)(t.a,{href:"https://github.com/open-tool/ultron/blob/master/ultron/src/main/java/com/atiurin/ultron/listeners/UltronLifecycleListener.kt",children:"UltronLifecycleListener"})," - once Ultron operation finished with any result. Sample - ",(0,r.jsx)(t.a,{href:"https://github.com/open-tool/ultron/blob/master/ultron-allure/src/main/java/com/atiurin/ultron/allure/listeners/ScreenshotAttachListener.kt",children:"ScreenshotAttachListener.kt"})]}),"\n"]}),"\n",(0,r.jsxs)(t.li,{children:["\n",(0,r.jsxs)(t.p,{children:[(0,r.jsx)(t.a,{href:"https://github.com/open-tool/ultron/blob/master/ultron/src/main/java/com/atiurin/ultron/runner/UltronRunListener.kt",children:"UltronRunListener"})," which is inherited from ",(0,r.jsx)(t.a,{href:"https://github.com/open-tool/ultron/blob/master/ultron/src/main/java/com/atiurin/ultron/runner/RunListener.kt",children:"RunListener"}),". This type can be used to add artifact in different test lifecycle state. Sample - ",(0,r.jsx)(t.a,{href:"https://github.com/open-tool/ultron/blob/master/ultron-allure/src/main/java/com/atiurin/ultron/allure/runner/WindowHierarchyAttachRunListener.kt",children:"WindowHierarchyAttachRunListener.kt"})]}),"\n"]}),"\n"]}),"\n",(0,r.jsxs)(t.p,{children:["Refer to the ",(0,r.jsx)(t.a,{href:"/ultron/docs/common/listeners",children:"Listeners wiki page"})," for details."]})]})}function u(e={}){const{wrapper:t}={...(0,o.R)(),...e.components};return t?(0,r.jsx)(t,{...e,children:(0,r.jsx)(d,{...e})}):d(e)}},8453:(e,t,n)=>{n.d(t,{R:()=>i,x:()=>s});var r=n(6540);const o={},l=r.createContext(o);function i(e){const t=r.useContext(l);return r.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function s(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:i(e.components),r.createElement(l.Provider,{value:t},e.children)}}}]);
\ No newline at end of file
+"use strict";(self.webpackChunkmy_website=self.webpackChunkmy_website||[]).push([[338],{8092:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>a,contentTitle:()=>i,default:()=>u,frontMatter:()=>l,metadata:()=>s,toc:()=>c});var r=n(4848),o=n(8453);const l={sidebar_position:1},i="Allure",s={id:"common/allure",title:"Allure",description:"Ultron can generate artifacts for Allure report.",source:"@site/docs/common/allure.md",sourceDirName:"common",slug:"/common/allure",permalink:"/ultron/docs/common/allure",draft:!1,unlisted:!1,tags:[],version:"current",sidebarPosition:1,frontMatter:{sidebar_position:1},sidebar:"tutorialSidebar",previous:{title:"withSuitableRoot",permalink:"/ultron/docs/android/rootview"},next:{title:"Ultron Extension",permalink:"/ultron/docs/common/extension"}},a={},c=[{value:"Custom results directory",id:"custom-results-directory",level:2},{value:"Ultron Allure report contains:",id:"ultron-allure-report-contains",level:2},{value:"Ultron step",id:"ultron-step",level:2},{value:"Best practice",id:"best-practice",level:3},{value:"Custom config",id:"custom-config",level:2},{value:"Add detailed info about your conditions to report",id:"add-detailed-info-about-your-conditions-to-report",level:2},{value:"How to add custom artifacts to Allure report?",id:"how-to-add-custom-artifacts-to-allure-report",level:2},{value:"Write artifact to report",id:"write-artifact-to-report",level:3},{value:"Manage artifact creation",id:"manage-artifact-creation",level:3}];function d(e){const t={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",hr:"hr",img:"img",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,o.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(t.h1,{id:"allure",children:"Allure"}),"\n",(0,r.jsx)(t.p,{children:"Ultron can generate artifacts for Allure report."}),"\n",(0,r.jsxs)(t.p,{children:["Just set Ultron ",(0,r.jsx)(t.code,{children:"testInstrumentationRunner"})," in your app build.gradle file (",(0,r.jsx)(t.a,{href:"https://github.com/open-tool/ultron/blob/master/sample-app/build.gradle.kts#L14",children:"example build.gradle.kts"}),")"]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-kotlin",children:'android {\n defaultConfig {\n testInstrumentationRunner = "com.atiurin.ultron.allure.UltronAllureTestRunner"\n ...\n }\n'})}),"\n",(0,r.jsxs)(t.p,{children:["and apply recommended config in your BaseTest class (",(0,r.jsx)(t.a,{href:"https://github.com/open-tool/ultron/blob/master/sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/BaseTest.kt#L31",children:"example BaseTest"}),")."]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-kotlin",children:"@BeforeClass @JvmStatic\nfun setConfig() {\n UltronConfig.applyRecommended()\n UltronAllureConfig.applyRecommended()\n}\n"})}),"\n",(0,r.jsx)(t.h2,{id:"custom-results-directory",children:"Custom results directory"}),"\n",(0,r.jsxs)(t.p,{children:["Ultron allows you to specify the directory where the Allure results will be stored.\nBy default, the results are stored in the ",(0,r.jsx)(t.code,{children:"/files/allure-results"})," directory in the root of the project.\nYou can change this directory by calling ",(0,r.jsx)(t.code,{children:"UltronAllureConfig.setAllureResultsDirectory()"})]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-kotlin",children:"@BeforeClass @JvmStatic\nfun setConfig() {\n ...\n UltronAllureConfig.applyRecommended()\n UltronAllureConfig.setAllureResultsDirectory(Environment.DIRECTORY_DOWNLOADS)\n}\n"})}),"\n",(0,r.jsx)(t.h2,{id:"ultron-allure-report-contains",children:"Ultron Allure report contains:"}),"\n",(0,r.jsxs)(t.ul,{children:["\n",(0,r.jsx)(t.li,{children:"Detailed report about all operations in your test"}),"\n",(0,r.jsx)(t.li,{children:"Logcat file (in case of failure)"}),"\n",(0,r.jsx)(t.li,{children:"Screenshot (in case of failure)"}),"\n",(0,r.jsx)(t.li,{children:"Ultron log file (in case of failure)"}),"\n"]}),"\n",(0,r.jsx)(t.p,{children:"You also can add any artifact you need. It will be described later."}),"\n",(0,r.jsx)(t.p,{children:(0,r.jsx)(t.img,{src:"https://github.com/open-tool/ultron/assets/12834123/c05c813a-ece6-45e6-a04f-e1c92b82ffb1",alt:"allure"})}),"\n",(0,r.jsx)(t.hr,{}),"\n",(0,r.jsxs)(t.h2,{id:"ultron-step",children:["Ultron ",(0,r.jsx)(t.code,{children:"step"})]}),"\n",(0,r.jsxs)(t.p,{children:["Ultron wraps Allure ",(0,r.jsx)(t.code,{children:"step"})," method into it's own one."]}),"\n",(0,r.jsx)(t.p,{children:"It's recommended to use Ultron method cause it will provide more info to report in future releases."}),"\n",(0,r.jsx)(t.h3,{id:"best-practice",children:"Best practice"}),"\n",(0,r.jsxs)(t.p,{children:["Wraps all steps with Ultron ",(0,r.jsx)(t.code,{children:"step"})," method e.g."]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-kotlin",children:'object ChatPage: Page(){\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",(0,r.jsx)(t.h2,{id:"custom-config",children:"Custom config"}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-kotlin",children:"UltronConfig.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",(0,r.jsx)(t.h2,{id:"add-detailed-info-about-your-conditions-to-report",children:"Add detailed info about your conditions to report"}),"\n",(0,r.jsxs)(t.p,{children:["Ultron provides cool feature called ",(0,r.jsx)(t.strong,{children:"Test condition management"})," (",(0,r.jsx)(t.a,{href:"https://github.com/open-tool/ultron/wiki/Full-control-of-your-tests",children:"https://github.com/open-tool/ultron/wiki/Full-control-of-your-tests"}),")"]}),"\n",(0,r.jsxs)(t.p,{children:["With recommended config all conditions will be added to Allure report automatically. The ",(0,r.jsx)(t.code,{children:"name"})," of rule and condition is used as Allure ",(0,r.jsx)(t.code,{children:"step"})," name."]}),"\n",(0,r.jsx)(t.p,{children:"For example this code"}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-kotlin",children:' 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",(0,r.jsx)(t.p,{children:"generate following marked steps"}),"\n",(0,r.jsx)(t.p,{children:(0,r.jsx)(t.img,{src:"https://user-images.githubusercontent.com/12834123/232789449-1b6a0bc8-5c68-4dd3-836c-8d39696ce8dd.png",alt:"conditions"})}),"\n",(0,r.jsx)(t.h2,{id:"how-to-add-custom-artifacts-to-allure-report",children:"How to add custom artifacts to Allure report?"}),"\n",(0,r.jsx)(t.h3,{id:"write-artifact-to-report",children:"Write artifact to report"}),"\n",(0,r.jsx)(t.p,{children:"The framework has special methods to write your artifacts into report."}),"\n",(0,r.jsxs)(t.p,{children:[(0,r.jsx)(t.code,{children:"createCacheFile"})," - creates temp file to write the content (",(0,r.jsx)(t.a,{href:"https://github.com/open-tool/ultron/blob/master/ultron/src/main/java/com/atiurin/ultron/utils/InstrumentationUtil.kt",children:"see InstrumentationUtil.kt"}),")\\"]}),"\n",(0,r.jsxs)(t.p,{children:[(0,r.jsx)(t.code,{children:"AttachUtil.attachFile(...)"})," - to attach file to report ",(0,r.jsx)(t.a,{href:"https://github.com/open-tool/ultron/blob/master/ultron-allure/src/main/java/com/atiurin/ultron/allure/attachment/AttachUtil.kt",children:"see AttachUtil"})]}),"\n",(0,r.jsx)(t.p,{children:"You method can looks like"}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-kotlin",children:'fun 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",(0,r.jsxs)(t.p,{children:[(0,r.jsx)(t.code,{children:"writeContentToFile(tempFile)"})," - you should implement it."]}),"\n",(0,r.jsx)(t.h3,{id:"manage-artifact-creation",children:"Manage artifact creation"}),"\n",(0,r.jsx)(t.p,{children:"You can attach artifact using 2 types of Ultron listeners:"}),"\n",(0,r.jsxs)(t.ul,{children:["\n",(0,r.jsxs)(t.li,{children:["\n",(0,r.jsxs)(t.p,{children:[(0,r.jsx)(t.a,{href:"https://github.com/open-tool/ultron/blob/master/ultron/src/main/java/com/atiurin/ultron/listeners/UltronLifecycleListener.kt",children:"UltronLifecycleListener"})," - once Ultron operation finished with any result. Sample - ",(0,r.jsx)(t.a,{href:"https://github.com/open-tool/ultron/blob/master/ultron-allure/src/main/java/com/atiurin/ultron/allure/listeners/ScreenshotAttachListener.kt",children:"ScreenshotAttachListener.kt"})]}),"\n"]}),"\n",(0,r.jsxs)(t.li,{children:["\n",(0,r.jsxs)(t.p,{children:[(0,r.jsx)(t.a,{href:"https://github.com/open-tool/ultron/blob/master/ultron/src/main/java/com/atiurin/ultron/runner/UltronRunListener.kt",children:"UltronRunListener"})," which is inherited from ",(0,r.jsx)(t.a,{href:"https://github.com/open-tool/ultron/blob/master/ultron/src/main/java/com/atiurin/ultron/runner/RunListener.kt",children:"RunListener"}),". This type can be used to add artifact in different test lifecycle state. Sample - ",(0,r.jsx)(t.a,{href:"https://github.com/open-tool/ultron/blob/master/ultron-allure/src/main/java/com/atiurin/ultron/allure/runner/WindowHierarchyAttachRunListener.kt",children:"WindowHierarchyAttachRunListener.kt"})]}),"\n"]}),"\n"]}),"\n",(0,r.jsxs)(t.p,{children:["Refer to the ",(0,r.jsx)(t.a,{href:"/ultron/docs/common/listeners",children:"Listeners doc page"})," for details."]})]})}function u(e={}){const{wrapper:t}={...(0,o.R)(),...e.components};return t?(0,r.jsx)(t,{...e,children:(0,r.jsx)(d,{...e})}):d(e)}},8453:(e,t,n)=>{n.d(t,{R:()=>i,x:()=>s});var r=n(6540);const o={},l=r.createContext(o);function i(e){const t=r.useContext(l);return r.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function s(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:i(e.components),r.createElement(l.Provider,{value:t},e.children)}}}]);
\ No newline at end of file
diff --git a/assets/js/4b05c0af.5bfdbbcd.js b/assets/js/4b05c0af.11b15f11.js
similarity index 65%
rename from assets/js/4b05c0af.5bfdbbcd.js
rename to assets/js/4b05c0af.11b15f11.js
index 17eee7ee..ccd9d6b1 100644
--- a/assets/js/4b05c0af.5bfdbbcd.js
+++ b/assets/js/4b05c0af.11b15f11.js
@@ -1 +1 @@
-"use strict";(self.webpackChunkmy_website=self.webpackChunkmy_website||[]).push([[460],{6974:(e,t,s)=>{s.r(t),s.d(t,{assets:()=>l,contentTitle:()=>a,default:()=>m,frontMatter:()=>o,metadata:()=>c,toc:()=>r});var n=s(4848),i=s(8453);const o={sidebar_position:5},a="LazyList",c={id:"compose/lazylist",title:"LazyList",description:"Ultron LazyColumn/LazyRow",source:"@site/docs/compose/lazylist.md",sourceDirName:"compose",slug:"/compose/lazylist",permalink:"/ultron/docs/compose/lazylist",draft:!1,unlisted:!1,tags:[],version:"current",sidebarPosition:5,frontMatter:{sidebar_position:5},sidebar:"tutorialSidebar",previous:{title:"Ultron Compose API",permalink:"/ultron/docs/compose/api"},next:{title:"Espresso",permalink:"/ultron/docs/android/espress"}},l={},r=[{value:"Ultron LazyColumn/LazyRow",id:"ultron-lazycolumnlazyrow",level:2},{value:"UltronComposeList",id:"ultroncomposelist",level:2},{value:"Best practice - define UltronComposeList object as page class property",id:"best-practice---define-ultroncomposelist-object-as-page-class-property",level:3},{value:"UltronComposeList API",id:"ultroncomposelist-api",level:3},{value:"useUnmergedTree",id:"useunmergedtree",level:3},{value:"UltronComposeListItem",id:"ultroncomposelistitem",level:2},{value:"Simple UltronComposeListItem",id:"simple-ultroncomposelistitem",level:3},{value:"Complex UltronComposeListItem with children",id:"complex-ultroncomposelistitem-with-children",level:3},{value:"Best practice",id:"best-practice",level:3},{value:"UltronComposeListItem API",id:"ultroncomposelistitem-api",level:2},{value:"Efficient Strategies for Locating Items in Compose LazyList",id:"efficient-strategies-for-locating-items-in-compose-lazylist",level:2},{value:"1. ..visibleItem",id:"1-visibleitem",level:3},{value:"2. Item by unique SemanticsMatcher",id:"2-item-by-unique-semanticsmatcher",level:3},{value:"3. Set up positionPropertyKey",id:"3-set-up-positionpropertykey",level:3},{value:"4. Set up item testTag",id:"4-set-up-item-testtag",level:3}];function d(e){const t={a:"a",blockquote:"blockquote",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",hr:"hr",img:"img",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,i.R)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(t.h1,{id:"lazylist",children:"LazyList"}),"\n",(0,n.jsx)(t.h2,{id:"ultron-lazycolumnlazyrow",children:"Ultron LazyColumn/LazyRow"}),"\n",(0,n.jsxs)(t.p,{children:["It's pretty much familiar with ",(0,n.jsx)(t.code,{children:"UltronRecyclerView"})," approach. The difference is in internal structure of ",(0,n.jsx)(t.code,{children:"RecyclerView "}),"and ",(0,n.jsx)(t.code,{children:"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",(0,n.jsx)(t.p,{children:"Before we go forward we need to clarify some terms:"}),"\n",(0,n.jsxs)(t.ul,{children:["\n",(0,n.jsxs)(t.li,{children:["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 - ",(0,n.jsx)(t.code,{children:"UltronComposeList"}),"."]}),"\n",(0,n.jsxs)(t.li,{children:["ComposeListItem - single item of ComposeList (there is a class ",(0,n.jsx)(t.code,{children:"UltronComposeListItem"}),")"]}),"\n",(0,n.jsxs)(t.li,{children:["ComposeListItemChild - child element of ComposeListItem (just a term, there is no special class to work with child elements). So ",(0,n.jsx)(t.em,{children:"ComposeListItemChild"})," could be considered as a simple compose node."]}),"\n"]}),"\n",(0,n.jsx)(t.p,{children:(0,n.jsx)(t.img,{src:"https://user-images.githubusercontent.com/12834123/188237127-32e501ca-ae8b-4cd4-8114-e3e17843dc55.PNG",alt:"lazyColumn"})}),"\n",(0,n.jsx)(t.hr,{}),"\n",(0,n.jsx)(t.h2,{id:"ultroncomposelist",children:"UltronComposeList"}),"\n",(0,n.jsxs)(t.p,{children:["Create an instance of ",(0,n.jsx)(t.code,{children:"UltronComposeList"})," by calling a method ",(0,n.jsx)(t.code,{children:"composeList(..)"})]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-kotlin",children:"composeList(hasTestTag(contactsListTestTag)).assertNotEmpty()\n"})}),"\n",(0,n.jsxs)(t.h3,{id:"best-practice---define-ultroncomposelist-object-as-page-class-property",children:[(0,n.jsx)(t.em,{children:"Best practice"})," - define ",(0,n.jsx)(t.code,{children:"UltronComposeList"})," object as page class property"]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-kotlin",children:"object ContactsListPage : Page() {\n val lazyList = composeList(hasContentDescription(contactsListContentDesc))\n fun someStep(){\n lazyList.assertNotEmpty() \n lazyList.assertContentDescriptionEquals(contactsListContentDesc)\n }\n}\n"})}),"\n",(0,n.jsxs)(t.h3,{id:"ultroncomposelist-api",children:[(0,n.jsx)(t.code,{children:"UltronComposeList"})," API"]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-kotlin",children:"withTimeout(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 performOnList(block: (SemanticsNode, SemanticsNodeInteraction) -> T): T\n"})}),"\n",(0,n.jsx)(t.h3,{id:"useunmergedtree",children:"useUnmergedTree"}),"\n",(0,n.jsxs)(t.p,{children:["It is really important to understand the difference btwn merged and unmerged tree. There is a property ",(0,n.jsx)(t.code,{children:"useUnmergedTree"})," that defines a behaviour."]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-kotlin",children:"composeList(hasTestTag(contactsListTestTag), useUnmergedTree = false)\n"})}),"\n",(0,n.jsxs)(t.ul,{children:["\n",(0,n.jsxs)(t.li,{children:["By default ",(0,n.jsx)(t.code,{children:"UltronComposeList"})," uses unmerged tree (",(0,n.jsx)(t.code,{children:"useUnmergedTree = true"}),"). All child elements contain info in seperate nodes."]}),"\n",(0,n.jsxs)(t.li,{children:["In case we use merged tree (",(0,n.jsx)(t.code,{children:"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"]}),"\n",(0,n.jsx)(t.p,{children:"Why it's important? Cause you need to use different SemanticsMatchers to find appropriate child."}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-kotlin",children:"mergedTreeList.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",(0,n.jsx)(t.hr,{}),"\n",(0,n.jsx)(t.h2,{id:"ultroncomposelistitem",children:"UltronComposeListItem"}),"\n",(0,n.jsxs)(t.p,{children:[(0,n.jsx)(t.code,{children:"UltronComposeList"})," provides an access to ",(0,n.jsx)(t.code,{children:"UltronComposeListItem"})]}),"\n",(0,n.jsxs)(t.p,{children:["There is a set of methods to create ",(0,n.jsx)(t.code,{children:"UltronComposeListItem"}),". It's listed upper in ",(0,n.jsx)(t.code,{children:"UltronComposeList"})," api."]}),"\n",(0,n.jsxs)(t.h3,{id:"simple-ultroncomposelistitem",children:["Simple ",(0,n.jsx)(t.code,{children:"UltronComposeListItem"})]}),"\n",(0,n.jsxs)(t.p,{children:["If you don't need to interact with item child just use methods like ",(0,n.jsx)(t.code,{children:"item"}),", ",(0,n.jsx)(t.code,{children:"firstItem"}),", ",(0,n.jsx)(t.code,{children:"visibleItem"}),", ",(0,n.jsx)(t.code,{children:"firstVisibleItem"}),", ",(0,n.jsx)(t.code,{children:"lastVisibleItem"})]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-kotlin",children:"listWithMergedTree.item(hasText(contact.name)).assertTextContains(contact.name)\nlistWithMergedTree.firstVisibleItem()\n .assertIsDisplayed()\n .assertTextContains(contact.name)\n .assertTextContains(contact.status)\n"})}),"\n",(0,n.jsx)(t.p,{children:"You don't need to worry about scroll to item. It's executed automatically."}),"\n",(0,n.jsxs)(t.h3,{id:"complex-ultroncomposelistitem-with-children",children:["Complex ",(0,n.jsx)(t.code,{children:"UltronComposeListItem"})," with children"]}),"\n",(0,n.jsx)(t.p,{children:"It's often required to interact with item child. The best solution will be to describe children as properties of UltronComposeListItem subclass."}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-kotlin",children:"class ComposeFriendListItem : UltronComposeListItem(){\n val name by child { hasTestTag(contactNameTestTag) }\n val status by child { hasTestTag(contactStatusTestTag) }\n}\n"})}),"\n",(0,n.jsx)(t.p,{children:(0,n.jsxs)(t.strong,{children:["Note: you have to use delegated initialisation with ",(0,n.jsx)(t.code,{children:"by child"}),"."]})}),"\n",(0,n.jsxs)(t.p,{children:["Now you're able to get ",(0,n.jsx)(t.code,{children:"ComposeFriendListItem"})," object using methods ",(0,n.jsx)(t.code,{children:"getItem"}),", ",(0,n.jsx)(t.code,{children:"getVisibleItem"}),", ",(0,n.jsx)(t.code,{children:"getFirstVisibleItem"}),", ",(0,n.jsx)(t.code,{children:"getLastVisibleItem"})]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-kotlin",children:"lazyList.getFirstVisibleItem()\nlazyList.getVisibleItem(index)\nlazyList.getItem(hasTestTag(..))\n"})}),"\n",(0,n.jsx)(t.h3,{id:"best-practice",children:(0,n.jsx)(t.em,{children:"Best practice"})}),"\n",(0,n.jsxs)(t.blockquote,{children:["\n",(0,n.jsxs)(t.p,{children:["Add a method to ",(0,n.jsx)(t.code,{children:"Page"})," class that returns ",(0,n.jsx)(t.code,{children:"UltronComposeListItem"})," subclass"]}),"\n"]}),"\n",(0,n.jsxs)(t.p,{children:["Mark such methods with ",(0,n.jsx)(t.code,{children:"private"})," visibility modifier. e.g. ",(0,n.jsx)(t.code,{children:"getContactItem"})]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-kotlin",children:"object ComposeListPage : Page() {\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"})}),"\n",(0,n.jsxs)(t.p,{children:["Use ",(0,n.jsx)(t.code,{children:"getContactItem"})," in ",(0,n.jsx)(t.code,{children:"Page"})," steps like ",(0,n.jsx)(t.code,{children:"assertContactStatus"})]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-kotlin",children:"object ComposeListPage : Page() {\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",(0,n.jsxs)(t.h2,{id:"ultroncomposelistitem-api",children:[(0,n.jsx)(t.code,{children:"UltronComposeListItem"})," API"]}),"\n",(0,n.jsxs)(t.p,{children:["It's pretty much the same as ",(0,n.jsx)(t.a,{href:"https://github.com/open-tool/ultron/wiki/Compose#ultron--compose-api",children:"simple node api"}),", but extends it mostly for internal features."]}),"\n",(0,n.jsx)(t.hr,{}),"\n",(0,n.jsx)(t.h2,{id:"efficient-strategies-for-locating-items-in-compose-lazylist",children:"Efficient Strategies for Locating Items in Compose LazyList"}),"\n",(0,n.jsxs)(t.p,{children:["Let's start with approaches that you can use without additional efforts. For example, you have identified ",(0,n.jsx)(t.code,{children:"LazyList"})," in your tests code like"]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-kotlin",children:'val lazyList = composeList(listMatcher = hasTestTag("listTestTag"))\n\nclass ComposeListItem : UltronComposeListItem() {\n val name by lazy { getChild(hasTestTag(contactNameTestTag)) }\n val status by lazy { getChild(hasTestTag(contactStatusTestTag)) }\n}\n'})}),"\n",(0,n.jsxs)(t.h3,{id:"1-visibleitem",children:["1. ",(0,n.jsx)(t.code,{children:"..visibleItem"})]}),"\n",(0,n.jsxs)(t.p,{children:["This is probably the most unstable approach. It's only suitable in case you didn't interact with ",(0,n.jsx)(t.code,{children:"LazyList"})," and would like to reach an item that is on the screen."]}),"\n",(0,n.jsx)(t.p,{children:"Use the following methods:"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-kotlin",children:"lazyList.firstVisibleItem()\nlazyList.visibleItem(index = 3)\nlazyList.lastVisibleItem()\n\nlazyList.getFirstVisibleItem()\nlazyList.getVisibleItem(index = 3)\nlazyList.getLastVisibleItem()\n"})}),"\n",(0,n.jsxs)(t.h3,{id:"2-item-by-unique-semanticsmatcher",children:["2. Item by unique ",(0,n.jsx)(t.code,{children:"SemanticsMatcher"})]}),"\n",(0,n.jsxs)(t.p,{children:["A more stable way to find the item is to use ",(0,n.jsx)(t.code,{children:"SemanticsMatcher"}),". It allows you to find the item not only on the screen."]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-kotlin",children:'lazyList.item(hasAnyDescendant(hasText("Some unique text")) \nlazyList.getItem(hasAnyDescendant(hasText("Some unique text")) \n'})}),"\n",(0,n.jsx)(t.hr,{}),"\n",(0,n.jsx)(t.p,{children:"The next two approaches require additional code in the application. These are the most stable and preferable ways."}),"\n",(0,n.jsxs)(t.h3,{id:"3-set-up-positionpropertykey",children:["3. Set up ",(0,n.jsx)(t.code,{children:"positionPropertyKey"})]}),"\n",(0,n.jsx)(t.p,{children:"By default, a compose list item doesn't have a property that stores its position in the list. We can add this property in a really simple way."}),"\n",(0,n.jsx)(t.p,{children:"Here is the application code:"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-kotlin",children:'// create custom SemanticsPropertyKey\nval ListItemPositionPropertyKey = SemanticsPropertyKey("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\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",(0,n.jsxs)(t.p,{children:["After that, you need to specify the custom ",(0,n.jsx)(t.code,{children:"SemanticsPropertyKey"})," in the test code:"]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-kotlin",children:'val lazyList = composeList(\n listMatcher = hasTestTag("listTestTag"),\n positionPropertyKey = ListItemPositionPropertyKey\n)\n'})}),"\n",(0,n.jsx)(t.p,{children:"It allows you to reach the item by its position in the list:"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-kotlin",children:"lazyList.firstItem()\nlazyList.item(position = 25)\nlazyList.getFirstItem()\nlazyList.getItem(position = 7)\n"})}),"\n",(0,n.jsxs)(t.h3,{id:"4-set-up-item-testtag",children:["4. Set up item ",(0,n.jsx)(t.code,{children:"testTag"})]}),"\n",(0,n.jsxs)(t.p,{children:["It is recommended to build ",(0,n.jsx)(t.code,{children:"testTag"})," in a separate function based on data object."]}),"\n",(0,n.jsxs)(t.p,{children:["For example, let's assume we have a ",(0,n.jsx)(t.code,{children:"Contact"})," data class that stores data to be presented in the item."]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-kotlin",children:"data class Contact(val id: Int, val name: String, val status: String, val avatar: String)\n"})}),"\n",(0,n.jsxs)(t.p,{children:["We can create function to build ",(0,n.jsx)(t.code,{children:"testTag"})," based on ",(0,n.jsx)(t.code,{children:"contact.id"})]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-kotlin",children:'fun getContactItemTestTag(contact: Contact) = "contactId=${contact.id}"\n'})}),"\n",(0,n.jsxs)(t.p,{children:["We can use this function in the application code to specify ",(0,n.jsx)(t.code,{children:"testTag"})," and in the test code to find the item by ",(0,n.jsx)(t.code,{children:"testTag"}),":"]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-kotlin",children:'// application code\n@Composable\nfun ContactsListWithPosition(contacts: List\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(hasTestTag(getContactItemTestTag(contact)))\n\n'})})]})}function m(e={}){const{wrapper:t}={...(0,i.R)(),...e.components};return t?(0,n.jsx)(t,{...e,children:(0,n.jsx)(d,{...e})}):d(e)}},8453:(e,t,s)=>{s.d(t,{R:()=>a,x:()=>c});var n=s(6540);const i={},o=n.createContext(i);function a(e){const t=n.useContext(o);return n.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function c(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:a(e.components),n.createElement(o.Provider,{value:t},e.children)}}}]);
\ No newline at end of file
+"use strict";(self.webpackChunkmy_website=self.webpackChunkmy_website||[]).push([[460],{6974:(e,t,s)=>{s.r(t),s.d(t,{assets:()=>l,contentTitle:()=>a,default:()=>m,frontMatter:()=>o,metadata:()=>c,toc:()=>r});var n=s(4848),i=s(8453);const o={sidebar_position:5},a="LazyList",c={id:"compose/lazylist",title:"LazyList",description:"Ultron LazyColumn/LazyRow",source:"@site/docs/compose/lazylist.md",sourceDirName:"compose",slug:"/compose/lazylist",permalink:"/ultron/docs/compose/lazylist",draft:!1,unlisted:!1,tags:[],version:"current",sidebarPosition:5,frontMatter:{sidebar_position:5},sidebar:"tutorialSidebar",previous:{title:"Ultron Compose API",permalink:"/ultron/docs/compose/api"},next:{title:"Espresso",permalink:"/ultron/docs/android/espress"}},l={},r=[{value:"Ultron LazyColumn/LazyRow",id:"ultron-lazycolumnlazyrow",level:2},{value:"UltronComposeList",id:"ultroncomposelist",level:2},{value:"Best practice - define UltronComposeList object as page class property",id:"best-practice---define-ultroncomposelist-object-as-page-class-property",level:3},{value:"UltronComposeList API",id:"ultroncomposelist-api",level:3},{value:"useUnmergedTree",id:"useunmergedtree",level:3},{value:"UltronComposeListItem",id:"ultroncomposelistitem",level:2},{value:"Simple UltronComposeListItem",id:"simple-ultroncomposelistitem",level:3},{value:"Complex UltronComposeListItem with children",id:"complex-ultroncomposelistitem-with-children",level:3},{value:"Best practice",id:"best-practice",level:3},{value:"UltronComposeListItem API",id:"ultroncomposelistitem-api",level:2},{value:"Efficient Strategies for Locating Items in Compose LazyList",id:"efficient-strategies-for-locating-items-in-compose-lazylist",level:2},{value:"1. ..visibleItem",id:"1-visibleitem",level:3},{value:"2. Item by unique SemanticsMatcher",id:"2-item-by-unique-semanticsmatcher",level:3},{value:"3. Set up positionPropertyKey",id:"3-set-up-positionpropertykey",level:3},{value:"4. Set up item testTag",id:"4-set-up-item-testtag",level:3}];function d(e){const t={a:"a",blockquote:"blockquote",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",hr:"hr",img:"img",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,i.R)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(t.h1,{id:"lazylist",children:"LazyList"}),"\n",(0,n.jsx)(t.h2,{id:"ultron-lazycolumnlazyrow",children:"Ultron LazyColumn/LazyRow"}),"\n",(0,n.jsxs)(t.p,{children:["It's pretty much familiar with ",(0,n.jsx)(t.code,{children:"UltronRecyclerView"})," approach. The difference is in internal structure of ",(0,n.jsx)(t.code,{children:"RecyclerView "}),"and ",(0,n.jsx)(t.code,{children:"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",(0,n.jsx)(t.p,{children:"Before we go forward we need to clarify some terms:"}),"\n",(0,n.jsxs)(t.ul,{children:["\n",(0,n.jsxs)(t.li,{children:["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 - ",(0,n.jsx)(t.code,{children:"UltronComposeList"}),"."]}),"\n",(0,n.jsxs)(t.li,{children:["ComposeListItem - single item of ComposeList (there is a class ",(0,n.jsx)(t.code,{children:"UltronComposeListItem"}),")"]}),"\n",(0,n.jsxs)(t.li,{children:["ComposeListItemChild - child element of ComposeListItem (just a term, there is no special class to work with child elements). So ",(0,n.jsx)(t.em,{children:"ComposeListItemChild"})," could be considered as a simple compose node."]}),"\n"]}),"\n",(0,n.jsx)(t.p,{children:(0,n.jsx)(t.img,{src:"https://user-images.githubusercontent.com/12834123/188237127-32e501ca-ae8b-4cd4-8114-e3e17843dc55.PNG",alt:"lazyColumn"})}),"\n",(0,n.jsx)(t.hr,{}),"\n",(0,n.jsx)(t.h2,{id:"ultroncomposelist",children:"UltronComposeList"}),"\n",(0,n.jsxs)(t.p,{children:["Create an instance of ",(0,n.jsx)(t.code,{children:"UltronComposeList"})," by calling a method ",(0,n.jsx)(t.code,{children:"composeList(..)"})]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-kotlin",children:"composeList(hasTestTag(contactsListTestTag)).assertNotEmpty()\n"})}),"\n",(0,n.jsxs)(t.h3,{id:"best-practice---define-ultroncomposelist-object-as-page-class-property",children:[(0,n.jsx)(t.em,{children:"Best practice"})," - define ",(0,n.jsx)(t.code,{children:"UltronComposeList"})," object as page class property"]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-kotlin",children:"object ContactsListPage : Page() {\n val lazyList = composeList(hasContentDescription(contactsListContentDesc))\n fun someStep(){\n lazyList.assertNotEmpty() \n lazyList.assertContentDescriptionEquals(contactsListContentDesc)\n }\n}\n"})}),"\n",(0,n.jsxs)(t.h3,{id:"ultroncomposelist-api",children:[(0,n.jsx)(t.code,{children:"UltronComposeList"})," API"]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-kotlin",children:"withTimeout(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 performOnList(block: (SemanticsNode, SemanticsNodeInteraction) -> T): T\n"})}),"\n",(0,n.jsx)(t.h3,{id:"useunmergedtree",children:"useUnmergedTree"}),"\n",(0,n.jsxs)(t.p,{children:["It is really important to understand the difference btwn merged and unmerged tree. There is a property ",(0,n.jsx)(t.code,{children:"useUnmergedTree"})," that defines a behaviour."]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-kotlin",children:"composeList(hasTestTag(contactsListTestTag), useUnmergedTree = false)\n"})}),"\n",(0,n.jsxs)(t.ul,{children:["\n",(0,n.jsxs)(t.li,{children:["By default ",(0,n.jsx)(t.code,{children:"UltronComposeList"})," uses unmerged tree (",(0,n.jsx)(t.code,{children:"useUnmergedTree = true"}),"). All child elements contain info in seperate nodes."]}),"\n",(0,n.jsxs)(t.li,{children:["In case we use merged tree (",(0,n.jsx)(t.code,{children:"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"]}),"\n",(0,n.jsx)(t.p,{children:"Why it's important? Cause you need to use different SemanticsMatchers to find appropriate child."}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-kotlin",children:"mergedTreeList.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",(0,n.jsx)(t.hr,{}),"\n",(0,n.jsx)(t.h2,{id:"ultroncomposelistitem",children:"UltronComposeListItem"}),"\n",(0,n.jsxs)(t.p,{children:[(0,n.jsx)(t.code,{children:"UltronComposeList"})," provides an access to ",(0,n.jsx)(t.code,{children:"UltronComposeListItem"})]}),"\n",(0,n.jsxs)(t.p,{children:["There is a set of methods to create ",(0,n.jsx)(t.code,{children:"UltronComposeListItem"}),". It's listed upper in ",(0,n.jsx)(t.code,{children:"UltronComposeList"})," api."]}),"\n",(0,n.jsxs)(t.h3,{id:"simple-ultroncomposelistitem",children:["Simple ",(0,n.jsx)(t.code,{children:"UltronComposeListItem"})]}),"\n",(0,n.jsxs)(t.p,{children:["If you don't need to interact with item child just use methods like ",(0,n.jsx)(t.code,{children:"item"}),", ",(0,n.jsx)(t.code,{children:"firstItem"}),", ",(0,n.jsx)(t.code,{children:"visibleItem"}),", ",(0,n.jsx)(t.code,{children:"firstVisibleItem"}),", ",(0,n.jsx)(t.code,{children:"lastVisibleItem"})]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-kotlin",children:"listWithMergedTree.item(hasText(contact.name)).assertTextContains(contact.name)\nlistWithMergedTree.firstVisibleItem()\n .assertIsDisplayed()\n .assertTextContains(contact.name)\n .assertTextContains(contact.status)\n"})}),"\n",(0,n.jsx)(t.p,{children:"You don't need to worry about scroll to item. It's executed automatically."}),"\n",(0,n.jsxs)(t.h3,{id:"complex-ultroncomposelistitem-with-children",children:["Complex ",(0,n.jsx)(t.code,{children:"UltronComposeListItem"})," with children"]}),"\n",(0,n.jsx)(t.p,{children:"It's often required to interact with item child. The best solution will be to describe children as properties of UltronComposeListItem subclass."}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-kotlin",children:"class ComposeFriendListItem : UltronComposeListItem(){\n val name by child { hasTestTag(contactNameTestTag) }\n val status by child { hasTestTag(contactStatusTestTag) }\n}\n"})}),"\n",(0,n.jsx)(t.p,{children:(0,n.jsxs)(t.strong,{children:["Note: you have to use delegated initialisation with ",(0,n.jsx)(t.code,{children:"by child"}),"."]})}),"\n",(0,n.jsxs)(t.p,{children:["Now you're able to get ",(0,n.jsx)(t.code,{children:"ComposeFriendListItem"})," object using methods ",(0,n.jsx)(t.code,{children:"getItem"}),", ",(0,n.jsx)(t.code,{children:"getVisibleItem"}),", ",(0,n.jsx)(t.code,{children:"getFirstVisibleItem"}),", ",(0,n.jsx)(t.code,{children:"getLastVisibleItem"})]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-kotlin",children:"lazyList.getFirstVisibleItem()\nlazyList.getVisibleItem(index)\nlazyList.getItem(hasTestTag(..))\n"})}),"\n",(0,n.jsx)(t.h3,{id:"best-practice",children:(0,n.jsx)(t.em,{children:"Best practice"})}),"\n",(0,n.jsxs)(t.blockquote,{children:["\n",(0,n.jsxs)(t.p,{children:["Add a method to ",(0,n.jsx)(t.code,{children:"Page"})," class that returns ",(0,n.jsx)(t.code,{children:"UltronComposeListItem"})," subclass"]}),"\n"]}),"\n",(0,n.jsxs)(t.p,{children:["Mark such methods with ",(0,n.jsx)(t.code,{children:"private"})," visibility modifier. e.g. ",(0,n.jsx)(t.code,{children:"getContactItem"})]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-kotlin",children:"object ComposeListPage : Page() {\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"})}),"\n",(0,n.jsxs)(t.p,{children:["Use ",(0,n.jsx)(t.code,{children:"getContactItem"})," in ",(0,n.jsx)(t.code,{children:"Page"})," steps like ",(0,n.jsx)(t.code,{children:"assertContactStatus"})]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-kotlin",children:"object ComposeListPage : Page() {\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",(0,n.jsxs)(t.h2,{id:"ultroncomposelistitem-api",children:[(0,n.jsx)(t.code,{children:"UltronComposeListItem"})," API"]}),"\n",(0,n.jsxs)(t.p,{children:["It's pretty much the same as ",(0,n.jsx)(t.a,{href:"/ultron/docs/compose/api",children:"simple node api"}),", but extends it mostly for internal features."]}),"\n",(0,n.jsx)(t.hr,{}),"\n",(0,n.jsx)(t.h2,{id:"efficient-strategies-for-locating-items-in-compose-lazylist",children:"Efficient Strategies for Locating Items in Compose LazyList"}),"\n",(0,n.jsxs)(t.p,{children:["Let's start with approaches that you can use without additional efforts. For example, you have identified ",(0,n.jsx)(t.code,{children:"LazyList"})," in your tests code like"]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-kotlin",children:'val lazyList = composeList(listMatcher = hasTestTag("listTestTag"))\n\nclass ComposeListItem : UltronComposeListItem() {\n val name by lazy { getChild(hasTestTag(contactNameTestTag)) }\n val status by lazy { getChild(hasTestTag(contactStatusTestTag)) }\n}\n'})}),"\n",(0,n.jsxs)(t.h3,{id:"1-visibleitem",children:["1. ",(0,n.jsx)(t.code,{children:"..visibleItem"})]}),"\n",(0,n.jsxs)(t.p,{children:["This is probably the most unstable approach. It's only suitable in case you didn't interact with ",(0,n.jsx)(t.code,{children:"LazyList"})," and would like to reach an item that is on the screen."]}),"\n",(0,n.jsx)(t.p,{children:"Use the following methods:"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-kotlin",children:"lazyList.firstVisibleItem()\nlazyList.visibleItem(index = 3)\nlazyList.lastVisibleItem()\n\nlazyList.getFirstVisibleItem()\nlazyList.getVisibleItem(index = 3)\nlazyList.getLastVisibleItem()\n"})}),"\n",(0,n.jsxs)(t.h3,{id:"2-item-by-unique-semanticsmatcher",children:["2. Item by unique ",(0,n.jsx)(t.code,{children:"SemanticsMatcher"})]}),"\n",(0,n.jsxs)(t.p,{children:["A more stable way to find the item is to use ",(0,n.jsx)(t.code,{children:"SemanticsMatcher"}),". It allows you to find the item not only on the screen."]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-kotlin",children:'lazyList.item(hasAnyDescendant(hasText("Some unique text")) \nlazyList.getItem(hasAnyDescendant(hasText("Some unique text")) \n'})}),"\n",(0,n.jsx)(t.hr,{}),"\n",(0,n.jsx)(t.p,{children:"The next two approaches require additional code in the application. These are the most stable and preferable ways."}),"\n",(0,n.jsxs)(t.h3,{id:"3-set-up-positionpropertykey",children:["3. Set up ",(0,n.jsx)(t.code,{children:"positionPropertyKey"})]}),"\n",(0,n.jsx)(t.p,{children:"By default, a compose list item doesn't have a property that stores its position in the list. We can add this property in a really simple way."}),"\n",(0,n.jsx)(t.p,{children:"Here is the application code:"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-kotlin",children:'// create custom SemanticsPropertyKey\nval ListItemPositionPropertyKey = SemanticsPropertyKey("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\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",(0,n.jsxs)(t.p,{children:["After that, you need to specify the custom ",(0,n.jsx)(t.code,{children:"SemanticsPropertyKey"})," in the test code:"]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-kotlin",children:'val lazyList = composeList(\n listMatcher = hasTestTag("listTestTag"),\n positionPropertyKey = ListItemPositionPropertyKey\n)\n'})}),"\n",(0,n.jsx)(t.p,{children:"It allows you to reach the item by its position in the list:"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-kotlin",children:"lazyList.firstItem()\nlazyList.item(position = 25)\nlazyList.getFirstItem()\nlazyList.getItem(position = 7)\n"})}),"\n",(0,n.jsxs)(t.h3,{id:"4-set-up-item-testtag",children:["4. Set up item ",(0,n.jsx)(t.code,{children:"testTag"})]}),"\n",(0,n.jsxs)(t.p,{children:["It is recommended to build ",(0,n.jsx)(t.code,{children:"testTag"})," in a separate function based on data object."]}),"\n",(0,n.jsxs)(t.p,{children:["For example, let's assume we have a ",(0,n.jsx)(t.code,{children:"Contact"})," data class that stores data to be presented in the item."]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-kotlin",children:"data class Contact(val id: Int, val name: String, val status: String, val avatar: String)\n"})}),"\n",(0,n.jsxs)(t.p,{children:["We can create function to build ",(0,n.jsx)(t.code,{children:"testTag"})," based on ",(0,n.jsx)(t.code,{children:"contact.id"})]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-kotlin",children:'fun getContactItemTestTag(contact: Contact) = "contactId=${contact.id}"\n'})}),"\n",(0,n.jsxs)(t.p,{children:["We can use this function in the application code to specify ",(0,n.jsx)(t.code,{children:"testTag"})," and in the test code to find the item by ",(0,n.jsx)(t.code,{children:"testTag"}),":"]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-kotlin",children:'// application code\n@Composable\nfun ContactsListWithPosition(contacts: List\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(hasTestTag(getContactItemTestTag(contact)))\n\n'})})]})}function m(e={}){const{wrapper:t}={...(0,i.R)(),...e.components};return t?(0,n.jsx)(t,{...e,children:(0,n.jsx)(d,{...e})}):d(e)}},8453:(e,t,s)=>{s.d(t,{R:()=>a,x:()=>c});var n=s(6540);const i={},o=n.createContext(i);function a(e){const t=n.useContext(o);return n.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function c(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:a(e.components),n.createElement(o.Provider,{value:t},e.children)}}}]);
\ No newline at end of file
diff --git a/assets/js/c377a04b.30833c89.js b/assets/js/c377a04b.30833c89.js
new file mode 100644
index 00000000..6952384c
--- /dev/null
+++ b/assets/js/c377a04b.30833c89.js
@@ -0,0 +1 @@
+"use strict";(self.webpackChunkmy_website=self.webpackChunkmy_website||[]).push([[361],{8321:(e,n,s)=>{s.r(n),s.d(n,{assets:()=>l,contentTitle:()=>i,default:()=>h,frontMatter:()=>r,metadata:()=>a,toc:()=>c});var t=s(4848),o=s(8453);const r={sidebar_position:1},i="Introduction",a={id:"index",title:"Introduction",description:"Docusaurus themed imageDocusaurus themed image",source:"@site/docs/index.md",sourceDirName:".",slug:"/",permalink:"/ultron/docs/",draft:!1,unlisted:!1,tags:[],version:"current",sidebarPosition:1,frontMatter:{sidebar_position:1},sidebar:"tutorialSidebar",next:{title:"Connect to project",permalink:"/ultron/docs/intro/connect"}},l={},c=[{value:"What are the benefits of using the framework?",id:"what-are-the-benefits-of-using-the-framework",level:2},{value:"A few words about syntax",id:"a-few-words-about-syntax",level:3},{value:"1. Simple compose operation (refer to the doc here)",id:"1-simple-compose-operation-refer-to-the-doc-here",level:4},{value:"2. Compose list operation (refer to the doc)",id:"2-compose-list-operation-refer-to-the-doc",level:4},{value:"3. Simple Espresso assertion and action.",id:"3-simple-espresso-assertion-and-action",level:4},{value:"4. Action on RecyclerView list item",id:"4-action-on-recyclerview-list-item",level:4},{value:"5. Espresso WebView operations",id:"5-espresso-webview-operations",level:4},{value:"6. UI Automator operations",id:"6-ui-automator-operations",level:4},{value:"Acquiring the result of any operation as Boolean value",id:"acquiring-the-result-of-any-operation-as-boolean-value",level:3},{value:"Why are all Ultron actions and assertions more stable?",id:"why-are-all-ultron-actions-and-assertions-more-stable",level:3},{value:"3 steps to develop a test using Ultron",id:"3-steps-to-develop-a-test-using-ultron",level:2},{value:"Allure report",id:"allure-report",level:2}];function d(e){const n={a:"a",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",h4:"h4",hr:"hr",img:"img",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,o.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(n.h1,{id:"introduction",children:"Introduction"}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.img,{alt:"Docusaurus themed image",src:s(443).A+"#gh-light-mode-only",width:"12610",height:"2326"}),(0,t.jsx)(n.img,{alt:"Docusaurus themed image",src:s(5859).A+"#gh-dark-mode-only",width:"12610",height:"2326"})]}),"\n",(0,t.jsxs)(n.p,{children:["Ultron is the simplest framework to develop UI tests for ",(0,t.jsx)(n.strong,{children:"Android"})," & ",(0,t.jsx)(n.strong,{children:"Compose Multiplatform"}),"."]}),"\n",(0,t.jsx)(n.p,{children:"It's constructed upon the Espresso, UI Automator and Compose UI testing frameworks. Ultron introduces a range of remarkable new features. Furthermore, Ultron puts you in complete control of your tests!"}),"\n",(0,t.jsx)(n.p,{children:"You don't need to learn any new classes or special syntax. All magic actions and assertions are provided from crunch. Ultron can be easially customised and extended."}),"\n",(0,t.jsx)(n.h2,{id:"what-are-the-benefits-of-using-the-framework",children:"What are the benefits of using the framework?"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:["Exceptional support for ",(0,t.jsx)(n.a,{href:"/ultron/docs/compose/",children:(0,t.jsx)(n.strong,{children:"Compose"})})]}),"\n",(0,t.jsxs)(n.li,{children:["Out-of-the-box generation of ",(0,t.jsx)(n.a,{href:"/ultron/docs/common/allure",children:(0,t.jsx)(n.strong,{children:"Allure report"})})," (Now, for Android UI tests only)"]}),"\n",(0,t.jsx)(n.li,{children:"A straightforward and expressive syntax"}),"\n",(0,t.jsxs)(n.li,{children:["Ensured ",(0,t.jsx)(n.strong,{children:"Stability"})," for all actions and assertions"]}),"\n",(0,t.jsx)(n.li,{children:"Complete control over every action and assertion"}),"\n",(0,t.jsxs)(n.li,{children:["Incredible interaction with ",(0,t.jsx)(n.a,{href:"/ultron/docs/android/recyclerview",children:(0,t.jsx)(n.strong,{children:"RecyclerView"})})," and ",(0,t.jsx)(n.a,{href:"/ultron/docs/compose/lazylist",children:(0,t.jsx)(n.strong,{children:"Compose\xa0lists"})}),"."]}),"\n",(0,t.jsxs)(n.li,{children:["An ",(0,t.jsx)(n.strong,{children:"Architectural"})," approach to developing UI tests"]}),"\n",(0,t.jsx)(n.li,{children:"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",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.a,{href:"/ultron/docs/common/extension",children:"The ability to effortlessly extend the framework with your own operations"})}),"\n",(0,t.jsx)(n.li,{children:"Accelerated UI Automator operations"}),"\n",(0,t.jsxs)(n.li,{children:["Ability to monitor each stage of operation execution with ",(0,t.jsx)(n.a,{href:"/ultron/docs/common/listeners",children:"Listeners"})]}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.a,{href:"https://github.com/open-tool/ultron/wiki/Custom-operation-assertions",children:"Custom operation assertions"})}),"\n"]}),"\n",(0,t.jsx)(n.hr,{}),"\n",(0,t.jsx)(n.h3,{id:"a-few-words-about-syntax",children:"A few words about syntax"}),"\n",(0,t.jsxs)(n.p,{children:["The standard syntax provided by Google is intricate and not intuitive. This is especially evident when dealing with ",(0,t.jsx)(n.strong,{children:"LazyList"})," and ",(0,t.jsx)(n.strong,{children:"RecyclerView"})," interactions."]}),"\n",(0,t.jsx)(n.p,{children:"Let's explore some examples:"}),"\n",(0,t.jsxs)(n.h4,{id:"1-simple-compose-operation-refer-to-the-doc-here",children:["1. Simple compose operation (refer to the doc ",(0,t.jsx)(n.a,{href:"/ultron/docs/compose/",children:"here"}),")"]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Compose framework"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-kotlin",children:'composeTestRule.onNode(hasTestTag("Continue")).performClick()\ncomposeTestRule.onNodeWithText("Welcome").assertIsDisplayed()\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Ultron"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-kotlin",children:'hasTestTag("Continue").click()\nhasText("Welcome").assertIsDisplayed()\n'})}),"\n",(0,t.jsxs)(n.h4,{id:"2-compose-list-operation-refer-to-the-doc",children:["2. Compose list operation (refer to the ",(0,t.jsx)(n.a,{href:"/ultron/docs/compose/lazylist",children:"doc"}),")"]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Compose framework"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-kotlin",children:"val itemMatcher = hasText(contact.name)\ncomposeRule\n .onNodeWithTag(contactsListTestTag)\n .performScrollToNode(itemMatcher)\n .onChildren()\n .filterToOne(itemMatcher)\n .assertTextContains(contact.name)\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Ultron"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-kotlin",children:"composeList(hasTestTag(contactsListTestTag))\n .item(hasText(contact.name))\n .assertTextContains(contact.name)\n"})}),"\n",(0,t.jsx)(n.h4,{id:"3-simple-espresso-assertion-and-action",children:"3. Simple Espresso assertion and action."}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Espresso"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-kotlin",children:"onView(withId(R.id.send_button)).check(isDisplayed()).perform(click())\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Ultron"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-kotlin",children:"withId(R.id.send_button).isDisplayed().click()\n"})}),"\n",(0,t.jsx)(n.p,{children:"This presents a cleaner approach. Ultron's operation names mirror Espresso's, while also providing additional operations."}),"\n",(0,t.jsxs)(n.p,{children:["Refer to the ",(0,t.jsx)(n.a,{href:"/ultron/docs/android/espress",children:"doc"})," for further details."]}),"\n",(0,t.jsx)(n.h4,{id:"4-action-on-recyclerview-list-item",children:"4. Action on RecyclerView list item"}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Espresso"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-kotlin",children:'onView(withId(R.id.recycler_friends))\n .perform(\n RecyclerViewActions\n .actionOnItem(\n hasDescendant(withText("Janice")),\n click()\n )\n )\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Ultron"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-kotlin",children:'withRecyclerView(R.id.recycler_friends)\n .item(hasDescendant(withText("Janice")))\n .click()\n'})}),"\n",(0,t.jsxs)(n.p,{children:["Explore the ",(0,t.jsx)(n.a,{href:"/ultron/docs/android/espress",children:"doc"})," to unveil Ultron's magic with RecyclerView interactions."]}),"\n",(0,t.jsx)(n.h4,{id:"5-espresso-webview-operations",children:"5. Espresso WebView operations"}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Espresso"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-kotlin",children:'onWebView()\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",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Ultron"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-kotlin",children:'id("text_input").webKeys(newTitle)\nid("button1").webClick()\nid("title").hasText(newTitle)\n'})}),"\n",(0,t.jsxs)(n.p,{children:["Refer to the ",(0,t.jsx)(n.a,{href:"/ultron/docs/android/webview",children:"doc"})," for more details."]}),"\n",(0,t.jsx)(n.h4,{id:"6-ui-automator-operations",children:"6. UI Automator operations"}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"UI Automator"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-kotlin",children:'val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())\ndevice\n .findObject(By.res("com.atiurin.sampleapp:id", "button1"))\n .click()\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Ultron"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-kotlin",children:"byResId(R.id.button1).click() \n"})}),"\n",(0,t.jsxs)(n.p,{children:["Refer to the ",(0,t.jsx)(n.a,{href:"/ultron/docs/android/uiautomator",children:"doc"})]}),"\n",(0,t.jsx)(n.hr,{}),"\n",(0,t.jsx)(n.h3,{id:"acquiring-the-result-of-any-operation-as-boolean-value",children:"Acquiring the result of any operation as Boolean value"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-kotlin",children:"val isButtonDisplayed = withId(R.id.button).isSuccess { isDisplayed() }\nif (isButtonDisplayed) {\n //do some reasonable actions\n}\n"})}),"\n",(0,t.jsx)(n.hr,{}),"\n",(0,t.jsx)(n.h3,{id:"why-are-all-ultron-actions-and-assertions-more-stable",children:"Why are all Ultron actions and assertions more stable?"}),"\n",(0,t.jsx)(n.p,{children:"The framework captures a list of specified exceptions and attempts to repeat the operation during a timeout period (default is 5 seconds). Of course, you have the ability to customize the list of handled exceptions. You can also set a custom timeout for any operation."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-kotlin",children:'withId(R.id.result).withTimeout(10_000).hasText("Passed")\n'})}),"\n",(0,t.jsx)(n.hr,{}),"\n",(0,t.jsx)(n.h2,{id:"3-steps-to-develop-a-test-using-ultron",children:"3 steps to develop a test using Ultron"}),"\n",(0,t.jsx)(n.p,{children:"We advocate for a proper test framework architecture, division of responsibilities between layers, and other best practices. Therefore, when using Ultron, we recommend the following approach:"}),"\n",(0,t.jsxs)(n.ol,{children:["\n",(0,t.jsxs)(n.li,{children:["Create a Page Object and specify screen UI elements as ",(0,t.jsx)(n.code,{children:"Matcher"})," objects."]}),"\n"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-kotlin",children:'object ChatPage : Page() {\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",(0,t.jsxs)(n.p,{children:["It's recommended to make all Page Objects as ",(0,t.jsx)(n.code,{children:"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",(0,t.jsxs)(n.ol,{start:"2",children:["\n",(0,t.jsx)(n.li,{children:"Describe user step methods in Page Object."}),"\n"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-kotlin",children:"object ChatPage : Page() {\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"})}),"\n",(0,t.jsxs)(n.p,{children:["Refer to the full code sample ",(0,t.jsx)(n.a,{href:"https://github.com/open-tool/ultron/blob/master/sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/ChatPage.kt",children:"ChatPage.class"})]}),"\n",(0,t.jsxs)(n.ol,{start:"3",children:["\n",(0,t.jsx)(n.li,{children:"Call user steps in test"}),"\n"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-kotlin",children:' @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'})}),"\n",(0,t.jsxs)(n.p,{children:["Refer to the full code sample ",(0,t.jsx)(n.a,{href:"https://github.com/open-tool/ultron/blob/master/sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso/DemoEspressoTest.kt",children:"DemoEspressoTest.class"})]}),"\n",(0,t.jsx)(n.p,{children:"In essence, your project's architecture will look like this:"}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.a,{href:"https://github.com/open-tool/ultron/assets/12834123/b0882d34-a18d-4f1f-959b-f75796d11036",children:"acrchitecture"})}),"\n",(0,t.jsx)(n.hr,{}),"\n",(0,t.jsx)(n.h2,{id:"allure-report",children:"Allure report"}),"\n",(0,t.jsx)(n.p,{children:"Ultron has built in support to generate artifacts for Allure reports. Just apply the recommended configuration and set testIntrumentationRunner."}),"\n",(0,t.jsxs)(n.p,{children:["For the complete guide, refer to the ",(0,t.jsx)(n.a,{href:"/ultron/docs/common/allure",children:"Allure description"})]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-kotlin",children:"@BeforeClass @JvmStatic\nfun setConfig() {\n UltronConfig.applyRecommended()\n UltronAllureConfig.applyRecommended()\n UltronComposeConfig.applyRecommended() \n}\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{src:"https://github.com/open-tool/ultron/assets/12834123/c05c813a-ece6-45e6-a04f-e1c92b82ffb1",alt:"allure"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{src:"https://github.com/open-tool/ultron/assets/12834123/1f751f3d-fc58-4874-a850-acd9181bfb70",alt:"allure compose"})})]})}function h(e={}){const{wrapper:n}={...(0,o.R)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(d,{...e})}):d(e)}},5859:(e,n,s)=>{s.d(n,{A:()=>t});const t=s.p+"assets/images/ultron_banner_dark-19d655c3ec0e489a21954509ebf83391.png"},443:(e,n,s)=>{s.d(n,{A:()=>t});const t=s.p+"assets/images/ultron_banner_light-5ce63312ccf79dcbe3e8a666f2f6c2ea.png"},8453:(e,n,s)=>{s.d(n,{R:()=>i,x:()=>a});var t=s(6540);const o={},r=t.createContext(o);function i(e){const n=t.useContext(r);return t.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:i(e.components),t.createElement(r.Provider,{value:n},e.children)}}}]);
\ No newline at end of file
diff --git a/assets/js/c377a04b.840e0f1d.js b/assets/js/c377a04b.840e0f1d.js
deleted file mode 100644
index 7acf403b..00000000
--- a/assets/js/c377a04b.840e0f1d.js
+++ /dev/null
@@ -1 +0,0 @@
-"use strict";(self.webpackChunkmy_website=self.webpackChunkmy_website||[]).push([[361],{8321:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>a,contentTitle:()=>r,default:()=>h,frontMatter:()=>i,metadata:()=>l,toc:()=>c});var s=t(4848),o=t(8453);const i={sidebar_position:1},r="Introduction",l={id:"index",title:"Introduction",description:"Docusaurus themed imageDocusaurus themed image",source:"@site/docs/index.md",sourceDirName:".",slug:"/",permalink:"/ultron/docs/",draft:!1,unlisted:!1,tags:[],version:"current",sidebarPosition:1,frontMatter:{sidebar_position:1},sidebar:"tutorialSidebar",next:{title:"Connect to project",permalink:"/ultron/docs/intro/connect"}},a={},c=[{value:"What are the benefits of using the framework?",id:"what-are-the-benefits-of-using-the-framework",level:2},{value:"A few words about syntax",id:"a-few-words-about-syntax",level:3},{value:"1. Simple compose operation (refer to the wiki here)",id:"1-simple-compose-operation-refer-to-the-wiki-here",level:4},{value:"2. Compose list operation (refer to the wiki here)",id:"2-compose-list-operation-refer-to-the-wiki-here",level:4},{value:"3. Simple Espresso assertion and action.",id:"3-simple-espresso-assertion-and-action",level:4},{value:"4. Action on RecyclerView list item",id:"4-action-on-recyclerview-list-item",level:4},{value:"5. Espresso WebView operations",id:"5-espresso-webview-operations",level:4},{value:"6. UI Automator operations",id:"6-ui-automator-operations",level:4},{value:"Acquiring the result of any operation as Boolean value",id:"acquiring-the-result-of-any-operation-as-boolean-value",level:3},{value:"Why are all Ultron actions and assertions more stable?",id:"why-are-all-ultron-actions-and-assertions-more-stable",level:3},{value:"3 steps to develop a test using Ultron",id:"3-steps-to-develop-a-test-using-ultron",level:2},{value:"Allure report",id:"allure-report",level:2}];function d(e){const n={a:"a",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",h4:"h4",hr:"hr",img:"img",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,o.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.h1,{id:"introduction",children:"Introduction"}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.img,{alt:"Docusaurus themed image",src:t(443).A+"#gh-light-mode-only",width:"12610",height:"2326"}),(0,s.jsx)(n.img,{alt:"Docusaurus themed image",src:t(5859).A+"#gh-dark-mode-only",width:"12610",height:"2326"})]}),"\n",(0,s.jsxs)(n.p,{children:["Ultron is the simplest framework to develop UI tests for ",(0,s.jsx)(n.strong,{children:"Android"})," & ",(0,s.jsx)(n.strong,{children:"Compose Multiplatform"}),"."]}),"\n",(0,s.jsx)(n.p,{children:"It's constructed upon the Espresso, UI Automator and Compose UI testing frameworks. Ultron introduces a range of remarkable new features. Furthermore, Ultron puts you in complete control of your tests!"}),"\n",(0,s.jsx)(n.p,{children:"You don't need to learn any new classes or special syntax. All magic actions and assertions are provided from crunch. Ultron can be easially customised and extended."}),"\n",(0,s.jsx)(n.h2,{id:"what-are-the-benefits-of-using-the-framework",children:"What are the benefits of using the framework?"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["Exceptional support for ",(0,s.jsx)(n.a,{href:"https://github.com/open-tool/ultron/wiki/Compose",children:(0,s.jsx)(n.strong,{children:"Compose"})})]}),"\n",(0,s.jsxs)(n.li,{children:["Out-of-the-box generation of ",(0,s.jsx)(n.a,{href:"https://github.com/open-tool/ultron/wiki/Allure",children:(0,s.jsx)(n.strong,{children:"Allure report"})})," (Now, for Android UI tests only)"]}),"\n",(0,s.jsx)(n.li,{children:"A straightforward and expressive syntax"}),"\n",(0,s.jsxs)(n.li,{children:["Ensured ",(0,s.jsx)(n.strong,{children:"Stability"})," for all actions and assertions"]}),"\n",(0,s.jsx)(n.li,{children:"Complete control over every action and assertion"}),"\n",(0,s.jsxs)(n.li,{children:["Incredible interaction with ",(0,s.jsx)(n.a,{href:"https://github.com/open-tool/ultron/wiki/RecyclerView",children:(0,s.jsx)(n.strong,{children:"RecyclerView"})})," and ",(0,s.jsx)(n.a,{href:"https://github.com/open-tool/ultron/wiki/Compose#ultron-compose-lazycolumnlazyrow",children:(0,s.jsx)(n.strong,{children:"Compose\xa0lists"})}),"."]}),"\n",(0,s.jsxs)(n.li,{children:["An ",(0,s.jsx)(n.strong,{children:"Architectural"})," approach to developing UI tests"]}),"\n",(0,s.jsx)(n.li,{children:"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",(0,s.jsx)(n.li,{children:(0,s.jsx)(n.a,{href:"https://github.com/open-tool/ultron/wiki/Ultron-Extension",children:"The ability to effortlessly extend the framework with your own operations"})}),"\n",(0,s.jsx)(n.li,{children:"Accelerated UI Automator operations"}),"\n",(0,s.jsxs)(n.li,{children:["Ability to monitor each stage of operation execution with ",(0,s.jsx)(n.a,{href:"https://github.com/open-tool/ultron/wiki/Listeners",children:"Listeners"})]}),"\n",(0,s.jsx)(n.li,{children:(0,s.jsx)(n.a,{href:"https://github.com/open-tool/ultron/wiki/Custom-operation-assertions",children:"Custom operation assertions"})}),"\n"]}),"\n",(0,s.jsx)(n.hr,{}),"\n",(0,s.jsx)(n.h3,{id:"a-few-words-about-syntax",children:"A few words about syntax"}),"\n",(0,s.jsxs)(n.p,{children:["The standard syntax provided by Google is intricate and not intuitive. This is especially evident when dealing with ",(0,s.jsx)(n.strong,{children:"LazyList"})," and ",(0,s.jsx)(n.strong,{children:"RecyclerView"})," interactions."]}),"\n",(0,s.jsx)(n.p,{children:"Let's explore some examples:"}),"\n",(0,s.jsxs)(n.h4,{id:"1-simple-compose-operation-refer-to-the-wiki-here",children:["1. Simple compose operation (refer to the wiki ",(0,s.jsx)(n.a,{href:"https://github.com/open-tool/ultron/wiki/Compose#ultron-compose",children:"here"}),")"]}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"Compose framework"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-kotlin",children:'composeTestRule.onNode(hasTestTag("Continue")).performClick()\ncomposeTestRule.onNodeWithText("Welcome").assertIsDisplayed()\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"Ultron"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-kotlin",children:'hasTestTag("Continue").click()\nhasText("Welcome").assertIsDisplayed()\n'})}),"\n",(0,s.jsxs)(n.h4,{id:"2-compose-list-operation-refer-to-the-wiki-here",children:["2. Compose list operation (refer to the wiki ",(0,s.jsx)(n.a,{href:"https://github.com/open-tool/ultron/wiki/Compose#ultron-compose-lazycolumnlazyrow",children:"here"}),")"]}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"Compose framework"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-kotlin",children:"val itemMatcher = hasText(contact.name)\ncomposeRule\n .onNodeWithTag(contactsListTestTag)\n .performScrollToNode(itemMatcher)\n .onChildren()\n .filterToOne(itemMatcher)\n .assertTextContains(contact.name)\n"})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"Ultron"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-kotlin",children:"composeList(hasTestTag(contactsListTestTag))\n .item(hasText(contact.name))\n .assertTextContains(contact.name)\n"})}),"\n",(0,s.jsx)(n.h4,{id:"3-simple-espresso-assertion-and-action",children:"3. Simple Espresso assertion and action."}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"Espresso"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-kotlin",children:"onView(withId(R.id.send_button)).check(isDisplayed()).perform(click())\n"})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"Ultron"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-kotlin",children:"withId(R.id.send_button).isDisplayed().click()\n"})}),"\n",(0,s.jsx)(n.p,{children:"This presents a cleaner approach. Ultron's operation names mirror Espresso's, while also providing additional operations."}),"\n",(0,s.jsxs)(n.p,{children:["Refer to the ",(0,s.jsx)(n.a,{href:"https://github.com/open-tool/ultron/wiki/Espresso-operations",children:"wiki"})," for further details."]}),"\n",(0,s.jsx)(n.h4,{id:"4-action-on-recyclerview-list-item",children:"4. Action on RecyclerView list item"}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"Espresso"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-kotlin",children:'onView(withId(R.id.recycler_friends))\n .perform(\n RecyclerViewActions\n .actionOnItem(\n hasDescendant(withText("Janice")),\n click()\n )\n )\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"Ultron"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-kotlin",children:'withRecyclerView(R.id.recycler_friends)\n .item(hasDescendant(withText("Janice")))\n .click()\n'})}),"\n",(0,s.jsxs)(n.p,{children:["Explore the ",(0,s.jsx)(n.a,{href:"https://github.com/open-tool/ultron/wiki/RecyclerView",children:"wiki"})," to unveil Ultron's magic with RecyclerView interactions."]}),"\n",(0,s.jsx)(n.h4,{id:"5-espresso-webview-operations",children:"5. Espresso WebView operations"}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"Espresso"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-kotlin",children:'onWebView()\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",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"Ultron"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-kotlin",children:'id("text_input").webKeys(newTitle)\nid("button1").webClick()\nid("title").hasText(newTitle)\n'})}),"\n",(0,s.jsxs)(n.p,{children:["Refer to the ",(0,s.jsx)(n.a,{href:"https://github.com/open-tool/ultron/wiki/WebView",children:"wiki"})," for more details."]}),"\n",(0,s.jsx)(n.h4,{id:"6-ui-automator-operations",children:"6. UI Automator operations"}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"UI Automator"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-kotlin",children:'val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())\ndevice\n .findObject(By.res("com.atiurin.sampleapp:id", "button1"))\n .click()\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"Ultron"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-kotlin",children:"byResId(R.id.button1).click() \n"})}),"\n",(0,s.jsxs)(n.p,{children:["Refer to the ",(0,s.jsx)(n.a,{href:"https://github.com/open-tool/ultron/wiki/UI-Automator-operation",children:"wiki"})]}),"\n",(0,s.jsx)(n.hr,{}),"\n",(0,s.jsx)(n.h3,{id:"acquiring-the-result-of-any-operation-as-boolean-value",children:"Acquiring the result of any operation as Boolean value"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-kotlin",children:"val isButtonDisplayed = withId(R.id.button).isSuccess { isDisplayed() }\nif (isButtonDisplayed) {\n //do some reasonable actions\n}\n"})}),"\n",(0,s.jsx)(n.hr,{}),"\n",(0,s.jsx)(n.h3,{id:"why-are-all-ultron-actions-and-assertions-more-stable",children:"Why are all Ultron actions and assertions more stable?"}),"\n",(0,s.jsx)(n.p,{children:"The framework captures a list of specified exceptions and attempts to repeat the operation during a timeout period (default is 5 seconds). Of course, you have the ability to customize the list of handled exceptions. You can also set a custom timeout for any operation."}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-kotlin",children:'withId(R.id.result).withTimeout(10_000).hasText("Passed")\n'})}),"\n",(0,s.jsx)(n.hr,{}),"\n",(0,s.jsx)(n.h2,{id:"3-steps-to-develop-a-test-using-ultron",children:"3 steps to develop a test using Ultron"}),"\n",(0,s.jsx)(n.p,{children:"We advocate for a proper test framework architecture, division of responsibilities between layers, and other best practices. Therefore, when using Ultron, we recommend the following approach:"}),"\n",(0,s.jsxs)(n.ol,{children:["\n",(0,s.jsxs)(n.li,{children:["Create a Page Object and specify screen UI elements as ",(0,s.jsx)(n.code,{children:"Matcher"})," objects."]}),"\n"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-kotlin",children:'object ChatPage : Page() {\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",(0,s.jsxs)(n.p,{children:["It's recommended to make all Page Objects as ",(0,s.jsx)(n.code,{children:"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",(0,s.jsxs)(n.ol,{start:"2",children:["\n",(0,s.jsx)(n.li,{children:"Describe user step methods in Page Object."}),"\n"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-kotlin",children:"object ChatPage : Page() {\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"})}),"\n",(0,s.jsxs)(n.p,{children:["Refer to the full code sample ",(0,s.jsx)(n.a,{href:"https://github.com/open-tool/ultron/blob/master/sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/ChatPage.kt",children:"ChatPage.class"})]}),"\n",(0,s.jsxs)(n.ol,{start:"3",children:["\n",(0,s.jsx)(n.li,{children:"Call user steps in test"}),"\n"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-kotlin",children:' @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'})}),"\n",(0,s.jsxs)(n.p,{children:["Refer to the full code sample ",(0,s.jsx)(n.a,{href:"https://github.com/open-tool/ultron/blob/master/sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/espresso/DemoEspressoTest.kt",children:"DemoEspressoTest.class"})]}),"\n",(0,s.jsx)(n.p,{children:"In essence, your project's architecture will look like this:"}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.a,{href:"https://github.com/open-tool/ultron/assets/12834123/b0882d34-a18d-4f1f-959b-f75796d11036",children:"acrchitecture"})}),"\n",(0,s.jsx)(n.hr,{}),"\n",(0,s.jsx)(n.h2,{id:"allure-report",children:"Allure report"}),"\n",(0,s.jsx)(n.p,{children:"Ultron has built in support to generate artifacts for Allure reports. Just apply the recommended configuration and set testIntrumentationRunner."}),"\n",(0,s.jsxs)(n.p,{children:["For the complete guide, refer to the ",(0,s.jsx)(n.a,{href:"/ultron/docs/common/allure",children:"Allure description"})]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-kotlin",children:"@BeforeClass @JvmStatic\nfun setConfig() {\n UltronConfig.applyRecommended()\n UltronAllureConfig.applyRecommended()\n UltronComposeConfig.applyRecommended() \n}\n"})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{src:"https://github.com/open-tool/ultron/assets/12834123/c05c813a-ece6-45e6-a04f-e1c92b82ffb1",alt:"allure"})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{src:"https://github.com/open-tool/ultron/assets/12834123/1f751f3d-fc58-4874-a850-acd9181bfb70",alt:"allure compose"})})]})}function h(e={}){const{wrapper:n}={...(0,o.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(d,{...e})}):d(e)}},5859:(e,n,t)=>{t.d(n,{A:()=>s});const s=t.p+"assets/images/ultron_banner_dark-19d655c3ec0e489a21954509ebf83391.png"},443:(e,n,t)=>{t.d(n,{A:()=>s});const s=t.p+"assets/images/ultron_banner_light-5ce63312ccf79dcbe3e8a666f2f6c2ea.png"},8453:(e,n,t)=>{t.d(n,{R:()=>r,x:()=>l});var s=t(6540);const o={},i=s.createContext(o);function r(e){const n=s.useContext(i);return s.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:r(e.components),s.createElement(i.Provider,{value:n},e.children)}}}]);
\ No newline at end of file
diff --git a/assets/js/runtime~main.b2189802.js b/assets/js/runtime~main.40cf50e0.js
similarity index 95%
rename from assets/js/runtime~main.b2189802.js
rename to assets/js/runtime~main.40cf50e0.js
index e807986e..27ba76a7 100644
--- a/assets/js/runtime~main.b2189802.js
+++ b/assets/js/runtime~main.40cf50e0.js
@@ -1 +1 @@
-(()=>{"use strict";var e,a,t,r,o,n={},c={};function f(e){var a=c[e];if(void 0!==a)return a.exports;var t=c[e]={id:e,loaded:!1,exports:{}};return n[e].call(t.exports,t,t.exports,f),t.loaded=!0,t.exports}f.m=n,f.c=c,e=[],f.O=(a,t,r,o)=>{if(!t){var n=1/0;for(d=0;d=o)&&Object.keys(f.O).every((e=>f.O[e](t[b])))?t.splice(b--,1):(c=!1,o0&&e[d-1][2]>o;d--)e[d]=e[d-1];e[d]=[t,r,o]},f.n=e=>{var a=e&&e.__esModule?()=>e.default:()=>e;return f.d(a,{a:a}),a},t=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,f.t=function(e,r){if(1&r&&(e=this(e)),8&r)return e;if("object"==typeof e&&e){if(4&r&&e.__esModule)return e;if(16&r&&"function"==typeof e.then)return e}var o=Object.create(null);f.r(o);var n={};a=a||[null,t({}),t([]),t(t)];for(var c=2&r&&e;"object"==typeof c&&!~a.indexOf(c);c=t(c))Object.getOwnPropertyNames(c).forEach((a=>n[a]=()=>e[a]));return n.default=()=>e,f.d(o,n),o},f.d=(e,a)=>{for(var t in a)f.o(a,t)&&!f.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:a[t]})},f.f={},f.e=e=>Promise.all(Object.keys(f.f).reduce(((a,t)=>(f.f[t](e,a),a)),[])),f.u=e=>"assets/js/"+({21:"88818515",48:"a94703ab",61:"1f391b9e",92:"c0ee5955",98:"a7bd4aaa",134:"393be207",138:"1a4e3797",178:"5d6ab7e3",190:"2658ced2",217:"a77aa84e",231:"420cca70",235:"a7456010",338:"1e721490",361:"c377a04b",373:"81f2d5b0",374:"98c49a89",391:"af741876",401:"17896441",460:"4b05c0af",501:"8f1ada2a",583:"1df93b7f",646:"c5572234",647:"5e95c892",693:"9973bec8",732:"16ce6071",742:"aba21aa0",801:"6e76835b",957:"c141421f",991:"259e32a1"}[e]||e)+"."+{21:"81363da0",48:"85d946e2",61:"138a803f",92:"41a959fc",98:"32fcb869",134:"a522be53",138:"9c6a3875",178:"6f515de3",190:"0350895c",217:"ac4f5317",231:"9f6c9d67",235:"d27e4924",237:"4e0d99f9",338:"a241dfb2",361:"840e0f1d",373:"994f54b7",374:"247f6978",391:"2ed2da0f",401:"fcfc2dc8",416:"b3671cb8",460:"5bfdbbcd",462:"7e5401b7",490:"e5b5b59a",501:"ec279b40",583:"845f21e3",646:"8de64269",647:"9e810129",693:"8965b56e",732:"af810646",742:"bddde0da",801:"54dec303",913:"b23e39c0",957:"a8bd9081",991:"94d23305"}[e]+".js",f.miniCssF=e=>{},f.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),f.o=(e,a)=>Object.prototype.hasOwnProperty.call(e,a),r={},o="my-website:",f.l=(e,a,t,n)=>{if(r[e])r[e].push(a);else{var c,b;if(void 0!==t)for(var i=document.getElementsByTagName("script"),d=0;d{c.onerror=c.onload=null,clearTimeout(s);var o=r[e];if(delete r[e],c.parentNode&&c.parentNode.removeChild(c),o&&o.forEach((e=>e(t))),a)return a(t)},s=setTimeout(l.bind(null,void 0,{type:"timeout",target:c}),12e4);c.onerror=l.bind(null,c.onerror),c.onload=l.bind(null,c.onload),b&&document.head.appendChild(c)}},f.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},f.p="/ultron/",f.gca=function(e){return e={17896441:"401",88818515:"21",a94703ab:"48","1f391b9e":"61",c0ee5955:"92",a7bd4aaa:"98","393be207":"134","1a4e3797":"138","5d6ab7e3":"178","2658ced2":"190",a77aa84e:"217","420cca70":"231",a7456010:"235","1e721490":"338",c377a04b:"361","81f2d5b0":"373","98c49a89":"374",af741876:"391","4b05c0af":"460","8f1ada2a":"501","1df93b7f":"583",c5572234:"646","5e95c892":"647","9973bec8":"693","16ce6071":"732",aba21aa0:"742","6e76835b":"801",c141421f:"957","259e32a1":"991"}[e]||e,f.p+f.u(e)},(()=>{var e={354:0,869:0};f.f.j=(a,t)=>{var r=f.o(e,a)?e[a]:void 0;if(0!==r)if(r)t.push(r[2]);else if(/^(354|869)$/.test(a))e[a]=0;else{var o=new Promise(((t,o)=>r=e[a]=[t,o]));t.push(r[2]=o);var n=f.p+f.u(a),c=new Error;f.l(n,(t=>{if(f.o(e,a)&&(0!==(r=e[a])&&(e[a]=void 0),r)){var o=t&&("load"===t.type?"missing":t.type),n=t&&t.target&&t.target.src;c.message="Loading chunk "+a+" failed.\n("+o+": "+n+")",c.name="ChunkLoadError",c.type=o,c.request=n,r[1](c)}}),"chunk-"+a,a)}},f.O.j=a=>0===e[a];var a=(a,t)=>{var r,o,n=t[0],c=t[1],b=t[2],i=0;if(n.some((a=>0!==e[a]))){for(r in c)f.o(c,r)&&(f.m[r]=c[r]);if(b)var d=b(f)}for(a&&a(t);i{"use strict";var e,a,t,r,o,n={},c={};function f(e){var a=c[e];if(void 0!==a)return a.exports;var t=c[e]={id:e,loaded:!1,exports:{}};return n[e].call(t.exports,t,t.exports,f),t.loaded=!0,t.exports}f.m=n,f.c=c,e=[],f.O=(a,t,r,o)=>{if(!t){var n=1/0;for(d=0;d=o)&&Object.keys(f.O).every((e=>f.O[e](t[b])))?t.splice(b--,1):(c=!1,o0&&e[d-1][2]>o;d--)e[d]=e[d-1];e[d]=[t,r,o]},f.n=e=>{var a=e&&e.__esModule?()=>e.default:()=>e;return f.d(a,{a:a}),a},t=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,f.t=function(e,r){if(1&r&&(e=this(e)),8&r)return e;if("object"==typeof e&&e){if(4&r&&e.__esModule)return e;if(16&r&&"function"==typeof e.then)return e}var o=Object.create(null);f.r(o);var n={};a=a||[null,t({}),t([]),t(t)];for(var c=2&r&&e;"object"==typeof c&&!~a.indexOf(c);c=t(c))Object.getOwnPropertyNames(c).forEach((a=>n[a]=()=>e[a]));return n.default=()=>e,f.d(o,n),o},f.d=(e,a)=>{for(var t in a)f.o(a,t)&&!f.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:a[t]})},f.f={},f.e=e=>Promise.all(Object.keys(f.f).reduce(((a,t)=>(f.f[t](e,a),a)),[])),f.u=e=>"assets/js/"+({21:"88818515",48:"a94703ab",61:"1f391b9e",92:"c0ee5955",98:"a7bd4aaa",134:"393be207",138:"1a4e3797",178:"5d6ab7e3",190:"2658ced2",217:"a77aa84e",231:"420cca70",235:"a7456010",338:"1e721490",361:"c377a04b",373:"81f2d5b0",374:"98c49a89",391:"af741876",401:"17896441",460:"4b05c0af",501:"8f1ada2a",583:"1df93b7f",646:"c5572234",647:"5e95c892",693:"9973bec8",732:"16ce6071",742:"aba21aa0",801:"6e76835b",957:"c141421f",991:"259e32a1"}[e]||e)+"."+{21:"81363da0",48:"85d946e2",61:"138a803f",92:"41a959fc",98:"32fcb869",134:"a522be53",138:"9c6a3875",178:"6f515de3",190:"0350895c",217:"ac4f5317",231:"9f6c9d67",235:"d27e4924",237:"4e0d99f9",338:"4f7d5cce",361:"30833c89",373:"994f54b7",374:"247f6978",391:"2ed2da0f",401:"fcfc2dc8",416:"b3671cb8",460:"11b15f11",462:"7e5401b7",490:"e5b5b59a",501:"ec279b40",583:"845f21e3",646:"8de64269",647:"9e810129",693:"8965b56e",732:"af810646",742:"bddde0da",801:"54dec303",913:"b23e39c0",957:"a8bd9081",991:"94d23305"}[e]+".js",f.miniCssF=e=>{},f.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),f.o=(e,a)=>Object.prototype.hasOwnProperty.call(e,a),r={},o="my-website:",f.l=(e,a,t,n)=>{if(r[e])r[e].push(a);else{var c,b;if(void 0!==t)for(var i=document.getElementsByTagName("script"),d=0;d{c.onerror=c.onload=null,clearTimeout(s);var o=r[e];if(delete r[e],c.parentNode&&c.parentNode.removeChild(c),o&&o.forEach((e=>e(t))),a)return a(t)},s=setTimeout(l.bind(null,void 0,{type:"timeout",target:c}),12e4);c.onerror=l.bind(null,c.onerror),c.onload=l.bind(null,c.onload),b&&document.head.appendChild(c)}},f.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},f.p="/ultron/",f.gca=function(e){return e={17896441:"401",88818515:"21",a94703ab:"48","1f391b9e":"61",c0ee5955:"92",a7bd4aaa:"98","393be207":"134","1a4e3797":"138","5d6ab7e3":"178","2658ced2":"190",a77aa84e:"217","420cca70":"231",a7456010:"235","1e721490":"338",c377a04b:"361","81f2d5b0":"373","98c49a89":"374",af741876:"391","4b05c0af":"460","8f1ada2a":"501","1df93b7f":"583",c5572234:"646","5e95c892":"647","9973bec8":"693","16ce6071":"732",aba21aa0:"742","6e76835b":"801",c141421f:"957","259e32a1":"991"}[e]||e,f.p+f.u(e)},(()=>{var e={354:0,869:0};f.f.j=(a,t)=>{var r=f.o(e,a)?e[a]:void 0;if(0!==r)if(r)t.push(r[2]);else if(/^(354|869)$/.test(a))e[a]=0;else{var o=new Promise(((t,o)=>r=e[a]=[t,o]));t.push(r[2]=o);var n=f.p+f.u(a),c=new Error;f.l(n,(t=>{if(f.o(e,a)&&(0!==(r=e[a])&&(e[a]=void 0),r)){var o=t&&("load"===t.type?"missing":t.type),n=t&&t.target&&t.target.src;c.message="Loading chunk "+a+" failed.\n("+o+": "+n+")",c.name="ChunkLoadError",c.type=o,c.request=n,r[1](c)}}),"chunk-"+a,a)}},f.O.j=a=>0===e[a];var a=(a,t)=>{var r,o,n=t[0],c=t[1],b=t[2],i=0;if(n.some((a=>0!==e[a]))){for(r in c)f.o(c,r)&&(f.m[r]=c[r]);if(b)var d=b(f)}for(a&&a(t);iEspresso | Ultron
-
+
diff --git a/docs/android/recyclerview/index.html b/docs/android/recyclerview/index.html
index 35646aca..d5b62393 100644
--- a/docs/android/recyclerview/index.html
+++ b/docs/android/recyclerview/index.html
@@ -4,7 +4,7 @@
RecyclerView | Ultron
-
+
diff --git a/docs/android/rootview/index.html b/docs/android/rootview/index.html
index f8352a43..86e143d5 100644
--- a/docs/android/rootview/index.html
+++ b/docs/android/rootview/index.html
@@ -4,7 +4,7 @@
withSuitableRoot | Ultron
-
+
diff --git a/docs/android/uiautomator/index.html b/docs/android/uiautomator/index.html
index 19d14330..2b5752c9 100644
--- a/docs/android/uiautomator/index.html
+++ b/docs/android/uiautomator/index.html
@@ -4,7 +4,7 @@
UI Automator | Ultron
-
+
diff --git a/docs/android/webview/index.html b/docs/android/webview/index.html
index eb66fecd..260db606 100644
--- a/docs/android/webview/index.html
+++ b/docs/android/webview/index.html
@@ -4,7 +4,7 @@
WebView | Ultron
-
+
diff --git a/docs/common/allure/index.html b/docs/common/allure/index.html
index 7fc998ec..b0822cc7 100644
--- a/docs/common/allure/index.html
+++ b/docs/common/allure/index.html
@@ -4,7 +4,7 @@
Allure | Ultron
-
+
@@ -62,6 +62,6 @@