-
Notifications
You must be signed in to change notification settings - Fork 200
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
♻️ Enhanced ContributorsScreen testing. #503
Changes from all commits
97196db
91b9cd9
15e3c3a
ae10a1d
61bd452
2910200
d4f51f9
9a83476
f16b5ce
729cc96
2178993
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
package io.github.droidkaigi.confsched.testing.robot | ||
|
||
import androidx.compose.ui.test.assertContentDescriptionEquals | ||
import androidx.compose.ui.test.assertIsDisplayed | ||
import androidx.compose.ui.test.assertTextEquals | ||
import androidx.compose.ui.test.hasTestTag | ||
import androidx.compose.ui.test.hasText | ||
import androidx.compose.ui.test.performScrollToIndex | ||
import io.github.droidkaigi.confsched.contributors.ContributorsItemTestTagPrefix | ||
import io.github.droidkaigi.confsched.contributors.ContributorsScreen | ||
import io.github.droidkaigi.confsched.contributors.ContributorsTestTag | ||
import io.github.droidkaigi.confsched.contributors.component.ContributorsItemImageTestTagPrefix | ||
import io.github.droidkaigi.confsched.contributors.component.ContributorsUserNameTextTestTagPrefix | ||
import io.github.droidkaigi.confsched.model.Contributor | ||
import io.github.droidkaigi.confsched.model.fakes | ||
import io.github.droidkaigi.confsched.testing.utils.assertCountAtLeast | ||
import io.github.droidkaigi.confsched.testing.utils.hasTestTag | ||
import io.github.droidkaigi.confsched.ui.Inject | ||
|
||
class ContributorsScreenRobot @Inject constructor( | ||
screenRobot: DefaultScreenRobot, | ||
contributorsServerRobot: DefaultContributorsServerRobot, | ||
) : ScreenRobot by screenRobot, | ||
ContributorsServerRobot by contributorsServerRobot { | ||
fun setupScreenContent() { | ||
robotTestRule.setContent { | ||
ContributorsScreen( | ||
onNavigationIconClick = { }, | ||
onContributorsItemClick = { }, | ||
) | ||
} | ||
} | ||
|
||
fun scrollToIndex10() { | ||
composeTestRule | ||
.onNode(hasTestTag(ContributorsTestTag)) | ||
.performScrollToIndex(10) | ||
} | ||
|
||
fun checkRangeContributorItemsDisplayed( | ||
fromTo: IntRange, | ||
) { | ||
val contributorsList = Contributor.fakes().subList(fromTo.first, fromTo.last) | ||
contributorsList.forEach { contributor -> | ||
composeTestRule | ||
.onNode(hasTestTag(ContributorsItemTestTagPrefix.plus(contributor.id))) | ||
.assertExists() | ||
.assertIsDisplayed() | ||
|
||
composeTestRule | ||
.onNode( | ||
matcher = hasTestTag(ContributorsItemImageTestTagPrefix.plus(contributor.username)), | ||
useUnmergedTree = true, | ||
) | ||
.assertExists() | ||
.assertIsDisplayed() | ||
.assertContentDescriptionEquals(contributor.username) | ||
|
||
composeTestRule | ||
.onNode( | ||
matcher = hasTestTag(ContributorsUserNameTextTestTagPrefix.plus(contributor.username)), | ||
useUnmergedTree = true, | ||
) | ||
.assertExists() | ||
.assertIsDisplayed() | ||
.assertTextEquals(contributor.username) | ||
} | ||
} | ||
|
||
fun checkContributorItemsDisplayed() { | ||
// Check there are two contributors | ||
composeTestRule | ||
.onAllNodes(hasTestTag(ContributorsItemTestTagPrefix, substring = true)) | ||
.assertCountAtLeast(2) | ||
} | ||
|
||
fun checkDoesNotFirstContributorItemDisplayed() { | ||
val contributor = Contributor.fakes().first() | ||
composeTestRule | ||
.onNode(hasTestTag(ContributorsItemTestTagPrefix.plus(contributor.id))) | ||
.assertDoesNotExist() | ||
|
||
composeTestRule | ||
.onNode( | ||
matcher = hasTestTag(ContributorsItemImageTestTagPrefix.plus(contributor.username)), | ||
useUnmergedTree = true, | ||
) | ||
.assertDoesNotExist() | ||
|
||
composeTestRule | ||
.onNode( | ||
matcher = hasTestTag(ContributorsUserNameTextTestTagPrefix.plus(contributor.username)), | ||
useUnmergedTree = true, | ||
) | ||
.assertDoesNotExist() | ||
} | ||
|
||
fun checkErrorSnackbarDisplayed() { | ||
composeTestRule | ||
.onNode( | ||
hasText("Fake IO Exception"), | ||
useUnmergedTree = true, | ||
).assertIsDisplayed() | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
package io.github.droidkaigi.confsched.testing.utils | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These Utils are very helpful as I am sure they will be used frequently in other test cases! 🙇♂️ |
||
|
||
import androidx.compose.ui.semantics.SemanticsNode | ||
import androidx.compose.ui.semantics.SemanticsProperties | ||
import androidx.compose.ui.semantics.getOrNull | ||
import androidx.compose.ui.test.SemanticsMatcher | ||
import androidx.compose.ui.test.SemanticsNodeInteractionCollection | ||
|
||
fun hasTestTag( | ||
testTag: String, | ||
substring: Boolean = false, | ||
ignoreCase: Boolean = false, | ||
): SemanticsMatcher { | ||
return SemanticsMatcher( | ||
"TestTag ${if (substring) "contains" else "is"} '$testTag' (ignoreCase: $ignoreCase)", | ||
) { node -> | ||
val nodeTestTag: String? = node.config.getOrNull(SemanticsProperties.TestTag) | ||
when { | ||
nodeTestTag == null -> false | ||
substring -> nodeTestTag.contains(testTag, ignoreCase) | ||
else -> nodeTestTag.equals(testTag, ignoreCase) | ||
} | ||
} | ||
} | ||
|
||
fun SemanticsNodeInteractionCollection.assertCountAtLeast( | ||
minimumExpectedSize: Int, | ||
): SemanticsNodeInteractionCollection { | ||
val errorOnFail = "Failed to assert minimum count of nodes." | ||
val matchedNodes = fetchSemanticsNodes( | ||
atLeastOneRootRequired = minimumExpectedSize > 0, | ||
errorOnFail, | ||
) | ||
if (matchedNodes.size < minimumExpectedSize) { | ||
throw AssertionError( | ||
buildErrorMessageForMinimumCountMismatch( | ||
errorMessage = errorOnFail, | ||
foundNodes = matchedNodes, | ||
minimumExpectedCount = minimumExpectedSize, | ||
), | ||
) | ||
} | ||
return this | ||
} | ||
|
||
private fun buildErrorMessageForMinimumCountMismatch( | ||
errorMessage: String, | ||
foundNodes: List<SemanticsNode>, | ||
minimumExpectedCount: Int, | ||
): String { | ||
return buildString { | ||
appendLine(errorMessage) | ||
appendLine("Expected at least: $minimumExpectedCount") | ||
appendLine("Found: ${foundNodes.size}") | ||
appendLine("Matched nodes:") | ||
foundNodes.forEachIndexed { index, node -> | ||
appendLine("$index: $node") | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -35,6 +35,8 @@ import kotlinx.collections.immutable.PersistentList | |
|
||
const val contributorsScreenRoute = "contributors" | ||
const val ContributorsScreenTestTag = "ContributorsScreenTestTag" | ||
const val ContributorsTestTag = "ContributorsTestTag" | ||
const val ContributorsItemTestTagPrefix = "ContributorsItemTestTag:" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Surely this is a prefix. |
||
|
||
fun NavGraphBuilder.contributorsScreens( | ||
onNavigationIconClick: () -> Unit, | ||
|
@@ -153,14 +155,16 @@ private fun Contributors( | |
contentPadding: PaddingValues = PaddingValues(), | ||
) { | ||
LazyColumn( | ||
modifier = modifier, | ||
modifier = modifier.testTag(ContributorsTestTag), | ||
contentPadding = contentPadding, | ||
) { | ||
items(contributors) { | ||
ContributorsItem( | ||
contributor = it, | ||
onClick = onContributorsItemClick, | ||
modifier = Modifier.fillMaxWidth(), | ||
modifier = Modifier | ||
.fillMaxWidth() | ||
.testTag(ContributorsItemTestTagPrefix.plus(it.id)), | ||
) | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,12 +17,16 @@ import androidx.compose.ui.Alignment | |
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.draw.clip | ||
import androidx.compose.ui.graphics.vector.rememberVectorPainter | ||
import androidx.compose.ui.platform.testTag | ||
import androidx.compose.ui.text.style.TextOverflow | ||
import androidx.compose.ui.unit.dp | ||
import io.github.droidkaigi.confsched.model.Contributor | ||
import io.github.droidkaigi.confsched.ui.previewOverride | ||
import io.github.droidkaigi.confsched.ui.rememberAsyncImagePainter | ||
|
||
const val ContributorsItemImageTestTagPrefix = "ContributorsItemImageTestTag:" | ||
const val ContributorsUserNameTextTestTagPrefix = "ContributorsUserNameTextTestTag:" | ||
Comment on lines
+27
to
+28
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @takahirom |
||
|
||
private val contributorIconShape = CircleShape | ||
|
||
@Composable | ||
|
@@ -44,21 +48,23 @@ fun ContributorsItem( | |
painter = previewOverride(previewPainter = { rememberVectorPainter(image = Icons.Default.Person) }) { | ||
rememberAsyncImagePainter(contributor.iconUrl) | ||
}, | ||
contentDescription = null, | ||
contentDescription = contributor.username, | ||
modifier = Modifier | ||
.size(52.dp) | ||
.clip(contributorIconShape) | ||
.border( | ||
width = 1.dp, | ||
color = MaterialTheme.colorScheme.outline, | ||
shape = contributorIconShape, | ||
), | ||
) | ||
.testTag(ContributorsItemImageTestTagPrefix.plus(contributor.username)), | ||
) | ||
Text( | ||
text = contributor.username, | ||
style = MaterialTheme.typography.bodyLarge, | ||
maxLines = 2, | ||
overflow = TextOverflow.Ellipsis, | ||
modifier = Modifier.testTag(ContributorsUserNameTextTestTagPrefix.plus(contributor.username)), | ||
) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, in this case the scrolling method using Index was better. ✍️