diff options
Diffstat (limited to 'android/app/src')
11 files changed, 465 insertions, 1 deletions
diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ChangelogDialogTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ChangelogDialogTest.kt new file mode 100644 index 0000000000..dab5bf0a60 --- /dev/null +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ChangelogDialogTest.kt @@ -0,0 +1,74 @@ +package net.mullvad.mullvadvpn.compose.screen + +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.just +import io.mockk.verify +import kotlinx.coroutines.flow.MutableStateFlow +import net.mullvad.mullvadvpn.compose.component.AppTheme +import net.mullvad.mullvadvpn.compose.component.ChangelogDialog +import net.mullvad.mullvadvpn.viewmodel.ChangelogDialogUiState +import net.mullvad.mullvadvpn.viewmodel.ChangelogViewModel +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class ChangelogDialogTest { + @get:Rule + val composeTestRule = createComposeRule() + + @MockK + lateinit var mockedViewModel: ChangelogViewModel + + @Before + fun setup() { + MockKAnnotations.init(this) + } + + @Test + fun testShowChangeLogWhenNeeded() { + // Arrange + every { + mockedViewModel.changelogDialogUiState + } returns MutableStateFlow(ChangelogDialogUiState.Show(listOf(CHANGELOG_ITEM))) + every { + mockedViewModel.dismissChangelogDialog() + } just Runs + + composeTestRule.setContent { + AppTheme { + ChangelogDialog( + changesList = listOf(CHANGELOG_ITEM), + version = CHANGELOG_VERSION, + onDismiss = { + mockedViewModel.dismissChangelogDialog() + } + ) + } + } + + // Check changelog content showed within dialog + composeTestRule + .onNodeWithText(CHANGELOG_ITEM) + .assertExists() + + // perform click on Got It button to check if dismiss occur + composeTestRule + .onNodeWithText(CHANGELOG_BUTTON_TEXT) + .performClick() + + // Assert + verify { mockedViewModel.dismissChangelogDialog() } + } + + companion object { + private const val CHANGELOG_BUTTON_TEXT = "Got it!" + private const val CHANGELOG_ITEM = "Changelog item" + private const val CHANGELOG_VERSION = "1234.5" + } +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/ChangelogDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/ChangelogDialog.kt new file mode 100644 index 0000000000..976abc5364 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/ChangelogDialog.kt @@ -0,0 +1,95 @@ +package net.mullvad.mullvadvpn.compose.component + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material.AlertDialog +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.DialogProperties +import net.mullvad.mullvadvpn.R + +@Composable +fun ChangelogDialog( + changesList: List<String>, + version: String, + onDismiss: () -> Unit +) { + AlertDialog( + onDismissRequest = { + onDismiss() + }, + title = { + Text( + text = version, + color = colorResource(id = R.color.white), + fontSize = 30.sp, + fontStyle = FontStyle.Normal, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + ) + }, + + text = { + Column { + Text( + text = stringResource(R.string.changes_dialog_subtitle), + fontSize = 18.sp, + color = Color.White, + modifier = Modifier + .padding( + vertical = dimensionResource(id = R.dimen.medium_padding) + ) + ) + + changesList.forEach { changeItem -> + ChangeListItem( + text = changeItem + ) + } + } + }, + buttons = { + Button( + modifier = Modifier + .wrapContentHeight() + .padding(all = dimensionResource(id = R.dimen.medium_padding)) + .defaultMinSize( + minHeight = dimensionResource(id = R.dimen.button_height) + ) + .fillMaxWidth(), + colors = ButtonDefaults.buttonColors( + backgroundColor = colorResource(id = R.color.blue), + contentColor = colorResource(id = R.color.white) + ), + onClick = { + onDismiss() + } + + ) { + Text( + text = stringResource(R.string.changes_dialog_dismiss_button), + fontSize = 18.sp + ) + } + }, + properties = DialogProperties( + dismissOnClickOutside = true, + dismissOnBackPress = true, + ), + backgroundColor = colorResource(id = R.color.darkBlue) + ) +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/List.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/List.kt index 3babb29a21..bd7aa0926b 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/List.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/List.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.absolutePadding import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -16,9 +17,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.constraintlayout.compose.ConstraintLayout import net.mullvad.mullvadvpn.R @Composable @@ -73,3 +76,44 @@ fun ListItem( } } } + +@Composable +fun ChangeListItem( + text: String +) { + ConstraintLayout { + val (bullet, changeLog) = createRefs() + val smallPadding = dimensionResource(id = R.dimen.small_padding) + Box( + modifier = Modifier + .constrainAs(bullet) { + top.linkTo(parent.top) + start.linkTo(parent.absoluteLeft) + } + ) { + Text( + text = "•", + fontSize = 14.sp, + color = Color.White + ) + } + Box( + modifier = Modifier + .absolutePadding(left = dimensionResource(id = R.dimen.medium_padding)) + .constrainAs(changeLog) { + top.linkTo(parent.top) + bottom.linkTo(parent.bottom, margin = smallPadding) + start.linkTo(parent.start) + end.linkTo(parent.end) + } + ) { + Text( + text = text, + fontSize = 14.sp, + color = Color.White, + modifier = Modifier + + ) + } + } +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt index 1309a0e6cf..bdabbcaeff 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt @@ -1,11 +1,15 @@ package net.mullvad.mullvadvpn.di +import android.content.Context +import android.content.SharedPreferences import android.content.pm.PackageManager import android.os.Messenger import kotlinx.coroutines.Dispatchers +import net.mullvad.mullvadvpn.BuildConfig import net.mullvad.mullvadvpn.applist.ApplicationsIconManager import net.mullvad.mullvadvpn.applist.ApplicationsProvider import net.mullvad.mullvadvpn.ipc.EventDispatcher +import net.mullvad.mullvadvpn.repository.ChangelogRepository import net.mullvad.mullvadvpn.ui.notification.AccountExpiryNotification import net.mullvad.mullvadvpn.ui.notification.TunnelStateNotification import net.mullvad.mullvadvpn.ui.notification.VersionInfoNotification @@ -13,11 +17,15 @@ import net.mullvad.mullvadvpn.ui.serviceconnection.AccountRepository import net.mullvad.mullvadvpn.ui.serviceconnection.DeviceRepository import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager import net.mullvad.mullvadvpn.ui.serviceconnection.SplitTunneling +import net.mullvad.mullvadvpn.util.ChangelogDataProvider +import net.mullvad.mullvadvpn.util.IChangelogDataProvider +import net.mullvad.mullvadvpn.viewmodel.ChangelogViewModel import net.mullvad.mullvadvpn.viewmodel.ConnectViewModel import net.mullvad.mullvadvpn.viewmodel.DeviceListViewModel import net.mullvad.mullvadvpn.viewmodel.DeviceRevokedViewModel import net.mullvad.mullvadvpn.viewmodel.LoginViewModel import net.mullvad.mullvadvpn.viewmodel.SplitTunnelingViewModel +import org.koin.android.ext.koin.androidApplication import org.koin.android.ext.koin.androidContext import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.core.qualifier.named @@ -26,6 +34,10 @@ import org.koin.dsl.onClose val uiModule = module { + single<SharedPreferences>(named(APP_PREFERENCES_NAME)) { + androidApplication().getSharedPreferences(APP_PREFERENCES_NAME, Context.MODE_PRIVATE) + } + single<PackageManager> { androidContext().packageManager } single<String>(named(SELF_PACKAGE_NAME)) { androidContext().packageName } @@ -43,6 +55,9 @@ val uiModule = module { single { ServiceConnectionManager(androidContext()) } single { androidContext().resources } + single { androidContext().assets } + + single { ChangelogRepository(get(named(APP_PREFERENCES_NAME)), get()) } single { AccountExpiryNotification(get()) } single { TunnelStateNotification(get()) } @@ -51,13 +66,23 @@ val uiModule = module { single { AccountRepository(get()) } single { DeviceRepository(get()) } + single<IChangelogDataProvider> { ChangelogDataProvider(get()) } + // View models viewModel { ConnectViewModel() } viewModel { DeviceRevokedViewModel(get(), get()) } viewModel { DeviceListViewModel(get(), get()) } viewModel { LoginViewModel(get(), get()) } + viewModel { + ChangelogViewModel( + get(), + BuildConfig.VERSION_CODE, + BuildConfig.ALWAYS_SHOW_CHANGELOG + ) + } } const val APPS_SCOPE = "APPS_SCOPE" const val SERVICE_CONNECTION_SCOPE = "SERVICE_CONNECTION_SCOPE" const val SELF_PACKAGE_NAME = "SELF_PACKAGE_NAME" +const val APP_PREFERENCES_NAME = "net.mullvad.mullvadvpn.app_preferences" diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/ChangelogRepository.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/ChangelogRepository.kt new file mode 100644 index 0000000000..3e8150bf97 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/ChangelogRepository.kt @@ -0,0 +1,24 @@ +package net.mullvad.mullvadvpn.repository + +import android.content.SharedPreferences +import net.mullvad.mullvadvpn.util.IChangelogDataProvider + +private const val MISSING_VERSION_CODE = -1 +private const val NEWLINE_CHAR = '\n' +private const val LAST_SHOWED_CHANGELOG_VERSION_CODE = "last_showed_changelog_version_code" + +class ChangelogRepository( + private val preferences: SharedPreferences, + private val dataProvider: IChangelogDataProvider +) { + fun getVersionCodeOfMostRecentChangelogShowed(): Int { + return preferences.getInt(LAST_SHOWED_CHANGELOG_VERSION_CODE, MISSING_VERSION_CODE) + } + + fun setVersionCodeOfMostRecentChangelogShowed(versionCode: Int) = + preferences.edit().putInt(LAST_SHOWED_CHANGELOG_VERSION_CODE, versionCode).apply() + + fun getLastVersionChanges(): List<String> { + return dataProvider.getChangelog().split(NEWLINE_CHAR).filter { it.isNotEmpty() } + } +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt index a72d74c917..becf82a69e 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt @@ -12,6 +12,9 @@ import android.util.Log import android.view.WindowManager import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentManager @@ -28,6 +31,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeoutOrNull import net.mullvad.mullvadvpn.BuildConfig import net.mullvad.mullvadvpn.R +import net.mullvad.mullvadvpn.compose.component.ChangelogDialog import net.mullvad.mullvadvpn.dataproxy.MullvadProblemReport import net.mullvad.mullvadvpn.di.uiModule import net.mullvad.mullvadvpn.model.AccountExpiry @@ -39,6 +43,8 @@ import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager import net.mullvad.mullvadvpn.util.SdkUtils.isNotificationPermissionGranted import net.mullvad.mullvadvpn.util.UNKNOWN_STATE_DEBOUNCE_DELAY_MILLISECONDS import net.mullvad.mullvadvpn.util.addDebounceForUnknownState +import net.mullvad.mullvadvpn.viewmodel.ChangelogDialogUiState +import net.mullvad.mullvadvpn.viewmodel.ChangelogViewModel import org.koin.android.ext.android.getKoin import org.koin.core.context.loadKoinModules @@ -63,6 +69,7 @@ open class MainActivity : FragmentActivity() { private lateinit var accountRepository: AccountRepository private lateinit var deviceRepository: DeviceRepository private lateinit var serviceConnectionManager: ServiceConnectionManager + private lateinit var changelogViewModel: ChangelogViewModel override fun onCreate(savedInstanceState: Bundle?) { loadKoinModules(uiModule) @@ -71,6 +78,7 @@ open class MainActivity : FragmentActivity() { accountRepository = get() deviceRepository = get() serviceConnectionManager = get() + changelogViewModel = get() } requestedOrientation = if (deviceIsTv) { @@ -186,6 +194,31 @@ open class MainActivity : FragmentActivity() { } } } + lifecycleScope.launch { + deviceRepository.deviceState + .flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED) + .filter { it is DeviceState.LoggedIn || it is DeviceState.LoggedOut } + .collect { loadChangelogComponent() } + } + } + + private fun loadChangelogComponent() { + findViewById<ComposeView>(R.id.compose_view).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnDetachedFromWindow) + setContent { + val state = changelogViewModel.changelogDialogUiState.collectAsState().value + if (state is ChangelogDialogUiState.Show) { + ChangelogDialog( + changesList = state.changes, + version = BuildConfig.VERSION_NAME, + onDismiss = { + changelogViewModel.dismissChangelogDialog() + } + ) + } + } + changelogViewModel.refreshChangelogDialogUiState() + } } @Suppress("DEPRECATION") diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/ChangelogDataProvider.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/ChangelogDataProvider.kt new file mode 100644 index 0000000000..8ce36fd717 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/ChangelogDataProvider.kt @@ -0,0 +1,23 @@ +package net.mullvad.mullvadvpn.util + +import android.content.res.AssetManager +import android.util.Log +import java.io.IOException + +private const val CHANGELOG_FILE = "en-US/default.txt" +private const val EMPTY_DEFAULT_STRING_WHEN_UNABLE_TO_READ_CHANGELOG = "" + +class ChangelogDataProvider(var assets: AssetManager) : IChangelogDataProvider { + override fun getChangelog(): String { + return try { + assets.open(CHANGELOG_FILE).bufferedReader().use { it.readText() } + } catch (ex: IOException) { + Log.e("mullvad", "Unable to read bundled changelog file.") + EMPTY_DEFAULT_STRING_WHEN_UNABLE_TO_READ_CHANGELOG + } + } +} + +interface IChangelogDataProvider { + fun getChangelog(): String +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ChangelogViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ChangelogViewModel.kt new file mode 100644 index 0000000000..003eb962ad --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ChangelogViewModel.kt @@ -0,0 +1,41 @@ +package net.mullvad.mullvadvpn.viewmodel + +import androidx.lifecycle.ViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import net.mullvad.mullvadvpn.repository.ChangelogRepository + +class ChangelogViewModel( + private val changelogRepository: ChangelogRepository, + private val buildVersionCode: Int, + private val alwaysShowChangelog: Boolean +) : ViewModel() { + private val _changelogDialogUiState = + MutableStateFlow<ChangelogDialogUiState>(ChangelogDialogUiState.Hide) + val changelogDialogUiState = _changelogDialogUiState.asStateFlow() + + fun refreshChangelogDialogUiState() { + val shouldShowChangelogDialog = alwaysShowChangelog || changelogRepository + .getVersionCodeOfMostRecentChangelogShowed() < buildVersionCode + _changelogDialogUiState.value = if (shouldShowChangelogDialog) { + val changelogList = changelogRepository.getLastVersionChanges() + if (changelogList.isNotEmpty()) { + ChangelogDialogUiState.Show(changelogList) + } else { + ChangelogDialogUiState.Hide + } + } else { + ChangelogDialogUiState.Hide + } + } + + fun dismissChangelogDialog() { + changelogRepository.setVersionCodeOfMostRecentChangelogShowed(buildVersionCode) + _changelogDialogUiState.value = ChangelogDialogUiState.Hide + } +} + +sealed class ChangelogDialogUiState { + data class Show(val changes: List<String>) : ChangelogDialogUiState() + object Hide : ChangelogDialogUiState() +} diff --git a/android/app/src/main/res/layout/main.xml b/android/app/src/main/res/layout/main.xml index 7839409631..8e7356e766 100644 --- a/android/app/src/main/res/layout/main.xml +++ b/android/app/src/main/res/layout/main.xml @@ -2,4 +2,9 @@ android:id="@+id/main_fragment" android:layout_width="match_parent" android:layout_height="match_parent" - android:filterTouchesWhenObscured="true" /> + android:filterTouchesWhenObscured="true"> + <!-- this component added to be able show compose dialog --> + <androidx.compose.ui.platform.ComposeView android:id="@+id/compose_view" + android:layout_width="match_parent" + android:layout_height="@dimen/zero_size" /> +</FrameLayout> diff --git a/android/app/src/main/res/values/dimensions.xml b/android/app/src/main/res/values/dimensions.xml index ca7ff1ac81..4f35637a64 100644 --- a/android/app/src/main/res/values/dimensions.xml +++ b/android/app/src/main/res/values/dimensions.xml @@ -48,6 +48,9 @@ <dimen name="expanded_toolbar_height">104dp</dimen> <dimen name="information_icon_size">28dp</dimen> <dimen name="information_action_margin">20dp</dimen> + <dimen name="medium_padding">16dp</dimen> + <dimen name="small_padding">8dp</dimen> + <dimen name="zero_size">0px</dimen> <!-- Switch Dimens--> <dimen name="switch_width">46dp</dimen> <dimen name="switch_height">30dp</dimen> diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ChangelogViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ChangelogViewModelTest.kt new file mode 100644 index 0000000000..eafac23bfc --- /dev/null +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ChangelogViewModelTest.kt @@ -0,0 +1,97 @@ +package net.mullvad.mullvadvpn.viewmodel + +import app.cash.turbine.test +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.just +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import io.mockk.verify +import junit.framework.Assert +import kotlin.test.assertEquals +import kotlinx.coroutines.test.runBlockingTest +import net.mullvad.mullvadvpn.repository.ChangelogRepository +import org.junit.After +import org.junit.Before +import org.junit.Test + +class ChangelogViewModelTest { + + @MockK + private lateinit var mockedChangelogRepository: ChangelogRepository + + private lateinit var viewModel: ChangelogViewModel + + @Before + fun setup() { + MockKAnnotations.init(this) + mockkStatic(EVENT_NOTIFIER_EXTENSION_CLASS) + every { + mockedChangelogRepository.setVersionCodeOfMostRecentChangelogShowed(any()) + } just Runs + viewModel = ChangelogViewModel(mockedChangelogRepository, 1, false) + } + + @After + fun teardown() { + unmockkAll() + } + + @Test + fun testInitialState() = runBlockingTest { + // Arrange, Act, Assert + viewModel.changelogDialogUiState.test { + Assert.assertEquals(ChangelogDialogUiState.Hide, awaitItem()) + } + } + + @Test + fun testShowAndDismissChangelogDialog() = runBlockingTest { + viewModel.changelogDialogUiState.test { + // Arrange + val fakeList = listOf("test") + every { + mockedChangelogRepository.getVersionCodeOfMostRecentChangelogShowed() + } returns -1 + every { mockedChangelogRepository.getLastVersionChanges() } returns fakeList + + // Assert initial ui state + assertEquals(ChangelogDialogUiState.Hide, awaitItem()) + + // Refresh and verify that the dialog should be shown + viewModel.refreshChangelogDialogUiState() + assertEquals(ChangelogDialogUiState.Show(fakeList), awaitItem()) + + // Dismiss dialog and verify that the dialog should be hidden + viewModel.dismissChangelogDialog() + assertEquals(ChangelogDialogUiState.Hide, awaitItem()) + verify { mockedChangelogRepository.setVersionCodeOfMostRecentChangelogShowed(1) } + } + } + + @Test + fun testShowCaseChangelogWithEmptyListDialog() = runBlockingTest { + viewModel.changelogDialogUiState.test { + // Arrange + val fakeEmptyList = emptyList<String>() + every { + mockedChangelogRepository.getVersionCodeOfMostRecentChangelogShowed() + } returns -1 + every { mockedChangelogRepository.getLastVersionChanges() } returns fakeEmptyList + + // Assert initial ui state + assertEquals(ChangelogDialogUiState.Hide, awaitItem()) + + // Refresh and verify that the Ui state remain same due list being empty + viewModel.refreshChangelogDialogUiState() + expectNoEvents() + } + } + + companion object { + private const val EVENT_NOTIFIER_EXTENSION_CLASS = + "net.mullvad.talpid.util.EventNotifierExtensionsKt" + } +} |
