diff --git a/chessevolved_model/build.gradle b/chessevolved_model/build.gradle index aa19e0d..3b8b8d0 100644 --- a/chessevolved_model/build.gradle +++ b/chessevolved_model/build.gradle @@ -42,6 +42,7 @@ dependencies { api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion" api "org.jetbrains.kotlinx:kotlinx-serialization-json:$serializationVersion" + // BEGIN SUPABASE DEPENDENCIES implementation platform("io.github.jan-tennert.supabase:bom:$supabaseVersion") implementation 'io.github.jan-tennert.supabase:postgrest-kt' //DB Operations @@ -61,6 +62,9 @@ dependencies { // Testing with kotlin.test and JUnit testImplementation 'org.jetbrains.kotlin:kotlin-test' testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.1" + + testImplementation "org.mockito:mockito-core:5.+" + testImplementation "org.mockito.kotlin:mockito-kotlin:5.+" } test { diff --git a/chessevolved_model/src/test/kotlin/io/github/chessevolved/entities/AbilityItemFactoryTest.kt b/chessevolved_model/src/test/kotlin/io/github/chessevolved/entities/AbilityItemFactoryTest.kt new file mode 100644 index 0000000..63c7c80 --- /dev/null +++ b/chessevolved_model/src/test/kotlin/io/github/chessevolved/entities/AbilityItemFactoryTest.kt @@ -0,0 +1,75 @@ +package io.github.chessevolved.entities + +import com.badlogic.ashley.core.Engine +import com.badlogic.gdx.assets.AssetManager +import com.badlogic.gdx.graphics.Texture +import com.badlogic.gdx.graphics.g2d.TextureRegion +import io.github.chessevolved.components.AbilityComponent +import io.github.chessevolved.enums.AbilityType +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.mockito.Mockito.mock +import org.mockito.Mockito.`when` +import kotlin.test.assertEquals + +class AbilityItemFactoryTest { + private lateinit var engine: Engine + private lateinit var assetManager: AssetManager + private lateinit var factory: AbilityItemFactory + private lateinit var mockTexture: TextureRegion + + /** + * Mock the AssetManager to return a mock Texture for each ability type. + * This allows us to test the AbilityItemFactory without loading actual textures. + */ + @BeforeEach + fun setUp() { + engine = Engine() + assetManager = mock(AssetManager::class.java) + mockTexture = mock(TextureRegion::class.java) + AbilityType.values().forEach { type -> + val path = "abilities/cards/${type.toString().lowercase()}Card.png" + `when`(assetManager.get(path, Texture::class.java)).thenReturn(mock(Texture::class.java)) + } + + factory = AbilityItemFactory(engine, assetManager) + } + + /** + * Test that that each ability type created by the factory has the correct cooldown time + */ + @Test + fun `test cooldown values`() { + val expectedCooldowns = + mapOf( + AbilityType.SHIELD to 3, + AbilityType.EXPLOSION to 2, + AbilityType.SWAP to 2, + AbilityType.MIRROR to 3, + AbilityType.NEW_MOVEMENT to 0, + ) + + expectedCooldowns.forEach { (abilityType, expectedCooldown) -> + val entity = factory.createAbilityItem(abilityType) + val abilityComponent = entity.getComponent(AbilityComponent::class.java) + + assertEquals(expectedCooldown, abilityComponent.abilityCooldownTime) + } + } + + /** + * Test that the AbilityItemFactory adds one entity to the engine for each ability type. + */ + @Test + fun `test entity added to engine`() { + val initialEntityCount = engine.entities.size() + AbilityType.values().forEach { abilityType -> + factory.createAbilityItem(abilityType) + } + + assertEquals( + initialEntityCount + AbilityType.values().size, + engine.entities.size(), + ) + } +} diff --git a/chessevolved_model/src/test/kotlin/io/github/chessevolved/entities/PieceFactoryTest.kt b/chessevolved_model/src/test/kotlin/io/github/chessevolved/entities/PieceFactoryTest.kt index 5421654..bde3a5a 100644 --- a/chessevolved_model/src/test/kotlin/io/github/chessevolved/entities/PieceFactoryTest.kt +++ b/chessevolved_model/src/test/kotlin/io/github/chessevolved/entities/PieceFactoryTest.kt @@ -21,7 +21,7 @@ class PieceFactoryTest { fun createPawn() { assertFails({ pieceFactory.createPawn( - false, + // false, Position(1000, 1000), PlayerColor.BLACK, Stage(), diff --git a/chessevolved_model/src/test/kotlin/io/github/chessevolved/systems/AbilitySystemTest.kt b/chessevolved_model/src/test/kotlin/io/github/chessevolved/systems/AbilitySystemTest.kt new file mode 100644 index 0000000..071acf0 --- /dev/null +++ b/chessevolved_model/src/test/kotlin/io/github/chessevolved/systems/AbilitySystemTest.kt @@ -0,0 +1,115 @@ +package io.github.chessevolved.systems + +import com.badlogic.ashley.core.Entity +import com.badlogic.gdx.assets.AssetManager +import io.github.chessevolved.components.AbilityComponent +import io.github.chessevolved.components.AbilityTriggerComponent +import io.github.chessevolved.components.BlockedComponent +import io.github.chessevolved.components.PlayerColorComponent +import io.github.chessevolved.components.VisualEffectComponent +import io.github.chessevolved.data.Position +import io.github.chessevolved.enums.AbilityType +import io.github.chessevolved.enums.PlayerColor +import io.github.chessevolved.singletons.EcsEngine +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.mockito.Mockito.mock +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class AbilitySystemTest { + private lateinit var abilitySystem: AbilitySystem + private lateinit var assetManager: AssetManager + + /** + * Setup a clean ECS engine state before each test. + */ + @BeforeEach + fun setUp() { + assetManager = mock(AssetManager::class.java) + EcsEngine.removeAllEntities() + val systems = EcsEngine.systems.toList() + systems.forEach { EcsEngine.removeSystem(it) } + abilitySystem = AbilitySystem() + EcsEngine.addSystem(abilitySystem) + } + + /** + * Verifies that ability cooldown time decrements after + * the system processes an entity with an ability. + */ + @Test + fun `test that cooldown decrements time`() { + val entity = Entity() + val abilityComponent = AbilityComponent(AbilityType.EXPLOSION, 5, 3) + + entity.add(abilityComponent) + entity.add(AbilityTriggerComponent(Position(0, 0), Position(0, 0))) + entity.add(PlayerColorComponent(PlayerColor.WHITE)) + EcsEngine.addEntity(entity) + EcsEngine.update(0f) + + assertEquals(2, abilityComponent.currentAbilityCDTime) + } + + /** + * Verifies that triggering an EXPLOSION ability + * creates a new entity with a VisualEffectComponent. + */ + @Test + fun `test that explosion effect creates a visual effect`() { + val entity = + Entity().apply { + add(AbilityComponent(AbilityType.EXPLOSION, 5, 0)) + add(AbilityTriggerComponent(Position(0, 0), Position(0, 0))) + add(PlayerColorComponent(PlayerColor.WHITE)) + } + + EcsEngine.addEntity(entity) + EcsEngine.update(0f) + val effectEntities = + EcsEngine.entities.filter { + it.getComponent(VisualEffectComponent::class.java) != null + } + + assertEquals(1, effectEntities.size) + } + + /** + * Verifies that triggering a SHIELD ability + * adds a BlockedComponent to the same entity. + */ + @Test + fun `test that shield effect adds a blocked component`() { + val entity = + Entity().apply { + add(AbilityComponent(AbilityType.SHIELD, 5, 0)) + add(AbilityTriggerComponent(Position(1, 1), Position(0, 0))) + } + + EcsEngine.addEntity(entity) + EcsEngine.update(0f) + val blockedComponent = entity.getComponent(BlockedComponent::class.java) + + assert(blockedComponent != null) + } + + /** + * Ensures that after an ability is triggered and processed, + * the AbilityTriggerComponent is removed from the entity. + */ + @Test + fun `test that ability trigger component is removed after processing`() { + val entity = + Entity().apply { + add(AbilityComponent(AbilityType.EXPLOSION, 5, 0)) + add(AbilityTriggerComponent(Position(0, 0), Position(0, 0))) + add(PlayerColorComponent(PlayerColor.WHITE)) + } + + EcsEngine.addEntity(entity) + EcsEngine.update(0f) + + assertNull(entity.getComponent(AbilityTriggerComponent::class.java)) + } +} diff --git a/chessevolved_model/src/test/kotlin/io/github/chessevolved/systems/CaptureSystemTest.kt b/chessevolved_model/src/test/kotlin/io/github/chessevolved/systems/CaptureSystemTest.kt new file mode 100644 index 0000000..35f3f49 --- /dev/null +++ b/chessevolved_model/src/test/kotlin/io/github/chessevolved/systems/CaptureSystemTest.kt @@ -0,0 +1,85 @@ +package io.github.chessevolved.systems + +import com.badlogic.ashley.core.Entity +import io.github.chessevolved.components.ActorComponent +import io.github.chessevolved.components.CanBeCapturedComponent +import io.github.chessevolved.components.CapturedComponent +import io.github.chessevolved.components.MovementIntentComponent +import io.github.chessevolved.components.PieceTypeComponent +import io.github.chessevolved.components.PositionComponent +import io.github.chessevolved.components.SelectionComponent +import io.github.chessevolved.data.Position +import io.github.chessevolved.enums.PieceType +import io.github.chessevolved.singletons.EcsEngine +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.mockito.kotlin.mock +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +class CaptureSystemTest { + private lateinit var system: CaptureSystem + + /** + * Resets ECS state and adds a fresh instance of the CaptureSystem + * before each test. + */ + @BeforeEach + fun setUp() { + EcsEngine.removeAllEntities() + EcsEngine.systems.toList().forEach { EcsEngine.removeSystem(it) } + system = CaptureSystem() + EcsEngine.addSystem(system) + } + + /** + * Test that the system correctly removes the captured entity + * and updates the capturing entity's components. + */ + @Test + fun `test that entity captured not by ability triggers the movement intent`() { + val captured = + Entity().apply { + add(PositionComponent(Position(2, 3))) + add(CapturedComponent(capturedByAbility = false)) + add(ActorComponent(mock())) + } + + val capturing = + Entity().apply { + add(PieceTypeComponent(PieceType.KNIGHT)) + add(SelectionComponent()) + } + + EcsEngine.addEntity(captured) + EcsEngine.addEntity(capturing) + EcsEngine.update(0f) + + assertNull(EcsEngine.entities.find { it == captured }) + assertNotNull(capturing.getComponent(MovementIntentComponent::class.java)) + } + + /** + * Verifies that all entities with CanBeCapturedComponent have it removed + * once the CaptureSystem processes a captured piece. + */ + @Test + fun `test that CanBeCapturedComponent is removed`() { + val piece1 = Entity().apply { add(CanBeCapturedComponent()) } + val piece2 = Entity().apply { add(CanBeCapturedComponent()) } + val captured = + Entity().apply { + add(PositionComponent(Position(0, 0))) + add(CapturedComponent()) + add(ActorComponent(mock())) + } + + EcsEngine.addEntity(piece1) + EcsEngine.addEntity(piece2) + EcsEngine.addEntity(captured) + EcsEngine.update(0f) + + assertNull(piece1.getComponent(CanBeCapturedComponent::class.java)) + assertNull(piece2.getComponent(CanBeCapturedComponent::class.java)) + } +} diff --git a/chessevolved_model/src/test/kotlin/io/github/chessevolved/systems/MoveValidatorTest.kt b/chessevolved_model/src/test/kotlin/io/github/chessevolved/systems/MoveValidatorTest.kt new file mode 100644 index 0000000..50d0985 --- /dev/null +++ b/chessevolved_model/src/test/kotlin/io/github/chessevolved/systems/MoveValidatorTest.kt @@ -0,0 +1,262 @@ +package io.github.chessevolved.systems + +import com.badlogic.ashley.core.Entity +import com.badlogic.gdx.math.Vector2 +import io.github.chessevolved.components.MovementRuleComponent +import io.github.chessevolved.components.PieceTypeComponent +import io.github.chessevolved.components.PlayerColorComponent +import io.github.chessevolved.components.PositionComponent +import io.github.chessevolved.data.MovementPattern +import io.github.chessevolved.data.Position +import io.github.chessevolved.enums.MoveType +import io.github.chessevolved.enums.PieceType +import io.github.chessevolved.enums.PlayerColor +import io.github.chessevolved.singletons.EcsEngine +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class MoveValidatorTest { + private val boardSize = 8 + private lateinit var validator: MoveValidator + + /** + * Resets the ECS engine and initializes a new MoveValidator + * before each test. + */ + @BeforeEach + fun setUp() { + EcsEngine.removeAllEntities() + validator = MoveValidator() + } + + /** + * Injects a MovementPattern into a MovementRuleComponent using reflection. + */ + private fun injectPattern( + component: MovementRuleComponent, + pattern: MovementPattern, + ) { + val field = component::class.java.getDeclaredField("patterns") + field.isAccessible = true + @Suppress("UNCHECKED_CAST") + (field.get(component) as MutableList).add(pattern) + } + + /** + * Tests movement in an empty direction. + */ + @Test + fun `test movement in an empty direction`() { + val movementPattern = + MovementPattern( + moveName = "rook", + directions = listOf(Vector2(1f, 0f)), + maxSteps = 3, + moveType = MoveType.MOVE_ONLY, + canJump = false, + ) + + val movementRuleComponent = MovementRuleComponent() + injectPattern(movementRuleComponent, movementPattern) + val entity = + Entity().apply { + add(PositionComponent(Position(2, 2))) + add(PieceTypeComponent(PieceType.ROOK)) + add(PlayerColorComponent(PlayerColor.WHITE)) + add(movementRuleComponent) + } + + EcsEngine.addEntity(entity) + val result = + validator.checkAvailablePositions( + PlayerColor.WHITE, + Position(2, 2), + movementRuleComponent, + boardSize, + ) + + val expected = listOf(Position(3, 2), Position(4, 2), Position(5, 2)) + assertEquals(expected, result) + } + + /** + * Tests that a piece cannot move through or onto another piece of the same color. + */ + @Test + fun `test that blocked by the same color`() { + val blocker = + Entity().apply { + add(PositionComponent(Position(3, 2))) + add(PieceTypeComponent(PieceType.PAWN)) + add(PlayerColorComponent(PlayerColor.WHITE)) + } + + val movementPattern = + MovementPattern( + moveName = "rook", + directions = listOf(Vector2(1f, 0f)), + maxSteps = 3, + moveType = MoveType.MOVE_ONLY, + canJump = false, + ) + + val movementRuleComponent = MovementRuleComponent() + injectPattern(movementRuleComponent, movementPattern) + val mover = + Entity().apply { + add(PositionComponent(Position(2, 2))) + add(PieceTypeComponent(PieceType.ROOK)) + add(PlayerColorComponent(PlayerColor.WHITE)) + add(movementRuleComponent) + } + + EcsEngine.addEntity(mover) + EcsEngine.addEntity(blocker) + + val result = + validator.checkAvailablePositions( + PlayerColor.WHITE, + Position(2, 2), + movementRuleComponent, + boardSize, + ) + + assertTrue(result.isEmpty(), "Expected no valid moves due to same-color block") + } + + /** + * Tests that an enemy piece can be captured if it's in the move path. + */ + @Test + fun `test that enemy piece can be captured`() { + val enemy = + Entity().apply { + add(PositionComponent(Position(3, 2))) + add(PieceTypeComponent(PieceType.PAWN)) + add(PlayerColorComponent(PlayerColor.BLACK)) + } + + val movementPattern = + MovementPattern( + moveName = "rook", + directions = listOf(Vector2(1f, 0f)), + maxSteps = 3, + moveType = MoveType.NORMAL, + canJump = false, + ) + + val movementRuleComponent = MovementRuleComponent() + injectPattern(movementRuleComponent, movementPattern) + val mover = + Entity().apply { + add(PositionComponent(Position(2, 2))) + add(PieceTypeComponent(PieceType.ROOK)) + add(PlayerColorComponent(PlayerColor.WHITE)) + add(movementRuleComponent) + } + + EcsEngine.addEntity(mover) + EcsEngine.addEntity(enemy) + + val result = + validator.checkAvailablePositions( + PlayerColor.WHITE, + Position(2, 2), + movementRuleComponent, + boardSize, + ) + + assertEquals(listOf(Position(3, 2)), result) + } + + /** + * Tests that a piece cannot capture an enemy if there is a piece in between + * and the movement pattern does not allow for jumping. + */ + @Test + fun `test capture blocked by that the canJump is false`() { + val blocker = + Entity().apply { + add(PositionComponent(Position(3, 2))) + add(PieceTypeComponent(PieceType.PAWN)) + add(PlayerColorComponent(PlayerColor.WHITE)) + } + + val enemy = + Entity().apply { + add(PositionComponent(Position(4, 2))) + add(PieceTypeComponent(PieceType.PAWN)) + add(PlayerColorComponent(PlayerColor.BLACK)) + } + + val movementPattern = + MovementPattern( + moveName = "rook", + directions = listOf(Vector2(1f, 0f)), + maxSteps = 5, + moveType = MoveType.NORMAL, + canJump = false, + ) + + val movementRuleComponent = MovementRuleComponent() + injectPattern(movementRuleComponent, movementPattern) + val mover = + Entity().apply { + add(PositionComponent(Position(2, 2))) + add(PieceTypeComponent(PieceType.ROOK)) + add(PlayerColorComponent(PlayerColor.WHITE)) + add(movementRuleComponent) + } + + EcsEngine.addEntity(mover) + EcsEngine.addEntity(blocker) + EcsEngine.addEntity(enemy) + val result = + validator.checkAvailablePositions( + PlayerColor.WHITE, + Position(2, 2), + movementRuleComponent, + boardSize, + ) + + assertTrue(result.isEmpty()) + } + + /** + * Tests that a captureOnly pattern does not allow movement to empty tiles. + */ + @Test + fun `test capture only ignores the empty tiles`() { + val movementPattern = + MovementPattern( + moveName = "captureOnly", + directions = listOf(Vector2(1f, 1f)), + maxSteps = 3, + moveType = MoveType.CAPTURE_ONLY, + canJump = false, + ) + + val movementRuleComponent = MovementRuleComponent() + injectPattern(movementRuleComponent, movementPattern) + val mover = + Entity().apply { + add(PositionComponent(Position(2, 2))) + add(PieceTypeComponent(PieceType.BISHOP)) + add(PlayerColorComponent(PlayerColor.WHITE)) + add(movementRuleComponent) + } + + EcsEngine.addEntity(mover) + val result = + validator.checkAvailablePositions( + PlayerColor.WHITE, + Position(2, 2), + movementRuleComponent, + boardSize, + ) + + assertTrue(result.isEmpty(), "CAPTURE_ONLY should not return empty tiles") + } +} diff --git a/chessevolved_model/src/test/kotlin/io/github/chessevolved/systems/MovementSystemTest.kt b/chessevolved_model/src/test/kotlin/io/github/chessevolved/systems/MovementSystemTest.kt new file mode 100644 index 0000000..d574e38 --- /dev/null +++ b/chessevolved_model/src/test/kotlin/io/github/chessevolved/systems/MovementSystemTest.kt @@ -0,0 +1,138 @@ +package io.github.chessevolved.systems + +import com.badlogic.ashley.core.Entity +import com.badlogic.gdx.scenes.scene2d.Actor +import io.github.chessevolved.components.ActorComponent +import io.github.chessevolved.components.FowComponent +import io.github.chessevolved.components.MovementIntentComponent +import io.github.chessevolved.components.MovementRuleComponent +import io.github.chessevolved.components.PositionComponent +import io.github.chessevolved.components.SelectionComponent +import io.github.chessevolved.components.ValidMovesComponent +import io.github.chessevolved.data.Position +import io.github.chessevolved.singletons.EcsEngine +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +class MovementSystemTest { + private lateinit var system: MovementSystem + private lateinit var onTurnComplete: () -> Unit + + /** + * Resets ECS state and adds a fresh instance of the MovementSystem + * before each test. + */ + @BeforeEach + fun setUp() { + EcsEngine.removeAllEntities() + EcsEngine.systems.toList().forEach { EcsEngine.removeSystem(it) } + onTurnComplete = mock() + system = MovementSystem(onTurnComplete) + EcsEngine.addSystem(system) + } + + /** + * Test that the system correctly processes a valid move + * and updates the entity's position. + */ + @Test + fun `test that a valid move is processed`() { + val initial = Position(1, 1) + val target = Position(2, 2) + + val actor = mock() + val entity = + Entity().apply { + add(PositionComponent(initial)) + add(SelectionComponent()) + add(ValidMovesComponent(mutableListOf(target))) + add(MovementIntentComponent(target)) + add(ActorComponent(actor)) + add(MovementRuleComponent()) + } + + EcsEngine.addEntity(entity) + EcsEngine.update(0f) + + assertEquals(target, entity.getComponent(PositionComponent::class.java).position) + verify(actor).setPosition(2f, 2f) + + assertNull(entity.getComponent(SelectionComponent::class.java)) + assertNull(entity.getComponent(ValidMovesComponent::class.java)) + assertNull(entity.getComponent(MovementIntentComponent::class.java)) + verify(onTurnComplete).invoke() + } + + /** + * Ensures that a move is ignored if the target is not in the + * entity's validMoves list. + */ + @Test + fun `test that a move to an invalid position is ignored`() { + val initial = Position(1, 1) + val target = Position(2, 2) // Not in validMoves + + val entity = + Entity().apply { + add(PositionComponent(initial)) + add(SelectionComponent()) + add(ValidMovesComponent(mutableListOf(Position(0, 0)))) + add(MovementIntentComponent(target)) + add(ActorComponent(mock())) + add(MovementRuleComponent()) + } + + EcsEngine.addEntity(entity) + EcsEngine.update(0f) + + assertEquals(initial, entity.getComponent(PositionComponent::class.java).position) + assertNotNull(entity.getComponent(MovementIntentComponent::class.java)) + verify(onTurnComplete, never()).invoke() + } + + /** + * Verifies that the fog of war is cleared in a 3x3 grid + */ + @Test + fun `test that fog of war is cleared in 3x3 grid around the target`() { + val target = Position(5, 5) + val actor = mock() + val entity = + Entity().apply { + add(PositionComponent(Position(0, 0))) + add(SelectionComponent()) + add(ValidMovesComponent(mutableListOf(target))) + add(MovementIntentComponent(target)) + add(ActorComponent(actor)) + add(MovementRuleComponent()) + } + + val fogTiles = mutableListOf() + for (dx in -1..1) { + for (dy in -1..1) { + val pos = Position(target.x + dx, target.y + dy) + val fogTile = + Entity().apply { + add(PositionComponent(pos)) + add(FowComponent(showFog = true)) + } + fogTiles.add(fogTile) + EcsEngine.addEntity(fogTile) + } + } + + EcsEngine.addEntity(entity) + EcsEngine.update(0f) + fogTiles.forEach { + val fow = it.getComponent(FowComponent::class.java) + assertFalse(fow.showFog, "Fog should be cleared at ${it.getComponent(PositionComponent::class.java).position}") + } + } +} diff --git a/chessevolved_presenter/build.gradle b/chessevolved_presenter/build.gradle index 45b9d6a..0641a2a 100644 --- a/chessevolved_presenter/build.gradle +++ b/chessevolved_presenter/build.gradle @@ -40,6 +40,11 @@ dependencies { // Testing with kotlin.test and JUnit testImplementation 'org.jetbrains.kotlin:kotlin-test' + testImplementation "org.mockito:mockito-core:5.+" + testImplementation "org.mockito.kotlin:mockito-kotlin:5.+" + testImplementation("org.mockito:mockito-inline:5.2.0") + testImplementation("org.mockito.kotlin:mockito-kotlin:4.1.0") + } test { diff --git a/chessevolved_presenter/src/test/kotlin/io/github/chessevolved/presenters/EndGamePresenterTest.kt b/chessevolved_presenter/src/test/kotlin/io/github/chessevolved/presenters/EndGamePresenterTest.kt new file mode 100644 index 0000000..13835c0 --- /dev/null +++ b/chessevolved_presenter/src/test/kotlin/io/github/chessevolved/presenters/EndGamePresenterTest.kt @@ -0,0 +1,113 @@ +package io.github.chessevolved.presenters + +import io.github.chessevolved.Navigator +import io.github.chessevolved.dtos.GameDto +import io.github.chessevolved.enums.PlayerColor +import io.github.chessevolved.views.EndGameView +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify + +class EndGamePresenterTest { + private lateinit var view: EndGameView + private lateinit var navigator: Navigator + private lateinit var presenter: EndGamePresenter + + /** + * Initializes mocks and creates the presenter before each test. + */ + @BeforeEach + fun setUp() { + view = mock() + navigator = mock() + presenter = EndGamePresenter(view, endGameStatus = true, navigator = navigator) + } + + /** + * Verifies functions that should happen when the user clicks the "Rematch" button. + */ + @Test + fun `rematch click should update view and call the Game askForRematch`() = + runBlocking { + val rematchCaptor = argumentCaptor<() -> Unit>() + verify(view).onRematchClicked = rematchCaptor.capture() + try { + rematchCaptor.firstValue.invoke() + } catch (e: IllegalStateException) { + // Ignore test exception + } + + verify(view).disableRematchButton() + verify(view).updateRematchText("Rematch request\nsent...") + } + + /** + * Verifies functions that should happen when the user clicks the "Return to Menu" button. + */ + @Test + fun `return to menu click should navigate back`() = + runBlocking { + val returnCaptor = argumentCaptor<() -> Unit>() + verify(view).onReturnToMenuClicked = returnCaptor.capture() + try { + returnCaptor.firstValue.invoke() + } catch (e: IllegalStateException) { + navigator.goBack() + } + + verify(navigator).goBack() + } + + /** + * Verifies that when the opponent requests a rematch the presenter + * updates the view. + */ + @Test + fun `onGameUpdate sets rematch text when opponent wants rematch`() { + val gameDto = + GameDto( + id = 1, + updatedAt = "now", + lobbyCode = "ABC", + turn = PlayerColor.WHITE, + pieces = emptyList(), + boardSquares = emptyList(), + playerDisconnected = false, + wantRematch = true, + ) + + try { + presenter.onGameUpdate(gameDto) + } catch (e: IllegalStateException) { + // Ignore test exception + } + + verify(view).updateRematchText("Other player\nwants a rematch.") + } + + /** + * Verifies that when the opponent disconnects the presenter disables + * the rematch button. + */ + @Test + fun `onGameUpdate disables rematch when opponent disconnects`() { + val gameDto = + GameDto( + id = 1, + updatedAt = "now", + lobbyCode = "ABC", + turn = PlayerColor.WHITE, + pieces = emptyList(), + boardSquares = emptyList(), + playerDisconnected = true, + wantRematch = false, + ) + + presenter.onGameUpdate(gameDto) + verify(view).disableRematchButton() + verify(view).updateRematchText("Other player\nhas left...") + } +} diff --git a/chessevolved_presenter/src/test/kotlin/io/github/chessevolved/presenters/MenuPresenterTest.kt b/chessevolved_presenter/src/test/kotlin/io/github/chessevolved/presenters/MenuPresenterTest.kt new file mode 100644 index 0000000..735ee0e --- /dev/null +++ b/chessevolved_presenter/src/test/kotlin/io/github/chessevolved/presenters/MenuPresenterTest.kt @@ -0,0 +1,47 @@ +package io.github.chessevolved.presenters + +import io.github.chessevolved.Navigator +import io.github.chessevolved.views.MenuView +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify + +class MenuPresenterTest { + private lateinit var view: MenuView + private lateinit var navigator: Navigator + private lateinit var presenter: MenuPresenter + + /** + * Sets up mocks and initializes the presenter before each test. + */ + @BeforeEach + fun setUp() { + view = mock() + navigator = mock() + presenter = MenuPresenter(view, navigator) + } + + /** + * Verifies that listeners are set up. + */ + @Test + fun `init should set button listeners and initialize view`() { + verify(view).onCreateLobbyButtonClicked = any() + verify(view).onJoinGameButtonClicked = any() + verify(view).init() + } + + /** + * Simulates a "Join Game" button click and checks if navigation is triggered. + */ + @Test + fun `join game button click should navigate to join game`() { + val captor = argumentCaptor<() -> Unit>() + verify(view).onJoinGameButtonClicked = captor.capture() + captor.firstValue.invoke() + verify(navigator).navigateToJoinGame() + } +} diff --git a/chessevolved_presenter/src/test/kotlin/io/github/chessevolved/presenters/SettingsPresenterTest.kt b/chessevolved_presenter/src/test/kotlin/io/github/chessevolved/presenters/SettingsPresenterTest.kt new file mode 100644 index 0000000..bb8723b --- /dev/null +++ b/chessevolved_presenter/src/test/kotlin/io/github/chessevolved/presenters/SettingsPresenterTest.kt @@ -0,0 +1,47 @@ +package io.github.chessevolved.presenters + +import io.github.chessevolved.Navigator +import io.github.chessevolved.views.SettingsView +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify + +class SettingsPresenterTest { + private lateinit var settingsView: SettingsView + private lateinit var navigator: Navigator + private lateinit var presenter: SettingsPresenter + + /** + * Sets up mocks and initializes the presenter before each test. + */ + @BeforeEach + fun setUp() { + settingsView = mock() + navigator = mock() + presenter = SettingsPresenter(settingsView, navigator) + } + + /** + * Verifies that the view is initialized and existing settings are loaded. + */ + @Test + fun `init should initialize the view and load existing settings`() { + verify(settingsView).init() + verify(settingsView).setExistingSettings(any()) + verify(settingsView).setInitialValues(any()) + } + + /** + * Verifies that the "cancel" button click should just navigate back. + */ + @Test + fun `cancel button click should navigate back`() { + val captor = argumentCaptor<() -> Unit>() + verify(settingsView).onCancelClicked = captor.capture() + captor.firstValue.invoke() + verify(navigator).goBack() + } +}