diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 2ef481b2ef2..6d705854902 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -10,6 +10,7 @@ - [***] Orders: Merchants can now bulk update the status of their orders [https://github.com/woocommerce/woocommerce-android/pull/13245] - [*] Fixed a crash on the order details [https://github.com/woocommerce/woocommerce-android/pull/13191] - [**] Introduced fallback logic for the barcode scanner to use the front-facing camera when a back-facing camera is unavailable [https://github.com/woocommerce/woocommerce-android/pull/13230] +- [*] It possible to quickly open plugins page from the plugins list in the up to update an outdated plugin [https://github.com/woocommerce/woocommerce-android/pull/13246] - [**] Fixed a bug when refunded items were displayed on the refund screen [https://github.com/woocommerce/woocommerce-android/pull/13212] 21.3 diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsEvent.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsEvent.kt new file mode 100644 index 00000000000..ed659390759 --- /dev/null +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsEvent.kt @@ -0,0 +1,7 @@ +package com.woocommerce.android.ui.prefs.plugins + +import com.woocommerce.android.viewmodel.MultiLiveEvent + +sealed class PluginsEvent : MultiLiveEvent.Event() { + data class NavigateToPluginsWeb(val url: String) : PluginsEvent() +} diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsFragment.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsFragment.kt index b1c12d44c29..7f43878b59e 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsFragment.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsFragment.kt @@ -7,6 +7,7 @@ import android.view.ViewGroup import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController +import com.woocommerce.android.NavGraphSettingsDirections import com.woocommerce.android.analytics.AnalyticsTracker import com.woocommerce.android.ui.base.BaseFragment import com.woocommerce.android.ui.compose.theme.WooThemeWithBackground @@ -46,6 +47,10 @@ class PluginsFragment : BaseFragment() { viewModel.event.observe(viewLifecycleOwner) { event -> when (event) { is MultiLiveEvent.Event.Exit -> findNavController().navigateUp() + is PluginsEvent.NavigateToPluginsWeb -> { + findNavController() + .navigate(NavGraphSettingsDirections.actionGlobalWPComWebViewFragment(event.url)) + } } } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsScreen.kt index bfd66d7571c..9a7e15ed6dc 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsScreen.kt @@ -8,6 +8,7 @@ import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.tween import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -16,16 +17,19 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.Divider +import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme import androidx.compose.material.Scaffold import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Refresh import androidx.compose.runtime.Composable import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment @@ -44,12 +48,12 @@ import com.woocommerce.android.extensions.isNotNullOrEmpty import com.woocommerce.android.ui.compose.component.Toolbar import com.woocommerce.android.ui.compose.component.WCColoredButton import com.woocommerce.android.ui.compose.preview.LightDarkThemePreviews -import com.woocommerce.android.ui.prefs.plugins.PluginsViewModel.ViewState -import com.woocommerce.android.ui.prefs.plugins.PluginsViewModel.ViewState.Loaded.Plugin -import com.woocommerce.android.ui.prefs.plugins.PluginsViewModel.ViewState.Loaded.Plugin.PluginStatus.Inactive -import com.woocommerce.android.ui.prefs.plugins.PluginsViewModel.ViewState.Loaded.Plugin.PluginStatus.Unknown -import com.woocommerce.android.ui.prefs.plugins.PluginsViewModel.ViewState.Loaded.Plugin.PluginStatus.UpToDate -import com.woocommerce.android.ui.prefs.plugins.PluginsViewModel.ViewState.Loaded.Plugin.PluginStatus.UpdateAvailable +import com.woocommerce.android.ui.compose.theme.WooThemeWithBackground +import com.woocommerce.android.ui.prefs.plugins.PluginsViewState.Loaded.Plugin +import com.woocommerce.android.ui.prefs.plugins.PluginsViewState.Loaded.Plugin.PluginStatus.Inactive +import com.woocommerce.android.ui.prefs.plugins.PluginsViewState.Loaded.Plugin.PluginStatus.Unknown +import com.woocommerce.android.ui.prefs.plugins.PluginsViewState.Loaded.Plugin.PluginStatus.UpToDate +import com.woocommerce.android.ui.prefs.plugins.PluginsViewState.Loaded.Plugin.PluginStatus.UpdateAvailable @Composable fun PluginsScreen(viewModel: PluginsViewModel) { @@ -69,34 +73,50 @@ fun PluginsScreen(viewModel: PluginsViewModel) { .padding(paddingValues) ) { viewModel.viewState.observeAsState().value?.let { state -> - PluginsScreen(state, viewModel::onRetryClicked) + PluginsScreen( + state = state, + onRetryTapped = viewModel::onRetryClicked, + onPluginClicked = viewModel::onPluginClicked, + ) } } } } @Composable -private fun PluginsScreen(state: ViewState, onRetryTapped: () -> Unit) { +private fun PluginsScreen( + state: PluginsViewState, + onRetryTapped: () -> Unit, + onPluginClicked: (Plugin) -> Unit, +) { Crossfade(targetState = state, label = "") { when (it) { - is ViewState.Loading -> { + is PluginsViewState.Loading -> { ShimmerPluginsList() } - is ViewState.Error -> { + + is PluginsViewState.Error -> { Error(onRetryTapped) } - is ViewState.Loaded -> { - Plugins(it.plugins) + + is PluginsViewState.Loaded -> { + Plugins( + it.plugins, + onPluginClicked, + ) } } } } @Composable -private fun Plugins(plugins: List) { +private fun Plugins( + plugins: List, + onPluginClicked: (Plugin) -> Unit +) { LazyColumn { items(plugins) { plugin -> - PluginItem(plugin) + PluginItem(plugin, onPluginClicked) if (plugins.last() != plugin) { Divider() @@ -106,10 +126,14 @@ private fun Plugins(plugins: List) { } @Composable -private fun PluginItem(plugin: Plugin) { +private fun PluginItem( + plugin: Plugin, + onPluginClicked: (Plugin) -> Unit +) { Row( modifier = Modifier .fillMaxWidth() + .clickable(onClick = { onPluginClicked(plugin) }) .padding(dimensionResource(R.dimen.major_100)) ) { Column( @@ -133,12 +157,41 @@ private fun PluginItem(plugin: Plugin) { ) } - if (plugin.status !is Unknown) { - Text( + when (plugin.status) { + is Inactive -> Text( text = plugin.status.title, color = colorResource(id = plugin.status.color), fontWeight = FontWeight.Bold ) + + is UpToDate -> Text( + text = plugin.status.title, + color = colorResource(id = plugin.status.color), + fontWeight = FontWeight.Bold + ) + + is UpdateAvailable -> { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Icon( + imageVector = Icons.Default.Refresh, + contentDescription = plugin.status.title, + tint = colorResource(id = plugin.status.color), + modifier = Modifier + .size(24.dp) + ) + + Text( + text = plugin.status.title, + color = colorResource(id = plugin.status.color), + fontWeight = FontWeight.Bold + ) + } + } + + Unknown -> {} } } } @@ -252,33 +305,47 @@ private fun Error(onRetryTapped: () -> Unit) { @LightDarkThemePreviews @Composable private fun PreviewPlugins() { - PluginsScreen( - ViewState.Loaded( - plugins = listOf( - Plugin("Plugin 1", "Automattic", "1.0", UpToDate("Up-to-date")), - Plugin("Plugin 2", null, "2.0", UpdateAvailable("Update available (4.9)")), - Plugin("Plugin 3", "Gutenberg", "3.0", Inactive("Inactive")), - Plugin("Plugin 5", "Blabla", "5.0", Unknown) - ) - ), - onRetryTapped = {} - ) + WooThemeWithBackground { + PluginsScreen( + PluginsViewState.Loaded( + plugins = listOf( + Plugin("Plugin 1", "Automattic", "1.0", UpToDate("Up-to-date", R.color.color_info)), + Plugin( + "Plugin 2", + "Something", + "2.0", + UpdateAvailable("Update available (4.9)", R.color.color_primary) + ), + Plugin("Plugin 3", "Gutenberg", "3.0", Inactive("Inactive", R.color.color_on_surface_disabled)), + Plugin("Plugin 5", "Blabla", "5.0", Unknown) + ) + ), + onRetryTapped = {}, + onPluginClicked = {}, + ) + } } @LightDarkThemePreviews @Composable private fun PreviewError() { - PluginsScreen( - ViewState.Error, - onRetryTapped = {} - ) + WooThemeWithBackground { + PluginsScreen( + PluginsViewState.Error, + onRetryTapped = {}, + onPluginClicked = {}, + ) + } } @LightDarkThemePreviews @Composable private fun PreviewLoading() { - PluginsScreen( - ViewState.Loading, - onRetryTapped = {} - ) + WooThemeWithBackground { + PluginsScreen( + PluginsViewState.Loading, + onRetryTapped = {}, + onPluginClicked = {}, + ) + } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsViewModel.kt index 591bee9769b..c3de9c8bec3 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsViewModel.kt @@ -1,25 +1,26 @@ package com.woocommerce.android.ui.prefs.plugins -import androidx.annotation.ColorRes import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope import com.woocommerce.android.R +import com.woocommerce.android.extensions.adminUrlOrDefault import com.woocommerce.android.extensions.isNotNullOrEmpty import com.woocommerce.android.tools.SelectedSite -import com.woocommerce.android.ui.prefs.plugins.PluginsViewModel.ViewState.Error -import com.woocommerce.android.ui.prefs.plugins.PluginsViewModel.ViewState.Loaded -import com.woocommerce.android.ui.prefs.plugins.PluginsViewModel.ViewState.Loaded.Plugin -import com.woocommerce.android.ui.prefs.plugins.PluginsViewModel.ViewState.Loaded.Plugin.PluginStatus.Inactive -import com.woocommerce.android.ui.prefs.plugins.PluginsViewModel.ViewState.Loaded.Plugin.PluginStatus.Unknown -import com.woocommerce.android.ui.prefs.plugins.PluginsViewModel.ViewState.Loaded.Plugin.PluginStatus.UpToDate -import com.woocommerce.android.ui.prefs.plugins.PluginsViewModel.ViewState.Loaded.Plugin.PluginStatus.UpdateAvailable -import com.woocommerce.android.ui.prefs.plugins.PluginsViewModel.ViewState.Loading +import com.woocommerce.android.ui.prefs.plugins.PluginsViewState.Error +import com.woocommerce.android.ui.prefs.plugins.PluginsViewState.Loaded +import com.woocommerce.android.ui.prefs.plugins.PluginsViewState.Loaded.Plugin +import com.woocommerce.android.ui.prefs.plugins.PluginsViewState.Loaded.Plugin.PluginStatus.Inactive +import com.woocommerce.android.ui.prefs.plugins.PluginsViewState.Loaded.Plugin.PluginStatus.Unknown +import com.woocommerce.android.ui.prefs.plugins.PluginsViewState.Loaded.Plugin.PluginStatus.UpToDate +import com.woocommerce.android.ui.prefs.plugins.PluginsViewState.Loaded.Plugin.PluginStatus.UpdateAvailable +import com.woocommerce.android.ui.prefs.plugins.PluginsViewState.Loading import com.woocommerce.android.util.isGreaterThanPluginVersion import com.woocommerce.android.viewmodel.MultiLiveEvent.Event.Exit import com.woocommerce.android.viewmodel.ResourceProvider import com.woocommerce.android.viewmodel.ScopedViewModel import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.launch @@ -33,9 +34,10 @@ class PluginsViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private val site: SelectedSite, private val wooCommerceStore: WooCommerceStore, - private val resourceProvider: ResourceProvider + private val resourceProvider: ResourceProvider, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO, ) : ScopedViewModel(savedStateHandle) { - private val _viewState = MutableSharedFlow(1) + private val _viewState = MutableSharedFlow(1) val viewState = _viewState.asLiveData() init { @@ -43,7 +45,7 @@ class PluginsViewModel @Inject constructor( } private fun loadPlugins() { - viewModelScope.launch(Dispatchers.IO) { + viewModelScope.launch(dispatcher) { _viewState.emit(Loading) val response = wooCommerceStore.fetchSystemPlugins(site.get()) if (!response.isError && response.model != null) { @@ -56,7 +58,7 @@ class PluginsViewModel @Inject constructor( name = StringEscapeUtils.unescapeHtml4(it.name), authorName = StringEscapeUtils.unescapeHtml4(it.authorName), version = it.version!!, - status = it.getState() + status = it.getStatus() ) } ) @@ -67,14 +69,18 @@ class PluginsViewModel @Inject constructor( } } - private fun SystemPluginModel.getState(): Plugin.PluginStatus { + private fun SystemPluginModel.getStatus(): Plugin.PluginStatus { return when { - !isActive -> Inactive(resourceProvider.getString(R.string.plugin_state_inactive)) + !isActive -> Inactive( + resourceProvider.getString(R.string.plugin_state_inactive), + R.color.color_on_surface_disabled, + ) versionLatest.isNullOrEmpty() -> Unknown isUpdateAvailable() -> UpdateAvailable( - resourceProvider.getString(R.string.plugin_state_update_available, versionLatest!!) + resourceProvider.getString(R.string.plugin_state_update_available, versionLatest!!), + R.color.color_primary, ) - else -> UpToDate(resourceProvider.getString(R.string.plugin_state_up_to_date)) + else -> UpToDate(resourceProvider.getString(R.string.plugin_state_up_to_date), R.color.color_info) } } @@ -86,32 +92,18 @@ class PluginsViewModel @Inject constructor( loadPlugins() } - fun onBackPressed() { - triggerEvent(Exit) - } - - sealed interface ViewState { - data object Loading : ViewState - data object Error : ViewState - data class Loaded( - val plugins: List = emptyList() - ) : ViewState { - data class Plugin( - val name: String, - val authorName: String?, - val version: String, - val status: PluginStatus - ) { - sealed class PluginStatus(open val title: String, @ColorRes val color: Int) { - data class UpToDate(override val title: String) : PluginStatus(title, R.color.color_info) - data class UpdateAvailable(override val title: String) : PluginStatus(title, R.color.color_alert) - data class Inactive(override val title: String) : PluginStatus( - title, - R.color.color_on_surface_disabled - ) - data object Unknown : PluginStatus("", R.color.color_on_surface_disabled) - } + fun onPluginClicked(plugin: Plugin) { + when (plugin.status) { + is Inactive -> {} + Unknown -> {} + is UpToDate -> {} + is UpdateAvailable -> { + triggerEvent(PluginsEvent.NavigateToPluginsWeb("${site.get().adminUrlOrDefault}/plugins.php")) } } } + + fun onBackPressed() { + triggerEvent(Exit) + } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsViewState.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsViewState.kt new file mode 100644 index 00000000000..256068ad217 --- /dev/null +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsViewState.kt @@ -0,0 +1,25 @@ +package com.woocommerce.android.ui.prefs.plugins + +import androidx.annotation.ColorRes + +sealed interface PluginsViewState { + data object Loading : PluginsViewState + data object Error : PluginsViewState + data class Loaded( + val plugins: List = emptyList() + ) : PluginsViewState { + data class Plugin( + val name: String, + val authorName: String?, + val version: String, + val status: PluginStatus + ) { + sealed class PluginStatus { + data class UpToDate(val title: String, @ColorRes val color: Int) : PluginStatus() + data class UpdateAvailable(val title: String, @ColorRes val color: Int) : PluginStatus() + data class Inactive(val title: String, @ColorRes val color: Int) : PluginStatus() + data object Unknown : PluginStatus() + } + } + } +} diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsViewModelTest.kt new file mode 100644 index 00000000000..a6c59f41388 --- /dev/null +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsViewModelTest.kt @@ -0,0 +1,309 @@ +package com.woocommerce.android.ui.prefs.plugins + +import androidx.lifecycle.SavedStateHandle +import com.woocommerce.android.R +import com.woocommerce.android.tools.SelectedSite +import com.woocommerce.android.ui.prefs.plugins.PluginsViewState.Loaded +import com.woocommerce.android.ui.prefs.plugins.PluginsViewState.Loaded.Plugin +import com.woocommerce.android.ui.prefs.plugins.PluginsViewState.Loaded.Plugin.PluginStatus.Inactive +import com.woocommerce.android.ui.prefs.plugins.PluginsViewState.Loaded.Plugin.PluginStatus.Unknown +import com.woocommerce.android.ui.prefs.plugins.PluginsViewState.Loaded.Plugin.PluginStatus.UpToDate +import com.woocommerce.android.ui.prefs.plugins.PluginsViewState.Loaded.Plugin.PluginStatus.UpdateAvailable +import com.woocommerce.android.util.captureValues +import com.woocommerce.android.viewmodel.BaseUnitTest +import com.woocommerce.android.viewmodel.MultiLiveEvent.Event.Exit +import com.woocommerce.android.viewmodel.ResourceProvider +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.network.BaseRequest.GenericErrorType +import org.wordpress.android.fluxc.network.rest.wpcom.wc.WooError +import org.wordpress.android.fluxc.network.rest.wpcom.wc.WooErrorType +import org.wordpress.android.fluxc.network.rest.wpcom.wc.WooResult +import org.wordpress.android.fluxc.network.rest.wpcom.wc.system.WCSystemPluginResponse.SystemPluginModel +import org.wordpress.android.fluxc.store.WooCommerceStore + +@OptIn(ExperimentalCoroutinesApi::class) +class PluginsViewModelTest : BaseUnitTest() { + private val savedStateHandle: SavedStateHandle = mock() + private val selectedSite: SelectedSite = mock { + on { get() }.thenReturn(mock()) + } + private val wooCommerceStore: WooCommerceStore = mock() + private val resourceProvider: ResourceProvider = mock { + on { getString(R.string.plugin_state_update_available, "1.1.0") } + .thenReturn("Update available to 1.1.0") + on { getString(R.string.plugin_state_update_available, "2.1.0") } + .thenReturn("Update available to 2.1.0") + on { getString(R.string.plugin_state_inactive) } + .thenReturn("Inactive") + } + + private fun createViewModel() = PluginsViewModel( + savedStateHandle, + selectedSite, + wooCommerceStore, + resourceProvider, + UnconfinedTestDispatcher() + ) + + @Test + fun `given plugins are fetched successfully, when vm init, then viewState should be Loaded`() = testBlocking { + // GIVEN + val pluginsResponse = listOf( + SystemPluginModel( + name = "Plugin A", + authorName = "Author A", + plugin = "plugin.php", + version = "1.0.0", + versionLatest = "1.1.0", + url = "https://example.com/plugins.php", + isActive = true + ), + SystemPluginModel( + name = "Plugin B", + authorName = "Author B", + plugin = "plugin.php", + version = "2.0.0", + versionLatest = "2.0.0", + url = "https://example.com/plugins.php", + isActive = false + ) + ) + whenever(wooCommerceStore.fetchSystemPlugins(selectedSite.get())) + .thenReturn(WooResult(pluginsResponse)) + + // WHEN + val viewModel = createViewModel() + + // THEN + val values = viewModel.viewState.captureValues() + assertThat(values.last()).isEqualTo( + Loaded( + plugins = listOf( + Plugin( + "Plugin A", + "Author A", + "1.0.0", + UpdateAvailable("Update available to 1.1.0", R.color.color_primary) + ), + Plugin( + "Plugin B", + "Author B", + "2.0.0", + Inactive("Inactive", R.color.color_on_surface_disabled) + ) + ) + ) + ) + } + + @Test + fun `given plugins fetch fails, when vm init, then viewState should be Error`() = testBlocking { + // GIVEN + whenever(wooCommerceStore.fetchSystemPlugins(selectedSite.get())).thenReturn( + WooResult( + WooError( + type = WooErrorType.GENERIC_ERROR, + original = GenericErrorType.SERVER_ERROR + ) + ) + ) + + // WHEN + val viewModel = createViewModel() + + // THEN + val values = viewModel.viewState.captureValues() + assertThat(values.last()).isEqualTo(PluginsViewState.Error) + } + + @Test + fun `given plugin status is UpdateAvailable, when onPluginClicked is called, then navigate event is triggered`() = + testBlocking { + // GIVEN + val plugin = Plugin( + name = "Plugin A", + authorName = "Author A", + version = "1.0.0", + status = UpdateAvailable("Update available to 1.1.0", R.color.color_primary) + ) + val siteModel = mock { + on { adminUrl }.thenReturn("https://example.com/wp-admin") + } + whenever(selectedSite.get()).thenReturn(siteModel) + val viewModel = createViewModel() + + // WHEN + viewModel.onPluginClicked(plugin) + + // THEN + assertThat(viewModel.event.value).isEqualTo( + PluginsEvent.NavigateToPluginsWeb("https://example.com/wp-admin/plugins.php") + ) + } + + @Test + fun `when onBackPressed is called, then Exit event is triggered`() { + // GIVEN + val viewModel = createViewModel() + + // WHEN + viewModel.onBackPressed() + + // THEN + assertThat(viewModel.event.value).isEqualTo(Exit) + } + + @Test + fun `given plugin status is Inactive, when onPluginClicked is called, then no event is triggered`() = testBlocking { + // GIVEN + val plugin = Plugin( + name = "Plugin A", + authorName = "Author A", + version = "1.0.0", + status = Inactive("Inactive", R.color.color_on_surface_disabled) + ) + val viewModel = createViewModel() + + // WHEN + viewModel.onPluginClicked(plugin) + + // THEN + assertThat(viewModel.event.value).isNull() + } + + @Test + fun `given plugin status is Unknown, when onPluginClicked is called, then no event is triggered`() = testBlocking { + // GIVEN + val plugin = Plugin( + name = "Plugin A", + authorName = "Author A", + version = "1.2.3", + status = Unknown + ) + val viewModel = createViewModel() + + // WHEN + viewModel.onPluginClicked(plugin) + + // THEN + assertThat(viewModel.event.value).isNull() + } + + @Test + fun `given plugin status is UpToDate, when onPluginClicked is called, then no event is triggered`() = testBlocking { + // GIVEN + val plugin = Plugin( + name = "Plugin A", + authorName = "Author A", + version = "1.2.3", + status = UpToDate("Up-to-date", R.color.color_info) + ) + val viewModel = createViewModel() + + // WHEN + viewModel.onPluginClicked(plugin) + + // THEN + assertThat(viewModel.event.value).isNull() + } + + @Test + fun `given store returns success, when onRetryClicked is called, then viewState should be Loaded`() = testBlocking { + // GIVEN + val initialResponse = listOf( + SystemPluginModel( + name = "Plugin A", + authorName = "Author A", + plugin = "plugin.php", + version = "1.0.0", + versionLatest = "1.1.0", + url = "https://example.com/plugins.php", + isActive = true + ) + ) + whenever(wooCommerceStore.fetchSystemPlugins(selectedSite.get())) + .thenReturn(WooResult(initialResponse)) + + val viewModel = createViewModel() + + var values = viewModel.viewState.captureValues() + + val newResponse = listOf( + SystemPluginModel( + name = "Plugin B", + authorName = "Author B", + plugin = "plugin.php", + version = "2.0.0", + versionLatest = "2.1.0", + url = "https://example.com/plugins.php", + isActive = true + ) + ) + whenever(wooCommerceStore.fetchSystemPlugins(selectedSite.get())) + .thenReturn(WooResult(newResponse)) + + // WHEN + viewModel.onRetryClicked() + + // THEN + values = viewModel.viewState.captureValues() + assertThat(values.last()).isEqualTo( + Loaded( + plugins = listOf( + Plugin( + name = "Plugin B", + authorName = "Author B", + version = "2.0.0", + status = UpdateAvailable("Update available to 2.1.0", R.color.color_primary) + ) + ) + ) + ) + } + + @Test + fun `given store returns error, when onRetryClicked is called, then viewState should be Error`() = testBlocking { + // GIVEN + // Return success for the initial load + val initialResponse = listOf( + SystemPluginModel( + name = "Plugin A", + authorName = "Author A", + plugin = "plugin.php", + version = "1.0.0", + versionLatest = "1.1.0", + url = "https://example.com/plugins.php", + isActive = true + ) + ) + whenever(wooCommerceStore.fetchSystemPlugins(selectedSite.get())) + .thenReturn(WooResult(initialResponse)) + + val viewModel = createViewModel() + + var values = viewModel.viewState.captureValues() + + whenever(wooCommerceStore.fetchSystemPlugins(selectedSite.get())) + .thenReturn( + WooResult( + WooError( + type = WooErrorType.GENERIC_ERROR, + original = GenericErrorType.SERVER_ERROR + ) + ) + ) + + // WHEN + viewModel.onRetryClicked() + + // THEN + values = viewModel.viewState.captureValues() + assertThat(values.last()).isEqualTo(PluginsViewState.Error) + } +}