Add tests
This commit is contained in:
@@ -6,5 +6,6 @@ class GridFocusable(
|
||||
manager: GridSelectionManager,
|
||||
primaryAction: (() -> Unit)?,
|
||||
secondaryAction: (() -> Unit)?,
|
||||
onKeyEvent: ((KeyEvent) -> Boolean)? = null
|
||||
onKeyEvent: ((KeyEvent) -> Boolean)? = null,
|
||||
val position: String
|
||||
) : Focusable<GridFocusable>(manager, primaryAction, secondaryAction, onKeyEvent)
|
||||
@@ -21,7 +21,8 @@ class GridSelectionManager : SelectionManager<GridFocusable>() {
|
||||
manager = this,
|
||||
primaryAction = null,
|
||||
secondaryAction = null,
|
||||
onKeyEvent = onKeyEvent
|
||||
onKeyEvent = onKeyEvent,
|
||||
position = "${grid.last().size + 1}/${grid.size}"
|
||||
)
|
||||
grid.last().add(new)
|
||||
return new
|
||||
@@ -34,7 +35,8 @@ class GridSelectionManager : SelectionManager<GridFocusable>() {
|
||||
val new = GridFocusable(
|
||||
manager = this,
|
||||
primaryAction = primaryAction,
|
||||
secondaryAction = secondaryAction
|
||||
secondaryAction = secondaryAction,
|
||||
position = "${grid.last().size + 1}/${grid.size}"
|
||||
)
|
||||
grid.last().add(new)
|
||||
return new
|
||||
|
||||
@@ -17,6 +17,7 @@ import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.KeyEvent
|
||||
import androidx.compose.ui.input.key.isShiftPressed
|
||||
import androidx.compose.ui.input.key.key
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ch.dissem.yaep.domain.GameCell
|
||||
@@ -112,7 +113,8 @@ private fun RowScope.PuzzleCell(
|
||||
modifier = Modifier
|
||||
.focus(focusable)
|
||||
.padding(spacing)
|
||||
.weight(1f),
|
||||
.weight(1f)
|
||||
.testTag(focusable.position),
|
||||
spacing,
|
||||
options = options,
|
||||
onOptionRemoved = { selectedItem ->
|
||||
|
||||
@@ -89,7 +89,6 @@ internal fun Placeable.PlacementScope.portrait(
|
||||
offsetY = verticalCluesOffsetY,
|
||||
maxWidth = gridSize
|
||||
)
|
||||
verticalCluesBoxPlaceable.place(0, verticalCluesOffsetY)
|
||||
}
|
||||
// Position the time
|
||||
val remainingSpace = constraints.maxHeight - offsetY
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
package ch.dissem.yaep.ui.common
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.test.ExperimentalTestApi
|
||||
import androidx.compose.ui.test.SkikoComposeUiTest
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.test.runSkikoComposeUiTest
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ch.dissem.yaep.domain.Game
|
||||
import ch.dissem.yaep.domain.UnsolvablePuzzleException
|
||||
import ch.dissem.yaep.ui.common.focus.FocusFollowingSelectionManager
|
||||
import ch.tutteli.atrium.api.fluent.en_GB.toEqual
|
||||
import ch.tutteli.atrium.api.verbs.expect
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.fail
|
||||
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
class AppTest {
|
||||
val game = createGame()
|
||||
var resetCluesBeacon by mutableStateOf(Any())
|
||||
|
||||
@Test
|
||||
fun ensure_app_can_be_rendered_on_1920x1080_and_can_be_solved() = ensure_app_can_be_rendered_on(
|
||||
Size(1920f, 1080f)
|
||||
) {
|
||||
val clues = game.clues
|
||||
val grid = game.grid
|
||||
var removedOptions: Boolean
|
||||
do {
|
||||
removedOptions = false
|
||||
try {
|
||||
clues.forEach { clue ->
|
||||
val optionsRemoved = clue.removeForbiddenOptions(grid)
|
||||
removedOptions = optionsRemoved || removedOptions
|
||||
if (optionsRemoved) {
|
||||
resetCluesBeacon = Any()
|
||||
}
|
||||
}
|
||||
} catch (_: UnsolvablePuzzleException) {
|
||||
fail("puzzle can't be solved")
|
||||
}
|
||||
grid.cleanupOptions()
|
||||
resetCluesBeacon = Any()
|
||||
} while (removedOptions)
|
||||
|
||||
expect(game.isSolved).toEqual(true)
|
||||
onNodeWithTag("EndOfGame.solved_time").assertExists("End of Game must be shown when puzzle is solved")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ensure_app_can_be_rendered_on_1080x1920() = ensure_app_can_be_rendered_on(
|
||||
Size(1080f, 1920f)
|
||||
)
|
||||
|
||||
@Test
|
||||
fun ensure_app_can_be_rendered_on_800x600() = ensure_app_can_be_rendered_on(
|
||||
Size(800f, 600f)
|
||||
)
|
||||
|
||||
@Test
|
||||
fun ensure_app_can_be_rendered_on_600x800() = ensure_app_can_be_rendered_on(
|
||||
Size(600f, 800f)
|
||||
)
|
||||
|
||||
fun ensure_app_can_be_rendered_on(
|
||||
screenSize: Size,
|
||||
block: suspend SkikoComposeUiTest.() -> Unit = {}
|
||||
) = runSkikoComposeUiTest(size = screenSize) {
|
||||
setContent {
|
||||
App(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
rootSelectionManager = FocusFollowingSelectionManager,
|
||||
spacing = 8.dp,
|
||||
game = game,
|
||||
onNewGame = { },
|
||||
resetCluesBeacon = resetCluesBeacon
|
||||
)
|
||||
}
|
||||
block()
|
||||
}
|
||||
|
||||
private fun createGame(): Game = Game.parse(
|
||||
"""
|
||||
* MATE is between the neighbours TEACHER and GOAT to both sides
|
||||
* SCIENTIST is between the neighbours SPAIN and FIREFIGHTER to both sides
|
||||
* TEA is left of COFFEE
|
||||
* ZEBRA is between the neighbours LEMON and CAKE to both sides
|
||||
* CUPCAKE is left of GOAT
|
||||
* ROCK_STAR is left of SPAIN
|
||||
* PEAR is at position 1
|
||||
* CHOCOLATE is between the neighbours TEA and MILK to both sides
|
||||
* FARMER is between the neighbours PEAR and CHERRIES to both sides
|
||||
* SWITZERLAND is between the neighbours JAPAN and DOG to both sides
|
||||
* MANGO and SPAIN are in the same column
|
||||
* SPAIN is next to SNAIL
|
||||
* CANADA is next to PIE
|
||||
* JAPAN is between the neighbours SWITZERLAND and CAKE to both sides
|
||||
* FIREFIGHTER is between the neighbours GOAT and STRAWBERRY to both sides
|
||||
* UKRAINE is between the neighbours ANT and BEVERAGE to both sides
|
||||
* CHOCOLATE and SWITZERLAND are in the same column
|
||||
* PEAR is left of COFFEE
|
||||
* JAPAN is between the neighbours CAKE and COFFEE to both sides
|
||||
* BEVERAGE is between the neighbours CUSTARD and UKRAINE to both sides
|
||||
* CANADA and CAKE are in the same column
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import ch.tutteli.atrium.api.fluent.en_GB.toEqual
|
||||
import ch.tutteli.atrium.api.verbs.expect
|
||||
import kotlin.test.Test
|
||||
|
||||
class CluesSelectionManagerTest : SelectionManagerTest<CluesSelectionManager>() {
|
||||
class CluesSelectionManagerTest : SelectionManagerTest<CluesFocusable, CluesSelectionManager>() {
|
||||
|
||||
var primaryActionCalled: Int? = null
|
||||
var secondaryActionCalled: Int? = null
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package ch.dissem.yaep.ui.common.focus
|
||||
|
||||
import androidx.compose.ui.focus.FocusState
|
||||
import ch.tutteli.atrium.api.fluent.en_GB.notToEqual
|
||||
import ch.tutteli.atrium.api.fluent.en_GB.toEqual
|
||||
import ch.tutteli.atrium.api.verbs.expect
|
||||
import kotlin.test.Test
|
||||
|
||||
class FocusFollowingSelectionManagerTest :
|
||||
SelectionManagerTest<FocusFollowingFocusable, FocusFollowingSelectionManager>() {
|
||||
|
||||
var focusables = mutableListOf<FocusFollowingFocusable>()
|
||||
|
||||
val active = object : FocusState {
|
||||
override val isFocused: Boolean = true
|
||||
override val hasFocus: Boolean = true
|
||||
override val isCaptured: Boolean = true
|
||||
}
|
||||
val inactive = object : FocusState {
|
||||
override val isFocused: Boolean = false
|
||||
override val hasFocus: Boolean = false
|
||||
override val isCaptured: Boolean = false
|
||||
}
|
||||
|
||||
override fun setUp() {
|
||||
manager = FocusFollowingSelectionManager
|
||||
manager.focused = null
|
||||
|
||||
focusables = mutableListOf()
|
||||
repeat(4) {
|
||||
focusables.add(manager.add())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ensure_focus_is_followed() {
|
||||
expect(manager.focused).toEqual(null)
|
||||
for (focusable in focusables) {
|
||||
expect(manager.focused).notToEqual(focusable)
|
||||
focusable.setFocus(active)
|
||||
expect(manager.focused).toEqual(focusable)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ensure_focus_is_lost_when_same_focusable_is_set_inactive() {
|
||||
for (focusable in focusables) {
|
||||
focusable.setFocus(active)
|
||||
expect(manager.focused).toEqual(focusable)
|
||||
focusable.setFocus(inactive)
|
||||
expect(manager.focused).toEqual(null)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ensure_focus_is_not_lost_if_different_focusable_is_set_inactive() {
|
||||
var lastFocused: FocusFollowingFocusable? = null
|
||||
for (focusable in focusables) {
|
||||
focusable.setFocus(inactive)
|
||||
expect(manager.focused).toEqual(lastFocused)
|
||||
focusable.setFocus(active)
|
||||
expect(manager.focused).toEqual(focusable)
|
||||
lastFocused = focusable
|
||||
}
|
||||
expect(manager.focused).toEqual(lastFocused)
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import ch.tutteli.atrium.api.fluent.en_GB.toEqual
|
||||
import ch.tutteli.atrium.api.verbs.expect
|
||||
import kotlin.test.Test
|
||||
|
||||
class GridSelectionManagerTest : SelectionManagerTest<GridSelectionManager>() {
|
||||
class GridSelectionManagerTest : SelectionManagerTest<GridFocusable, GridSelectionManager>() {
|
||||
|
||||
lateinit var focusables: MutableList<GridFocusable>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import ch.tutteli.atrium.api.fluent.en_GB.toEqual
|
||||
import ch.tutteli.atrium.api.verbs.expect
|
||||
import kotlin.test.Test
|
||||
|
||||
class LinearSelectionManagerTest : SelectionManagerTest<LinearSelectionManager>() {
|
||||
class LinearSelectionManagerTest : SelectionManagerTest<LinearFocusable, LinearSelectionManager>() {
|
||||
|
||||
override fun setUp() {
|
||||
manager = LinearSelectionManager(
|
||||
|
||||
@@ -4,14 +4,60 @@ import androidx.compose.ui.InternalComposeUiApi
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.KeyEvent
|
||||
import androidx.compose.ui.input.key.KeyEventType
|
||||
import ch.tutteli.atrium.api.fluent.en_GB.toEqual
|
||||
import ch.tutteli.atrium.api.verbs.expect
|
||||
import kotlin.test.BeforeTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.fail
|
||||
|
||||
abstract class SelectionManagerTest<M : SelectionManager<*>> {
|
||||
abstract class SelectionManagerTest<F : Focusable<F>, M : SelectionManager<F>> {
|
||||
lateinit var manager: M
|
||||
|
||||
@BeforeTest
|
||||
abstract fun setUp()
|
||||
|
||||
@Test
|
||||
fun ensure_key_event_is_ignored_if_not_keyup() {
|
||||
for (t in arrayOf(KeyEventType.KeyDown, KeyEventType.Unknown)) {
|
||||
manager.focused = manager.add {
|
||||
fail("Action must not be called, event type $t should be ignored")
|
||||
}
|
||||
manager.onKeyEvent(keyEvent(Key.Enter, t))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ensure_primary_action_is_called() {
|
||||
for (k in arrayOf(Key.Enter, Key.Spacebar)) {
|
||||
var primary = false
|
||||
var secondary = false
|
||||
|
||||
manager.focused = manager.add(
|
||||
primaryAction = { primary = true },
|
||||
secondaryAction = { secondary = true }
|
||||
)
|
||||
manager.onKeyEvent(keyEvent(k))
|
||||
expect(primary).toEqual(true)
|
||||
expect(secondary).toEqual(false)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ensure_secondary_action_is_called() {
|
||||
for (k in arrayOf(Key.Backspace, Key.Delete)) {
|
||||
var primary = false
|
||||
var secondary = false
|
||||
|
||||
manager.focused = manager.add(
|
||||
primaryAction = { primary = true },
|
||||
secondaryAction = { secondary = true }
|
||||
)
|
||||
manager.onKeyEvent(keyEvent(k))
|
||||
expect(primary).toEqual(false)
|
||||
expect(secondary).toEqual(true)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(InternalComposeUiApi::class)
|
||||
fun keyEvent(key: Key, type: KeyEventType = KeyEventType.KeyUp) = KeyEvent(
|
||||
key = key,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package ch.dissem.yaep.domain
|
||||
|
||||
@Suppress("JavaDefaultMethodsNotOverriddenByDelegation")
|
||||
class GameRow<C : ItemClass<C>>(
|
||||
val category: ItemClassCompanion<C>,
|
||||
val options: List<Item<C>>,
|
||||
@@ -22,12 +21,8 @@ class GameRow<C : ItemClass<C>>(
|
||||
fun indexOf(element: C): Int = indexOfFirst { it.selection?.itemType == element }
|
||||
|
||||
fun cleanupOptions() {
|
||||
if (isSolved && all {
|
||||
it.solution != null
|
||||
&& it.options.size == 1
|
||||
&& it.options.single() == it.selection
|
||||
}
|
||||
) return
|
||||
if (isSolved && all { it.options.size == 1 && it.options.single() == it.selection })
|
||||
return
|
||||
do {
|
||||
var changed = false
|
||||
val selections = filter { it.selection != null }.let { cellsWithSelection ->
|
||||
@@ -58,11 +53,7 @@ class GameRow<C : ItemClass<C>>(
|
||||
}
|
||||
}
|
||||
} while (changed)
|
||||
isSolved = all {
|
||||
it.solution != null
|
||||
&& it.options.size == 1
|
||||
&& it.options.single() == it.selection
|
||||
}
|
||||
isSolved = all { it.options.size == 1 && it.options.single() == it.selection }
|
||||
}
|
||||
|
||||
fun snapshot() {
|
||||
@@ -72,11 +63,7 @@ class GameRow<C : ItemClass<C>>(
|
||||
fun undo(): Boolean {
|
||||
val didChange = cells.map { it.undo() }.reduce { a, b -> a || b }
|
||||
if (didChange) {
|
||||
isSolved = all {
|
||||
it.solution != null
|
||||
&& it.options.size == 1
|
||||
&& it.options.single() == it.selection
|
||||
}
|
||||
isSolved = all { it.options.size == 1 && it.options.single() == it.selection }
|
||||
}
|
||||
return didChange
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user