diff --git a/sharedUI/src/commonTest/kotlin/org/shahondin1624/NoOverlappingCardsTest.kt b/sharedUI/src/commonTest/kotlin/org/shahondin1624/NoOverlappingCardsTest.kt new file mode 100644 index 0000000..3eb7bca --- /dev/null +++ b/sharedUI/src/commonTest/kotlin/org/shahondin1624/NoOverlappingCardsTest.kt @@ -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>) { + 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>() + + // 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) +}