{"id":15132421,"url":"https://github.com/KakaoCup/Compose","last_synced_at":"2025-09-29T00:32:02.972Z","repository":{"id":41834660,"uuid":"403825613","full_name":"KakaoCup/Compose","owner":"KakaoCup","description":"Nice and simple DSL for Espresso Compose UI testing in Kotlin","archived":false,"fork":false,"pushed_at":"2025-03-12T04:08:39.000Z","size":6313,"stargazers_count":154,"open_issues_count":12,"forks_count":15,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-03-12T04:18:45.634Z","etag":null,"topics":["android","android-testing","compose","dsl","espresso","hacktoberfest","kotlin","testing-framework","testing-library","ui-testing"],"latest_commit_sha":null,"homepage":"https://kakaocup.github.io/Compose/","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/KakaoCup.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":"vacxe"}},"created_at":"2021-09-07T03:15:48.000Z","updated_at":"2025-03-12T03:33:49.000Z","dependencies_parsed_at":"2023-02-16T06:15:36.160Z","dependency_job_id":"7a87db76-3e22-4db3-a49e-e33840cc5622","html_url":"https://github.com/KakaoCup/Compose","commit_stats":null,"previous_names":[],"tags_count":24,"template":false,"template_full_name":null,"purl":"pkg:github/KakaoCup/Compose","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KakaoCup%2FCompose","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KakaoCup%2FCompose/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KakaoCup%2FCompose/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KakaoCup%2FCompose/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/KakaoCup","download_url":"https://codeload.github.com/KakaoCup/Compose/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KakaoCup%2FCompose/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":277450938,"owners_count":25819971,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-09-28T02:00:08.834Z","response_time":79,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android","android-testing","compose","dsl","espresso","hacktoberfest","kotlin","testing-framework","testing-library","ui-testing"],"created_at":"2024-09-26T04:04:44.988Z","updated_at":"2025-09-29T00:32:02.965Z","avatar_url":"https://github.com/KakaoCup.png","language":"Kotlin","funding_links":["https://github.com/sponsors/vacxe"],"categories":["Articles"],"sub_categories":["Testings"],"readme":"# Kakao Compose\n[![Kotlin version badge](https://img.shields.io/badge/kotlin-2.0.0-blue.svg)](https://kotlinlang.org/)\n[![Telegram](https://img.shields.io/static/v1?label=Telegram\u0026message=RU\u0026color=0088CC)](https://t.me/kaspresso)\n[![Telegram](https://img.shields.io/static/v1?label=Telegram\u0026message=EN\u0026color=0088CC)](https://t.me/kaspresso_en)\n[![Documentation](https://img.shields.io/static/v1?label=Documentation\u0026message=EN\u0026color=008000)](https://kakaocup.github.io/Compose/docs/introduction)\n\nNice and simple DSL for Espresso Compose in Kotlin\n\n![coco](https://user-images.githubusercontent.com/2812510/30947310-3825724c-a433-11e7-8a0d-3c3bfe00d584.png)\n\n#### Benefits\n- Readability\n- Reusability\n- Extensible DSL\n- Interceptors\n\n### Concept\nThe one of the main concepts of Jetpack Compose is a presentation of UI according to UI tree (UI hierarchy) approach with a parent-children relationships support.\nIt means that the related UI test library has to support a parent-children relationships for Nodes by default.\nIt will be discovered below how Kakao Compose library supports mentioned parent-children relationships approach.\n\n### How to use it\n#### Create Screen\nCreate your entity `ComposeScreen` where you will add the views involved in the interactions of the tests:\n```Kotlin\nclass MainActivityScreen(semanticsProvider: SemanticsNodeInteractionsProvider) :\n    ComposeScreen\u003cMainActivityScreen\u003e(\n        semanticsProvider = semanticsProvider\n    )\n```\n`ComposeScreen` can represent the whole user interface or a portion of UI.\nIf you are using [Page Object pattern](https://martinfowler.com/bliki/PageObject.html) you can put the interactions of Kakao inside the Page Objects.\n\nDescribed way of Screen definition is very similar with the way that Kakao library offers.\nBut, usually, `Screen` in Jetpack Compose is a UI element (Node) too. That's why there is an additional option to declare `ComposeScreen`:\n```Kotlin\nclass MainActivityScreen(semanticsProvider: SemanticsNodeInteractionsProvider) :\n    ComposeScreen\u003cMainActivityScreen\u003e(\n        semanticsProvider = semanticsProvider,\n        viewBuilderAction = { hasTestTag(\"MainScreen\") }\n    )\n```\nSo, `ComposeScreen` is a `BaseNode`'s inheritor in Kakao-Compose library. And, as you've seen above, there is a possibility to describe\n`ComposeScreen` without mandatory `viewBuilderAction` in cases when Screen is an abstraction without clear connection with any Node.\n\n#### Create KNode\n`ComposeScreen` contains `KNode`, these are the Jetpack Compose nodes where you want to do the interactions:\n```Kotlin\nclass MainActivityScreen(semanticsProvider: SemanticsNodeInteractionsProvider) :\n    ComposeScreen\u003cMainActivityScreen\u003e(\n        semanticsProvider = semanticsProvider,\n        viewBuilderAction = { hasTestTag(\"MainScreen\") }\n    ) {\n\n    val myButton: KNode = child {\n        hasTestTag(\"myTestButton\")\n    }\n}\n```\n\n`myButton` was declared as a child of `MainActivityScreen`.\nIt means that `myButton` will be calculated using matchers specified in a lambda explicitly and a parent matcher implicitly (`MainActivityScreen`).\nUnder the hood, the `SemanticMatcher` of `myButton` is equal to `hasTestTag(\"myTestButton\") + hasParent(MainActivityScreen.matcher)`.\n\nAlso, `KNode` can be declared as a child of another `KNode`:\n```Kotlin\nclass MainActivityScreen(semanticsProvider: SemanticsNodeInteractionsProvider) :\n    ComposeScreen\u003cMainActivityScreen\u003e(\n        semanticsProvider = semanticsProvider,\n        viewBuilderAction = { hasTestTag(\"MainScreen\") }\n    ) {\n\n    val myButton: KNode = child {\n        hasTestTag(\"myTestButton\")\n    }\n\n    val myButton2: KNode = myButton.child {\n        hasTestTag(\"myTestButton2\")\n    }\n}\n```\n`myButton2` will be calculated with the following\n`SemanticMatcher = hasTestTag(\"myTestButton\") + hasParent(myButton.matcher) + hasParent(MainActivityScreen.matcher)`.\nBut, we advise not to abuse inheritance and use only the following chain: \"ComposeScreen\" - \"Element of ComposeScreen\".\n\nThe last, `KNode` can be declared without `child` function using only explicit matchers:\n```Kotlin\nclass MainActivityScreen(semanticsProvider: SemanticsNodeInteractionsProvider) :\n    ComposeScreen\u003cMainActivityScreen\u003e(\n        semanticsProvider = semanticsProvider,\n        viewBuilderAction = { hasTestTag(\"MainScreen\") }\n    ) {\n\n    val myButton = KNode(this) {\n        hasTestTag(\"myTestButton\")\n    }\n}\n```\n\nEvery `KNode` contains many matches. Some examples of matchers provided by Kakao Compose:\n\n* `hasText`\n* `hasTestTag`\n* \u003cb\u003eand more\u003c/b\u003e\n\nLike in Espresso you can combine different matchers:\n```Kotlin\nval myButton = KNode(this) {\n    hasTestTag(\"myTestButton\")\n    hasText(\"Button 1\")\n}\n```\n\n#### Write the interaction.\n\nThe syntax of the test with Kakao is very easy, once you have the `ComposeScreen` and the `KNode` defined, you only have to apply\nthe actions or assertions like in Espresso:\n```Kotlin\nclass ExampleInstrumentedTest {\n    @Rule\n    @JvmField\n    val composeTestRule = createAndroidComposeRule\u003cMainActivity\u003e()\n\n    @Test\n    fun simpleTest() {\n        onComposeScreen\u003cMainActivityScreen\u003e(composeTestRule) {\n            myButton {\n                assertIsDisplayed()\n                assertTextContains(\"Button 1\")\n            }\n\n            onNode {\n                hasTestTag(\"doesNotExist\")\n            }.invoke {\n                assertDoesNotExist()\n            }\n        }\n    }\n}\n```\n\n#### Lazy lists testing\n\n:warning: This API is experimental and might change in future!\n\nTo test lazy lists such as `LazyRow` or `LazyColumn` you should add `KLazyListNode` into your `ComposeScreen`:\n```Kotlin\nval list = KLazyListNode(\n    semanticsProvider = semanticsProvider,\n    viewBuilderAction = { hasTestTag(\"LazyList\") },\n    itemTypeBuilder = {\n        itemType(::LazyListItemNode)\n        itemType(::LazyListHeaderNode)\n    },\n    positionMatcher = { position -\u003e SemanticsMatcher.expectValue(LazyListItemPosition, position) }\n)\n```\n\nInside `itemTypeBuilder` function you should register `KLazyListItemNode` types to differentiate elements in lazy list:\n```kotlin\nclass LazyListItemNode(\n    semanticsNode: SemanticsNode,\n    semanticsProvider: SemanticsNodeInteractionsProvider,\n) : KLazyListItemNode\u003cLazyListItemNode\u003e(semanticsNode, semanticsProvider)\n\nclass LazyListHeaderNode(\n    semanticsNode: SemanticsNode,\n    semanticsProvider: SemanticsNodeInteractionsProvider,\n) : KLazyListItemNode\u003cLazyListHeaderNode\u003e(semanticsNode, semanticsProvider) {\n    val title: KNode = child {\n        hasTestTag(\"LazyListHeaderTitle\")\n    }\n}\n```\n\nThe element position might be changed during the scroll due to lazy list construction, that’s why we should provide `positionMatcher` to determine the element position correctly. It could be achieved in different ways, for example you can determine item position through `TestTag`:\n```kotlin\nLazyColumn(\n    Modifier\n        .fillMaxSize()\n        .testTag(\"LazyList\")\n) {\n    itemsIndexed(items) { index, item -\u003e\n        when (item) {\n            is LazyListItem.Header -\u003e ListItemHeader(item, Modifier.testTag(\"position=$index\"))\n            is LazyListItem.Item -\u003e ListItemCell(item, Modifier.testTag(\"position=$index\"))\n        }\n    }\n}\n```\n\nAnd then check this position inside `positionMatcher` lambda:\n```kotlin\npositionMatcher = { position -\u003e hasTestTag(\"position=$position\") }\n```\n\nBut it will be more convenient and less error prone to create custom semantics property and custom modifier:\n```kotlin\nval LazyListItemPosition = SemanticsPropertyKey\u003cInt\u003e(\"LazyListItemPosition\")\nvar SemanticsPropertyReceiver.lazyListItemPosition by LazyListItemPosition\n\nfun Modifier.lazyListItemPosition(position: Int): Modifier {\n    return semantics { lazyListItemPosition = position }\n}\n```\n\nAnd check an item position with `SemanticsMatcher`:\n```kotlin\npositionMatcher = { position -\u003e SemanticsMatcher.expectValue(LazyListItemPosition, position) }\n```\n\nSo the typical lazy list test may look like this:\n```kotlin\n @Test\nfun lazyListTest() {\n    onComposeScreen\u003cLazyListScreen\u003e(composeTestRule) {\n        list {\n            firstChild\u003cLazyListHeaderNode\u003e {\n                title.assertTextEquals(\"Items from 1 to 10\")\n            }\n            childWith\u003cLazyListItemNode\u003e {\n                hasText(\"Item 1\")\n            } perform {\n                assertTextEquals(\"Item 1\")\n            }\n            childAt\u003cLazyListItemNode\u003e(10) {\n                assertTextEquals(\"Item 10\")\n            }\n        }\n    }\n}\n```\n\nCheck the lazy list test [example](sample/src/androidTest/java/io/github/kakaocup/compose/test/LazyListTest.kt) for more information.\n\n#### Intercepting\n\nIf you need to add custom logic during the `Kakao-Compose -\u003e Espresso(Compose)` call chain (for example, logging) or\nif you need to completely change the events/commands that are being sent to Espresso\nduring runtime in some cases, you can use the intercepting mechanism.\n\nInterceptors are lambdas that you pass to a configuration DSL that will be invoked before calls happening from inside Kakao-Compose.\n\nYou have the ability to provide interceptors at two main different levels: Kakao-Compose runtime and any individual `BaseNode` instance.\nInterceptors in Kakao-Compose support a parent-children concept too.\nIt means that any `BaseNode` inherits interceptors of his parents.\n\nOn each invocation of Espresso function that can be intercepted, Kakao-Compose will aggregate all available interceptors\nfor this particular call and invoke them in descending order: `Active BaseNode interceptor -\u003e Interceptor of the parent Active BaseNode -\u003e\n... -\u003e Kakao-Compose interceptor`.\n\nEach of the interceptors in the chain can break the chain call by setting `isOverride` to true during configuration.\nIn that case Kakao-Compose will not only stop invoking remaining interceptors in the chain, **but will not perform the Espresso\ncall**. It means that in such case, the responsibility to actually invoke Espresso lies on the shoulders\nof the developer.\n\nHere's the examples of intercepting configurations:\n```Kotlin\nclass SomeTest {\n    @Before\n    fun setup() {\n        KakaoCompose { // KakaoCompose runtime\n            intercept {\n                onComposeInteraction {\n                    onAll { list.add(\"ALL\") }\n                    onCheck { _, _ -\u003e list.add(\"CHECK\") }\n                    onPerform { _, _ -\u003e list.add(\"PERFORM\") }\n                }\n            }\n        }\n    }\n\n    @Test\n    fun test() {\n        onComposeScreen\u003cMyScreen\u003e {\n            intercept {\n                onCheck { interaction, assertion -\u003e // Intercept check() call\n                    Log.d(\"KAKAO\", \"$interaction is checking $assertion\")\n                }\n            }\n\n            myView {\n                intercept { // Intercepting ComposeInteraction calls on this individual Node\n                    onPerform(true) { interaction, action -\u003e // Intercept perform() call and overriding the chain\n                        // When performing actions on this view, Kakao level interceptor will not be called\n                        // and we have to manually call Espresso now.\n                        Log.d(\"KAKAO_NODE\", \"$interaction is performing $action\")\n                        interaction.perform(action)\n                    }\n                }\n            }\n        }\n    }\n}\n```\nFor more detailed info please refer to the documentation.\n\n### `KakaoComposeTestRule`\n\nBy default Espresso using `useUnmergedTree = true` and it create a lot of inconveniences with node matching.\nHowever you can override global parameter with `KakaoComposeTestRule`.\n\nEvery Compose screen require `composeTestRule` whats creating a lot of boilerplate like `onComposeScreen\u003cLazyListScreen\u003e**(composeTestRule)** {`\nBut you can provide `composeTestRule` into `KakaoComposeTestRule` and use all screens without injection like `onComposeScreen\u003cLazyListScreen\u003e {`, it possible to mix both \nof implementation, injected `composeTestRule` will override provided via `KakaoComposeTestRule`.\n\nYou can find examples of it in [Simple project](https://github.com/KakaoCup/Compose/blob/master/sample/src/androidTest/java/io/github/kakaocup/compose/test/SimpleTestGlobalSemantic.kt)\n\n### Setup\nMaven\n```xml\n\u003cdependency\u003e\n    \u003cgroupId\u003eio.github.kakaocup\u003c/groupId\u003e\n    \u003cartifactId\u003ecompose\u003c/artifactId\u003e\n    \u003cversion\u003e\u003clatest version\u003e\u003c/version\u003e\n    \u003ctype\u003epom\u003c/type\u003e\n\u003c/dependency\u003e\n```\nor Gradle:\n```groovy\ndependencies {\n    androidTestImplementation 'io.github.kakaocup:compose:\u003clatest version\u003e'\n}\n```\n\n```kotlin\ndependencies {\n    androidTestImplementation(\"io.github.kakaocup:compose:\u003clatest version\u003e\")\n}\n```\n\n### Contribution Policy\n\n**Kakao Compose** is an open source project, and depends on its users to improve it. We are more than happy to find you interested in taking the project forward.\n\nKindly refer to the [Contribution Guidelines](https://github.com/kakaocup/compose/blob/master/CONTRIBUTING.md) for detailed information.\n\n### Code of Conduct\n\nPlease refer to [Code of Conduct](https://github.com/kakaocup/compose/blob/master/CODE_OF_CONDUCT.md) document.\n\n### License\n\nKakao Compose is open source and available under the [Apache License, Version 2.0](https://github.com/kakaocup/compose/blob/master/LICENSE).\n\n### Thanks for supporting Open Source\n\u003cimg src=\"https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.png\" height=\"100\"\u003e \u003cimg src=\"https://firebase.google.com/static/downloads/brand-guidelines/SVG/logo-logomark.svg\" width=\"100\" height=\"100\"\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FKakaoCup%2FCompose","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FKakaoCup%2FCompose","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FKakaoCup%2FCompose/lists"}