diff options
Diffstat (limited to 'app/src')
3 files changed, 290 insertions, 0 deletions
diff --git a/app/src/androidTest/java/org/terst/listen/ui/EpisodeTransportTest.kt b/app/src/androidTest/java/org/terst/listen/ui/EpisodeTransportTest.kt new file mode 100644 index 0000000..26c6d4a --- /dev/null +++ b/app/src/androidTest/java/org/terst/listen/ui/EpisodeTransportTest.kt @@ -0,0 +1,105 @@ +package org.terst.listen.ui + +import androidx.compose.ui.test.assertIsEnabled +import androidx.compose.ui.test.assertIsNotEnabled +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +/** + * The episode transport, driven as a composable rather than looked at. + * + * The first Compose test in this app. Everything visual has been verified by eye on the emulator + * until now, which catches "does it look right" and never catches "does the button do the thing + * when the state changes". + */ +@RunWith(AndroidJUnit4::class) +class EpisodeTransportTest { + + @get:Rule + val compose = createComposeRule() + + private fun playback(isPlaying: Boolean, positionMs: Long = 0) = EpisodePlayback( + positionMs = positionMs, + durationMs = 600_000, + isPlaying = isPlaying, + breaks = emptyList(), + ) + + private fun actions( + onAd: () -> Unit = {}, + onSkip: (Long) -> Unit = {}, + onToggle: () -> Unit = {}, + ) = EpisodeDetailActions( + enqueue = {}, download = {}, downloadFromPublisher = {}, togglePlayed = {}, + markOlderPlayed = {}, togglePlayPause = onToggle, seekTo = {}, skip = onSkip, + setSpeed = {}, reportAd = onAd, + ) + + @Test + fun theAdButtonIsOfferedWhileSomethingIsPlaying() { + compose.setContent { EpisodeTransport(playback(isPlaying = true), 1.0f, actions()) } + + compose.onNodeWithContentDescription("This is an ad").assertIsEnabled() + } + + /** + * Nothing is playing, so there is no advertising to be in the middle of. An enabled button + * would record a claim about a position nobody is at. + */ + @Test + fun theAdButtonIsNotOfferedWhenNothingIsPlaying() { + compose.setContent { EpisodeTransport(playback(isPlaying = false), 1.0f, actions()) } + + compose.onNodeWithContentDescription("This is an ad").assertIsNotEnabled() + } + + @Test + fun pressingTheAdButtonReportsIt() { + var reported = 0 + compose.setContent { EpisodeTransport(playback(isPlaying = true), 1.0f, actions(onAd = { reported++ })) } + + compose.onNodeWithContentDescription("This is an ad").performClick() + + assertEquals(1, reported) + } + + /** The primary control says what pressing it will do, which is the whole reason it is worded. */ + @Test + fun theMainControlSaysPauseWhilePlaying() { + compose.setContent { EpisodeTransport(playback(isPlaying = true), 1.0f, actions()) } + + compose.onNodeWithText("Pause").assertIsEnabled() + } + + @Test + fun theMainControlSaysResumeWhenStoppedPartWayThrough() { + compose.setContent { EpisodeTransport(playback(isPlaying = false, positionMs = 60_000), 1.0f, actions()) } + + compose.onNodeWithText("Resume").assertIsEnabled() + } + + @Test + fun theMainControlSaysPlayFromTheStart() { + compose.setContent { EpisodeTransport(playback(isPlaying = false), 1.0f, actions()) } + + compose.onNodeWithText("Play").assertIsEnabled() + } + + @Test + fun theSkipButtonsMoveByTheAmountTheyAdvertise() { + val moved = mutableListOf<Long>() + compose.setContent { EpisodeTransport(playback(isPlaying = true), 1.0f, actions(onSkip = { moved += it })) } + + compose.onNodeWithContentDescription("Back 15 seconds").performClick() + compose.onNodeWithContentDescription("Forward 25 seconds").performClick() + + assertEquals(listOf(-15_000L, 25_000L), moved) + } +} diff --git a/app/src/androidTest/java/org/terst/listen/ui/NowPlayingBarTest.kt b/app/src/androidTest/java/org/terst/listen/ui/NowPlayingBarTest.kt new file mode 100644 index 0000000..483e934 --- /dev/null +++ b/app/src/androidTest/java/org/terst/listen/ui/NowPlayingBarTest.kt @@ -0,0 +1,91 @@ +package org.terst.listen.ui + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.terst.listen.playback.NowPlaying + +/** + * The only persistent chrome in the app, and the way back to whatever is playing. + * + * Worth testing because it is the thing that saves you when a screen is showing something else: + * if it stops naming the episode, or its play button stops reporting state, the app looks fine + * and strands you. + */ +@RunWith(AndroidJUnit4::class) +class NowPlayingBarTest { + + @get:Rule + val compose = createComposeRule() + + private fun playing(isPlaying: Boolean = true) = NowPlaying( + episodeId = "e1", + title = "Ukraine's President Sits With NPR", + showTitle = "Up First from NPR", + isPlaying = isPlaying, + positionMs = 281_000, + durationMs = 908_000, + speed = 1.25f, + ) + + private fun actions( + onToggle: () -> Unit = {}, + onOpenEpisode: () -> Unit = {}, + onOpenQueue: () -> Unit = {}, + ) = NowPlayingActions( + togglePlayPause = onToggle, + skipBack = {}, + skipForward = {}, + openEpisode = onOpenEpisode, + openQueue = onOpenQueue, + ) + + @Test + fun namesTheEpisodeAndTheShow() { + compose.setContent { NowPlayingBar(playing(), actions()) } + + compose.onNodeWithText("Ukraine's President Sits With NPR", substring = true).assertIsDisplayed() + compose.onNodeWithText("Up First from NPR", substring = true).assertIsDisplayed() + } + + @Test + fun tappingTheBarOpensWhatIsPlaying() { + var opened = 0 + compose.setContent { NowPlayingBar(playing(), actions(onOpenEpisode = { opened++ })) } + + compose.onNodeWithText("Ukraine's President Sits With NPR", substring = true).performClick() + + assertEquals("the bar is the way back to the player", 1, opened) + } + + @Test + fun offersPauseWhilePlaying() { + compose.setContent { NowPlayingBar(playing(isPlaying = true), actions()) } + + compose.onNodeWithContentDescription("Pause").assertIsDisplayed() + } + + @Test + fun offersPlayWhileStopped() { + compose.setContent { NowPlayingBar(playing(isPlaying = false), actions()) } + + compose.onNodeWithContentDescription("Play").assertIsDisplayed() + } + + @Test + fun theControlActuallyToggles() { + var toggled = 0 + compose.setContent { NowPlayingBar(playing(), actions(onToggle = { toggled++ })) } + + compose.onNodeWithContentDescription("Pause").performClick() + + assertEquals(1, toggled) + } +} diff --git a/app/src/androidTest/java/org/terst/listen/ui/SkipReadoutTest.kt b/app/src/androidTest/java/org/terst/listen/ui/SkipReadoutTest.kt new file mode 100644 index 0000000..49d8da3 --- /dev/null +++ b/app/src/androidTest/java/org/terst/listen/ui/SkipReadoutTest.kt @@ -0,0 +1,94 @@ +package org.terst.listen.ui + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.terst.listen.fingerprint.EpisodeSkips +import org.terst.listen.fingerprint.SkipSummary + +/** + * The three numbers under the timeline, and the legend beside them. + * + * This is the sanity check Peter asked for: the gap between served and declared is the + * advertising in the file, and "cut" is what the detector took out, so a cut far from that gap + * is the detector being wrong. The point of testing it is that a readout which quietly stops + * saying one of the three looks completely normal. + */ +@RunWith(AndroidJUnit4::class) +class SkipReadoutTest { + + @get:Rule + val compose = createComposeRule() + + private fun marker(kind: String) = + SkipSummary.BreakMarker(start = 0.1f, end = 0.2f, kind = kind, startMs = 60_000, endMs = 120_000) + + @Test + fun showsAllThreeNumbersOnOneLine() { + val skips = EpisodeSkips(served = "15:08", declared = "13:30", cut = "0:47", status = null) + + compose.setContent { SkipReadout(skips, listOf(marker("ad"))) } + + compose.onNodeWithText("Served 15:08 · Declared 13:30 · Cut 0:47").assertIsDisplayed() + } + + /** + * "Nothing found" and "not checked yet" are different answers, and collapsing them is the + * mistake the old readout made. + */ + @Test + fun saysWhyWhenThereIsNothingToCut() { + val skips = EpisodeSkips(served = "15:08", declared = "13:30", cut = null, status = "Not checked yet") + + compose.setContent { SkipReadout(skips, emptyList()) } + + compose.onNodeWithText("Not checked yet").assertIsDisplayed() + } + + @Test + fun theLegendNamesAdvertisingWhenSomeIsMarked() { + compose.setContent { + SkipReadout(EpisodeSkips(null, null, "0:47", null), listOf(marker("ad"))) + } + + compose.onNodeWithText(SkipSummary.LEGEND_ADVERTISING).assertIsDisplayed() + } + + /** + * The one distinction that changes what happens: advertising is always cut, the show's own + * audio follows the setting. The legend only claims what is actually on the track. + */ + @Test + fun theLegendNamesTheShowsOwnAudioWhenThatIsWhatIsMarked() { + compose.setContent { + SkipReadout(EpisodeSkips(null, null, "0:12", null), listOf(marker("outro"))) + } + + compose.onNodeWithText(SkipSummary.LEGEND_OWN_AUDIO).assertIsDisplayed() + compose.onNodeWithText(SkipSummary.LEGEND_ADVERTISING).assertDoesNotExist() + } + + @Test + fun bothAreNamedWhenBothAreOnTheTrack() { + compose.setContent { + SkipReadout(EpisodeSkips(null, null, "1:00", null), listOf(marker("ad"), marker("cue"))) + } + + compose.onNodeWithText(SkipSummary.LEGEND_ADVERTISING).assertIsDisplayed() + compose.onNodeWithText(SkipSummary.LEGEND_OWN_AUDIO).assertIsDisplayed() + } + + @Test + fun noLegendWhenNothingIsMarked() { + compose.setContent { + SkipReadout(EpisodeSkips("15:08", "13:30", null, "None found"), emptyList()) + } + + compose.onNodeWithText(SkipSummary.LEGEND_ADVERTISING).assertDoesNotExist() + compose.onNodeWithText(SkipSummary.LEGEND_OWN_AUDIO).assertDoesNotExist() + } +} |
