Skip to content

Commit 60d323c

Browse files
committed
Add unit tests for NewPlayer
1 parent 6f6d224 commit 60d323c

File tree

9 files changed

+1070
-4
lines changed

9 files changed

+1070
-4
lines changed

gradle/libs.versions.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@ junit = "4.13.2"
2525
junitVersion = "1.2.1"
2626
espressoCore = "3.6.1"
2727
appcompat = "1.7.0"
28+
kotlinxCoroutinesTest = "1.9.0"
2829
material = "1.12.0"
2930
androidx = "1.9.0"
3031
constraintlayout = "2.1.4"
3132
material3 = "1.2.1"
33+
mockk = "1.13.13"
3234
uiTooling = "1.6.8"
3335
materialIconsExtendedAndroid = "1.7.0-beta05"
3436
media3 = "1.3.1"
@@ -55,6 +57,7 @@ junit = { group = "junit", name = "junit", version.ref = "junit" }
5557
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
5658
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
5759
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
60+
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutinesTest" }
5861
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
5962
androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "androidx" }
6063
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
@@ -79,6 +82,7 @@ androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
7982
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
8083
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
8184
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
85+
mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
8286
okhttp-android = { group = "com.squareup.okhttp3", name = "okhttp-android", version.ref = "okhttpAndroid" }
8387
coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" }
8488
reorderable = { group = "sh.calvin.reorderable", name = "reorderable", version.ref = "reorderable" }

new-player/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ dependencies {
8181
ksp(libs.androidx.hilt.compiler)
8282

8383
testImplementation(libs.junit)
84+
testImplementation(libs.kotlinx.coroutines.test)
85+
testImplementation(libs.mockk)
8486

8587
androidTestImplementation(libs.androidx.junit)
8688
androidTestImplementation(libs.androidx.espresso.core)

new-player/src/main/java/net/newpipe/newplayer/repository/PrefetchingRepository.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,10 @@ class PrefetchingRepository(
118118
* Manually trigger a prefetch of [item] without performing an actual request.
119119
*/
120120
suspend fun prefetch(item:String) {
121-
requestAll(item)
121+
if(!hasBeenSeenBefore.contains(item)) {
122+
hasBeenSeenBefore.add(item)
123+
requestAll(item)
124+
}
122125
}
123126

124127
/**

new-player/src/main/java/net/newpipe/newplayer/uiModel/NewPlayerViewModelImpl.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -514,9 +514,8 @@ class NewPlayerViewModelImpl @Inject constructor(
514514
}
515515

516516
override fun seekingFinished() {
517-
val seekerPosition = mutableUiState.value.seekerPosition
518-
val seekPositionInMs = (newPlayer?.duration?.toFloat() ?: 0F) * seekerPosition
519-
newPlayer?.currentPosition = seekPositionInMs.toLong()
517+
newPlayer?.currentPosition = getSeekerPositionInMs(mutableUiState.value)
518+
520519
mutableUiState.update {
521520
it.copy(seekPreviewVisible = false)
522521
}
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
package net.newpipe.newplayer
2+
3+
import android.app.Activity
4+
import android.app.Application
5+
import android.content.ComponentName
6+
import android.content.Intent
7+
import android.content.pm.PackageManager
8+
import android.content.pm.ResolveInfo
9+
import android.content.pm.ServiceInfo
10+
import android.os.Looper
11+
import android.text.TextUtils
12+
import android.util.Log
13+
import androidx.media3.common.MediaItem
14+
import androidx.media3.exoplayer.ExoPlayer
15+
import androidx.media3.session.MediaController
16+
import androidx.media3.session.SessionToken
17+
import com.google.common.util.concurrent.Futures
18+
import io.mockk.clearMocks
19+
import io.mockk.every
20+
import io.mockk.mockk
21+
import io.mockk.mockkConstructor
22+
import io.mockk.mockkStatic
23+
import io.mockk.verify
24+
import kotlinx.coroutines.Dispatchers
25+
import kotlinx.coroutines.ExperimentalCoroutinesApi
26+
import kotlinx.coroutines.test.UnconfinedTestDispatcher
27+
import kotlinx.coroutines.test.resetMain
28+
import kotlinx.coroutines.test.setMain
29+
import net.newpipe.newplayer.repository.DelayTestRepository
30+
import net.newpipe.newplayer.repository.MockMediaRepository
31+
import org.junit.Before
32+
import org.junit.BeforeClass
33+
import org.junit.Ignore
34+
import org.junit.Test
35+
36+
@OptIn(ExperimentalCoroutinesApi::class)
37+
class NewPlayerImpltest {
38+
val mockExoPlayer = mockk<ExoPlayer>(relaxed = true)
39+
val mockMediaController = mockk<MediaController>(relaxed = true)
40+
val mockApp = mockk<Application>(relaxed = true)
41+
val playerActivityClass = Activity::class.java
42+
val repository = DelayTestRepository(MockMediaRepository(), 100)
43+
var player = NewPlayerImpl(mockApp, playerActivityClass, repository)
44+
45+
init {
46+
mockkStatic(Looper::class)
47+
mockkStatic(TextUtils::class)
48+
mockkStatic(Log::class)
49+
mockkStatic(ExoPlayer::class)
50+
mockkStatic(ExoPlayer.Builder::class)
51+
mockkStatic(TextUtils::class)
52+
mockkConstructor(ComponentName::class)
53+
mockkConstructor(Intent::class)
54+
mockkConstructor(SessionToken::class)
55+
mockkConstructor(ExoPlayer.Builder::class)
56+
mockkConstructor(MediaController.Builder::class)
57+
58+
val mockLooper = mockk<Looper>(relaxed = true)
59+
val mockPackageManager = mockk<PackageManager>(relaxed = true)
60+
val mockResolveInfo = mockk<ResolveInfo>(relaxed = true)
61+
val mockServiceInfo = mockk<ServiceInfo>(relaxed = true)
62+
mockResolveInfo.serviceInfo = mockServiceInfo
63+
mockServiceInfo.name = "ComponentNameTest"
64+
65+
every { Looper.myLooper() } returns mockLooper
66+
every { TextUtils.isEmpty(any()) } returns true
67+
every { Log.i(any(), any()) } returns 1
68+
every { anyConstructed<ComponentName>().packageName } returns "net.newpipe.newplayer.test"
69+
every { anyConstructed<ComponentName>().className } returns "ComponentNameTest"
70+
every { anyConstructed<ComponentName>().hashCode() } returns 1
71+
every { anyConstructed<Intent>().setPackage(any()) } returns mockk<Intent>(relaxed = true)
72+
every { anyConstructed<ExoPlayer.Builder>().build() } returns mockExoPlayer
73+
every { TextUtils.equals(any(), any()) } returns true
74+
every { anyConstructed<MediaController.Builder>().buildAsync() } returns Futures.immediateFuture(mockMediaController)
75+
every { mockApp.packageManager } returns mockPackageManager
76+
every { mockPackageManager.queryIntentServices(any(), any<Int>()) } returns listOf(mockResolveInfo)
77+
}
78+
79+
companion object {
80+
@JvmStatic
81+
@BeforeClass
82+
fun init() {
83+
Dispatchers.setMain(UnconfinedTestDispatcher())
84+
}
85+
86+
@JvmStatic
87+
@BeforeClass
88+
fun reset() {
89+
Dispatchers.resetMain()
90+
}
91+
}
92+
93+
@Before
94+
fun resetSpy() {
95+
clearMocks(mockExoPlayer)
96+
player = NewPlayerImpl(mockApp, playerActivityClass, repository)
97+
}
98+
99+
@Test
100+
fun onPlayBackError_pauseExoPlayer() {
101+
player.prepare()
102+
player.onPlayBackError(Exception("test"))
103+
verify (exactly = 1) { mockExoPlayer.pause() }
104+
}
105+
106+
@Test
107+
fun prepare_setupNewExoplayer() {
108+
player.prepare()
109+
verify (exactly = 1) { anyConstructed<ExoPlayer.Builder>().build() }
110+
}
111+
112+
@Test
113+
fun prepare_notSetupNewExoPlayerWhenAlreadySetUp() {
114+
// Setup new ExoPlayer
115+
player.prepare()
116+
117+
// Call prepare with an already set up ExoPlayer
118+
player.prepare()
119+
120+
//ExoPlayer should be built only one time
121+
verify (exactly = 1) { anyConstructed<ExoPlayer.Builder>().build() }
122+
}
123+
124+
@Test
125+
fun prepare_prepareExoPlayer() {
126+
player.prepare()
127+
verify (exactly = 1) { mockExoPlayer.prepare() }
128+
}
129+
130+
@Test
131+
fun prepare_setUpMediaController() {
132+
player.prepare()
133+
verify (exactly = 1) { anyConstructed<MediaController.Builder>().buildAsync() }
134+
}
135+
136+
@Test
137+
fun prepare_notSetUpMediaController() {
138+
// Setup media controller
139+
player.prepare()
140+
141+
// Call prepare with an already set up media controller
142+
player.prepare()
143+
144+
//Media controller should be built only one time
145+
verify (exactly = 1) { anyConstructed<MediaController.Builder>().buildAsync() }
146+
}
147+
148+
@Test
149+
fun play_playIfCurrentMediaItemIsNotNull() {
150+
player.prepare()
151+
player.play()
152+
verify (exactly = 1) { mockExoPlayer.play() }
153+
}
154+
155+
@Test
156+
fun play_notPlayIfCurrentMediaItemIsNull() {
157+
player.prepare()
158+
every { mockExoPlayer.currentMediaItem } returns null
159+
player.play()
160+
verify (exactly = 0) { mockExoPlayer.play() }
161+
}
162+
163+
@Test
164+
fun pause() {
165+
player.prepare()
166+
player.pause()
167+
verify (exactly = 1) { mockExoPlayer.pause() }
168+
}
169+
170+
@Test
171+
fun addToPlaylist_prepareExoPlayerIfNotPrepared() {
172+
player.addToPlaylist("item")
173+
verify (exactly = 1) { mockExoPlayer.prepare() }
174+
}
175+
176+
@Test
177+
fun addToPlaylist_notPrepareExoPlayerIfAlreadyPrepared() {
178+
player.prepare()
179+
clearMocks(mockExoPlayer)
180+
player.addToPlaylist("item")
181+
verify (exactly = 0) { mockExoPlayer.prepare() }
182+
}
183+
184+
@Ignore("Find a way to test code inside launchJobAndCollectError")
185+
@Test
186+
fun addToPlaylist_addMediaSource() {
187+
player.addToPlaylist("item")
188+
// try {
189+
// Dispatchers.Main.job.join()
190+
// } catch (e : Exception) { }
191+
// coVerify (exactly = 2) { mockExoPlayer.addMediaSource(any()) }
192+
}
193+
194+
@Test
195+
fun movePlaylistItem() {
196+
player.prepare()
197+
player.movePlaylistItem(0, 1)
198+
verify (exactly = 1){ mockExoPlayer.moveMediaItem(0, 1) }
199+
}
200+
201+
@Test
202+
fun removePlaylistItem_removeItem() {
203+
player.prepare()
204+
val mediaItem = MediaItem.Builder().setMediaId("123").build()
205+
every { mockExoPlayer.mediaItemCount } returns 1
206+
every { mockExoPlayer.getMediaItemAt(any()) } returns mediaItem
207+
player.removePlaylistItem(123)
208+
verify (exactly = 1) { mockExoPlayer.removeMediaItem(0) }
209+
}
210+
211+
@Test
212+
fun removePlaylistItem_notRemoveItem() {
213+
player.prepare()
214+
val mediaItem = MediaItem.Builder().setMediaId("123").build()
215+
every { mockExoPlayer.mediaItemCount } returns 1
216+
every { mockExoPlayer.getMediaItemAt(any()) } returns mediaItem
217+
player.removePlaylistItem(124)
218+
verify (exactly = 0) { mockExoPlayer.removeMediaItem(0) }
219+
}
220+
221+
@Ignore("Mock currentChapters.value and test the selection of a chapter")
222+
@Test
223+
fun selectChapter() {
224+
player.selectChapter(0)
225+
}
226+
227+
@Test(expected = IndexOutOfBoundsException::class)
228+
fun selectChapter_throwsException() {
229+
player.selectChapter(3)
230+
}
231+
232+
@Test
233+
fun release() {
234+
player.prepare()
235+
player.release()
236+
verify (exactly = 1) { mockMediaController.release() }
237+
verify (exactly = 1) { mockExoPlayer.release() }
238+
}
239+
240+
}

0 commit comments

Comments
 (0)