mirror of
https://github.com/TeamNewPipe/NewPipe.git
synced 2025-10-03 09:49:21 +02:00
Updated ERefactor ErrorPanel to use ErrorInfo directly and remove UI models
This commit is contained in:
parent
19fb9899cd
commit
8292f729ea
11 changed files with 237 additions and 316 deletions
|
@ -0,0 +1,119 @@
|
|||
package org.schabi.newpipe.ui.components.video.comment
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.paging.LoadState
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.schabi.newpipe.error.ErrorInfo
|
||||
import org.schabi.newpipe.error.UserAction
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
|
||||
import org.schabi.newpipe.ui.components.common.ErrorAction
|
||||
import org.schabi.newpipe.ui.components.common.determineErrorAction
|
||||
import org.schabi.newpipe.viewmodels.util.Resource
|
||||
import java.io.IOException
|
||||
import java.net.SocketTimeoutException
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class CommentSectionErrorIntegrationTest {
|
||||
|
||||
private lateinit var context: Context
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
context = ApplicationProvider.getApplicationContext()
|
||||
}
|
||||
|
||||
// Test 1: Network error on initial load (Resource.Error)
|
||||
@Test
|
||||
fun testInitialCommentNetworkError() {
|
||||
val expectedMessage = "Connection timeout"
|
||||
val networkError = SocketTimeoutException(expectedMessage)
|
||||
val resourceError = Resource.Error(networkError)
|
||||
|
||||
val errorInfo = ErrorInfo(
|
||||
throwable = resourceError.throwable,
|
||||
userAction = UserAction.REQUESTED_COMMENTS,
|
||||
request = "comments"
|
||||
)
|
||||
assertEquals(networkError, errorInfo.throwable)
|
||||
assertEquals(ErrorAction.REPORT, determineErrorAction(errorInfo))
|
||||
assertEquals(expectedMessage, errorInfo.getExplanation())
|
||||
}
|
||||
|
||||
// Test 2: Network error on paging (LoadState.Error)
|
||||
@Test
|
||||
fun testPagingNetworkError() {
|
||||
val expectedMessage = "Paging failed"
|
||||
val pagingError = IOException(expectedMessage)
|
||||
val loadStateError = LoadState.Error(pagingError)
|
||||
|
||||
val errorInfo = ErrorInfo(
|
||||
throwable = loadStateError.error,
|
||||
userAction = UserAction.REQUESTED_COMMENTS,
|
||||
request = "comments"
|
||||
)
|
||||
assertEquals(pagingError, errorInfo.throwable)
|
||||
assertEquals(ErrorAction.REPORT, determineErrorAction(errorInfo))
|
||||
assertEquals(expectedMessage, errorInfo.getExplanation())
|
||||
}
|
||||
|
||||
// Test 3: ReCaptcha during comments load
|
||||
@Test
|
||||
fun testReCaptchaDuringComments() {
|
||||
val url = "https://www.google.com/recaptcha/api/fallback?k=test"
|
||||
val expectedMessage = "ReCaptcha needed"
|
||||
val captcha = ReCaptchaException(expectedMessage, url)
|
||||
val errorInfo = ErrorInfo(
|
||||
throwable = captcha,
|
||||
userAction = UserAction.REQUESTED_COMMENTS,
|
||||
request = "comments"
|
||||
)
|
||||
assertEquals(ErrorAction.SOLVE_CAPTCHA, determineErrorAction(errorInfo))
|
||||
assertEquals(expectedMessage, errorInfo.getExplanation())
|
||||
|
||||
val intent = Intent(context, org.schabi.newpipe.error.ReCaptchaActivity::class.java).apply {
|
||||
putExtra(org.schabi.newpipe.error.ReCaptchaActivity.RECAPTCHA_URL_EXTRA, url)
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
assertEquals(url, intent.getStringExtra(org.schabi.newpipe.error.ReCaptchaActivity.RECAPTCHA_URL_EXTRA))
|
||||
}
|
||||
|
||||
// Test 4: Retry functionality integration with ErrorPanel
|
||||
@Test
|
||||
fun testRetryIntegrationWithErrorPanel() {
|
||||
val expectedMessage = "Network request failed"
|
||||
val networkError = IOException(expectedMessage)
|
||||
val loadStateError = LoadState.Error(networkError)
|
||||
|
||||
val errorInfo = ErrorInfo(
|
||||
throwable = loadStateError.error,
|
||||
userAction = UserAction.REQUESTED_COMMENTS,
|
||||
request = "comments"
|
||||
)
|
||||
|
||||
val errorAction = determineErrorAction(errorInfo)
|
||||
assertEquals("Network errors should get REPORT action", ErrorAction.REPORT, errorAction)
|
||||
var retryCallbackInvoked = false
|
||||
val mockCommentRetry = {
|
||||
retryCallbackInvoked = true
|
||||
}
|
||||
|
||||
mockCommentRetry()
|
||||
assertTrue("Retry callback should be invoked when user clicks retry", retryCallbackInvoked)
|
||||
|
||||
assertEquals(
|
||||
"Error explanation should be available for retry scenarios",
|
||||
expectedMessage, errorInfo.getExplanation()
|
||||
)
|
||||
assertEquals(
|
||||
"Error should maintain comment context for retry",
|
||||
UserAction.REQUESTED_COMMENTS, errorInfo.userAction
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
package org.schabi.newpipe.ui.components.video.comments
|
||||
|
||||
import android.net.http.NetworkException
|
||||
import androidx.paging.LoadState
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.schabi.newpipe.MainActivity
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.ui.UiModel.UnableToLoadCommentsUiModel
|
||||
import org.schabi.newpipe.viewmodels.util.Resource
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class CommentSectionErrorTest {
|
||||
|
||||
@get:Rule
|
||||
val activityRule = ActivityScenarioRule(MainActivity::class.java)
|
||||
|
||||
/**
|
||||
* Test Resource.Error state - when initial comment info loading fails
|
||||
*/
|
||||
class TestNetworkException : NetworkException("Connection attempt timed out", null) {
|
||||
override fun getErrorCode(): Int = NetworkException.ERROR_CONNECTION_TIMED_OUT
|
||||
override fun isImmediatelyRetryable() = true
|
||||
}
|
||||
@Test
|
||||
fun testResourceErrorState_ShowsUnableToLoadCommentsUiModel() {
|
||||
|
||||
val networkException = TestNetworkException()
|
||||
val errorResource = Resource.Error(networkException)
|
||||
assertEquals("Should contain the network exception", networkException, errorResource.throwable)
|
||||
|
||||
val errorModel = UnableToLoadCommentsUiModel(networkException)
|
||||
val spec = errorModel.spec
|
||||
|
||||
assertEquals("Should have correct message resource", R.string.error_unable_to_load_comments, spec.messageRes)
|
||||
assertTrue("Should show retry button", spec.showRetry)
|
||||
assertTrue("Should show report button", spec.showReport)
|
||||
assertFalse("Should NOT show open in browser button", spec.showOpenInBrowser)
|
||||
}
|
||||
|
||||
/**
|
||||
* Test LoadState.Error state - when paging data loading fails
|
||||
*/
|
||||
@Test
|
||||
fun testLoadStateErrorState_ShowsUnableToLoadCommentsUiModel() {
|
||||
val pagingException = RuntimeException("Paging data loading failed")
|
||||
val loadStateError = LoadState.Error(pagingException)
|
||||
|
||||
assertEquals("Should contain the paging exception", pagingException, loadStateError.error)
|
||||
|
||||
val errorModel = UnableToLoadCommentsUiModel(pagingException)
|
||||
val spec = errorModel.spec
|
||||
|
||||
assertEquals("Should have correct message resource", R.string.error_unable_to_load_comments, spec.messageRes)
|
||||
assertTrue("Should show retry button", spec.showRetry)
|
||||
assertTrue("Should show report button", spec.showReport)
|
||||
assertFalse("Should NOT show open in browser button", spec.showOpenInBrowser)
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
package org.schabi.newpipe.error
|
||||
|
||||
import org.schabi.newpipe.ui.UiModel.ErrorUiModel
|
||||
import org.schabi.newpipe.ui.UiModel.GenericErrorUiModel
|
||||
import org.schabi.newpipe.ui.UiModel.UnableToLoadCommentsUiModel
|
||||
|
||||
fun mapThrowableToErrorUiModel(throwable: Throwable, userAction: UserAction? = null): ErrorUiModel {
|
||||
if (userAction == UserAction.REQUESTED_COMMENTS) {
|
||||
|
||||
return UnableToLoadCommentsUiModel(rawError = throwable)
|
||||
}
|
||||
// Other ErrorInfo logic and throwable + user actions
|
||||
return GenericErrorUiModel(rawError = throwable)
|
||||
}
|
|
@ -17,7 +17,7 @@ class CommentsSource(private val commentInfo: CommentInfo) : PagingSource<Page,
|
|||
override suspend fun load(params: LoadParams<Page>): LoadResult<Page, CommentsInfoItem> {
|
||||
// params.key is null the first time the load() function is called, so we need to return the
|
||||
// first batch of already-loaded comments
|
||||
return LoadResult.Error(IOException("💥 forced test error"))
|
||||
|
||||
if (params.key == null) {
|
||||
return LoadResult.Page(commentInfo.comments, null, commentInfo.nextPage)
|
||||
} else {
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
package org.schabi.newpipe.ui.UiModel
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.ui.components.common.ErrorPanelSpec
|
||||
|
||||
/**
|
||||
* Each concrete case from this hierarchy represents a different failure state that the UI can render with ErrorPanel
|
||||
*/
|
||||
@Immutable
|
||||
sealed interface ErrorUiModel {
|
||||
val spec: ErrorPanelSpec
|
||||
val rawError: Throwable? get() = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Concrete cases - Comments unable to load, Comments disabled, No connectivity, DNS failure, timeout etc
|
||||
*/
|
||||
@Immutable
|
||||
data class UnableToLoadCommentsUiModel(override val rawError: Throwable?) : ErrorUiModel {
|
||||
override val spec: ErrorPanelSpec =
|
||||
ErrorPanelSpec(
|
||||
messageRes = R.string.error_unable_to_load_comments,
|
||||
showRetry = true,
|
||||
showReport = true,
|
||||
showOpenInBrowser = false
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A generic ErrorUiModel for unhandled cases
|
||||
*/
|
||||
@Immutable
|
||||
data class GenericErrorUiModel(override val rawError: Throwable?) : ErrorUiModel {
|
||||
override val spec: ErrorPanelSpec =
|
||||
ErrorPanelSpec(
|
||||
messageRes = R.string.general_error,
|
||||
showRetry = true,
|
||||
showReport = true,
|
||||
)
|
||||
}
|
|
@ -1,117 +1,132 @@
|
|||
package org.schabi.newpipe.ui.components.common
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.error.ErrorInfo
|
||||
import org.schabi.newpipe.error.ErrorUtil
|
||||
import org.schabi.newpipe.error.ReCaptchaActivity
|
||||
import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
|
||||
import org.schabi.newpipe.extractor.timeago.patterns.it
|
||||
import org.schabi.newpipe.ui.theme.AppTheme
|
||||
import org.schabi.newpipe.ui.theme.SizeTokens.SpacingExtraLarge
|
||||
import org.schabi.newpipe.ui.theme.SizeTokens.SpacingLarge
|
||||
import org.schabi.newpipe.ui.theme.SizeTokens.SpacingMedium
|
||||
import org.schabi.newpipe.ui.theme.SizeTokens.SpacingSmall
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils
|
||||
|
||||
enum class ErrorAction(@StringRes val actionStringId: Int) {
|
||||
REPORT(R.string.error_snackbar_action),
|
||||
SOLVE_CAPTCHA(R.string.recaptcha_solve)
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the error action type based on the throwable in ErrorInfo
|
||||
*
|
||||
*/
|
||||
fun determineErrorAction(errorInfo: ErrorInfo): ErrorAction {
|
||||
return when (errorInfo.throwable) {
|
||||
is ReCaptchaException -> ErrorAction.SOLVE_CAPTCHA
|
||||
is AccountTerminatedException -> ErrorAction.REPORT
|
||||
else -> ErrorAction.REPORT
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ErrorPanel(
|
||||
spec: ErrorPanelSpec,
|
||||
onRetry: () -> Unit,
|
||||
onReport: () -> Unit,
|
||||
onOpenInBrowser: () -> Unit,
|
||||
errorInfo: ErrorInfo,
|
||||
onRetry: (() -> Unit)? = null,
|
||||
modifier: Modifier = Modifier
|
||||
|
||||
) {
|
||||
val explanation = errorInfo.getExplanation()
|
||||
val canOpenInBrowser = errorInfo.openInBrowserUrl != null
|
||||
val errorActionType = determineErrorAction(errorInfo)
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.wrapContentWidth()
|
||||
.padding(horizontal = SpacingLarge, vertical = SpacingMedium)
|
||||
|
||||
) {
|
||||
|
||||
val message = stringResource(spec.messageRes)
|
||||
|
||||
Text(
|
||||
text = message,
|
||||
text = stringResource(errorInfo.messageStringId),
|
||||
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
spec.serviceInfoRes?.let { infoRes ->
|
||||
if (explanation.isNotBlank()) {
|
||||
Spacer(Modifier.height(SpacingSmall))
|
||||
val serviceInfo = stringResource(infoRes)
|
||||
Text(
|
||||
text = serviceInfo,
|
||||
text = explanation,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
|
||||
spec.serviceExplanationRes?.let { explanationRes ->
|
||||
Spacer(Modifier.height(SpacingSmall))
|
||||
val serviceExplanation = stringResource(explanationRes)
|
||||
Text(
|
||||
text = serviceExplanation,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(SpacingMedium))
|
||||
if (spec.showReport) {
|
||||
ServiceColoredButton(onClick = onReport) {
|
||||
Text(stringResource(R.string.error_snackbar_action).uppercase())
|
||||
when (errorActionType) {
|
||||
ErrorAction.REPORT -> {
|
||||
ServiceColoredButton(onClick = {
|
||||
ErrorUtil.openActivity(context, errorInfo)
|
||||
}) {
|
||||
Text(stringResource(errorActionType.actionStringId).uppercase())
|
||||
}
|
||||
}
|
||||
ErrorAction.SOLVE_CAPTCHA -> {
|
||||
ServiceColoredButton(onClick = {
|
||||
// Starting ReCaptcha Challenge Activity
|
||||
val intent = Intent(context, ReCaptchaActivity::class.java).apply {
|
||||
putExtra(
|
||||
ReCaptchaActivity.RECAPTCHA_URL_EXTRA,
|
||||
(errorInfo.throwable as ReCaptchaException).url
|
||||
)
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
context.startActivity(intent)
|
||||
}) {
|
||||
Text(stringResource(errorActionType.actionStringId).uppercase())
|
||||
}
|
||||
}
|
||||
}
|
||||
if (spec.showRetry) {
|
||||
ServiceColoredButton(onClick = onRetry) {
|
||||
|
||||
onRetry?.let {
|
||||
ServiceColoredButton(onClick = it) {
|
||||
Text(stringResource(R.string.retry).uppercase())
|
||||
}
|
||||
}
|
||||
if (spec.showOpenInBrowser) {
|
||||
ServiceColoredButton(onClick = onOpenInBrowser) {
|
||||
if (canOpenInBrowser) {
|
||||
ServiceColoredButton(onClick = {
|
||||
errorInfo.openInBrowserUrl?.let { url ->
|
||||
ShareUtils.openUrlInBrowser(context, url)
|
||||
}
|
||||
}) {
|
||||
Text(stringResource(R.string.open_in_browser).uppercase())
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(SpacingExtraLarge))
|
||||
}
|
||||
}
|
||||
|
||||
data class ErrorPanelSpec(
|
||||
@StringRes val messageRes: Int,
|
||||
@StringRes val serviceInfoRes: Int? = null,
|
||||
val serviceExplanation: String? = null,
|
||||
@StringRes val serviceExplanationRes: Int? = null,
|
||||
val showRetry: Boolean = false,
|
||||
val showReport: Boolean = false,
|
||||
val showOpenInBrowser: Boolean = false
|
||||
)
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360, heightDp = 640)
|
||||
|
||||
@Composable
|
||||
fun ErrorPanelPreview() {
|
||||
AppTheme {
|
||||
ErrorPanel(
|
||||
spec = ErrorPanelSpec(
|
||||
messageRes = android.R.string.httpErrorBadUrl,
|
||||
showRetry = true,
|
||||
showReport = false,
|
||||
showOpenInBrowser = false
|
||||
),
|
||||
onRetry = {},
|
||||
onReport = {},
|
||||
onOpenInBrowser = {},
|
||||
modifier = Modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,64 +0,0 @@
|
|||
package org.schabi.newpipe.ui.components.video.comment
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource.Companion.SideEffect
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import org.schabi.newpipe.error.ErrorInfo
|
||||
import org.schabi.newpipe.error.UserAction
|
||||
import org.schabi.newpipe.error.mapThrowableToErrorUiModel
|
||||
import org.schabi.newpipe.ui.UiModel.ErrorUiModel
|
||||
import org.schabi.newpipe.ui.components.common.ErrorPanel
|
||||
import java.io.IOException
|
||||
|
||||
@Composable
|
||||
fun CommentErrorHandler(
|
||||
throwable: Throwable,
|
||||
userAction: UserAction,
|
||||
onRetry: () -> Unit,
|
||||
onReport: (ErrorInfo) -> Unit
|
||||
) {
|
||||
SideEffect {
|
||||
Log.d("CommentErrorHandler", "⛔️ rendered for error: ${throwable.message}")
|
||||
}
|
||||
|
||||
val uiModel: ErrorUiModel = mapThrowableToErrorUiModel(throwable, userAction)
|
||||
val errorInfo = ErrorInfo(
|
||||
throwable = throwable,
|
||||
userAction = userAction,
|
||||
request = ""
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
ErrorPanel(
|
||||
spec = uiModel.spec,
|
||||
onRetry = onRetry,
|
||||
onReport = { onReport(errorInfo) },
|
||||
onOpenInBrowser = {},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun PreviewCommentErrorHandler() {
|
||||
CommentErrorHandler(
|
||||
throwable = IOException("No network"),
|
||||
userAction = UserAction.REQUESTED_COMMENTS,
|
||||
onRetry = {},
|
||||
onReport = {}
|
||||
)
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
package org.schabi.newpipe.ui.components.video.comment
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
|
@ -11,6 +13,7 @@ import androidx.compose.material3.Surface
|
|||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
@ -26,11 +29,12 @@ import androidx.paging.compose.collectAsLazyPagingItems
|
|||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.error.ErrorUtil
|
||||
import org.schabi.newpipe.error.ErrorInfo
|
||||
import org.schabi.newpipe.error.UserAction
|
||||
import org.schabi.newpipe.extractor.Page
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
|
||||
import org.schabi.newpipe.extractor.stream.Description
|
||||
import org.schabi.newpipe.ui.components.common.ErrorPanel
|
||||
import org.schabi.newpipe.ui.components.common.LazyColumnThemedScrollbar
|
||||
import org.schabi.newpipe.ui.components.common.LoadingIndicator
|
||||
import org.schabi.newpipe.ui.emptystate.EmptyStateComposable
|
||||
|
@ -78,6 +82,7 @@ private fun CommentSection(
|
|||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 128.dp)
|
||||
|
||||
)
|
||||
}
|
||||
} else if (count == 0) {
|
||||
|
@ -109,13 +114,25 @@ private fun CommentSection(
|
|||
}
|
||||
}
|
||||
is LoadState.Error -> {
|
||||
val error = (comments.loadState.refresh as LoadState.Error).error
|
||||
val errorInfo = ErrorInfo(
|
||||
throwable = error,
|
||||
userAction = UserAction.REQUESTED_COMMENTS,
|
||||
request = "comments"
|
||||
)
|
||||
|
||||
item {
|
||||
CommentErrorHandler(
|
||||
throwable = (comments.loadState.refresh as LoadState.Error).error,
|
||||
userAction = UserAction.REQUESTED_COMMENTS,
|
||||
onRetry = { comments.retry() },
|
||||
onReport = { info -> ErrorUtil.openActivity(context, info) },
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
||||
) {
|
||||
ErrorPanel(
|
||||
errorInfo = errorInfo,
|
||||
onRetry = { comments.retry() },
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
|
@ -127,13 +144,23 @@ private fun CommentSection(
|
|||
}
|
||||
}
|
||||
is Resource.Error -> {
|
||||
val errorInfo = ErrorInfo(
|
||||
throwable = uiState.throwable,
|
||||
userAction = UserAction.REQUESTED_COMMENTS,
|
||||
request = "comments"
|
||||
)
|
||||
item {
|
||||
CommentErrorHandler(
|
||||
throwable = uiState.throwable,
|
||||
userAction = UserAction.REQUESTED_COMMENTS,
|
||||
onRetry = { comments.retry() },
|
||||
onReport = { info -> ErrorUtil.openActivity(context, info) },
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
ErrorPanel(
|
||||
errorInfo = errorInfo,
|
||||
onRetry = { comments.retry() },
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,6 @@ import org.schabi.newpipe.viewmodels.util.Resource
|
|||
class CommentsViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
|
||||
val uiState = savedStateHandle.getStateFlow(KEY_URL, "")
|
||||
.map {
|
||||
// Resource.Error(RuntimeException("Forced error for testing"))
|
||||
try {
|
||||
Resource.Success(CommentInfo(CommentsInfo.getInfo(it)))
|
||||
} catch (e: Exception) {
|
||||
|
|
|
@ -214,6 +214,14 @@
|
|||
android:layout_marginTop="@dimen/video_item_detail_error_panel_margin"
|
||||
android:visibility="gone"
|
||||
tools:visibility="gone" />
|
||||
<androidx.compose.ui.platform.ComposeView
|
||||
android:id="@+id/compose_error_panel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/detail_title_root_layout"
|
||||
android:layout_marginTop="@dimen/video_item_detail_error_panel_margin"
|
||||
android:visibility="gone"
|
||||
/>
|
||||
|
||||
<!--HIDING ROOT-->
|
||||
<LinearLayout
|
||||
|
|
|
@ -1,63 +0,0 @@
|
|||
package org.schabi.newpipe.error
|
||||
|
||||
import android.net.http.NetworkException
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.ui.UiModel.GenericErrorUiModel
|
||||
import org.schabi.newpipe.ui.UiModel.UnableToLoadCommentsUiModel
|
||||
|
||||
class ErrorUiModelTest {
|
||||
|
||||
/**
|
||||
* Test that when comments fail to load, the correct error panel is rendered
|
||||
*/
|
||||
@Test
|
||||
fun `mapThrowableToErrorUiModel with REQUESTED_COMMENTS returns UnableToLoadCommentsUiModel`() {
|
||||
// val throwable = RuntimeException("Comments failed to load")
|
||||
val networkException = object : NetworkException("Connection attempt timed out", null) {
|
||||
override fun getErrorCode() = NetworkException.ERROR_CONNECTION_TIMED_OUT
|
||||
override fun isImmediatelyRetryable() = true
|
||||
}
|
||||
val result = mapThrowableToErrorUiModel(networkException, UserAction.REQUESTED_COMMENTS)
|
||||
assertTrue("Result should be UnableToLoadCommentsUiModel", result is UnableToLoadCommentsUiModel)
|
||||
assertEquals("Raw error should be preserved for debugging", networkException, result.rawError)
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the fallback logic
|
||||
*/
|
||||
@Test
|
||||
fun `mapThrowableToErrorUiModel with null UserAction returns GenericErrorUiModel`() {
|
||||
val throwable = RuntimeException("Test error")
|
||||
val result = mapThrowableToErrorUiModel(throwable, null)
|
||||
|
||||
assertTrue("Should return GenericErrorUiModel", result is GenericErrorUiModel)
|
||||
assertEquals("Should preserve the original throwable", throwable, result.rawError)
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that UnableToLoadCommentsUiModel maps to the correct error panel configuration
|
||||
*/
|
||||
@Test
|
||||
fun `UnableToLoadCommentsUiModel has correct ErrorPanelSpec`() {
|
||||
val throwable = RuntimeException("Test error")
|
||||
val errorModel = UnableToLoadCommentsUiModel(throwable)
|
||||
val spec = errorModel.spec
|
||||
// Assert: Verify the spec has the correct configuration for comment loading errors
|
||||
assertEquals(
|
||||
"Error message should be 'Unable to load comments'",
|
||||
R.string.error_unable_to_load_comments,
|
||||
spec.messageRes,
|
||||
)
|
||||
|
||||
assertTrue("Retry button should be shown for comment loading errors", spec.showRetry)
|
||||
assertTrue("Report button should be shown for comment loading errors", spec.showReport)
|
||||
assertFalse("Open in browser should NOT be shown for comment loading errors", spec.showOpenInBrowser)
|
||||
|
||||
// Assert: Verify the raw error is set correctly
|
||||
assertEquals("Raw error should be preserved for debugging", throwable, errorModel.rawError)
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue