| 1 | package codewhale.pet |
| 2 | |
| 3 | import androidx.compose.ui.test.junit4.createAndroidComposeRule |
| 4 | import androidx.compose.ui.test.onNodeWithText |
| 5 | import androidx.compose.ui.test.performClick |
| 6 | import androidx.lifecycle.Lifecycle |
| 7 | import androidx.lifecycle.ViewModelProvider |
| 8 | import android.net.Uri |
| 9 | import org.json.JSONObject |
| 10 | import org.junit.Assert.* |
| 11 | import org.junit.Rule |
| 12 | import org.junit.Test |
| 13 | |
| 14 | class PetLifecycleTest { |
| 15 | @get:Rule val compose = createAndroidComposeRule<PetActivity>() |
| 16 | |
| 17 | @Test fun pauseStillSoundAndBackgroundLifecycleUseTheVisibleWorld() { |
| 18 | val model = ViewModelProvider(compose.activity)[PetViewModel::class.java] |
| 19 | model.mode(PetMode.WILD) // Explicit isolated lifecycle fixture. |
| 20 | compose.waitUntil(15_000) { model.ui.value.scene != null } |
| 21 | assertFalse(model.ui.value.sound) |
| 22 | compose.runOnIdle { model.setStill(false); model.setPaused(false) } |
| 23 | compose.onNodeWithText("Pause").performClick() |
| 24 | compose.waitUntil(5_000) { model.ui.value.paused && !model.ui.value.running } |
| 25 | val paused = model.ui.value.scene!! |
| 26 | Thread.sleep(300) |
| 27 | assertEquals(paused.timeMs, model.ui.value.scene!!.timeMs, 0.0) |
| 28 | assertArrayEquals(paused.dots, model.ui.value.scene!!.dots, 0f) |
| 29 | compose.onNodeWithText("More").performClick() |
| 30 | compose.onNodeWithText("Reopen saved habitat").performClick() |
| 31 | compose.onNodeWithText("Reopen the saved habitat?").assertExists() |
| 32 | compose.onNodeWithText("Cancel").performClick() |
| 33 | assertEquals(paused.timeMs, model.ui.value.scene!!.timeMs, 0.0) |
| 34 | compose.onNodeWithText("Still off").performClick() |
| 35 | compose.onNodeWithText("Sound off").performClick() |
| 36 | compose.onNodeWithText("Resume").performClick() |
| 37 | compose.waitUntil(5_000) { model.ui.value.still && model.ui.value.scene!!.timeMs > paused.timeMs + 200 } |
| 38 | assertTrue(model.ui.value.sound) |
| 39 | Thread.sleep(100) |
| 40 | val still = model.ui.value.scene!! |
| 41 | Thread.sleep(200) |
| 42 | assertArrayEquals(still.dots, model.ui.value.scene!!.dots, 0f) |
| 43 | compose.activityRule.scenario.moveToState(Lifecycle.State.CREATED) |
| 44 | compose.waitUntil(5_000) { !model.ui.value.running && model.ui.value.savedAtMs == model.ui.value.scene!!.timeMs } |
| 45 | val hidden = model.ui.value.scene!! |
| 46 | Thread.sleep(300) |
| 47 | assertEquals(hidden.timeMs, model.ui.value.scene!!.timeMs, 0.0) |
| 48 | assertFalse(Thread.getAllStackTraces().keys.any { it.name == "codewhale-pet-audio" && it.isAlive }) |
| 49 | assertEquals(hidden.timeMs, model.ui.value.savedAtMs!!, 0.0) |
| 50 | compose.activityRule.scenario.moveToState(Lifecycle.State.RESUMED) |
| 51 | compose.waitUntil(5_000) { model.ui.value.scene!!.timeMs > hidden.timeMs } |
| 52 | compose.onNodeWithText("Sound on").performClick() |
| 53 | compose.onNodeWithText("Still on").performClick() |
| 54 | assertFalse(model.ui.value.sound) |
| 55 | } |
| 56 | |
| 57 | @Test fun localLiveFileStaysFreshAcrossPauseBackgroundAndProducerRestart() { |
| 58 | val model = ViewModelProvider(compose.activity)[PetViewModel::class.java] |
| 59 | model.mode(PetMode.WILD) // This fixture imports an isolated telemetry file. |
| 60 | compose.waitUntil(15_000) { model.ui.value.scene != null } |
| 61 | val resolver = compose.activity.contentResolver |
| 62 | val uri = Uri.parse("content://dev.shannonlabs.codewhale.pet.test.live/tape") |
| 63 | val human = compose.activity.assets.open("demo.jsonl").bufferedReader().use { input -> |
| 64 | input.lineSequence().map(::JSONObject).first { it.getString("channel") == "human" && it.getBoolean("waiting") }.toString() |
| 65 | } |
| 66 | fun packet(sequence: Int) = JSONObject(human).put("sequence", sequence).put("simTimeMs", sequence * 400).toString() + "\n" |
| 67 | fun action(name: String, text: String? = null) = resolver.call(uri, name, text, null)!!.getInt("reads") |
| 68 | fun observed() = model.ui.value.scene!!.state.observed >= .92 |
| 69 | action("replace", packet(10)) |
| 70 | val initialReads = action("stats") |
| 71 | compose.runOnIdle { model.setPaused(false); model.followLocalTape(uri) } |
| 72 | try { |
| 73 | compose.waitUntil(10_000) { model.ui.value.mode == PetMode.LIVE && action("stats") >= initialReads + 2 } |
| 74 | assertFalse("Existing file must establish a baseline", observed()) |
| 75 | action("append", packet(11)) |
| 76 | compose.waitUntil(5_000) { observed() && model.ui.value.scene!!.state.channel == "human" } |
| 77 | compose.onNodeWithText("Pause").performClick() |
| 78 | compose.waitUntil(5_000) { model.ui.value.paused && !model.ui.value.running } |
| 79 | val stoppedReads = action("stats") |
| 80 | action("append", packet(12)); Thread.sleep(600) |
| 81 | assertEquals("Pause closes live polling", stoppedReads, action("stats")) |
| 82 | compose.onNodeWithText("Resume").performClick() |
| 83 | compose.waitUntil(5_000) { model.ui.value.running && action("stats") >= stoppedReads + 2 } |
| 84 | assertFalse("Background bytes are not fresh requests", observed()) |
| 85 | assertEquals("none", model.ui.value.scene!!.needs) |
| 86 | action("append", packet(13)) |
| 87 | compose.waitUntil(5_000) { observed() } |
| 88 | compose.waitUntil(5_000) { !observed() } |
| 89 | val beforeRestart = action("stats") |
| 90 | action("replace", packet(0)) |
| 91 | compose.waitUntil(5_000) { action("stats") >= beforeRestart + 2 } |
| 92 | assertFalse(observed()) |
| 93 | action("append", packet(1)) |
| 94 | compose.waitUntil(5_000) { observed() } |
| 95 | compose.activityRule.scenario.moveToState(Lifecycle.State.CREATED) |
| 96 | compose.waitUntil(5_000) { !model.ui.value.running } |
| 97 | val backgroundReads = action("stats") |
| 98 | action("append", packet(2)); Thread.sleep(600) |
| 99 | assertEquals(backgroundReads, action("stats")) |
| 100 | compose.activityRule.scenario.moveToState(Lifecycle.State.RESUMED) |
| 101 | compose.waitUntil(5_000) { model.ui.value.running && action("stats") >= backgroundReads + 2 } |
| 102 | assertFalse(observed()) |
| 103 | val beforeError = model.ui.value.scene!!.timeMs |
| 104 | action("replace", "{invalid}\n") |
| 105 | compose.waitUntil(5_000) { model.ui.value.message?.startsWith("Invalid local telemetry") == true } |
| 106 | compose.waitUntil(5_000) { model.ui.value.scene!!.timeMs > beforeError + 800 } |
| 107 | assertFalse(model.ui.value.paused) |
| 108 | assertFalse(observed()) |
| 109 | } finally { |
| 110 | compose.activityRule.scenario.moveToState(Lifecycle.State.RESUMED) |
| 111 | compose.runOnIdle { model.mode(PetMode.WILD) } |
| 112 | compose.waitUntil(5_000) { model.ui.value.mode == PetMode.WILD } |
| 113 | // Settle this activity's final frame/save before the next test opens |
| 114 | // the same private habitat with a new ViewModel and store revision. |
| 115 | compose.runOnIdle { model.setPaused(true) } |
| 116 | compose.waitUntil(5_000) { model.ui.value.paused && !model.ui.value.running && model.ui.value.savedAtMs == model.ui.value.scene!!.timeMs } |
| 117 | action("delete") |
| 118 | } |
| 119 | } |
| 120 | } |
| 121 |