Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,20 +1,31 @@
package com.interlinedlist.android.feature.profile.ui

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.assertIsOff
import androidx.compose.ui.test.assertIsOn
import androidx.compose.ui.test.assertIsSelected
import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performImeAction
import androidx.compose.ui.test.performTextClearance
import androidx.compose.ui.test.performTextInput
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.interlinedlist.android.core.designsystem.theme.InterlinedListTheme
import com.interlinedlist.android.feature.profile.domain.UserSettings
import com.interlinedlist.android.feature.profile.domain.ViewingPreference
import com.interlinedlist.android.feature.profile.ui.settings.SettingsScreen
import com.interlinedlist.android.feature.profile.ui.settings.SettingsTestTags
import com.interlinedlist.android.feature.profile.ui.settings.SettingsUiState
import com.interlinedlist.android.feature.profile.ui.settings.settingsDecrementTag
import com.interlinedlist.android.feature.profile.ui.settings.settingsIncrementTag
import com.interlinedlist.android.feature.profile.ui.settings.settingsNumberErrorTag
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
Expand All @@ -29,6 +40,10 @@ class SettingsScreenTest {
state: SettingsUiState,
onSelectViewingPreference: (ViewingPreference) -> Unit = {},
onToggleShowPreviews: (Boolean) -> Unit = {},
onSetMessagesPerPage: (Int) -> Unit = {},
onSetMaxMessageLength: (Int) -> Unit = {},
onToggleDefaultPubliclyVisible: (Boolean) -> Unit = {},
onToggleShowAdvancedPostSettings: (Boolean) -> Unit = {},
onRetry: () -> Unit = {},
onDismissError: () -> Unit = {},
) {
Expand All @@ -40,6 +55,10 @@ class SettingsScreenTest {
onRetry = onRetry,
onSelectViewingPreference = onSelectViewingPreference,
onToggleShowPreviews = onToggleShowPreviews,
onSetMessagesPerPage = onSetMessagesPerPage,
onSetMaxMessageLength = onSetMaxMessageLength,
onToggleDefaultPubliclyVisible = onToggleDefaultPubliclyVisible,
onToggleShowAdvancedPostSettings = onToggleShowAdvancedPostSettings,
onDismissError = onDismissError,
)
}
Expand Down Expand Up @@ -127,4 +146,169 @@ class SettingsScreenTest {

composeRule.onNodeWithTag(SettingsTestTags.PROGRESS).assertIsDisplayed()
}

// --- Message preferences (issue #32) -------------------------------------

@Test
fun messageSettings_showsTheStoredBooleanPreferences() {
setContent(
SettingsUiState(
settings = UserSettings(
defaultPubliclyVisible = false,
showAdvancedPostSettings = true,
),
),
)

composeRule.onNodeWithTag(SettingsTestTags.MESSAGE_SETTINGS).assertIsDisplayed()
composeRule.onNodeWithTag(SettingsTestTags.DEFAULT_PUBLICLY_VISIBLE).assertIsOff()
composeRule.onNodeWithTag(SettingsTestTags.SHOW_ADVANCED_POST_SETTINGS).assertIsOn()
}

@Test
fun togglingDefaultVisibility_reportsTheNewValue() {
var toggled: Boolean? = null
setContent(
state = SettingsUiState(settings = UserSettings(defaultPubliclyVisible = true)),
onToggleDefaultPubliclyVisible = { toggled = it },
)

composeRule.onNodeWithTag(SettingsTestTags.DEFAULT_PUBLICLY_VISIBLE).performClick()

assert(toggled == false)
}

@Test
fun togglingAdvancedPostSettings_reportsTheNewValue() {
var toggled: Boolean? = null
setContent(
state = SettingsUiState(settings = UserSettings(showAdvancedPostSettings = false)),
onToggleShowAdvancedPostSettings = { toggled = it },
)

composeRule.onNodeWithTag(SettingsTestTags.SHOW_ADVANCED_POST_SETTINGS).performClick()

assert(toggled == true)
}

@Test
fun characterLimit_showsTheStoredValueUnderProfile() {
setContent(SettingsUiState(settings = UserSettings(maxMessageLength = 666)))

composeRule.onNodeWithTag(SettingsTestTags.PROFILE).assertIsDisplayed()
composeRule.onNodeWithTag(SettingsTestTags.MAX_MESSAGE_LENGTH).assertTextEquals("666")
}

@Test
fun characterLimit_fallsBackToTheServerDefaultWhenUnset() {
setContent(SettingsUiState(settings = UserSettings(maxMessageLength = null)))

composeRule.onNodeWithTag(SettingsTestTags.MAX_MESSAGE_LENGTH).assertTextEquals("666")
}

@Test
fun steppingTheCharacterLimit_reportsTheSteppedValue() {
var saved: Int? = null
setContent(
state = SettingsUiState(settings = UserSettings(maxMessageLength = 666)),
onSetMaxMessageLength = { saved = it },
)

composeRule.onNodeWithTag(settingsIncrementTag(SettingsTestTags.MAX_MESSAGE_LENGTH))
.performClick()

assert(saved == 676) { "expected a 10-character step, got $saved" }
}

@Test
fun messagesPerPage_stepsWithinTheDocumentedRange() {
var saved: Int? = null
setContent(
state = SettingsUiState(settings = UserSettings(messagesPerPage = 20)),
onSetMessagesPerPage = { saved = it },
)

composeRule.onNodeWithTag(settingsDecrementTag(SettingsTestTags.MESSAGES_PER_PAGE))
.performClick()

assert(saved == 19)
}

@Test
fun messagesPerPage_cannotStepBelowTheMinimum() {
var saved: Int? = null
setContent(
state = SettingsUiState(settings = UserSettings(messagesPerPage = 10)),
onSetMessagesPerPage = { saved = it },
)

composeRule.onNodeWithTag(settingsDecrementTag(SettingsTestTags.MESSAGES_PER_PAGE))
.assertIsNotEnabled()

assert(saved == null)
}

@Test
fun messagesPerPage_outOfRangeEntryIsRejectedWithoutReportingAValue() {
var saved: Int? = null
setContent(
state = SettingsUiState(settings = UserSettings(messagesPerPage = 20)),
onSetMessagesPerPage = { saved = it },
)

composeRule.onNodeWithTag(SettingsTestTags.MESSAGES_PER_PAGE).performTextClearance()
composeRule.onNodeWithTag(SettingsTestTags.MESSAGES_PER_PAGE).performTextInput("99")
composeRule.onNodeWithTag(SettingsTestTags.MESSAGES_PER_PAGE).performImeAction()

composeRule.onNodeWithTag(settingsNumberErrorTag(SettingsTestTags.MESSAGES_PER_PAGE))
.assertIsDisplayed()
assert(saved == null) { "an out-of-range entry must not be saved, got $saved" }
}

@Test
fun messagesPerPage_inRangeEntryIsReported() {
var saved: Int? = null
setContent(
state = SettingsUiState(settings = UserSettings(messagesPerPage = 20)),
onSetMessagesPerPage = { saved = it },
)

composeRule.onNodeWithTag(SettingsTestTags.MESSAGES_PER_PAGE).performTextClearance()
composeRule.onNodeWithTag(SettingsTestTags.MESSAGES_PER_PAGE).performTextInput("25")
composeRule.onNodeWithTag(SettingsTestTags.MESSAGES_PER_PAGE).performImeAction()

assert(saved == 25)
}

@Test
fun characterLimit_rolledBackSaveRestoresTheFieldToTheStoredValue() {
// The screen is recomposed with the previous value after a rejected save;
// the field must follow rather than keep showing a value that never saved.
var settings by mutableStateOf(UserSettings(maxMessageLength = 666))
composeRule.setContent {
InterlinedListTheme {
SettingsScreen(
state = SettingsUiState(settings = settings, errorMessage = null),
onBack = {},
onRetry = {},
onSelectViewingPreference = {},
onToggleShowPreviews = {},
onSetMessagesPerPage = {},
// Optimistic apply, then the server refuses and it rolls back.
onSetMaxMessageLength = { settings = settings.copy(maxMessageLength = it) },
onToggleDefaultPubliclyVisible = {},
onToggleShowAdvancedPostSettings = {},
onDismissError = {},
)
}
}

composeRule.onNodeWithTag(settingsIncrementTag(SettingsTestTags.MAX_MESSAGE_LENGTH))
.performClick()
composeRule.onNodeWithTag(SettingsTestTags.MAX_MESSAGE_LENGTH).assertTextEquals("676")

settings = settings.copy(maxMessageLength = 666)

composeRule.onNodeWithTag(SettingsTestTags.MAX_MESSAGE_LENGTH).assertTextEquals("666")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.interlinedlist.android.feature.profile.domain

/**
* Bounds and server defaults for the numeric message preferences, plus the fallbacks
* the Settings UI shows when `GET /api/user` omits a preference.
*
* The ranges are enforced client-side so a stepper or a typed value can never PATCH
* nonsense (0, a negative page size, a nine-digit character limit). The server has
* the final say — it may refuse a value we consider valid — so callers must still
* handle a rejected save by restoring the previous value.
*/
object SettingsBounds {

/**
* How many messages the feed loads at a time. The help centre states the
* supported range outright: "Messages per page: How many messages to load at
* once (10 to 30)".
*/
val MESSAGES_PER_PAGE: IntRange = 10..30

/**
* The account's message character limit. Neither the help centre nor the OpenAPI
* spec publishes a range for this one (only the 666 default), so the bounds are
* deliberately wide: low enough to allow a deliberately terse limit, high enough
* to clear any plausible server cap, and tight enough to reject nonsense. A value
* in this range that the server still refuses surfaces as a failed save.
*/
val MAX_MESSAGE_LENGTH: IntRange = 10..10_000

/** Stepper increment for [MAX_MESSAGE_LENGTH]; the field is there for big jumps. */
const val MAX_MESSAGE_LENGTH_STEP: Int = 10

/** The character limit a fresh account gets (help centre: "default 666"). */
const val DEFAULT_MAX_MESSAGE_LENGTH: Int = 666

/** The page size a fresh account gets (observed live on a real account). */
const val DEFAULT_MESSAGES_PER_PAGE: Int = 20

/** New messages start public unless the account says otherwise. */
const val DEFAULT_PUBLICLY_VISIBLE: Boolean = true

/** The composer's gear options stay hidden unless the account opts in. */
const val DEFAULT_SHOW_ADVANCED_POST_SETTINGS: Boolean = false
}

/**
* The character limit to show, falling back to the server default when the account
* has no stored value. The preference fields are nullable because public profiles
* omit them; the Settings UI still has to render a concrete number.
*/
val UserSettings.maxMessageLengthOrDefault: Int
get() = maxMessageLength ?: SettingsBounds.DEFAULT_MAX_MESSAGE_LENGTH

/** The feed page size to show, falling back to the server default. */
val UserSettings.messagesPerPageOrDefault: Int
get() = messagesPerPage ?: SettingsBounds.DEFAULT_MESSAGES_PER_PAGE

/** The composer's starting visibility, falling back to the server default. */
val UserSettings.defaultPubliclyVisibleOrDefault: Boolean
get() = defaultPubliclyVisible ?: SettingsBounds.DEFAULT_PUBLICLY_VISIBLE

/** Whether the composer's advanced options show, falling back to the server default. */
val UserSettings.showAdvancedPostSettingsOrDefault: Boolean
get() = showAdvancedPostSettings ?: SettingsBounds.DEFAULT_SHOW_ADVANCED_POST_SETTINGS
Loading
Loading