mirror of
https://github.com/TeamNewPipe/NewPipe.git
synced 2025-10-03 01:39:38 +02:00
feat(ui):Add ErrorPanel composable to Comments Section, related UI models, and tests
This commit is contained in:
parent
abfde872f1
commit
da4878d264
10 changed files with 436 additions and 12 deletions
|
@ -0,0 +1,64 @@
|
|||
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
|
||||
*/
|
||||
@Test
|
||||
fun testResourceErrorState_ShowsUnableToLoadCommentsUiModel() {
|
||||
|
||||
val networkException = object : NetworkException("Connection attempt timed out", null) {
|
||||
override fun getErrorCode(): Int = NetworkException.ERROR_CONNECTION_TIMED_OUT
|
||||
override fun isImmediatelyRetryable() = true
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
14
app/src/main/java/org/schabi/newpipe/error/ErrorUIMapper.kt
Normal file
14
app/src/main/java/org/schabi/newpipe/error/ErrorUIMapper.kt
Normal file
|
@ -0,0 +1,14 @@
|
|||
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)
|
||||
}
|
|
@ -9,6 +9,7 @@ import org.schabi.newpipe.extractor.Page
|
|||
import org.schabi.newpipe.extractor.comments.CommentsInfo
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
|
||||
import org.schabi.newpipe.ui.components.video.comment.CommentInfo
|
||||
import java.io.IOException
|
||||
|
||||
class CommentsSource(private val commentInfo: CommentInfo) : PagingSource<Page, CommentsInfoItem>() {
|
||||
private val service = NewPipe.getService(commentInfo.serviceId)
|
||||
|
@ -16,12 +17,14 @@ 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 {
|
||||
val info = withContext(Dispatchers.IO) {
|
||||
CommentsInfo.getMoreItems(service, commentInfo.url, params.key)
|
||||
}
|
||||
|
||||
return LoadResult.Page(info.items, null, info.nextPage)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
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,
|
||||
)
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
package org.schabi.newpipe.ui.components.common
|
||||
|
||||
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.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.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
|
||||
|
||||
@Composable
|
||||
fun ErrorPanel(
|
||||
spec: ErrorPanelSpec,
|
||||
onRetry: () -> Unit,
|
||||
onReport: () -> Unit,
|
||||
onOpenInBrowser: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.wrapContentWidth()
|
||||
.padding(horizontal = SpacingLarge, vertical = SpacingMedium)
|
||||
|
||||
) {
|
||||
|
||||
val message = stringResource(spec.messageRes)
|
||||
|
||||
Text(
|
||||
text = message,
|
||||
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
spec.serviceInfoRes?.let { infoRes ->
|
||||
Spacer(Modifier.height(SpacingSmall))
|
||||
val serviceInfo = stringResource(infoRes)
|
||||
Text(
|
||||
text = serviceInfo,
|
||||
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())
|
||||
}
|
||||
}
|
||||
if (spec.showRetry) {
|
||||
ServiceColoredButton(onClick = onRetry) {
|
||||
Text(stringResource(R.string.retry).uppercase())
|
||||
}
|
||||
}
|
||||
if (spec.showOpenInBrowser) {
|
||||
ServiceColoredButton(onClick = onOpenInBrowser) {
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
package org.schabi.newpipe.ui.components.common
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.schabi.newpipe.ui.theme.AppTheme
|
||||
import org.schabi.newpipe.ui.theme.SizeTokens.SpacingMedium
|
||||
import org.schabi.newpipe.ui.theme.SizeTokens.SpacingSmall
|
||||
|
||||
@Composable
|
||||
fun ServiceColoredButton(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable() RowScope.() -> Unit,
|
||||
) {
|
||||
Button(
|
||||
onClick = onClick,
|
||||
modifier = modifier.wrapContentWidth(),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.error,
|
||||
contentColor = MaterialTheme.colorScheme.onError
|
||||
),
|
||||
contentPadding = PaddingValues(horizontal = SpacingMedium, vertical = SpacingSmall),
|
||||
shape = RectangleShape,
|
||||
elevation = ButtonDefaults.buttonElevation(
|
||||
defaultElevation = 8.dp,
|
||||
|
||||
),
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun ServiceColoredButtonPreview() {
|
||||
AppTheme {
|
||||
ServiceColoredButton(
|
||||
onClick = {},
|
||||
content = {
|
||||
Text("Button")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
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 = {}
|
||||
)
|
||||
}
|
|
@ -13,6 +13,7 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
@ -25,6 +26,8 @@ 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.UserAction
|
||||
import org.schabi.newpipe.extractor.Page
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
|
||||
import org.schabi.newpipe.extractor.stream.Description
|
||||
|
@ -50,6 +53,7 @@ private fun CommentSection(
|
|||
val comments = commentsFlow.collectAsLazyPagingItems()
|
||||
val nestedScrollInterop = rememberNestedScrollInteropConnection()
|
||||
val state = rememberLazyListState()
|
||||
val context = LocalContext.current
|
||||
|
||||
LazyColumnThemedScrollbar(state = state) {
|
||||
LazyColumn(
|
||||
|
@ -98,21 +102,22 @@ private fun CommentSection(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
when (comments.loadState.refresh) {
|
||||
is LoadState.Loading -> {
|
||||
item {
|
||||
LoadingIndicator(modifier = Modifier.padding(top = 8.dp))
|
||||
}
|
||||
}
|
||||
|
||||
is LoadState.Error -> {
|
||||
item {
|
||||
// TODO use error panel instead
|
||||
EmptyStateComposable(EmptyStateSpec.ErrorLoadingComments)
|
||||
CommentErrorHandler(
|
||||
throwable = (comments.loadState.refresh as LoadState.Error).error,
|
||||
userAction = UserAction.REQUESTED_COMMENTS,
|
||||
onRetry = { comments.retry() },
|
||||
onReport = { info -> ErrorUtil.openActivity(context, info) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
items(comments.itemCount) {
|
||||
Comment(comment = comments[it]!!) {}
|
||||
|
@ -121,15 +126,13 @@ private fun CommentSection(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
is Resource.Error -> {
|
||||
item {
|
||||
// TODO use error panel instead
|
||||
EmptyStateComposable(
|
||||
spec = EmptyStateSpec.ErrorLoadingComments,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 128.dp)
|
||||
CommentErrorHandler(
|
||||
throwable = uiState.throwable,
|
||||
userAction = UserAction.REQUESTED_COMMENTS,
|
||||
onRetry = { comments.retry() },
|
||||
onReport = { info -> ErrorUtil.openActivity(context, info) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ 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) {
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
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