package com.projectsacrifice.game; import android.app.Instrumentation; import androidx.test.core.app.ActivityScenario; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.platform.app.InstrumentationRegistry; import org.junit.Test; import org.junit.runner.RunWith; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; /** Device-level contract for leaving and returning to an active puzzle. */ @RunWith(AndroidJUnit4.class) public final class GameLifecycleTest { @Test public void returningFromBackgroundKeepsPuzzleVisiblePausedAndClearsHeldInput() { Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); try (ActivityScenario scenario = ActivityScenario.launch(MainActivity.class)) { final MainActivity[] activity = new MainActivity[1]; final GameView[] view = new GameView[1]; scenario.onActivity(current -> { activity[0] = current; view[0] = current.gameViewForAutomation(); }); createRetainedCorpse(instrumentation, view[0]); runOnGame(instrumentation, view[0], false, false, true, false); instrumentation.runOnMainSync(activity[0]::onPause); GameView.AutomationSnapshot backgroundStart = snapshot(instrumentation, view[0]); sleep(700L); GameView.AutomationSnapshot backgroundEnd = snapshot(instrumentation, view[0]); assertEquals(5, backgroundEnd.levelNumber()); assertEquals(backgroundStart.deaths(), backgroundEnd.deaths()); assertEquals(backgroundStart.corpseCount(), backgroundEnd.corpseCount()); assertEquals(backgroundStart.playerX(), backgroundEnd.playerX(), 0.01f); assertEquals(backgroundStart.elapsedMilliseconds(), backgroundEnd.elapsedMilliseconds()); instrumentation.runOnMainSync(activity[0]::onResume); GameView.AutomationSnapshot returned = snapshot(instrumentation, view[0]); assertTrue("Returning to an active puzzle must show the pause screen", returned.visiblyPaused()); assertFalse("Held touch input must not survive backgrounding", returned.inputActive()); assertEquals(backgroundEnd.deaths(), returned.deaths()); assertEquals(backgroundEnd.corpseCount(), returned.corpseCount()); scenario.onActivity(MainActivity::onBackPressed); sleep(80L); GameView.AutomationSnapshot resumed = snapshot(instrumentation, view[0]); assertFalse("Back from the visible pause screen must resume the puzzle", resumed.visiblyPaused()); runOnGame(instrumentation, view[0], false, false, true, false); GameView.AutomationSnapshot moving = waitForMovement( instrumentation, view[0], resumed.playerX(), 10_000L); runOnGame(instrumentation, view[0], false, false, false, false); assertTrue("The resumed simulation must accept movement", moving.playerX() > resumed.playerX() + 20f); } } private static GameView.AutomationSnapshot waitForMovement( Instrumentation instrumentation, GameView gameView, float startX, long timeoutMillis) { long deadline = System.currentTimeMillis() + timeoutMillis; GameView.AutomationSnapshot current = snapshot(instrumentation, gameView); while (System.currentTimeMillis() < deadline) { if (current.playerX() > startX + 20f) return current; sleep(24L); current = snapshot(instrumentation, gameView); } throw new AssertionError( "The resumed simulation did not move within " + timeoutMillis + "ms; startX=" + startX + ", finalX=" + current.playerX() + ", alive=" + current.playerAlive() + ", paused=" + current.visiblyPaused() + ", inputActive=" + current.inputActive()); } private static void createRetainedCorpse(Instrumentation instrumentation, GameView gameView) { runOnGame(instrumentation, gameView, true, false, true, false); long deadline = System.currentTimeMillis() + 8_000L; boolean died = false; while (System.currentTimeMillis() < deadline) { GameView.AutomationSnapshot value = snapshot(instrumentation, gameView); if (!value.playerAlive()) died = true; if (died && value.playerAlive() && value.corpseCount() >= 1) { runOnGame(instrumentation, gameView, false, false, false, false); return; } sleep(24L); } throw new AssertionError("Level 5 did not produce a retained corpse before lifecycle check"); } private static void runOnGame(Instrumentation instrumentation, GameView gameView, boolean startLevelFive, boolean left, boolean right, boolean jump) { instrumentation.runOnMainSync(() -> { if (startLevelFive) gameView.startLevelForAutomation(5); gameView.setAutomationInput(left, right, jump); }); } private static GameView.AutomationSnapshot snapshot(Instrumentation instrumentation, GameView gameView) { final GameView.AutomationSnapshot[] value = new GameView.AutomationSnapshot[1]; instrumentation.runOnMainSync(() -> value[0] = gameView.automationSnapshot()); return value[0]; } private static void sleep(long millis) { try { Thread.sleep(millis); } catch (InterruptedException interrupted) { Thread.currentThread().interrupt(); throw new AssertionError("Lifecycle test interrupted", interrupted); } } }