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,8 +24,8 @@ 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
28+ import io.element.android.tests.testutils.test
2929import kotlinx.coroutines.ExperimentalCoroutinesApi
3030import kotlinx.coroutines.flow.first
3131import kotlinx.coroutines.test.advanceUntilIdle
@@ -38,115 +38,110 @@ class DeveloperSettingsPresenterTest {
3838 val warmUpRule = WarmUpRule ()
3939
4040 @Test
41- fun `present - ensures initial state is correct` () = runTest {
42- val presenter = createDeveloperSettingsPresenter()
43- moleculeFlow(RecompositionMode .Immediate ) {
44- presenter.present()
45- }.test {
46- val initialState = awaitItem()
47- assertThat(initialState.features).isEmpty()
48- assertThat(initialState.clearCacheAction).isEqualTo(AsyncData .Uninitialized )
49- assertThat(initialState.cacheSize).isEqualTo(AsyncData .Uninitialized )
50- assertThat(initialState.customElementCallBaseUrlState).isNotNull()
51- assertThat(initialState.customElementCallBaseUrlState.baseUrl).isNull()
52- assertThat(initialState.isSimpleSlidingSyncEnabled).isFalse()
53- assertThat(initialState.hideImagesAndVideos).isFalse()
54- val loadedState = awaitItem()
55- assertThat(loadedState.rageshakeState.isEnabled).isFalse()
56- assertThat(loadedState.rageshakeState.isSupported).isTrue()
57- assertThat(loadedState.rageshakeState.sensitivity).isEqualTo(0.3f )
58- cancelAndIgnoreRemainingEvents()
59- }
60- }
61-
62- @Test
63- fun `present - ensures feature list is loaded` () = runTest {
41+ fun `present - ensures initial states are correct` () = runTest {
6442 val presenter = createDeveloperSettingsPresenter()
65- moleculeFlow(RecompositionMode .Immediate ) {
66- presenter.present()
67- }.test {
68- val state = awaitLastSequentialItem()
69- val numberOfModifiableFeatureFlags = FeatureFlags .entries.count { it.isFinished.not () }
70- assertThat(state.features).hasSize(numberOfModifiableFeatureFlags)
71- 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+ }
7264 }
7365 }
7466
7567 @Test
7668 fun `present - ensures Room directory search is not present on release Google Play builds` () = runTest {
7769 val buildMeta = aBuildMeta(buildType = BuildType .RELEASE , flavorDescription = " GooglePlay" )
7870 val presenter = createDeveloperSettingsPresenter(buildMeta = buildMeta)
79- moleculeFlow(RecompositionMode .Immediate ) {
80- presenter.present()
81- }.test {
82- val state = awaitLastSequentialItem()
83- assertThat(state.features).doesNotContain(FeatureFlags .RoomDirectorySearch )
84- cancelAndIgnoreRemainingEvents()
71+ presenter.test {
72+ skipItems(2 )
73+ awaitItem().also { state ->
74+ assertThat(state.features).doesNotContain(FeatureFlags .RoomDirectorySearch )
75+ }
8576 }
8677 }
8778
8879 @Test
8980 fun `present - ensures state is updated when enabled feature event is triggered` () = runTest {
9081 val presenter = createDeveloperSettingsPresenter()
91- moleculeFlow(RecompositionMode .Immediate ) {
92- presenter.present()
93- }.test {
94- skipItems(1 )
95- val stateBeforeEvent = awaitItem()
96- val featureBeforeEvent = stateBeforeEvent.features.first()
97- stateBeforeEvent.eventSink(DeveloperSettingsEvents .UpdateEnabledFeature (featureBeforeEvent, ! featureBeforeEvent.isEnabled))
98- val stateAfterEvent = awaitItem()
99- val featureAfterEvent = stateAfterEvent.features.first()
100- assertThat(featureBeforeEvent.key).isEqualTo(featureAfterEvent.key)
101- assertThat(featureBeforeEvent.isEnabled).isNotEqualTo(featureAfterEvent.isEnabled)
102- 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+ }
10393 }
10494 }
10595
10696 @Test
10797 fun `present - clear cache` () = runTest {
10898 val clearCacheUseCase = FakeClearCacheUseCase ()
10999 val presenter = createDeveloperSettingsPresenter(clearCacheUseCase = clearCacheUseCase)
110- moleculeFlow(RecompositionMode .Immediate ) {
111- presenter.present()
112- }.test {
113- skipItems(1 )
114- val initialState = awaitItem()
100+ presenter.test {
101+ skipItems(2 )
115102 assertThat(clearCacheUseCase.executeHasBeenCalled).isFalse()
116- initialState.eventSink(DeveloperSettingsEvents .ClearCache )
117- val stateAfterEvent = awaitItem()
118- assertThat(stateAfterEvent.clearCacheAction).isInstanceOf(AsyncData .Loading ::class .java)
119- skipItems(1 )
120- assertThat(awaitItem().clearCacheAction).isInstanceOf(AsyncData .Success ::class .java)
121- assertThat(clearCacheUseCase.executeHasBeenCalled).isTrue()
122- 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+ }
123119 }
124120 }
125121
126122 @Test
127123 fun `present - custom element call base url` () = runTest {
128124 val preferencesStore = InMemoryAppPreferencesStore ()
129125 val presenter = createDeveloperSettingsPresenter(preferencesStore = preferencesStore)
130- moleculeFlow( RecompositionMode . Immediate ) {
131- presenter.present( )
132- }.test {
133- skipItems( 1 )
134- val initialState = awaitItem( )
135- assertThat(initialState.customElementCallBaseUrlState.baseUrl).isNull()
136- initialState.eventSink( DeveloperSettingsEvents . SetCustomElementCallBaseUrl ( " https://call.element.ahoy " ))
137- val updatedItem = awaitItem( )
138- assertThat(updatedItem .customElementCallBaseUrlState.baseUrl ).isEqualTo(" https://call.element.ahoy " )
139- 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+ }
140136 }
141137 }
142138
143139 @Test
144140 fun `present - custom element call base url validator needs at least an HTTP scheme and host` () = runTest {
145141 val presenter = createDeveloperSettingsPresenter()
146- moleculeFlow(RecompositionMode .Immediate ) {
147- presenter.present()
148- }.test {
149- val urlValidator = awaitLastSequentialItem().customElementCallBaseUrlState.validator
142+ presenter.test {
143+ skipItems(2 )
144+ val urlValidator = awaitItem().customElementCallBaseUrlState.validator
150145 assertThat(urlValidator(" " )).isTrue() // We allow empty string to clear the value and use the default one
151146 assertThat(urlValidator(" test" )).isFalse()
152147 assertThat(urlValidator(" http://" )).isFalse()
@@ -155,48 +150,53 @@ class DeveloperSettingsPresenterTest {
155150 }
156151 }
157152
158- @OptIn(ExperimentalCoroutinesApi ::class )
159153 @Test
160154 fun `present - toggling simplified sliding sync changes the preferences and logs out the user` () = runTest {
161155 val logoutCallRecorder = lambdaRecorder<Boolean , String ?> { " " }
162156 val logoutUseCase = FakeLogoutUseCase (logoutLambda = logoutCallRecorder)
163157 val preferences = InMemoryAppPreferencesStore ()
164158 val presenter = createDeveloperSettingsPresenter(preferencesStore = preferences, logoutUseCase = logoutUseCase)
165- moleculeFlow(RecompositionMode .Immediate ) {
166- presenter.present()
167- }.test {
168- val initialState = awaitLastSequentialItem()
169- assertThat(initialState.isSimpleSlidingSyncEnabled).isFalse()
170-
171- initialState.eventSink(DeveloperSettingsEvents .SetSimplifiedSlidingSyncEnabled (true ))
172- assertThat(awaitItem().isSimpleSlidingSyncEnabled).isTrue()
173- assertThat(preferences.isSimplifiedSlidingSyncEnabledFlow().first()).isTrue()
174- advanceUntilIdle()
175- logoutCallRecorder.assertions().isCalledOnce()
176-
177- initialState.eventSink(DeveloperSettingsEvents .SetSimplifiedSlidingSyncEnabled (false ))
178- assertThat(awaitItem().isSimpleSlidingSyncEnabled).isFalse()
179- assertThat(preferences.isSimplifiedSlidingSyncEnabledFlow().first()).isFalse()
180- advanceUntilIdle()
181- 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+ }
182178 }
183179 }
184180
185181 @Test
186182 fun `present - toggling hide image and video` () = runTest {
187183 val preferences = InMemoryAppPreferencesStore ()
188184 val presenter = createDeveloperSettingsPresenter(preferencesStore = preferences)
189- moleculeFlow(RecompositionMode .Immediate ) {
190- presenter.present()
191- }.test {
192- val initialState = awaitLastSequentialItem()
193- assertThat(initialState.hideImagesAndVideos).isFalse()
194- initialState.eventSink(DeveloperSettingsEvents .SetHideImagesAndVideos (true ))
195- assertThat(awaitItem().hideImagesAndVideos).isTrue()
196- assertThat(preferences.doesHideImagesAndVideosFlow().first()).isTrue()
197- initialState.eventSink(DeveloperSettingsEvents .SetHideImagesAndVideos (false ))
198- assertThat(awaitItem().hideImagesAndVideos).isFalse()
199- 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+ }
200200 }
201201 }
202202
0 commit comments