Skip to content

Commit

Permalink
Merge pull request #24 from open-tool/compose-factory-methods
Browse files Browse the repository at this point in the history
Created new factory methods for compose rule
  • Loading branch information
alex-tiurin authored Sep 7, 2022
2 parents 739220b + c777f2f commit e20c5ef
Show file tree
Hide file tree
Showing 7 changed files with 160 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,15 @@ import com.atiurin.ultron.page.Page
object ComposeListPage : Page<ComposeListPage>() {
val lazyList = composeList(hasContentDescription(contactsListContentDesc))

class ComposeFriendListItem : UltronComposeListItem(){
val name by lazy { getChild(hasTestTag(contactNameTestTag)) }
val status by lazy { getChild(hasTestTag(contactStatusTestTag)) }
fun assertContactStatus(contact: Contact) = apply {
getContactItemById(contact).status.assertTextEquals(contact.status)
}
fun getFirstVisibleItem(): ComposeFriendListItem = lazyList.getFirstVisibleItem()
fun getItemByIndex(index: Int): ComposeFriendListItem = lazyList.getVisibleItem(index)
fun getContactItemById(contact: Contact): ComposeFriendListItem = lazyList.getItem(hasTestTag(getContactItemTestTagById(contact)))
fun getContactItemByName(contact: Contact): ComposeFriendListItem = lazyList.getItem(hasAnyDescendant(hasText(contact.name) and hasTestTag(contactNameTestTag)))
class ComposeFriendListItem : UltronComposeListItem(){
val name by lazy { getChild(hasTestTag(contactNameTestTag)) }
val status by lazy { getChild(hasTestTag(contactStatusTestTag)) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.atiurin.sampleapp.tests.compose

import androidx.compose.ui.test.hasTestTag
import com.atiurin.sampleapp.activity.ComposeListActivity
import com.atiurin.sampleapp.compose.contactNameTestTag
import com.atiurin.sampleapp.compose.contactsListTestTag
import com.atiurin.sampleapp.data.repositories.CONTACTS
import com.atiurin.ultron.core.compose.createUltronComposeRule
import com.atiurin.ultron.core.compose.operation.UltronComposeCollectionInteraction.Companion.allNodes
import org.junit.Rule
import org.junit.Test

class CollectionInteractionTest {
@get:Rule
val composeRule = createUltronComposeRule<ComposeListActivity>()
val list = hasTestTag(contactsListTestTag)
@Test
fun allNodes_getByIndex(){
val index = 4
val contact = CONTACTS[index]
allNodes(hasTestTag(contactNameTestTag), true).get(index).assertTextContains(contact.name)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.atiurin.sampleapp.tests.compose

import androidx.compose.material.Text
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.test.hasTestTag
import com.atiurin.ultron.core.compose.createDefaultUltronComposeRule
import com.atiurin.ultron.extensions.assertIsDisplayed
import org.junit.Rule
import org.junit.Test

class DefaultComponentActivityTest {
@get:Rule
val composeRule = createDefaultUltronComposeRule()

@Test
fun setContent() {
val testTagValue = "testTag"
composeRule.setContent {
Text(text = "Hello, world!", modifier = Modifier.semantics { testTag = testTagValue })
}
hasTestTag(testTagValue)
.assertIsDisplayed()
.assertTextEquals("Hello, world!")
}
}
8 changes: 8 additions & 0 deletions sample-app/src/debug/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.atiurin.sampleapp">
<application>
<activity android:name="androidx.activity.ComponentActivity" />
</application>

</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ package com.atiurin.sampleapp.compose
import android.content.Context
import android.content.Intent
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Divider
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Text
Expand All @@ -16,8 +19,12 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.modifier.modifierLocalOf
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.Role.Companion.Image
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag
Expand Down Expand Up @@ -69,9 +76,25 @@ fun ContactsList(contacts: List<Contact>, context: Context, addStickyHeader: Boo
testTag = testTagProvider.invoke(contact, index)
})
) {
Text(contact.name, Modifier.semantics { testTag = contactNameTestTag }, fontSize = TextUnit(16f, TextUnitType.Sp))
Spacer(modifier = Modifier.height(8.dp))
Text(text = contact.status, Modifier.semantics { testTag = contactStatusTestTag })
Row {
Image(
painter = painterResource(contact.avatar),
contentDescription = "avatar",
contentScale = ContentScale.Crop, // crop the image if it's not a square
modifier = Modifier
.size(80.dp)
.clip(CircleShape) // clip to the circle shape
.border(2.dp, Color.Transparent, CircleShape) // add a border (optional)
)
Spacer(modifier = Modifier.width(16.dp))
Column {
Text(contact.name, Modifier.semantics { testTag = contactNameTestTag }, fontSize = TextUnit(20f, TextUnitType.Sp))
Spacer(modifier = Modifier.height(8.dp))
Text(text = contact.status, Modifier.semantics { testTag = contactStatusTestTag }, fontSize = TextUnit(16f, TextUnitType.Sp))
Spacer(modifier = Modifier.height(8.dp))
}

}
Spacer(modifier = Modifier.height(8.dp))
Divider(color = Color.Black)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,21 @@ package com.atiurin.ultron.core.compose

import androidx.activity.ComponentActivity
import androidx.compose.ui.test.TestContext
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.junit4.*
import androidx.test.ext.junit.rules.ActivityScenarioRule
import com.atiurin.ultron.exceptions.UltronException
import com.atiurin.ultron.extensions.getProperty

object ComposeRuleContainer {
var rule : ComposeContentTestRule? = null
var rule: ComposeTestRule? = null
var testContext: TestContext? = null

fun getComposeRule(): ComposeContentTestRule {
fun init(composeRule: ComposeTestRule) {
rule = composeRule
testContext = composeRule.getTestContext()
}

fun getComposeRule(): ComposeTestRule {
return rule ?: throw UltronException("Initialise ComposeTestRule using createUltronComposeRule<A>() factory method.")
}

Expand All @@ -22,10 +25,59 @@ object ComposeRuleContainer {
}
}

inline fun <reified A: ComponentActivity> createUltronComposeRule(): AndroidComposeTestRule<ActivityScenarioRule<A>, A> {
/**
* Factory method to provide android specific implementation of [createComposeRule], for a given
* activity class type [A].
*
* This method is useful for tests that require a custom Activity. This is usually the case for
* tests where the compose content is set by that Activity, instead of via the test rule's
* [setContent][ComposeContentTestRule.setContent]. Make sure that you add the provided activity
* into your app's manifest file (usually in main/AndroidManifest.xml).
*
* This creates a test rule that is using [ActivityScenarioRule] as the activity launcher. If you
* would like to use a different one you can create [AndroidComposeTestRule] directly and supply
* it with your own launcher.
*
* If your test doesn't require a specific Activity, use [createDefaultUltronComposeRule] instead.
*/
inline fun <reified A : ComponentActivity> createUltronComposeRule(): AndroidComposeTestRule<ActivityScenarioRule<A>, A> {
val rule = createAndroidComposeRule<A>()
ComposeRuleContainer.rule = rule
ComposeRuleContainer.testContext = rule.getProperty<TestContext>("testContext")
ComposeRuleContainer.init(rule)
return rule
}

/**
* Factory method to provide implementation of [ComposeContentTestRule]
*
* This method is useful for tests that doesn't require a custom Activity.
*
* It's expected that compose content is set via the test rule's
* [setContent][ComposeContentTestRule.setContent]
*/
fun createDefaultUltronComposeRule(): ComposeContentTestRule {
val rule = createComposeRule()
ComposeRuleContainer.init(rule)
return rule
}

/**
* Factory method to provide an implementation of [ComposeTestRule] that doesn't create a compose
* host for you in which you can set content.
*
* This method is useful for tests that need to create their own compose host during the test.
* The returned test rule will not create a host, and consequently does not provide a
* `setContent` method. To set content in tests using this rule, use the appropriate `setContent`
* methods from your compose host.
*
* A typical use case on Android is when the test needs to launch an Activity (the compose host)
* after one or more dependencies have been injected.
*/
fun createEmptyUltronComposeRule(): ComposeTestRule {
val rule = createEmptyComposeRule()
ComposeRuleContainer.init(rule)
return rule
}

fun ComposeTestRule.getTestContext() = this.getProperty<TestContext>("testContext")


Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ class UltronComposeList(
): T {
return UltronComposeListItem.getInstance(this, index)
}

inline fun <reified T : UltronComposeListItem> getFirstVisibleItem(): T = getVisibleItem(0)
inline fun <reified T : UltronComposeListItem> getLastVisibleItem(): T = getVisibleItem(getMatcher().perform { it.fetchSemanticsNode().children.lastIndex })
/**
* Provide a scope with references to list SemanticsNode and SemanticsNodeInteraction.
* It is possible to evaluate any action or assertion on this node.
Expand Down Expand Up @@ -123,7 +125,6 @@ class UltronComposeList(
listInteraction.performScrollToNode(itemMatcher)
}
}

fun scrollToIndex(index: Int) = apply { getMatcher().perform { it.performScrollToIndex(index) } }
fun scrollToKey(key: Any) = apply { getMatcher().perform { it.performScrollToKey(key) } }
fun assertIsDisplayed() = apply { getMatcher().withTimeout(getOperationTimeout()).assertIsDisplayed() }
Expand All @@ -134,27 +135,28 @@ class UltronComposeList(
fun assertContentDescriptionContains(expected: String, option: ContentDescriptionContainsOption? = null) =
apply { getMatcher().withTimeout(getOperationTimeout()).assertContentDescriptionContains(expected, option) }

fun assertNotEmpty() = AssertUtils.assertTrue(
{ getVisibleItemsCount() > 0 }, getOperationTimeout(),
"Compose list (${listMatcher.description}) is NOT empty"
)
fun assertNotEmpty() = apply {
AssertUtils.assertTrue(
{ getVisibleItemsCount() > 0 }, getOperationTimeout(),
"Compose list (${listMatcher.description}) is NOT empty"
)
}

fun assertEmpty() {
fun assertEmpty() = apply {
AssertUtils.assertTrue(
{ getVisibleItemsCount() == 0 }, getOperationTimeout(),
"Compose list (${listMatcher.description}) has no items (visible items count = ${getVisibleItemsCount()})"
)
}

fun assertVisibleItemsCount(expected: Int) {
fun assertVisibleItemsCount(expected: Int) = apply {
AssertUtils.assertTrue(
{ getVisibleItemsCount() == expected }, getOperationTimeout(),
"Compose list (${listMatcher.description}) has visible items count = $expected (actual visible items count = ${getVisibleItemsCount()})"
)
}

fun getVisibleItemsCount(): Int = getMatcher().perform { it.fetchSemanticsNode().children.size }

fun getMatcher() = UltronComposeSemanticsMatcher(listMatcher, useUnmergedTree)
}

Expand Down

0 comments on commit e20c5ef

Please sign in to comment.