This commit was merged in pull request #121.
This commit is contained in:
@@ -35,6 +35,7 @@ import org.shahondin1624.lib.components.TestTags
|
|||||||
import org.shahondin1624.lib.components.UiConstants
|
import org.shahondin1624.lib.components.UiConstants
|
||||||
import org.shahondin1624.lib.components.charactermodel.CharacterSheetPage
|
import org.shahondin1624.lib.components.charactermodel.CharacterSheetPage
|
||||||
import org.shahondin1624.lib.components.charactermodel.DiceRollHistoryDialog
|
import org.shahondin1624.lib.components.charactermodel.DiceRollHistoryDialog
|
||||||
|
import org.shahondin1624.lib.components.charactermodel.LoadErrorDialog
|
||||||
import org.shahondin1624.lib.components.charactermodel.SaveStatusIndicator
|
import org.shahondin1624.lib.components.charactermodel.SaveStatusIndicator
|
||||||
import org.shahondin1624.lib.components.creation.CharacterCreationWizard
|
import org.shahondin1624.lib.components.creation.CharacterCreationWizard
|
||||||
import org.shahondin1624.lib.functions.DiceRollHistory
|
import org.shahondin1624.lib.functions.DiceRollHistory
|
||||||
@@ -49,6 +50,7 @@ import org.shahondin1624.theme.WindowSizeClass
|
|||||||
import org.shahondin1624.viewmodel.CharacterCreationViewModel
|
import org.shahondin1624.viewmodel.CharacterCreationViewModel
|
||||||
import org.shahondin1624.logging.AppLogger
|
import org.shahondin1624.logging.AppLogger
|
||||||
import org.shahondin1624.viewmodel.CharacterViewModel
|
import org.shahondin1624.viewmodel.CharacterViewModel
|
||||||
|
import org.shahondin1624.viewmodel.LoadStatus
|
||||||
import org.shahondin1624.viewmodel.SaveStatus
|
import org.shahondin1624.viewmodel.SaveStatus
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@@ -278,8 +280,19 @@ private fun MainScaffold(
|
|||||||
val currentRoute = currentRoute(navController)
|
val currentRoute = currentRoute(navController)
|
||||||
val character by characterViewModel.character.collectAsState()
|
val character by characterViewModel.character.collectAsState()
|
||||||
val saveStatus by characterViewModel.saveStatus.collectAsState()
|
val saveStatus by characterViewModel.saveStatus.collectAsState()
|
||||||
|
val loadStatus by characterViewModel.loadStatus.collectAsState()
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
|
||||||
|
// Show load error dialog when deserialization fails
|
||||||
|
if (loadStatus is LoadStatus.Error) {
|
||||||
|
val error = loadStatus as LoadStatus.Error
|
||||||
|
LoadErrorDialog(
|
||||||
|
errorMessage = error.message,
|
||||||
|
rawJson = error.rawJson,
|
||||||
|
onStartFresh = { characterViewModel.acknowledgeLoadError() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Show snackbar when a save error occurs
|
// Show snackbar when a save error occurs
|
||||||
LaunchedEffect(saveStatus) {
|
LaunchedEffect(saveStatus) {
|
||||||
if (saveStatus is SaveStatus.Error) {
|
if (saveStatus is SaveStatus.Error) {
|
||||||
|
|||||||
@@ -186,6 +186,13 @@ object TestTags {
|
|||||||
const val HEALING_DISMISS_BUTTON = "healing_dismiss_button"
|
const val HEALING_DISMISS_BUTTON = "healing_dismiss_button"
|
||||||
fun healButton(track: String): String = "heal_button_${track.lowercase()}"
|
fun healButton(track: String): String = "heal_button_${track.lowercase()}"
|
||||||
|
|
||||||
|
// --- Load error dialog ---
|
||||||
|
const val LOAD_ERROR_DIALOG = "load_error_dialog"
|
||||||
|
const val LOAD_ERROR_MESSAGE = "load_error_message"
|
||||||
|
const val LOAD_ERROR_START_FRESH = "load_error_start_fresh"
|
||||||
|
const val LOAD_ERROR_SHOW_RAW = "load_error_show_raw"
|
||||||
|
const val LOAD_ERROR_RAW_JSON = "load_error_raw_json"
|
||||||
|
|
||||||
// --- Recovery state ---
|
// --- Recovery state ---
|
||||||
const val RECOVERY_STATE_BADGE = "recovery_state_badge"
|
const val RECOVERY_STATE_BADGE = "recovery_state_badge"
|
||||||
const val DEATH_WARNING = "death_warning"
|
const val DEATH_WARNING = "death_warning"
|
||||||
|
|||||||
+121
@@ -0,0 +1,121 @@
|
|||||||
|
package org.shahondin1624.lib.components.charactermodel
|
||||||
|
|
||||||
|
import androidx.compose.foundation.horizontalScroll
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import org.shahondin1624.lib.components.TestTags
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dialog shown when the saved character data could not be deserialized.
|
||||||
|
* Gives the user two options:
|
||||||
|
* 1. Start Fresh — accept EXAMPLE_CHARACTER as the current character
|
||||||
|
* 2. Show Raw Data — display the raw JSON for manual recovery / export
|
||||||
|
*
|
||||||
|
* @param errorMessage A description of what went wrong during deserialization.
|
||||||
|
* @param rawJson The raw JSON string that failed to deserialize, preserved for recovery.
|
||||||
|
* @param onStartFresh Called when the user chooses to start fresh with a default character.
|
||||||
|
* @param onDismiss Called when the dialog is dismissed (same as start fresh, since we must proceed).
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun LoadErrorDialog(
|
||||||
|
errorMessage: String,
|
||||||
|
rawJson: String,
|
||||||
|
onStartFresh: () -> Unit,
|
||||||
|
onDismiss: () -> Unit = onStartFresh
|
||||||
|
) {
|
||||||
|
var showRawJson by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
modifier = Modifier.testTag(TestTags.LOAD_ERROR_DIALOG),
|
||||||
|
title = {
|
||||||
|
Text("Failed to Load Character")
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Text(
|
||||||
|
text = "Your saved character data could not be loaded. " +
|
||||||
|
"This may be due to corrupted data or an incompatible format change.",
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = "Error: $errorMessage",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
modifier = Modifier.testTag(TestTags.LOAD_ERROR_MESSAGE)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
Text(
|
||||||
|
text = "Your original data has been backed up and is shown below " +
|
||||||
|
"so you can copy it for manual recovery.",
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
if (!showRawJson) {
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = { showRawJson = true },
|
||||||
|
modifier = Modifier.testTag(TestTags.LOAD_ERROR_SHOW_RAW)
|
||||||
|
) {
|
||||||
|
Text("Show Raw Data")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = "Raw JSON:",
|
||||||
|
style = MaterialTheme.typography.labelMedium
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Surface(
|
||||||
|
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
shape = MaterialTheme.shapes.small,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.heightIn(max = 200.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = rawJson.ifEmpty { "(empty)" },
|
||||||
|
style = MaterialTheme.typography.bodySmall.copy(
|
||||||
|
fontFamily = FontFamily.Monospace
|
||||||
|
),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(8.dp)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.horizontalScroll(rememberScrollState())
|
||||||
|
.testTag(TestTags.LOAD_ERROR_RAW_JSON)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = onStartFresh,
|
||||||
|
modifier = Modifier.testTag(TestTags.LOAD_ERROR_START_FRESH)
|
||||||
|
) {
|
||||||
|
Text("Start Fresh")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -26,11 +26,16 @@ class CharacterViewModel : ViewModel() {
|
|||||||
|
|
||||||
private val settings = Settings()
|
private val settings = Settings()
|
||||||
|
|
||||||
|
private val _loadStatus = MutableStateFlow<LoadStatus>(LoadStatus.Success)
|
||||||
|
|
||||||
private val _character = MutableStateFlow(loadCharacter())
|
private val _character = MutableStateFlow(loadCharacter())
|
||||||
|
|
||||||
/** Observable character state. */
|
/** Observable character state. */
|
||||||
val character: StateFlow<ShadowrunCharacter> = _character.asStateFlow()
|
val character: StateFlow<ShadowrunCharacter> = _character.asStateFlow()
|
||||||
|
|
||||||
|
/** Observable load status — UI should check for [LoadStatus.Error] to show recovery dialog. */
|
||||||
|
val loadStatus: StateFlow<LoadStatus> = _loadStatus.asStateFlow()
|
||||||
|
|
||||||
private val _saveStatus = MutableStateFlow<SaveStatus>(SaveStatus.Idle)
|
private val _saveStatus = MutableStateFlow<SaveStatus>(SaveStatus.Idle)
|
||||||
|
|
||||||
/** Observable save status for UI display. */
|
/** Observable save status for UI display. */
|
||||||
@@ -75,8 +80,37 @@ class CharacterViewModel : ViewModel() {
|
|||||||
saveCharacter(_character.value)
|
saveCharacter(_character.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Acknowledge a load error and clear the error state.
|
||||||
|
* Called after the user has made a choice in the error dialog (e.g., "Start Fresh").
|
||||||
|
*/
|
||||||
|
fun acknowledgeLoadError() {
|
||||||
|
_loadStatus.value = LoadStatus.Success
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to deserialize a raw JSON string as a character.
|
||||||
|
* Used for manual recovery when the user edits or re-pastes corrupted data.
|
||||||
|
*
|
||||||
|
* @return the deserialized character, or null if it still fails
|
||||||
|
*/
|
||||||
|
fun tryRecoverFromJson(json: String): ShadowrunCharacter? {
|
||||||
|
return try {
|
||||||
|
val character = DataLoader.deserialize(json)
|
||||||
|
Napier.i(tag = TAG) { "Recovery successful: ${character.characterData.name}" }
|
||||||
|
_character.value = character
|
||||||
|
_loadStatus.value = LoadStatus.Success
|
||||||
|
character
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Napier.e(tag = TAG, throwable = e) { "Recovery attempt failed" }
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load character from local storage, falling back to EXAMPLE_CHARACTER.
|
* Load character from local storage, falling back to EXAMPLE_CHARACTER.
|
||||||
|
* On deserialization failure, the raw JSON is preserved in a backup key
|
||||||
|
* and the error is reported via [_loadStatus].
|
||||||
*/
|
*/
|
||||||
private fun loadCharacter(): ShadowrunCharacter {
|
private fun loadCharacter(): ShadowrunCharacter {
|
||||||
return try {
|
return try {
|
||||||
@@ -84,15 +118,27 @@ class CharacterViewModel : ViewModel() {
|
|||||||
if (json != null) {
|
if (json != null) {
|
||||||
val character = DataLoader.deserialize(json)
|
val character = DataLoader.deserialize(json)
|
||||||
Napier.i(tag = TAG) { "Character loaded: ${character.characterData.name}" }
|
Napier.i(tag = TAG) { "Character loaded: ${character.characterData.name}" }
|
||||||
|
_loadStatus.value = LoadStatus.Success
|
||||||
character
|
character
|
||||||
} else {
|
} else {
|
||||||
Napier.i(tag = TAG) { "No saved character found, using example character" }
|
Napier.i(tag = TAG) { "No saved character found, using example character" }
|
||||||
|
_loadStatus.value = LoadStatus.NoSavedData
|
||||||
EXAMPLE_CHARACTER
|
EXAMPLE_CHARACTER
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
val rawJson = settings.getStringOrNull(STORAGE_KEY) ?: ""
|
||||||
Napier.e(tag = TAG, throwable = e) {
|
Napier.e(tag = TAG, throwable = e) {
|
||||||
"Failed to deserialize saved character, falling back to example character"
|
"Failed to deserialize saved character, falling back to example character"
|
||||||
}
|
}
|
||||||
|
// Preserve the raw data so it is not lost
|
||||||
|
if (rawJson.isNotEmpty()) {
|
||||||
|
settings.putString(BACKUP_KEY, rawJson)
|
||||||
|
Napier.i(tag = TAG) { "Raw character data backed up under key '$BACKUP_KEY'" }
|
||||||
|
}
|
||||||
|
_loadStatus.value = LoadStatus.Error(
|
||||||
|
message = e.message ?: "Unknown deserialization error",
|
||||||
|
rawJson = rawJson
|
||||||
|
)
|
||||||
EXAMPLE_CHARACTER
|
EXAMPLE_CHARACTER
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -143,6 +189,8 @@ class CharacterViewModel : ViewModel() {
|
|||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "CharacterViewModel"
|
private const val TAG = "CharacterViewModel"
|
||||||
private const val STORAGE_KEY = "shadowrun_character"
|
private const val STORAGE_KEY = "shadowrun_character"
|
||||||
|
/** Backup key for preserving raw JSON when deserialization fails. */
|
||||||
|
private const val BACKUP_KEY = "shadowrun_character_backup"
|
||||||
/** How long to display the "Saved" status before reverting to Idle. */
|
/** How long to display the "Saved" status before reverting to Idle. */
|
||||||
private const val SAVED_DISPLAY_DURATION_MS = 2000L
|
private const val SAVED_DISPLAY_DURATION_MS = 2000L
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package org.shahondin1624.viewmodel
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the result of loading a character from local storage.
|
||||||
|
* Exposed by [CharacterViewModel] so the UI can display load errors
|
||||||
|
* and offer recovery options.
|
||||||
|
*/
|
||||||
|
sealed class LoadStatus {
|
||||||
|
/** Character loaded successfully from storage. */
|
||||||
|
data object Success : LoadStatus()
|
||||||
|
|
||||||
|
/** No saved data found — using default character. */
|
||||||
|
data object NoSavedData : LoadStatus()
|
||||||
|
|
||||||
|
/** Deserialization failed. Raw JSON is preserved for recovery. */
|
||||||
|
data class Error(
|
||||||
|
val message: String,
|
||||||
|
val rawJson: String
|
||||||
|
) : LoadStatus()
|
||||||
|
}
|
||||||
@@ -8,9 +8,11 @@ import org.shahondin1624.model.characterdata.CharacterData
|
|||||||
import org.shahondin1624.model.characterdata.Metatype
|
import org.shahondin1624.model.characterdata.Metatype
|
||||||
import org.shahondin1624.model.charactermodel.ShadowrunCharacter
|
import org.shahondin1624.model.charactermodel.ShadowrunCharacter
|
||||||
import org.shahondin1624.model.createNewCharacter
|
import org.shahondin1624.model.createNewCharacter
|
||||||
|
import org.shahondin1624.viewmodel.LoadStatus
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertFailsWith
|
import kotlin.test.assertFailsWith
|
||||||
|
import kotlin.test.assertIs
|
||||||
import kotlin.test.assertNotNull
|
import kotlin.test.assertNotNull
|
||||||
import kotlin.test.assertTrue
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
@@ -205,6 +207,49 @@ class CharacterViewModelTest {
|
|||||||
assertEquals(50, restored.characterData.totalKarma)
|
assertEquals(50, restored.characterData.totalKarma)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- LoadStatus model ----
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun loadStatusErrorPreservesRawJson() {
|
||||||
|
val rawJson = """{"characterData": {"name": "Corrupted"}}"""
|
||||||
|
val status = LoadStatus.Error(
|
||||||
|
message = "Missing required field",
|
||||||
|
rawJson = rawJson
|
||||||
|
)
|
||||||
|
assertIs<LoadStatus.Error>(status)
|
||||||
|
assertEquals("Missing required field", status.message)
|
||||||
|
assertEquals(rawJson, status.rawJson)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun loadStatusSuccessAndNoSavedDataAreSingletons() {
|
||||||
|
val s1 = LoadStatus.Success
|
||||||
|
val s2 = LoadStatus.Success
|
||||||
|
assertEquals(s1, s2)
|
||||||
|
|
||||||
|
val n1 = LoadStatus.NoSavedData
|
||||||
|
val n2 = LoadStatus.NoSavedData
|
||||||
|
assertEquals(n1, n2)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun deserializeMalformedJsonPreservesDataForRecovery() {
|
||||||
|
// Simulate what the ViewModel does: if deserialization fails,
|
||||||
|
// the raw JSON should be capturable for backup
|
||||||
|
val corruptedJson = """{"characterData": "not_an_object"}"""
|
||||||
|
var capturedRawJson: String? = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
DataLoader.deserialize(corruptedJson)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
// In the ViewModel, this is where we'd preserve the raw JSON
|
||||||
|
capturedRawJson = corruptedJson
|
||||||
|
}
|
||||||
|
|
||||||
|
assertNotNull(capturedRawJson, "Raw JSON should be preserved on failure")
|
||||||
|
assertEquals(corruptedJson, capturedRawJson)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun saveAndLoadNotesAndLifestyles() {
|
fun saveAndLoadNotesAndLifestyles() {
|
||||||
val modified = EXAMPLE_CHARACTER.copy(
|
val modified = EXAMPLE_CHARACTER.copy(
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
package org.shahondin1624
|
||||||
|
|
||||||
|
import androidx.compose.ui.test.ExperimentalTestApi
|
||||||
|
import androidx.compose.ui.test.assertIsDisplayed
|
||||||
|
import androidx.compose.ui.test.assertTextContains
|
||||||
|
import androidx.compose.ui.test.onNodeWithTag
|
||||||
|
import androidx.compose.ui.test.performClick
|
||||||
|
import androidx.compose.ui.test.runComposeUiTest
|
||||||
|
import org.shahondin1624.lib.components.TestTags
|
||||||
|
import org.shahondin1624.lib.components.charactermodel.LoadErrorDialog
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for the LoadErrorDialog composable.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalTestApi::class)
|
||||||
|
class LoadErrorDialogTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun dialogDisplaysErrorMessage() = runComposeUiTest {
|
||||||
|
setContent {
|
||||||
|
LoadErrorDialog(
|
||||||
|
errorMessage = "Unexpected JSON token",
|
||||||
|
rawJson = """{"broken": true}""",
|
||||||
|
onStartFresh = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
onNodeWithTag(TestTags.LOAD_ERROR_DIALOG).assertIsDisplayed()
|
||||||
|
onNodeWithTag(TestTags.LOAD_ERROR_MESSAGE).assertIsDisplayed()
|
||||||
|
onNodeWithTag(TestTags.LOAD_ERROR_MESSAGE)
|
||||||
|
.assertTextContains("Unexpected JSON token", substring = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun startFreshButtonCallsCallback() = runComposeUiTest {
|
||||||
|
var startFreshCalled = false
|
||||||
|
|
||||||
|
setContent {
|
||||||
|
LoadErrorDialog(
|
||||||
|
errorMessage = "Test error",
|
||||||
|
rawJson = """{"test": 1}""",
|
||||||
|
onStartFresh = { startFreshCalled = true }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
onNodeWithTag(TestTags.LOAD_ERROR_START_FRESH).assertIsDisplayed()
|
||||||
|
onNodeWithTag(TestTags.LOAD_ERROR_START_FRESH).performClick()
|
||||||
|
assertTrue(startFreshCalled, "Start Fresh callback should have been called")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun showRawDataButtonRevealsJson() = runComposeUiTest {
|
||||||
|
val rawJson = """{"characterData": {"name": "Lost Runner"}}"""
|
||||||
|
|
||||||
|
setContent {
|
||||||
|
LoadErrorDialog(
|
||||||
|
errorMessage = "Parse error",
|
||||||
|
rawJson = rawJson,
|
||||||
|
onStartFresh = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Raw JSON should not be visible initially
|
||||||
|
onNodeWithTag(TestTags.LOAD_ERROR_RAW_JSON).assertDoesNotExist()
|
||||||
|
|
||||||
|
// Click "Show Raw Data"
|
||||||
|
onNodeWithTag(TestTags.LOAD_ERROR_SHOW_RAW).assertIsDisplayed()
|
||||||
|
onNodeWithTag(TestTags.LOAD_ERROR_SHOW_RAW).performClick()
|
||||||
|
|
||||||
|
// Now raw JSON should be visible
|
||||||
|
onNodeWithTag(TestTags.LOAD_ERROR_RAW_JSON).assertIsDisplayed()
|
||||||
|
onNodeWithTag(TestTags.LOAD_ERROR_RAW_JSON)
|
||||||
|
.assertTextContains("Lost Runner", substring = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun showRawDataButtonHidesAfterClick() = runComposeUiTest {
|
||||||
|
setContent {
|
||||||
|
LoadErrorDialog(
|
||||||
|
errorMessage = "Error",
|
||||||
|
rawJson = "{}",
|
||||||
|
onStartFresh = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
onNodeWithTag(TestTags.LOAD_ERROR_SHOW_RAW).assertIsDisplayed()
|
||||||
|
onNodeWithTag(TestTags.LOAD_ERROR_SHOW_RAW).performClick()
|
||||||
|
|
||||||
|
// After clicking, the button should be replaced by the raw JSON display
|
||||||
|
onNodeWithTag(TestTags.LOAD_ERROR_SHOW_RAW).assertDoesNotExist()
|
||||||
|
onNodeWithTag(TestTags.LOAD_ERROR_RAW_JSON).assertIsDisplayed()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun emptyRawJsonShowsPlaceholder() = runComposeUiTest {
|
||||||
|
setContent {
|
||||||
|
LoadErrorDialog(
|
||||||
|
errorMessage = "Error",
|
||||||
|
rawJson = "",
|
||||||
|
onStartFresh = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
onNodeWithTag(TestTags.LOAD_ERROR_SHOW_RAW).performClick()
|
||||||
|
onNodeWithTag(TestTags.LOAD_ERROR_RAW_JSON)
|
||||||
|
.assertTextContains("(empty)", substring = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user