diff --git a/404.html b/404.html index 0ff25d6f..73d88552 100644 --- a/404.html +++ b/404.html @@ -4,7 +4,7 @@ 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);i Espresso | 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 @@

Man

UltronRunListener which is inherited from RunListener. This type can be used to add artifact in different test lifecycle state. Sample - WindowHierarchyAttachRunListener.kt

-

Refer to the Listeners wiki page for details.

+

Refer to the Listeners doc page for details.

\ No newline at end of file diff --git a/docs/common/extension/index.html b/docs/common/extension/index.html index a8134d3f..8d7c62ff 100644 --- a/docs/common/extension/index.html +++ b/docs/common/extension/index.html @@ -4,7 +4,7 @@ Ultron Extension | Ultron - + diff --git a/docs/common/listeners/index.html b/docs/common/listeners/index.html index e43aad1c..34aba07c 100644 --- a/docs/common/listeners/index.html +++ b/docs/common/listeners/index.html @@ -4,7 +4,7 @@ Listeners | Ultron - + diff --git a/docs/compose/android/index.html b/docs/compose/android/index.html index fff6966b..f757bfd4 100644 --- a/docs/compose/android/index.html +++ b/docs/compose/android/index.html @@ -4,7 +4,7 @@ Android | Ultron - + diff --git a/docs/compose/api/index.html b/docs/compose/api/index.html index f7161c70..318c14c4 100644 --- a/docs/compose/api/index.html +++ b/docs/compose/api/index.html @@ -4,7 +4,7 @@ Ultron Compose API | Ultron - + diff --git a/docs/compose/index.html b/docs/compose/index.html index 32713871..9db982a8 100644 --- a/docs/compose/index.html +++ b/docs/compose/index.html @@ -4,7 +4,7 @@ Compose | Ultron - + diff --git a/docs/compose/lazylist/index.html b/docs/compose/lazylist/index.html index cb5ab67f..18bd7ba4 100644 --- a/docs/compose/lazylist/index.html +++ b/docs/compose/lazylist/index.html @@ -4,7 +4,7 @@ LazyList | Ultron - + @@ -59,7 +59,7 @@

Best pract

Use getContactItem in Page steps like assertContactStatus

object ComposeListPage : Page<ComposeListPage>() {
private fun getContactItem(contact: Contact): ComposeFriendListItem = lazyList.getItem(hasTestTag(contact.id))
...
fun assertContactStatus(contact: Contact) = apply {
getContactItem(contact).status.assertTextEquals(contact.status)
}
}

UltronComposeListItem API

-

It's pretty much the same as simple node api, but extends it mostly for internal features.

+

It's pretty much the same as simple node api, but extends it mostly for internal features.


Efficient Strategies for Locating Items in Compose LazyList

Let's start with approaches that you can use without additional efforts. For example, you have identified LazyList in your tests code like

diff --git a/docs/compose/multiplatform/index.html b/docs/compose/multiplatform/index.html index 5e8570c8..df57628f 100644 --- a/docs/compose/multiplatform/index.html +++ b/docs/compose/multiplatform/index.html @@ -4,7 +4,7 @@ Multiplatform | Ultron - + diff --git a/docs/index.html b/docs/index.html index 9762b2f4..10903d86 100644 --- a/docs/index.html +++ b/docs/index.html @@ -4,7 +4,7 @@ Introduction | Ultron - + @@ -15,29 +15,29 @@

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

What are the benefits of using the framework?


A few words about syntax

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

Let's explore some examples:

-

1. Simple compose operation (refer to the wiki here)

+

1. Simple compose operation (refer to the doc here)

Compose framework

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

Ultron

hasTestTag("Continue").click()
hasText("Welcome").assertIsDisplayed()
-

2. Compose list operation (refer to the wiki here)

+

2. Compose list operation (refer to the doc)

Compose framework

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

Ultron

@@ -48,25 +48,25 @@

withId(R.id.send_button).isDisplayed().click()

This presents a cleaner approach. Ultron's operation names mirror Espresso's, while also providing additional operations.

-

Refer to the wiki for further details.

+

Refer to the doc for further details.

4. Action on RecyclerView list item

Espresso

onView(withId(R.id.recycler_friends))
.perform(
RecyclerViewActions
.actionOnItem<RecyclerView.ViewHolder>(
hasDescendant(withText("Janice")),
click()
)
)

Ultron

withRecyclerView(R.id.recycler_friends)
.item(hasDescendant(withText("Janice")))
.click()
-

Explore the wiki to unveil Ultron's magic with RecyclerView interactions.

+

Explore the doc to unveil Ultron's magic with RecyclerView interactions.

5. Espresso WebView operations

Espresso

onWebView()
.withElement(findElement(Locator.ID, "text_input"))
.perform(webKeys(newTitle))
.withElement(findElement(Locator.ID, "button1"))
.perform(webClick())
.withElement(findElement(Locator.ID, "title"))
.check(webMatches(getText(), containsString(newTitle)))

Ultron

id("text_input").webKeys(newTitle)
id("button1").webClick()
id("title").hasText(newTitle)
-

Refer to the wiki for more details.

+

Refer to the doc for more details.

6. UI Automator operations

UI Automator

val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
device
.findObject(By.res("com.atiurin.sampleapp:id", "button1"))
.click()

Ultron

byResId(R.id.button1).click() 
-

Refer to the wiki

+

Refer to the doc


Acquiring the result of any operation as Boolean value

val isButtonDisplayed = withId(R.id.button).isSuccess { isDisplayed() }
if (isButtonDisplayed) {
//do some reasonable actions
}
diff --git a/docs/intro/configuration/index.html b/docs/intro/configuration/index.html index d657a23f..0073c53a 100644 --- a/docs/intro/configuration/index.html +++ b/docs/intro/configuration/index.html @@ -4,7 +4,7 @@ Configuration | Ultron - + diff --git a/docs/intro/connect/index.html b/docs/intro/connect/index.html index 4295fdb3..d1893247 100644 --- a/docs/intro/connect/index.html +++ b/docs/intro/connect/index.html @@ -4,7 +4,7 @@ Connect to project | Ultron - + diff --git a/docs/intro/dependencies/index.html b/docs/intro/dependencies/index.html index 7041245e..5103e473 100644 --- a/docs/intro/dependencies/index.html +++ b/docs/intro/dependencies/index.html @@ -4,7 +4,7 @@ Dependencies Management | Ultron - + diff --git a/index.html b/index.html index 548f9ed7..dd0a179d 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ Home | Ultron - + diff --git a/markdown-page/index.html b/markdown-page/index.html index 2923ceb9..05e5bb06 100644 --- a/markdown-page/index.html +++ b/markdown-page/index.html @@ -4,7 +4,7 @@ Markdown page example | Ultron - + diff --git a/search/index.html b/search/index.html index 25fdc3b9..16602991 100644 --- a/search/index.html +++ b/search/index.html @@ -4,7 +4,7 @@ Search the documentation | Ultron - +