Add 1 day after read test

This commit is contained in:
andrew 2023-09-20 00:41:34 +09:30
parent a7111b0d49
commit 92cae9adde
4 changed files with 213 additions and 159 deletions

View File

@ -319,6 +319,7 @@ dependencies {
// Assertions // Assertions
androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.ext:truth:1.5.0' androidTestImplementation 'androidx.test.ext:truth:1.5.0'
testImplementation 'com.google.truth:truth:1.1.3'
androidTestImplementation 'com.google.truth:truth:1.1.3' androidTestImplementation 'com.google.truth:truth:1.1.3'
// Espresso dependencies // Espresso dependencies

View File

@ -48,7 +48,7 @@ data class State(
val isSelfAdmin: Boolean = true, val isSelfAdmin: Boolean = true,
val address: Address? = null, val address: Address? = null,
val isNoteToSelf: Boolean = false, val isNoteToSelf: Boolean = false,
val expiryMode: ExpiryMode? = ExpiryMode.NONE, val expiryMode: ExpiryMode? = null,
val isNewConfigEnabled: Boolean = true, val isNewConfigEnabled: Boolean = true,
val persistedMode: ExpiryMode? = null, val persistedMode: ExpiryMode? = null,
val showDebugOptions: Boolean = false val showDebugOptions: Boolean = false
@ -58,11 +58,10 @@ data class State(
else -> GetString(R.string.activity_expiration_settings_subtitle) else -> GetString(R.string.activity_expiration_settings_subtitle)
} }
val modeOptionsHidden get() = isNoteToSelf || (isGroup && isNewConfigEnabled) val typeOptionsHidden get() = isNoteToSelf || (isGroup && isNewConfigEnabled)
val duration get() = expiryMode?.duration val duration get() = expiryMode?.duration
val expiryType get() = expiryMode?.type val expiryType get() = expiryMode?.type
val expiryTypeOrNone: ExpiryType = expiryType ?: ExpiryType.NONE
val isTimeOptionsEnabled = isNoteToSelf || isSelfAdmin && (isNewConfigEnabled || expiryType == ExpiryType.LEGACY) val isTimeOptionsEnabled = isNoteToSelf || isSelfAdmin && (isNewConfigEnabled || expiryType == ExpiryType.LEGACY)
} }
@ -104,15 +103,12 @@ class ExpirationSettingsViewModel(
.map(::UiState) .map(::UiState)
.stateIn(viewModelScope, SharingStarted.Eagerly, UiState()) .stateIn(viewModelScope, SharingStarted.Eagerly, UiState())
private var expirationConfig: ExpirationConfiguration? = null
init { init {
viewModelScope.launch { viewModelScope.launch {
expirationConfig = storage.getExpirationConfiguration(threadId) val expiryMode = storage.getExpirationConfiguration(threadId)?.expiryMode ?: ExpiryMode.NONE
val expiryMode = expirationConfig?.expiryMode ?: ExpiryMode.NONE
val recipient = threadDb.getRecipientForThreadId(threadId) val recipient = threadDb.getRecipientForThreadId(threadId)
val groupRecord = recipient?.takeIf { it.isClosedGroupRecipient } val groupRecord = recipient?.takeIf { it.isClosedGroupRecipient }
?.run { address.toGroupString().let(groupDb::getGroup).orNull() } ?.run { groupDb.getGroup(address.toGroupString()).orNull() }
_state.update { state -> _state.update { state ->
state.copy( state.copy(
@ -127,23 +123,13 @@ class ExpirationSettingsViewModel(
} }
} }
override fun setMode(mode: ExpiryMode) { override fun setMode(mode: ExpiryMode) = _state.update { it.copy(expiryMode = mode) }
_state.update { it.copy(
expiryMode = mode
) }
}
override fun onSetClick() = viewModelScope.launch { override fun onSetClick() = viewModelScope.launch {
val state = _state.value val state = _state.value
val mode = state.expiryMode.let { val mode = state.expiryMode
when {
it !is ExpiryMode.Legacy -> it
state.isGroup -> ExpiryMode.AfterSend(it.expirySeconds)
else -> ExpiryMode.AfterRead(it.expirySeconds)
} ?: ExpiryMode.NONE
}
val address = state.address val address = state.address
if (address == null) { if (address == null || mode == null) {
_event.send(Event.FAIL) _event.send(Event.FAIL)
return@launch return@launch
} }
@ -194,12 +180,6 @@ class ExpirationSettingsViewModel(
} }
} }
private fun ExpiryType.defaultMode(persistedMode: ExpiryMode?) = when(this) {
persistedMode?.type -> persistedMode
ExpiryType.AFTER_READ -> mode(12.hours)
else -> mode(1.days)
}
data class UiState( data class UiState(
val cards: List<CardModel> = emptyList(), val cards: List<CardModel> = emptyList(),
val showGroupFooter: Boolean = false val showGroupFooter: Boolean = false
@ -211,62 +191,43 @@ data class UiState(
), ),
showGroupFooter = state.isGroup && state.isNewConfigEnabled showGroupFooter = state.isGroup && state.isNewConfigEnabled
) )
constructor(showGroupFooter: Boolean, vararg cards: CardModel): this(cards.asList(), showGroupFooter)
} }
data class CardModel( data class CardModel(
val title: GetString, val title: GetString,
val options: List<OptionModel> val options: List<OptionModel>
) ) {
constructor(title: GetString, vararg options: OptionModel): this(title, options.asList())
constructor(@StringRes title: Int, vararg options: OptionModel): this(GetString(title), options.asList())
}
private fun typeOptions(state: State) = fun offTypeOption(state: State) = typeOption(ExpiryType.NONE, state)
state.takeUnless { it.modeOptionsHidden }?.run { fun legacyTypeOption(state: State) = typeOption(ExpiryType.LEGACY, state)
fun afterReadTypeOption(state: State) = newTypeOption(ExpiryType.AFTER_READ, state)
fun afterSendTypeOption(state: State) = newTypeOption(ExpiryType.AFTER_SEND, state)
private fun newTypeOption(type: ExpiryType, state: State) = typeOption(type, state, state.run { isNewConfigEnabled && isSelfAdmin })
private fun typeOptions(state: State) = state.takeUnless { it.typeOptionsHidden }?.run {
listOfNotNull( listOfNotNull(
typeOption( offTypeOption(state),
ExpiryMode.NONE, takeUnless { isNewConfigEnabled }?.let(::legacyTypeOption),
state, takeUnless { isGroup }?.let(::afterReadTypeOption),
R.string.expiration_off, afterSendTypeOption(state)
contentDescription = R.string.AccessibilityId_disable_disappearing_messages,
enabled = isSelfAdmin
),
if (!isNewConfigEnabled) typeOption(
ExpiryType.LEGACY.defaultMode(persistedMode),
state,
R.string.expiration_type_disappear_legacy,
contentDescription = R.string.expiration_type_disappear_legacy_description,
enabled = isSelfAdmin
) else null,
if (!isGroup) typeOption(
ExpiryType.AFTER_READ.defaultMode(persistedMode),
state,
R.string.expiration_type_disappear_after_read,
R.string.expiration_type_disappear_after_read_description,
contentDescription = R.string.expiration_type_disappear_after_read_description,
enabled = isNewConfigEnabled && isSelfAdmin
) else null,
typeOption(
ExpiryType.AFTER_SEND.defaultMode(persistedMode),
state,
R.string.expiration_type_disappear_after_send,
R.string.expiration_type_disappear_after_read_description,
contentDescription = R.string.expiration_type_disappear_after_send_description,
enabled = isNewConfigEnabled && isSelfAdmin
)
) )
} }
private fun typeOption( private fun typeOption(
mode: ExpiryMode, type: ExpiryType,
state: State, state: State,
@StringRes title: Int, enabled: Boolean = state.isSelfAdmin,
@StringRes subtitle: Int? = null,
@StringRes contentDescription: Int = title,
enabled: Boolean = true,
) = OptionModel( ) = OptionModel(
value = mode, value = type.defaultMode(state.persistedMode),
title = GetString(title), title = GetString(type.title),
subtitle = subtitle?.let(::GetString), subtitle = type.subtitle?.let(::GetString),
contentDescription = GetString(contentDescription), contentDescription = GetString(type.contentDescription),
selected = state.expiryType == mode.type, selected = state.expiryType == type,
enabled = enabled enabled = enabled
) )
@ -292,26 +253,26 @@ val afterReadModes = afterReadTimes.map { it.inWholeSeconds }.map(ExpiryMode::Af
fun afterReadOptions(state: State) = afterReadModes.map { timeOption(it, state) } fun afterReadOptions(state: State) = afterReadModes.map { timeOption(it, state) }
private fun timeOptions(state: State): List<OptionModel>? = private fun timeOptions(state: State): List<OptionModel>? =
if (state.modeOptionsHidden) timeOptionsAfterSend(state) if (state.typeOptionsHidden) timeOptionsAfterSend(state)
else when (state.expiryMode) { else when (state.expiryMode) {
is ExpiryMode.Legacy, is ExpiryMode.AfterRead -> debugOptions(state) + afterReadOptions(state) is ExpiryMode.Legacy, is ExpiryMode.AfterRead -> debugOptions(state) + afterReadOptions(state)
is ExpiryMode.AfterSend -> debugOptions(state) + afterSendOptions(state) is ExpiryMode.AfterSend -> debugOptions(state) + afterSendOptions(state)
else -> null else -> null
} }
private fun timeOptionsAfterSend(state: State) = listOf( private fun timeOptionsAfterSend(state: State): List<OptionModel> =
typeOption(ExpiryMode.NONE, state, R.string.expiration_off, enabled = state.isSelfAdmin), listOf(offTypeOption(state)) + debugOptions(state) + afterSendOptions(state)
) + debugOptions(state) + afterSendModes.map { timeOption(it, state) }
private fun timeOption( fun timeOption(
mode: ExpiryMode, mode: ExpiryMode,
state: State, state: State,
title: GetString = GetString(mode.duration, ExpirationUtil::getExpirationDisplayValue), title: GetString = GetString(mode.duration),
subtitle: GetString? = null, subtitle: GetString? = null,
) = OptionModel( ) = OptionModel(
value = mode, value = mode,
title = title, title = title,
subtitle = subtitle, subtitle = subtitle,
contentDescription = title,
selected = state.expiryMode == mode, selected = state.expiryMode == mode,
enabled = state.isTimeOptionsEnabled enabled = state.isTimeOptionsEnabled
) )
@ -325,14 +286,43 @@ data class OptionModel(
val enabled: Boolean = true, val enabled: Boolean = true,
) )
enum class ExpiryType(private val createMode: (Long) -> ExpiryMode) { enum class ExpiryType(
NONE({ ExpiryMode.NONE }), private val createMode: (Long) -> ExpiryMode,
LEGACY(ExpiryMode::Legacy), @StringRes val title: Int,
AFTER_SEND(ExpiryMode::AfterSend), @StringRes val subtitle: Int? = null,
AFTER_READ(ExpiryMode::AfterRead); @StringRes val contentDescription: Int = title,
) {
NONE(
{ ExpiryMode.NONE },
R.string.expiration_off,
contentDescription = R.string.AccessibilityId_disable_disappearing_messages,
),
LEGACY(
ExpiryMode::Legacy,
R.string.expiration_type_disappear_legacy,
contentDescription = R.string.expiration_type_disappear_legacy_description
),
AFTER_SEND(
ExpiryMode::AfterSend,
R.string.expiration_type_disappear_after_send,
R.string.expiration_type_disappear_after_read_description,
R.string.expiration_type_disappear_after_send_description
),
AFTER_READ(
ExpiryMode::AfterRead,
R.string.expiration_type_disappear_after_read,
R.string.expiration_type_disappear_after_read_description,
R.string.expiration_type_disappear_after_read_description
);
fun mode(seconds: Long) = if (seconds != 0L) createMode(seconds) else ExpiryMode.NONE fun mode(seconds: Long) = if (seconds != 0L) createMode(seconds) else ExpiryMode.NONE
fun mode(duration: Duration) = mode(duration.inWholeSeconds) fun mode(duration: Duration) = mode(duration.inWholeSeconds)
fun defaultMode(persistedMode: ExpiryMode?) = when(this) {
persistedMode?.type -> persistedMode
AFTER_READ -> mode(12.hours)
else -> mode(1.days)
}
} }
val ExpiryMode.type: ExpiryType get() = when(this) { val ExpiryMode.type: ExpiryType get() = when(this) {

View File

@ -120,6 +120,3 @@ class RadioOptionBuilder<out T>(
contentDescription contentDescription
) )
} }
typealias StringRadioOption = RadioOption<String>
typealias ExpirationRadioOption = RadioOption<ExpiryMode>

View File

@ -1,76 +1,73 @@
package org.thoughtcrime.securesms.conversation.expiration package org.thoughtcrime.securesms.conversation.expiration
import android.app.Application import android.app.Application
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import network.loki.messenger.R
import network.loki.messenger.libsession_util.util.ExpiryMode import network.loki.messenger.libsession_util.util.ExpiryMode
import org.hamcrest.CoreMatchers
import org.hamcrest.MatcherAssert
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito import org.mockito.Mockito
import org.mockito.Mockito.mock import org.mockito.Mockito.mock
import org.mockito.junit.MockitoJUnitRunner
import org.mockito.kotlin.whenever import org.mockito.kotlin.whenever
import org.session.libsession.messaging.messages.ExpirationConfiguration import org.session.libsession.messaging.messages.ExpirationConfiguration
import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address
import org.session.libsession.utilities.SSKEnvironment import org.session.libsession.utilities.SSKEnvironment
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.guava.Optional
import org.thoughtcrime.securesms.MainCoroutineRule import org.thoughtcrime.securesms.MainCoroutineRule
import org.thoughtcrime.securesms.database.GroupDatabase import org.thoughtcrime.securesms.database.GroupDatabase
import org.thoughtcrime.securesms.database.Storage import org.thoughtcrime.securesms.database.Storage
import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.ui.GetString import org.thoughtcrime.securesms.ui.GetString
import kotlin.time.Duration.Companion.hours import kotlin.reflect.typeOf
import network.loki.messenger.R
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.hours
private const val THREAD_ID = 1L
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
@RunWith(MockitoJUnitRunner::class)
class ExpirationSettingsViewModelTest { class ExpirationSettingsViewModelTest {
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
@get:Rule @get:Rule
var mainCoroutineRule = MainCoroutineRule() var mainCoroutineRule = MainCoroutineRule()
private val application: Application = mock(Application::class.java) @Mock lateinit var application: Application
private val textSecurePreferences: TextSecurePreferences = mock(TextSecurePreferences::class.java) @Mock lateinit var textSecurePreferences: TextSecurePreferences
private val messageExpirationManager: SSKEnvironment.MessageExpirationManagerProtocol = mock(SSKEnvironment.MessageExpirationManagerProtocol::class.java) @Mock lateinit var messageExpirationManager: SSKEnvironment.MessageExpirationManagerProtocol
private val threadDb: ThreadDatabase = mock(ThreadDatabase::class.java) @Mock lateinit var threadDb: ThreadDatabase
private val groupDb: GroupDatabase = mock(GroupDatabase::class.java) @Mock lateinit var groupDb: GroupDatabase
private val storage: Storage = mock(Storage::class.java) @Mock lateinit var storage: Storage
private val recipient = mock(Recipient::class.java) @Mock lateinit var recipient: Recipient
@Test @Test
fun `UI should show a list of times and an Off option`() = runTest { fun `1-1 conversation, 12 hours after send, new config`() = runTest {
val threadId = 1L val time = 12.hours
val expirationConfig = ExpirationConfiguration(
threadId = threadId,
expiryMode = ExpiryMode.AfterSend(12.hours.inWholeSeconds),
updatedTimestampMs = 0
)
whenever(threadDb.getRecipientForThreadId(Mockito.anyLong())).thenReturn(recipient)
whenever(storage.getExpirationConfiguration(Mockito.anyLong())).thenReturn(expirationConfig)
whenever(textSecurePreferences.getLocalNumber()).thenReturn("05---LOCAL---ADDRESS")
val userAddress = Address.fromSerialized(textSecurePreferences.getLocalNumber()!!)
val someAddress = Address.fromSerialized("05---SOME---ADDRESS") val someAddress = Address.fromSerialized("05---SOME---ADDRESS")
val config = newExpirationConfiguration(time)
whenever(threadDb.getRecipientForThreadId(Mockito.anyLong())).thenReturn(recipient)
whenever(storage.getExpirationConfiguration(Mockito.anyLong())).thenReturn(config)
whenever(textSecurePreferences.getLocalNumber()).thenReturn("05---LOCAL---ADDRESS")
whenever(recipient.isClosedGroupRecipient).thenReturn(false) whenever(recipient.isClosedGroupRecipient).thenReturn(false)
whenever(recipient.address).thenReturn(someAddress) whenever(recipient.address).thenReturn(someAddress)
whenever(groupDb.getGroup(Mockito.anyString())).thenReturn(Optional.absent())
val viewModel = createViewModel() val viewModel = createViewModel()
advanceUntilIdle() advanceUntilIdle()
MatcherAssert.assertThat( assertThat(
viewModel.state.value, viewModel.state.value
CoreMatchers.equalTo( ).isEqualTo(
State( State(
isGroup = false, isGroup = false,
isSelfAdmin = true, isSelfAdmin = true,
@ -82,51 +79,110 @@ class ExpirationSettingsViewModelTest {
showDebugOptions = false showDebugOptions = false
) )
) )
)
val uiState = viewModel.uiState.value val newTypeOption = TypeOptionCreator(time)
val newTimeOption = TimeOptionCreator(ExpiryType.AFTER_SEND)
MatcherAssert.assertThat( assertThat(
uiState.cards.map { it.title }, viewModel.uiState.value
CoreMatchers.equalTo( ).isEqualTo(
listOf( UiState(
showGroupFooter = false,
CardModel(
R.string.activity_expiration_settings_delete_type, R.string.activity_expiration_settings_delete_type,
R.string.activity_expiration_settings_timer newTypeOption(ExpiryType.NONE),
).map(::GetString) newTypeOption(ExpiryType.AFTER_READ),
newTypeOption(ExpiryType.AFTER_SEND, selected = true)
),
CardModel(
GetString(R.string.activity_expiration_settings_timer),
newTimeOption(duration = 12.hours, selected = true),
newTimeOption(duration = 1.days),
newTimeOption(duration = 7.days),
newTimeOption(duration = 14.days)
)
)
)
}
@Test
fun `1-1 conversation, 1 day after send, new config`() = runTest {
val time = 1.days
val someAddress = Address.fromSerialized("05---SOME---ADDRESS")
val config = newExpirationConfiguration(time)
whenever(threadDb.getRecipientForThreadId(Mockito.anyLong())).thenReturn(recipient)
whenever(storage.getExpirationConfiguration(Mockito.anyLong())).thenReturn(config)
whenever(textSecurePreferences.getLocalNumber()).thenReturn("05---LOCAL---ADDRESS")
whenever(recipient.isClosedGroupRecipient).thenReturn(false)
whenever(recipient.address).thenReturn(someAddress)
val viewModel = createViewModel()
advanceUntilIdle()
assertThat(
viewModel.state.value
).isEqualTo(
State(
isGroup = false,
isSelfAdmin = true,
address = someAddress,
isNoteToSelf = false,
expiryMode = ExpiryMode.AfterSend(1.days.inWholeSeconds),
isNewConfigEnabled = true,
persistedMode = ExpiryMode.AfterSend(1.days.inWholeSeconds),
showDebugOptions = false
) )
) )
MatcherAssert.assertThat( val newTypeOption = TypeOptionCreator(time)
uiState.cards[0].options.map { it.title }, val newTimeOption = TimeOptionCreator(ExpiryType.AFTER_SEND)
CoreMatchers.equalTo(
listOf( assertThat(
R.string.expiration_off, viewModel.uiState.value
R.string.expiration_type_disappear_after_read, ).isEqualTo(
R.string.expiration_type_disappear_after_send, UiState(
).map(::GetString) showGroupFooter = false,
CardModel(
R.string.activity_expiration_settings_delete_type,
newTypeOption(ExpiryType.NONE),
typeOption(12.hours, ExpiryType.AFTER_READ),
newTypeOption(ExpiryType.AFTER_SEND, selected = true)
),
CardModel(
GetString(R.string.activity_expiration_settings_timer),
newTimeOption(duration = 12.hours),
newTimeOption(duration = 1.days, selected = true),
newTimeOption(duration = 7.days),
newTimeOption(duration = 14.days)
) )
) )
)
}
private fun newExpirationConfiguration(time: Duration) = ExpirationConfiguration(
threadId = THREAD_ID,
expiryMode = ExpiryMode.AfterSend(time.inWholeSeconds),
updatedTimestampMs = 0
)
MatcherAssert.assertThat( private class TypeOptionCreator(private val time: Duration) {
uiState.cards[1].options.map { it.title }, operator fun invoke(type: ExpiryType, selected: Boolean = false, enabled: Boolean = true) =
CoreMatchers.equalTo( typeOption(time, type, selected, enabled)
listOf( }
12.hours,
1.days,
7.days,
14.days,
).map(::GetString)
)
)
MatcherAssert.assertThat( private class TimeOptionCreator(private val type: ExpiryType) {
uiState.showGroupFooter, operator fun invoke(duration: Duration, selected: Boolean = false, enabled: Boolean = true) = OptionModel(
CoreMatchers.equalTo(false) value = type.mode(duration),
title = GetString(duration),
enabled = enabled,
selected = selected
) )
} }
private fun createViewModel(isNewConfigEnabled: Boolean = true) = ExpirationSettingsViewModel( private fun createViewModel(isNewConfigEnabled: Boolean = true) = ExpirationSettingsViewModel(
1L, THREAD_ID,
application, application,
textSecurePreferences, textSecurePreferences,
messageExpirationManager, messageExpirationManager,
@ -134,6 +190,16 @@ class ExpirationSettingsViewModelTest {
groupDb, groupDb,
storage, storage,
isNewConfigEnabled, isNewConfigEnabled,
false showDebugOptions = false
) )
} }
fun typeOption(time: Duration, type: ExpiryType, selected: Boolean = false, enabled: Boolean = true) =
OptionModel(
type.mode(time),
GetString(type.title),
type.subtitle?.let(::GetString),
GetString(type.contentDescription),
selected = selected,
enabled = enabled
)