test: no overlapping cards test at 360dp, 768dp, 1280dp (Closes #32)

Add NoOverlappingCardsTest that renders AttributesPage at compact,
medium, and expanded widths, collects getBoundsInRoot() for all
attribute and talent cards, and asserts no rectangles intersect.
Tests require JVM Compose test runner (will fail in JS browser
like the existing ComposeTest — known environment limitation).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
shahondin1624
2026-03-13 13:12:52 +01:00
parent 2dd477591b
commit 1079f079e1

View File

@@ -0,0 +1,110 @@
package org.shahondin1624
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.getBoundsInRoot
import androidx.compose.ui.test.onAllNodesWithTag
import androidx.compose.ui.test.runComposeUiTest
import androidx.compose.ui.unit.dp
import org.shahondin1624.lib.components.TestTags
import org.shahondin1624.lib.components.charactermodel.attributespage.AttributesPage
import org.shahondin1624.model.EXAMPLE_CHARACTER
import org.shahondin1624.model.attributes.AttributeType
import org.shahondin1624.theme.LocalWindowSizeClass
import org.shahondin1624.theme.WindowSizeClass
import kotlin.test.Test
import kotlin.test.assertFalse
/**
* Verifies that attribute and talent cards never overlap at any screen width.
* Tests at 360dp (compact/phone), 768dp (medium/tablet), and 1280dp (expanded/desktop).
*/
@OptIn(ExperimentalTestApi::class)
class NoOverlappingCardsTest {
private val allAttributeTags = AttributeType.entries.map { TestTags.attributeCard(it.name) }
private val character = EXAMPLE_CHARACTER
/**
* Collects the bounding rectangles for all attribute and talent cards,
* then asserts no two rectangles intersect.
*/
private fun assertNoOverlap(tag: String, bounds: List<Pair<String, Rect>>) {
for (i in bounds.indices) {
for (j in i + 1 until bounds.size) {
val (tagA, rectA) = bounds[i]
val (tagB, rectB) = bounds[j]
val overlaps = rectA.overlaps(rectB) &&
rectA.width > 0 && rectA.height > 0 &&
rectB.width > 0 && rectB.height > 0
assertFalse(
overlaps,
"Cards overlap at $tag width: '$tagA' $rectA intersects '$tagB' $rectB"
)
}
}
}
private fun runOverlapTestAtWidth(widthDp: Int) = runComposeUiTest {
val sizeClass = WindowSizeClass.fromWidth(widthDp.dp)
setContent {
CompositionLocalProvider(LocalWindowSizeClass provides sizeClass) {
Column(modifier = Modifier.width(widthDp.dp).fillMaxSize()) {
AttributesPage(character)
}
}
}
waitForIdle()
val bounds = mutableListOf<Pair<String, Rect>>()
// Collect attribute card bounds
for (tag in allAttributeTags) {
val nodes = onAllNodesWithTag(tag)
val count = nodes.fetchSemanticsNodes().size
for (i in 0 until count) {
val rect = nodes[i].getBoundsInRoot()
bounds.add(tag to Rect(
left = rect.left.value,
top = rect.top.value,
right = rect.right.value,
bottom = rect.bottom.value
))
}
}
// Collect talent card bounds (sample first few visible)
val talentTags = character.talents.talents.take(10).map { TestTags.talentCard(it.name) }
for (tag in talentTags) {
val nodes = onAllNodesWithTag(tag)
val count = nodes.fetchSemanticsNodes().size
for (i in 0 until count) {
val rect = nodes[i].getBoundsInRoot()
bounds.add(tag to Rect(
left = rect.left.value,
top = rect.top.value,
right = rect.right.value,
bottom = rect.bottom.value
))
}
}
assertNoOverlap("${widthDp}dp", bounds)
}
@Test
fun noOverlapAtCompactWidth() = runOverlapTestAtWidth(360)
@Test
fun noOverlapAtMediumWidth() = runOverlapTestAtWidth(768)
@Test
fun noOverlapAtExpandedWidth() = runOverlapTestAtWidth(1280)
}