mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2026-10-21 06:52:35 +00:00
add download tests and fix bugs
This commit is contained in:
committed by
Aayush Gupta
parent
1aef95837c
commit
acc9a39bb1
-4
@@ -47,7 +47,6 @@ class AndroidDownloadStorageActions(context: Context) : DownloadStorageActions {
|
||||
if (folder == Uri.EMPTY) {
|
||||
return null
|
||||
}
|
||||
// Plain file paths read better without their scheme; SAF trees have no readable path.
|
||||
return when (folder.scheme) {
|
||||
ContentResolver.SCHEME_FILE -> folder.path
|
||||
else -> folder.toString()
|
||||
@@ -56,13 +55,10 @@ class AndroidDownloadStorageActions(context: Context) : DownloadStorageActions {
|
||||
|
||||
@Composable
|
||||
override fun rememberFolderPicker(onResult: (FolderPickResult) -> Unit): () -> Unit {
|
||||
// The screen passes a fresh lambda on every recomposition, so read it through a state
|
||||
// holder instead of capturing the first one inside remember below.
|
||||
val currentOnResult by rememberUpdatedState(onResult)
|
||||
|
||||
val launcher = rememberLauncherForActivityResult(StartActivityForResult()) { result ->
|
||||
if (result.resultCode != Activity.RESULT_OK) {
|
||||
// The user backed out of the picker; the legacy screen stays silent too.
|
||||
return@rememberLauncherForActivityResult
|
||||
}
|
||||
val folder = result.data?.data
|
||||
|
||||
@@ -16,14 +16,14 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||
import androidx.compose.ui.unit.dp
|
||||
import net.newpipe.app.preview.ThemePreviewProvider
|
||||
|
||||
private const val DISABLED_ALPHA = 0.38f
|
||||
private val TRAILING_CONTENT_END_PADDING = 16.dp
|
||||
private val SWITCH_END_PADDING = 16.dp
|
||||
|
||||
/**
|
||||
* A preference row with a trailing switch, mirroring AndroidX SwitchPreferenceCompat.
|
||||
@@ -45,26 +45,27 @@ fun SwitchPreference(
|
||||
enabled: Boolean = true
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.alpha(if (enabled) 1f else DISABLED_ALPHA)
|
||||
.toggleable(
|
||||
value = checked,
|
||||
enabled = enabled,
|
||||
role = Role.Switch,
|
||||
onValueChange = onCheckedChange
|
||||
)
|
||||
.semantics(mergeDescendants = true) {},
|
||||
modifier = modifier.alpha(if (enabled) 1f else DISABLED_ALPHA),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
ListItem(
|
||||
modifier = Modifier.weight(1f),
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.toggleable(
|
||||
value = checked,
|
||||
enabled = enabled,
|
||||
role = Role.Switch,
|
||||
onValueChange = onCheckedChange
|
||||
),
|
||||
headlineContent = { Text(text = title) },
|
||||
supportingContent = summary?.let { text -> { Text(text = text) } }
|
||||
)
|
||||
Switch(
|
||||
modifier = Modifier.padding(end = TRAILING_CONTENT_END_PADDING),
|
||||
modifier = Modifier
|
||||
.padding(end = SWITCH_END_PADDING)
|
||||
.clearAndSetSemantics {},
|
||||
checked = checked,
|
||||
onCheckedChange = null,
|
||||
onCheckedChange = onCheckedChange,
|
||||
enabled = enabled
|
||||
)
|
||||
}
|
||||
|
||||
+9
-4
@@ -46,6 +46,7 @@ import net.newpipe.app.preferences.DownloadPreferences.DEFAULT_PAUSE_ON_METERED_
|
||||
import net.newpipe.app.preferences.DownloadPreferences.DEFAULT_REPLACEMENT_CHARACTER
|
||||
import net.newpipe.app.preferences.DownloadPreferences.DEFAULT_STORAGE_ASK
|
||||
import net.newpipe.app.preferences.DownloadPreferences.DEFAULT_STORAGE_USE_SAF
|
||||
import net.newpipe.app.preferences.DownloadPreferences.MAXIMUM_RETRIES_NONE
|
||||
import net.newpipe.app.preferences.DownloadPreferences.MAXIMUM_RETRY_COUNTS
|
||||
import net.newpipe.app.preview.ThemePreviewProvider
|
||||
import net.newpipe.app.viewmodel.settings.DownloadSettingsViewModel
|
||||
@@ -71,11 +72,11 @@ import newpipe.shared.generated.resources.general_error
|
||||
import newpipe.shared.generated.resources.invalid_directory
|
||||
import newpipe.shared.generated.resources.max_retry_desc
|
||||
import newpipe.shared.generated.resources.max_retry_msg
|
||||
import newpipe.shared.generated.resources.minimize_on_exit_none_description
|
||||
import newpipe.shared.generated.resources.no_app_to_open_intent
|
||||
import newpipe.shared.generated.resources.no_appropriate_file_manager_message
|
||||
import newpipe.shared.generated.resources.no_appropriate_file_manager_message_android_10
|
||||
import newpipe.shared.generated.resources.no_available_dir
|
||||
import newpipe.shared.generated.resources.none
|
||||
import newpipe.shared.generated.resources.ok
|
||||
import newpipe.shared.generated.resources.pause_downloads_on_mobile
|
||||
import newpipe.shared.generated.resources.pause_downloads_on_mobile_desc
|
||||
@@ -187,9 +188,13 @@ fun DownloadSettingsScreenContent(
|
||||
)
|
||||
)
|
||||
|
||||
val noRetriesLabel = stringResource(Res.string.minimize_on_exit_none_description)
|
||||
val maximumRetryEntries = listOf(ListPreferenceEntry(noRetriesLabel, noRetriesLabel)) +
|
||||
MAXIMUM_RETRY_COUNTS.map { count -> ListPreferenceEntry(count, count) }
|
||||
val noRetriesLabel = stringResource(Res.string.none)
|
||||
val maximumRetryEntries = MAXIMUM_RETRY_COUNTS.map { count ->
|
||||
ListPreferenceEntry(
|
||||
value = count,
|
||||
title = if (count == MAXIMUM_RETRIES_NONE) noRetriesLabel else count
|
||||
)
|
||||
}
|
||||
|
||||
val storageAskSummary = when {
|
||||
storageAccessFrameworkConfigurable ->
|
||||
|
||||
+35
-11
@@ -26,6 +26,7 @@ import net.newpipe.app.preferences.DownloadPreferences.KEY_PAUSE_ON_METERED_NETW
|
||||
import net.newpipe.app.preferences.DownloadPreferences.KEY_REPLACEMENT_CHARACTER
|
||||
import net.newpipe.app.preferences.DownloadPreferences.KEY_STORAGE_ASK
|
||||
import net.newpipe.app.preferences.DownloadPreferences.KEY_STORAGE_USE_SAF
|
||||
import net.newpipe.app.preferences.DownloadPreferences.normalizedMaximumRetries
|
||||
import org.koin.core.annotation.KoinViewModel
|
||||
|
||||
@KoinViewModel
|
||||
@@ -53,15 +54,19 @@ class DownloadSettingsViewModel(
|
||||
field = stringFlow(KEY_REPLACEMENT_CHARACTER, DEFAULT_REPLACEMENT_CHARACTER)
|
||||
|
||||
val maximumRetries: StateFlow<String>
|
||||
field = stringFlow(KEY_MAXIMUM_RETRIES, DEFAULT_MAXIMUM_RETRIES)
|
||||
field = MutableStateFlow(repairedMaximumRetries())
|
||||
|
||||
val pauseOnMeteredNetwork: StateFlow<Boolean>
|
||||
field = booleanFlow(KEY_PAUSE_ON_METERED_NETWORK, DEFAULT_PAUSE_ON_METERED_NETWORK)
|
||||
field = booleanFlow(
|
||||
KEY_PAUSE_ON_METERED_NETWORK,
|
||||
DEFAULT_PAUSE_ON_METERED_NETWORK
|
||||
)
|
||||
|
||||
val limitQueue: StateFlow<Boolean>
|
||||
field = booleanFlow(KEY_LIMIT_QUEUE, DEFAULT_LIMIT_QUEUE)
|
||||
|
||||
fun setStorageAsk(value: Boolean) = storageAsk.persist(KEY_STORAGE_ASK, value)
|
||||
fun setStorageAsk(value: Boolean) = storageAsk
|
||||
.persist(KEY_STORAGE_ASK, value)
|
||||
|
||||
fun setUseStorageAccessFramework(value: Boolean) {
|
||||
useStorageAccessFramework.persist(KEY_STORAGE_USE_SAF, value)
|
||||
@@ -81,23 +86,33 @@ class DownloadSettingsViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun setVideoDownloadFolder(value: String) = videoDownloadFolder.persistFolder(KEY_DOWNLOAD_FOLDER_VIDEO, value)
|
||||
fun setVideoDownloadFolder(value: String) = videoDownloadFolder
|
||||
.persistFolder(KEY_DOWNLOAD_FOLDER_VIDEO, value)
|
||||
|
||||
fun setAudioDownloadFolder(value: String) = audioDownloadFolder.persistFolder(KEY_DOWNLOAD_FOLDER_AUDIO, value)
|
||||
fun setAudioDownloadFolder(value: String) = audioDownloadFolder
|
||||
.persistFolder(KEY_DOWNLOAD_FOLDER_AUDIO, value)
|
||||
|
||||
fun setFileCharset(value: String) = fileCharset.persist(KEY_FILE_CHARSET, value)
|
||||
fun setFileCharset(value: String) = fileCharset
|
||||
.persist(KEY_FILE_CHARSET, value)
|
||||
|
||||
fun setReplacementCharacter(value: String) = replacementCharacter.persist(KEY_REPLACEMENT_CHARACTER, value)
|
||||
fun setReplacementCharacter(value: String) = replacementCharacter
|
||||
.persist(KEY_REPLACEMENT_CHARACTER, value)
|
||||
|
||||
fun setMaximumRetries(value: String) = maximumRetries.persist(KEY_MAXIMUM_RETRIES, value)
|
||||
fun setMaximumRetries(value: String) = maximumRetries
|
||||
.persist(KEY_MAXIMUM_RETRIES, value)
|
||||
|
||||
fun setPauseOnMeteredNetwork(value: Boolean) = pauseOnMeteredNetwork.persist(KEY_PAUSE_ON_METERED_NETWORK, value)
|
||||
fun setPauseOnMeteredNetwork(value: Boolean) = pauseOnMeteredNetwork
|
||||
.persist(KEY_PAUSE_ON_METERED_NETWORK, value)
|
||||
|
||||
fun setLimitQueue(value: Boolean) = limitQueue.persist(KEY_LIMIT_QUEUE, value)
|
||||
|
||||
private fun stringFlow(key: String, defaultValue: String) = MutableStateFlow(settings.getString(key, defaultValue))
|
||||
private fun stringFlow(key: String, defaultValue: String) = MutableStateFlow(
|
||||
settings.getString(key, defaultValue)
|
||||
)
|
||||
|
||||
private fun booleanFlow(key: String, defaultValue: Boolean) = MutableStateFlow(settings.getBoolean(key, defaultValue))
|
||||
private fun booleanFlow(key: String, defaultValue: Boolean) = MutableStateFlow(
|
||||
settings.getBoolean(key, defaultValue)
|
||||
)
|
||||
|
||||
private fun MutableStateFlow<String>.persist(key: String, newValue: String) {
|
||||
settings.putString(key, newValue)
|
||||
@@ -117,4 +132,13 @@ class DownloadSettingsViewModel(
|
||||
}
|
||||
value = newValue
|
||||
}
|
||||
|
||||
private fun repairedMaximumRetries(): String {
|
||||
val stored = settings.getString(KEY_MAXIMUM_RETRIES, DEFAULT_MAXIMUM_RETRIES)
|
||||
val repaired = normalizedMaximumRetries(stored)
|
||||
if (repaired != stored) {
|
||||
settings.putString(KEY_MAXIMUM_RETRIES, repaired)
|
||||
}
|
||||
return repaired
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
package net.newpipe.app.composable
|
||||
|
||||
import androidx.compose.ui.test.ExperimentalTestApi
|
||||
import androidx.compose.ui.test.assertIsNotEnabled
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.v2.runComposeUiTest
|
||||
@@ -44,4 +45,21 @@ class SwitchPreferenceTest {
|
||||
onNodeWithText(title).performClick()
|
||||
assertFalse(fired)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun disabledRowReportsItsStateOnTheTitle() = runComposeUiTest {
|
||||
val title = "Enable feature"
|
||||
setContent {
|
||||
SwitchPreference(
|
||||
title = title,
|
||||
summary = "Some explanation",
|
||||
checked = false,
|
||||
onCheckedChange = {},
|
||||
enabled = false
|
||||
)
|
||||
}
|
||||
|
||||
// The title and the switch state must be one node
|
||||
onNodeWithText(title).assertIsNotEnabled()
|
||||
}
|
||||
}
|
||||
|
||||
+4
-2
@@ -6,7 +6,9 @@
|
||||
package net.newpipe.app.screen.settings
|
||||
|
||||
import androidx.compose.ui.test.ExperimentalTestApi
|
||||
import androidx.compose.ui.test.assertCountEquals
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.onAllNodesWithText
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performScrollTo
|
||||
@@ -53,8 +55,8 @@ class DebugSettingsScreenTest {
|
||||
modules = listOf(emptySettings),
|
||||
content = { DebugSettingsScreenContent(leakCanaryAvailable = false) },
|
||||
onContent = {
|
||||
onNodeWithText(getString(Res.string.leak_canary_not_available))
|
||||
.assertIsDisplayed()
|
||||
onAllNodesWithText(getString(Res.string.leak_canary_not_available))
|
||||
.assertCountEquals(2)
|
||||
onNodeWithText(getString(Res.string.enable_leak_canary_summary))
|
||||
.assertDoesNotExist()
|
||||
}
|
||||
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package net.newpipe.app.screen.settings
|
||||
|
||||
import androidx.compose.ui.test.ExperimentalTestApi
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.assertIsNotEnabled
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.v2.runComposeUiTest
|
||||
import com.russhwolf.settings.MapSettings
|
||||
import com.russhwolf.settings.Settings
|
||||
import kotlin.test.Test
|
||||
import net.newpipe.app.extensions.withKoin
|
||||
import newpipe.shared.generated.resources.Res
|
||||
import newpipe.shared.generated.resources.download_path_summary
|
||||
import newpipe.shared.generated.resources.download_path_title
|
||||
import newpipe.shared.generated.resources.downloads_storage_use_saf_summary_api_29
|
||||
import newpipe.shared.generated.resources.downloads_storage_use_saf_title
|
||||
import newpipe.shared.generated.resources.max_retry_desc
|
||||
import newpipe.shared.generated.resources.settings_category_downloads_title
|
||||
import org.jetbrains.compose.resources.getString
|
||||
import org.koin.dsl.module
|
||||
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
class DownloadSettingsScreenTest {
|
||||
|
||||
private val emptySettings = module {
|
||||
single<Settings> { MapSettings() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rendersTitleAndUnsetDownloadFolder() = runComposeUiTest {
|
||||
withKoin(
|
||||
modules = listOf(emptySettings),
|
||||
content = { DownloadSettingsScreenContent() },
|
||||
onContent = {
|
||||
onNodeWithText(getString(Res.string.settings_category_downloads_title))
|
||||
.assertIsDisplayed()
|
||||
onNodeWithText(getString(Res.string.download_path_summary)).assertIsDisplayed()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun downloadFolderIsDisabledWhileEveryDownloadAsks() = runComposeUiTest {
|
||||
withKoin(
|
||||
modules = listOf(emptySettings),
|
||||
content = { DownloadSettingsScreenContent(storageAsk = true) },
|
||||
onContent = {
|
||||
onNodeWithText(getString(Res.string.download_path_title)).assertIsNotEnabled()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun storageAccessFrameworkIsLockedWhenNotConfigurable() = runComposeUiTest {
|
||||
withKoin(
|
||||
modules = listOf(emptySettings),
|
||||
content = {
|
||||
DownloadSettingsScreenContent(storageAccessFrameworkConfigurable = false)
|
||||
},
|
||||
onContent = {
|
||||
onNodeWithText(getString(Res.string.downloads_storage_use_saf_summary_api_29))
|
||||
.assertIsDisplayed()
|
||||
onNodeWithText(getString(Res.string.downloads_storage_use_saf_title))
|
||||
.assertIsNotEnabled()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun maximumRetriesKeepsItsStaticSummary() = runComposeUiTest {
|
||||
withKoin(
|
||||
modules = listOf(emptySettings),
|
||||
content = { DownloadSettingsScreenContent() },
|
||||
onContent = {
|
||||
onNodeWithText(getString(Res.string.max_retry_desc)).assertIsDisplayed()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user