Skip to content

Commit 508c9c6

Browse files
authored
Merge pull request #13974 from woocommerce/merge/release-22.2-into-trunk
Merge release/22.2 into trunk
2 parents 62bf9bd + 15c1848 commit 508c9c6

File tree

8 files changed

+167
-19
lines changed

8 files changed

+167
-19
lines changed

WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/common/data/WooPosProductsCache.kt

+2
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,6 @@ interface WooPosProductsCache {
1818
suspend fun deleteProduct(productId: Long)
1919

2020
suspend fun clear()
21+
22+
suspend fun setAll(products: List<Product>)
2123
}

WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/common/data/WooPosProductsInMemoryCache.kt

+16-7
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,7 @@ class WooPosProductsInMemoryCache @Inject constructor() : WooPosProductsCache {
1919
private val productsCache = LinkedHashMap<Long, Product>(INITIAL_CAPACITY, LOAD_FACTOR, true)
2020

2121
override suspend fun addAll(products: List<Product>) = mutex.withLock {
22-
products.forEach { product ->
23-
productsCache[product.remoteId] = product
24-
if (productsCache.size > MAX_CACHE_SIZE) {
25-
val keysToRemove = productsCache.keys.take(productsCache.size - MAX_CACHE_SIZE)
26-
keysToRemove.forEach { productsCache.remove(it) }
27-
}
28-
}
22+
addAllInternal(products)
2923
}
3024

3125
override suspend fun updateProduct(product: Product) = mutex.withLock {
@@ -49,4 +43,19 @@ class WooPosProductsInMemoryCache @Inject constructor() : WooPosProductsCache {
4943
override suspend fun clear() = mutex.withLock {
5044
productsCache.clear()
5145
}
46+
47+
override suspend fun setAll(products: List<Product>) = mutex.withLock {
48+
productsCache.clear()
49+
addAllInternal(products)
50+
}
51+
52+
private fun addAllInternal(products: List<Product>) {
53+
products.forEach { product ->
54+
productsCache[product.remoteId] = product
55+
if (productsCache.size > MAX_CACHE_SIZE) {
56+
val keysToRemove = productsCache.keys.take(productsCache.size - MAX_CACHE_SIZE)
57+
keysToRemove.forEach { productsCache.remove(it) }
58+
}
59+
}
60+
}
5261
}

WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsList.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,7 @@ private fun InfiniteListHandler(
402402
state: WooPosContentViewState,
403403
onEndOfProductsListReached: () -> Unit
404404
) {
405-
val buffer = 5
405+
val buffer = 5 // must be smaller than the page size, otherwise won't call onEndOfProductsListReached
406406
val loadMore = remember {
407407
derivedStateOf {
408408
val layoutInfo = listState.layoutInfo

WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/products/WooPosProductsDataSource.kt

+7-5
Original file line numberDiff line numberDiff line change
@@ -80,17 +80,19 @@ class WooPosProductsDataSource @Inject constructor(
8080

8181
fun loadProducts(forceRefreshProducts: Boolean): Flow<ProductsResult> = flow {
8282
offset.set(0)
83-
if (forceRefreshProducts) {
84-
productsCache.clear()
85-
}
8683
productsIndex.clearCache()
8784

88-
val cachedProducts = sortProducts(productsCache.getAll()).take(NORMAL_PAGE_SIZE)
89-
emit(ProductsResult.Cached(cachedProducts))
85+
if (!forceRefreshProducts) {
86+
val cachedProducts = sortProducts(productsCache.getAll()).take(NORMAL_PAGE_SIZE)
87+
emit(ProductsResult.Cached(cachedProducts))
88+
}
9089

9190
val fetchResult = fetchProducts()
9291

9392
if (fetchResult.isSuccess) {
93+
if (forceRefreshProducts) {
94+
productsCache.setAll(fetchResult.getOrThrow())
95+
}
9496
emit(ProductsResult.Remote(Result.success(fetchResult.getOrThrow())))
9597
} else {
9698
emit(ProductsResult.Remote(Result.failure(fetchResult.exceptionOrNull() ?: Exception("Unknown error"))))

WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/products/WooPosProductsViewModel.kt

+29-1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ class WooPosProductsViewModel @Inject constructor(
3535
private val analyticsTracker: WooPosAnalyticsTracker,
3636
) : ViewModel() {
3737
private var loadMoreProductsJob: Job? = null
38+
private var loadProductsJob: Job? = null
39+
private var loadMoreAfterLoadCompletes = false
3840

3941
private val _viewState = MutableStateFlow<WooPosProductsViewState>(
4042
WooPosProductsViewState.Loading()
@@ -113,7 +115,9 @@ class WooPosProductsViewModel @Inject constructor(
113115
forceRefreshProducts: Boolean,
114116
withPullToRefresh: Boolean,
115117
) {
116-
viewModelScope.launch {
118+
loadProductsJob?.cancel()
119+
loadMoreProductsJob?.cancel()
120+
loadProductsJob = viewModelScope.launch {
117121
_viewState.value = if (withPullToRefresh) {
118122
buildProductsReloadingState()
119123
} else {
@@ -157,12 +161,25 @@ class WooPosProductsViewModel @Inject constructor(
157161

158162
else -> WooPosProductsViewState.Error()
159163
}
164+
165+
if (loadMoreAfterLoadCompletes) {
166+
queueLoadMoreAfterLoadCompletes()
167+
}
160168
}
161169
}
162170
}
163171
}
164172
}
165173

174+
private fun queueLoadMoreAfterLoadCompletes() {
175+
loadProductsJob?.invokeOnCompletion { throwable ->
176+
if (throwable == null && _viewState.value is WooPosProductsViewState.Content) {
177+
loadMoreAfterLoadCompletes = false
178+
onEndOfProductsListReached()
179+
}
180+
}
181+
}
182+
166183
private fun buildProductsReloadingState() =
167184
when (val state = viewState.value) {
168185
is WooPosProductsViewState.Content -> state.copy(pullToRefreshState = WooPosPullToRefreshState.Refreshing)
@@ -199,12 +216,23 @@ class WooPosProductsViewModel @Inject constructor(
199216
}
200217
}
201218

219+
@Suppress("ReturnCount")
202220
private fun onEndOfProductsListReached() {
203221
val currentState = _viewState.value
204222
if (currentState !is WooPosProductsViewState.Content) {
205223
return
206224
}
207225

226+
if (loadProductsJob?.isActive == true) {
227+
loadMoreAfterLoadCompletes = true
228+
_viewState.value = currentState.copy(paginationState = WooPosPaginationState.Loading)
229+
return
230+
}
231+
232+
if (loadMoreProductsJob?.isActive == true) {
233+
return
234+
}
235+
208236
if (!productsDataSource.hasMorePages) {
209237
return
210238
}

WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosProductsDataSourceTest.kt

+32-3
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ class WooPosProductsDataSourceTest {
9191
private val productsTypesFilterConfig = WooPosProductsTypesFilterConfig()
9292

9393
@Test
94-
fun `given force refresh, when loadProducts called, then should clear cache`() = runTest {
94+
fun `given force refresh, when loadProducts called, then should clear cache and insert products again`() = runTest {
9595
// GIVEN
9696
whenever(productsIndex.getProductList()).thenReturn(emptyList(), sampleProducts)
9797

@@ -117,14 +117,13 @@ class WooPosProductsDataSourceTest {
117117
sut.loadProducts(forceRefreshProducts = true).first()
118118

119119
// THEN
120-
verify(productsCache).clear()
120+
verify(productsCache).setAll(any())
121121
verify(productsIndex).clearCache()
122122
}
123123

124124
@Test
125125
fun `given cached products, when loadProducts called, then should emit cached products first`() = runTest {
126126
// GIVEN
127-
128127
whenever(productsCache.getAll()).thenReturn(sampleProducts)
129128
whenever(
130129
productStore.fetchProducts(
@@ -154,6 +153,36 @@ class WooPosProductsDataSourceTest {
154153
assertThat(cachedResult.products).containsExactlyElementsOf(sampleProducts)
155154
}
156155

156+
@Test
157+
fun `given cached products, when loadProducts called with forceRefresh, then should not emit cached products`() = runTest {
158+
// GIVEN
159+
whenever(productsCache.getAll()).thenReturn(sampleProducts)
160+
whenever(
161+
productStore.fetchProducts(
162+
site = eq(siteModel),
163+
offset = any(),
164+
pageSize = any(),
165+
sortType = any(),
166+
filterOptions = any(),
167+
includeTypes = any()
168+
)
169+
).thenReturn(WooResult(listOf<WCProductModel>()))
170+
whenever(productsIndex.getProductList()).thenReturn(sampleProducts)
171+
val sut = WooPosProductsDataSource(
172+
productStore,
173+
selectedSite,
174+
productsCache,
175+
productsIndex,
176+
productsTypesFilterConfig
177+
)
178+
179+
// WHEN
180+
val result = sut.loadProducts(forceRefreshProducts = true).first()
181+
182+
// THEN
183+
assertThat(result).isInstanceOf(WooPosProductsDataSource.ProductsResult.Remote::class.java)
184+
}
185+
157186
@Test
158187
fun `given no products in list cache, when loadProducts called, then should return empty list`() = runTest {
159188
// GIVEN

WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/items/products/WooPosProductsViewModelTest.kt

+78
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,19 @@ import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Eve
1616
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsTracker
1717
import com.woocommerce.android.ui.woopos.util.format.WooPosFormatPrice
1818
import kotlinx.coroutines.ExperimentalCoroutinesApi
19+
import kotlinx.coroutines.flow.MutableSharedFlow
20+
import kotlinx.coroutines.flow.flow
1921
import kotlinx.coroutines.flow.flowOf
22+
import kotlinx.coroutines.test.advanceUntilIdle
2023
import kotlinx.coroutines.test.runTest
2124
import org.assertj.core.api.Assertions.assertThat
2225
import org.junit.Before
2326
import org.junit.Rule
2427
import org.junit.Test
2528
import org.mockito.kotlin.any
2629
import org.mockito.kotlin.mock
30+
import org.mockito.kotlin.never
31+
import org.mockito.kotlin.times
2732
import org.mockito.kotlin.verify
2833
import org.mockito.kotlin.whenever
2934
import java.math.BigDecimal
@@ -384,6 +389,79 @@ class WooPosProductsViewModelTest {
384389
}
385390
}
386391

392+
@Test
393+
@OptIn(ExperimentalCoroutinesApi::class)
394+
fun `given initial load in progress, when end of list reached, then pagination state set to loading and queue load more`() = runTest {
395+
// GIVEN
396+
whenever(productsDataSource.hasMorePages).thenReturn(true)
397+
whenever(productsDataSource.loadMore()).thenReturn(
398+
Result.success(
399+
listOf(
400+
ProductTestUtils.generateProduct(
401+
productId = 2,
402+
productName = "Product 2",
403+
amount = "20.0",
404+
productType = "simple"
405+
)
406+
)
407+
)
408+
)
409+
410+
val productsFlow = flow {
411+
emit(
412+
WooPosProductsDataSource.ProductsResult.Remote(
413+
Result.success(
414+
listOf(
415+
ProductTestUtils.generateProduct(
416+
productId = 1,
417+
productName = "Product 1",
418+
amount = "10.0",
419+
productType = "simple"
420+
)
421+
)
422+
)
423+
)
424+
)
425+
}
426+
427+
whenever(productsDataSource.loadProducts(any())).thenReturn(productsFlow)
428+
429+
val viewModel = createViewModel()
430+
431+
// WHEN
432+
viewModel.onUIEvent(WooPosProductsUIEvent.EndOfItemsListReached)
433+
advanceUntilIdle()
434+
435+
// THEN
436+
verify(productsDataSource, times(1)).loadMore()
437+
}
438+
439+
@Test
440+
@OptIn(ExperimentalCoroutinesApi::class)
441+
fun `given load more queued, when initial load fails, then load more is not triggered`() = runTest {
442+
// GIVEN
443+
whenever(productsDataSource.hasMorePages).thenReturn(true)
444+
445+
val productsFlow = MutableSharedFlow<WooPosProductsDataSource.ProductsResult>()
446+
whenever(productsDataSource.loadProducts(any())).thenReturn(productsFlow)
447+
448+
val viewModel = createViewModel()
449+
450+
// WHEN
451+
viewModel.onUIEvent(WooPosProductsUIEvent.EndOfItemsListReached)
452+
453+
// Then
454+
productsFlow.emit(
455+
WooPosProductsDataSource.ProductsResult.Remote(
456+
Result.failure(Exception("Test error"))
457+
)
458+
)
459+
advanceUntilIdle()
460+
461+
// THEN
462+
verify(productsDataSource, never()).loadMore()
463+
}
464+
387465
private fun createViewModel(): WooPosProductsViewModel {
388466
return WooPosProductsViewModel(
389467
productsDataSource = productsDataSource,

version.properties

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
versionName=22.2-rc-1
2-
versionCode=661
1+
versionName=22.2-rc-2
2+
versionCode=662

0 commit comments

Comments
 (0)