55 * Please see LICENSE in the repository root for full details.
66 */
77
8+ @file:OptIn(ExperimentalCoroutinesApi ::class )
9+
810package io.element.android.features.preferences.impl.developer
911
10- import app.cash.molecule.RecompositionMode
11- import app.cash.molecule.moleculeFlow
12- import app.cash.turbine.test
1312import com.google.common.truth.Truth.assertThat
1413import io.element.android.appconfig.ElementCallConfig
1514import io.element.android.features.logout.test.FakeLogoutUseCase
1615import io.element.android.features.preferences.impl.tasks.FakeClearCacheUseCase
1716import io.element.android.features.preferences.impl.tasks.FakeComputeCacheSizeUseCase
1817import io.element.android.features.rageshake.api.preferences.aRageshakePreferencesState
18+ import io.element.android.libraries.architecture.AsyncAction
1919import io.element.android.libraries.architecture.AsyncData
2020import io.element.android.libraries.core.meta.BuildMeta
2121import io.element.android.libraries.core.meta.BuildType
@@ -24,10 +24,11 @@ import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
2424import io.element.android.libraries.matrix.test.core.aBuildMeta
2525import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
2626import io.element.android.tests.testutils.WarmUpRule
27- import io.element.android.tests.testutils.awaitLastSequentialItem
2827import io.element.android.tests.testutils.lambda.lambdaRecorder
29- import kotlinx.coroutines.CompletableDeferred
28+ import io.element.android.tests.testutils.test
29+ import kotlinx.coroutines.ExperimentalCoroutinesApi
3030import kotlinx.coroutines.flow.first
31+ import kotlinx.coroutines.test.advanceUntilIdle
3132import kotlinx.coroutines.test.runTest
3233import org.junit.Rule
3334import org.junit.Test
@@ -37,115 +38,110 @@ class DeveloperSettingsPresenterTest {
3738 val warmUpRule = WarmUpRule ()
3839
3940 @Test
40- fun `present - ensures initial state is correct` () = runTest {
41+ fun `present - ensures initial states are correct` () = runTest {
4142 val presenter = createDeveloperSettingsPresenter()
42- moleculeFlow(RecompositionMode .Immediate ) {
43- presenter.present()
44- }.test {
45- val initialState = awaitItem()
46- assertThat(initialState.features).isEmpty()
47- assertThat(initialState.clearCacheAction).isEqualTo(AsyncData .Uninitialized )
48- assertThat(initialState.cacheSize).isEqualTo(AsyncData .Uninitialized )
49- assertThat(initialState.customElementCallBaseUrlState).isNotNull()
50- assertThat(initialState.customElementCallBaseUrlState.baseUrl).isNull()
51- assertThat(initialState.isSimpleSlidingSyncEnabled).isFalse()
52- assertThat(initialState.hideImagesAndVideos).isFalse()
53- val loadedState = awaitItem()
54- assertThat(loadedState.rageshakeState.isEnabled).isFalse()
55- assertThat(loadedState.rageshakeState.isSupported).isTrue()
56- assertThat(loadedState.rageshakeState.sensitivity).isEqualTo(0.3f )
57- cancelAndIgnoreRemainingEvents()
58- }
59- }
60-
61- @Test
62- fun `present - ensures feature list is loaded` () = runTest {
63- val presenter = createDeveloperSettingsPresenter()
64- moleculeFlow(RecompositionMode .Immediate ) {
65- presenter.present()
66- }.test {
67- val state = awaitLastSequentialItem()
68- val numberOfModifiableFeatureFlags = FeatureFlags .entries.count { it.isFinished.not () }
69- assertThat(state.features).hasSize(numberOfModifiableFeatureFlags)
70- cancelAndIgnoreRemainingEvents()
43+ presenter.test {
44+ awaitItem().also { state ->
45+ assertThat(state.features).isEmpty()
46+ assertThat(state.clearCacheAction).isEqualTo(AsyncAction .Uninitialized )
47+ assertThat(state.cacheSize).isEqualTo(AsyncData .Uninitialized )
48+ assertThat(state.customElementCallBaseUrlState).isNotNull()
49+ assertThat(state.customElementCallBaseUrlState.baseUrl).isNull()
50+ assertThat(state.isSimpleSlidingSyncEnabled).isFalse()
51+ assertThat(state.hideImagesAndVideos).isFalse()
52+ assertThat(state.rageshakeState.isEnabled).isFalse()
53+ assertThat(state.rageshakeState.isSupported).isTrue()
54+ assertThat(state.rageshakeState.sensitivity).isEqualTo(0.3f )
55+ }
56+ awaitItem().also { state ->
57+ assertThat(state.features).isNotEmpty()
58+ val numberOfModifiableFeatureFlags = FeatureFlags .entries.count { it.isFinished.not () }
59+ assertThat(state.features).hasSize(numberOfModifiableFeatureFlags)
60+ }
61+ awaitItem().also { state ->
62+ assertThat(state.cacheSize).isInstanceOf(AsyncData .Success ::class .java)
63+ }
7164 }
7265 }
7366
7467 @Test
7568 fun `present - ensures Room directory search is not present on release Google Play builds` () = runTest {
7669 val buildMeta = aBuildMeta(buildType = BuildType .RELEASE , flavorDescription = " GooglePlay" )
7770 val presenter = createDeveloperSettingsPresenter(buildMeta = buildMeta)
78- moleculeFlow(RecompositionMode .Immediate ) {
79- presenter.present()
80- }.test {
81- val state = awaitLastSequentialItem()
82- assertThat(state.features).doesNotContain(FeatureFlags .RoomDirectorySearch )
83- cancelAndIgnoreRemainingEvents()
71+ presenter.test {
72+ skipItems(2 )
73+ awaitItem().also { state ->
74+ assertThat(state.features).doesNotContain(FeatureFlags .RoomDirectorySearch )
75+ }
8476 }
8577 }
8678
8779 @Test
8880 fun `present - ensures state is updated when enabled feature event is triggered` () = runTest {
8981 val presenter = createDeveloperSettingsPresenter()
90- moleculeFlow(RecompositionMode .Immediate ) {
91- presenter.present()
92- }.test {
93- skipItems(1 )
94- val stateBeforeEvent = awaitItem()
95- val featureBeforeEvent = stateBeforeEvent.features.first()
96- stateBeforeEvent.eventSink(DeveloperSettingsEvents .UpdateEnabledFeature (featureBeforeEvent, ! featureBeforeEvent.isEnabled))
97- val stateAfterEvent = awaitItem()
98- val featureAfterEvent = stateAfterEvent.features.first()
99- assertThat(featureBeforeEvent.key).isEqualTo(featureAfterEvent.key)
100- assertThat(featureBeforeEvent.isEnabled).isNotEqualTo(featureAfterEvent.isEnabled)
101- cancelAndIgnoreRemainingEvents()
82+ presenter.test {
83+ skipItems(2 )
84+ awaitItem().also { state ->
85+ val feature = state.features.first()
86+ state.eventSink(DeveloperSettingsEvents .UpdateEnabledFeature (feature, ! feature.isEnabled))
87+ }
88+ awaitItem().also { state ->
89+ val feature = state.features.first()
90+ assertThat(feature.isEnabled).isTrue()
91+ assertThat(feature.key).isEqualTo(feature.key)
92+ }
10293 }
10394 }
10495
10596 @Test
10697 fun `present - clear cache` () = runTest {
10798 val clearCacheUseCase = FakeClearCacheUseCase ()
10899 val presenter = createDeveloperSettingsPresenter(clearCacheUseCase = clearCacheUseCase)
109- moleculeFlow(RecompositionMode .Immediate ) {
110- presenter.present()
111- }.test {
112- skipItems(1 )
113- val initialState = awaitItem()
100+ presenter.test {
101+ skipItems(2 )
114102 assertThat(clearCacheUseCase.executeHasBeenCalled).isFalse()
115- initialState.eventSink(DeveloperSettingsEvents .ClearCache )
116- val stateAfterEvent = awaitItem()
117- assertThat(stateAfterEvent.clearCacheAction).isInstanceOf(AsyncData .Loading ::class .java)
118- skipItems(1 )
119- assertThat(awaitItem().clearCacheAction).isInstanceOf(AsyncData .Success ::class .java)
120- assertThat(clearCacheUseCase.executeHasBeenCalled).isTrue()
121- cancelAndIgnoreRemainingEvents()
103+ awaitItem().also { state ->
104+ state.eventSink(DeveloperSettingsEvents .ClearCache )
105+ }
106+ awaitItem().also { state ->
107+ assertThat(state.clearCacheAction).isInstanceOf(AsyncAction .Loading ::class .java)
108+ }
109+ awaitItem().also { state ->
110+ assertThat(state.clearCacheAction).isInstanceOf(AsyncAction .Success ::class .java)
111+ assertThat(clearCacheUseCase.executeHasBeenCalled).isTrue()
112+ }
113+ awaitItem().also { state ->
114+ assertThat(state.cacheSize).isInstanceOf(AsyncData .Loading ::class .java)
115+ }
116+ awaitItem().also { state ->
117+ assertThat(state.cacheSize).isInstanceOf(AsyncData .Success ::class .java)
118+ }
122119 }
123120 }
124121
125122 @Test
126123 fun `present - custom element call base url` () = runTest {
127124 val preferencesStore = InMemoryAppPreferencesStore ()
128125 val presenter = createDeveloperSettingsPresenter(preferencesStore = preferencesStore)
129- moleculeFlow( RecompositionMode . Immediate ) {
130- presenter.present( )
131- }.test {
132- skipItems( 1 )
133- val initialState = awaitItem( )
134- assertThat(initialState.customElementCallBaseUrlState.baseUrl).isNull()
135- initialState.eventSink( DeveloperSettingsEvents . SetCustomElementCallBaseUrl ( " https://call.element.ahoy " ))
136- val updatedItem = awaitItem( )
137- assertThat(updatedItem .customElementCallBaseUrlState.baseUrl ).isEqualTo(" https://call.element.ahoy " )
138- assertThat(updatedItem.customElementCallBaseUrlState.defaultUrl).isEqualTo( ElementCallConfig . DEFAULT_BASE_URL )
126+ presenter.test {
127+ skipItems( 2 )
128+ awaitItem(). also { state ->
129+ assertThat(state.customElementCallBaseUrlState.baseUrl).isNull( )
130+ state.eventSink( DeveloperSettingsEvents . SetCustomElementCallBaseUrl ( " https://call.element.ahoy " ) )
131+ }
132+ awaitItem(). also { state ->
133+ assertThat(state.customElementCallBaseUrlState.baseUrl).isEqualTo( " https://call.element.ahoy " )
134+ assertThat(state .customElementCallBaseUrlState.defaultUrl ).isEqualTo(ElementCallConfig . DEFAULT_BASE_URL )
135+ }
139136 }
140137 }
141138
142139 @Test
143140 fun `present - custom element call base url validator needs at least an HTTP scheme and host` () = runTest {
144141 val presenter = createDeveloperSettingsPresenter()
145- moleculeFlow(RecompositionMode .Immediate ) {
146- presenter.present()
147- }.test {
148- val urlValidator = awaitLastSequentialItem().customElementCallBaseUrlState.validator
142+ presenter.test {
143+ skipItems(2 )
144+ val urlValidator = awaitItem().customElementCallBaseUrlState.validator
149145 assertThat(urlValidator(" " )).isTrue() // We allow empty string to clear the value and use the default one
150146 assertThat(urlValidator(" test" )).isFalse()
151147 assertThat(urlValidator(" http://" )).isFalse()
@@ -156,53 +152,51 @@ class DeveloperSettingsPresenterTest {
156152
157153 @Test
158154 fun `present - toggling simplified sliding sync changes the preferences and logs out the user` () = runTest {
159- val latch1 = CompletableDeferred <Unit >()
160- val latch2 = CompletableDeferred <Unit >()
161- val logoutCallRecorder = lambdaRecorder<Boolean , String ?> {
162- if (latch1.isActive) {
163- latch1.complete(Unit )
164- } else {
165- latch2.complete(Unit )
166- }
167- " "
168- }
155+ val logoutCallRecorder = lambdaRecorder<Boolean , String ?> { " " }
169156 val logoutUseCase = FakeLogoutUseCase (logoutLambda = logoutCallRecorder)
170157 val preferences = InMemoryAppPreferencesStore ()
171158 val presenter = createDeveloperSettingsPresenter(preferencesStore = preferences, logoutUseCase = logoutUseCase)
172- moleculeFlow(RecompositionMode .Immediate ) {
173- presenter.present()
174- }.test {
175- val initialState = awaitLastSequentialItem()
176- assertThat(initialState.isSimpleSlidingSyncEnabled).isFalse()
177-
178- initialState.eventSink(DeveloperSettingsEvents .SetSimplifiedSlidingSyncEnabled (true ))
179- assertThat(awaitItem().isSimpleSlidingSyncEnabled).isTrue()
180- assertThat(preferences.isSimplifiedSlidingSyncEnabledFlow().first()).isTrue()
181- latch1.await()
182- logoutCallRecorder.assertions().isCalledOnce()
183- initialState.eventSink(DeveloperSettingsEvents .SetSimplifiedSlidingSyncEnabled (false ))
184- assertThat(awaitItem().isSimpleSlidingSyncEnabled).isFalse()
185- assertThat(preferences.isSimplifiedSlidingSyncEnabledFlow().first()).isFalse()
186- latch2.await()
187- logoutCallRecorder.assertions().isCalledExactly(times = 2 )
159+ presenter.test {
160+ skipItems(2 )
161+ awaitItem().also { state ->
162+ assertThat(state.isSimpleSlidingSyncEnabled).isFalse()
163+ state.eventSink(DeveloperSettingsEvents .SetSimplifiedSlidingSyncEnabled (true ))
164+ }
165+ awaitItem().also { state ->
166+ assertThat(state.isSimpleSlidingSyncEnabled).isTrue()
167+ assertThat(preferences.isSimplifiedSlidingSyncEnabledFlow().first()).isTrue()
168+ advanceUntilIdle()
169+ logoutCallRecorder.assertions().isCalledOnce()
170+ state.eventSink(DeveloperSettingsEvents .SetSimplifiedSlidingSyncEnabled (false ))
171+ }
172+ awaitItem().also { state ->
173+ assertThat(state.isSimpleSlidingSyncEnabled).isFalse()
174+ assertThat(preferences.isSimplifiedSlidingSyncEnabledFlow().first()).isFalse()
175+ advanceUntilIdle()
176+ logoutCallRecorder.assertions().isCalledExactly(2 )
177+ }
188178 }
189179 }
190180
191181 @Test
192182 fun `present - toggling hide image and video` () = runTest {
193183 val preferences = InMemoryAppPreferencesStore ()
194184 val presenter = createDeveloperSettingsPresenter(preferencesStore = preferences)
195- moleculeFlow(RecompositionMode .Immediate ) {
196- presenter.present()
197- }.test {
198- val initialState = awaitLastSequentialItem()
199- assertThat(initialState.hideImagesAndVideos).isFalse()
200- initialState.eventSink(DeveloperSettingsEvents .SetHideImagesAndVideos (true ))
201- assertThat(awaitItem().hideImagesAndVideos).isTrue()
202- assertThat(preferences.doesHideImagesAndVideosFlow().first()).isTrue()
203- initialState.eventSink(DeveloperSettingsEvents .SetHideImagesAndVideos (false ))
204- assertThat(awaitItem().hideImagesAndVideos).isFalse()
205- assertThat(preferences.doesHideImagesAndVideosFlow().first()).isFalse()
185+ presenter.test {
186+ skipItems(2 )
187+ awaitItem().also { state ->
188+ assertThat(state.hideImagesAndVideos).isFalse()
189+ state.eventSink(DeveloperSettingsEvents .SetHideImagesAndVideos (true ))
190+ }
191+ awaitItem().also { state ->
192+ assertThat(state.hideImagesAndVideos).isTrue()
193+ assertThat(preferences.doesHideImagesAndVideosFlow().first()).isTrue()
194+ state.eventSink(DeveloperSettingsEvents .SetHideImagesAndVideos (false ))
195+ }
196+ awaitItem().also { state ->
197+ assertThat(state.hideImagesAndVideos).isFalse()
198+ assertThat(preferences.doesHideImagesAndVideosFlow().first()).isFalse()
199+ }
206200 }
207201 }
208202
0 commit comments