parent
dcd97fa6ac
commit
c4110e33c6
@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.saved
|
||||
|
||||
import androidx.benchmark.macro.MacrobenchmarkScope
|
||||
import androidx.test.uiautomator.By
|
||||
import androidx.test.uiautomator.Direction
|
||||
import androidx.test.uiautomator.Until
|
||||
|
||||
fun MacrobenchmarkScope.savedWaitForContent() {
|
||||
// Wait until content is loaded
|
||||
device.wait(Until.hasObject(By.res("saved:feed")), 30_000)
|
||||
}
|
||||
|
||||
fun MacrobenchmarkScope.savedScrollFeedDownUp() {
|
||||
val feedList = device.findObject(By.res("saved:feed"))
|
||||
feedList.fling(Direction.DOWN)
|
||||
device.waitForIdle()
|
||||
feedList.fling(Direction.UP)
|
||||
}
|
@ -0,0 +1 @@
|
||||
/build
|
@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
plugins {
|
||||
id("nowinandroid.android.library")
|
||||
id("nowinandroid.android.feature")
|
||||
id("nowinandroid.android.library.compose")
|
||||
id("nowinandroid.android.library.jacoco")
|
||||
id("dagger.hilt.android.plugin")
|
||||
id("nowinandroid.spotless")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.androidx.compose.material3.windowSizeClass)
|
||||
}
|
@ -0,0 +1,168 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.feature.bookmarks
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
|
||||
import androidx.compose.material3.windowsizeclass.WindowSizeClass
|
||||
import androidx.compose.ui.test.assertCountEquals
|
||||
import androidx.compose.ui.test.assertHasClickAction
|
||||
import androidx.compose.ui.test.filter
|
||||
import androidx.compose.ui.test.hasAnyAncestor
|
||||
import androidx.compose.ui.test.hasScrollToNodeAction
|
||||
import androidx.compose.ui.test.hasText
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onAllNodesWithContentDescription
|
||||
import androidx.compose.ui.test.onFirst
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performScrollToNode
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import com.google.samples.apps.nowinandroid.core.model.data.SaveableNewsResource
|
||||
import com.google.samples.apps.nowinandroid.core.model.data.previewNewsResources
|
||||
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* UI tests for [BookmarksScreen] composable.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
|
||||
class BookmarksScreenTest {
|
||||
|
||||
@get:Rule
|
||||
val composeTestRule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun loading_showsLoadingSpinner() {
|
||||
lateinit var windowSizeClass: WindowSizeClass
|
||||
composeTestRule.setContent {
|
||||
BoxWithConstraints {
|
||||
windowSizeClass = WindowSizeClass.calculateFromSize(
|
||||
DpSize(maxWidth, maxHeight)
|
||||
)
|
||||
BookmarksScreen(
|
||||
windowSizeClass = windowSizeClass,
|
||||
feedState = NewsFeedUiState.Loading,
|
||||
removeFromBookmarks = { }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription(
|
||||
composeTestRule.activity.resources.getString(R.string.saved_loading)
|
||||
)
|
||||
.assertExists()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun feed_whenHasBookmarks_showsBookmarks() {
|
||||
lateinit var windowSizeClass: WindowSizeClass
|
||||
|
||||
composeTestRule.setContent {
|
||||
BoxWithConstraints {
|
||||
windowSizeClass = WindowSizeClass.calculateFromSize(
|
||||
DpSize(maxWidth, maxHeight)
|
||||
)
|
||||
|
||||
BookmarksScreen(
|
||||
windowSizeClass = windowSizeClass,
|
||||
feedState = NewsFeedUiState.Success(
|
||||
previewNewsResources.take(2)
|
||||
.map { SaveableNewsResource(it, true) }
|
||||
),
|
||||
removeFromBookmarks = { }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText(
|
||||
previewNewsResources[0].title,
|
||||
substring = true
|
||||
)
|
||||
.assertExists()
|
||||
.assertHasClickAction()
|
||||
|
||||
composeTestRule.onNode(hasScrollToNodeAction())
|
||||
.performScrollToNode(
|
||||
hasText(
|
||||
previewNewsResources[1].title,
|
||||
substring = true
|
||||
)
|
||||
)
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText(
|
||||
previewNewsResources[1].title,
|
||||
substring = true
|
||||
)
|
||||
.assertExists()
|
||||
.assertHasClickAction()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun feed_whenRemovingBookmark_removesBookmark() {
|
||||
lateinit var windowSizeClass: WindowSizeClass
|
||||
|
||||
var removeFromBookmarksCalled = false
|
||||
|
||||
composeTestRule.setContent {
|
||||
BoxWithConstraints {
|
||||
windowSizeClass = WindowSizeClass.calculateFromSize(
|
||||
DpSize(maxWidth, maxHeight)
|
||||
)
|
||||
|
||||
BookmarksScreen(
|
||||
windowSizeClass = windowSizeClass,
|
||||
feedState = NewsFeedUiState.Success(
|
||||
previewNewsResources.take(2)
|
||||
.map { SaveableNewsResource(it, true) }
|
||||
),
|
||||
removeFromBookmarks = { newsResourceId ->
|
||||
assertEquals(previewNewsResources[0].id, newsResourceId)
|
||||
removeFromBookmarksCalled = true
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithContentDescription(
|
||||
composeTestRule.activity.getString(
|
||||
com.google.samples.apps.nowinandroid.core.ui.R.string.unbookmark
|
||||
)
|
||||
).filter(
|
||||
hasAnyAncestor(
|
||||
hasText(
|
||||
previewNewsResources[0].title,
|
||||
substring = true
|
||||
)
|
||||
)
|
||||
)
|
||||
.assertCountEquals(1)
|
||||
.onFirst()
|
||||
.performClick()
|
||||
|
||||
assertTrue(removeFromBookmarksCalled)
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2022 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.google.samples.apps.nowinandroid.feature.bookmarks">
|
||||
|
||||
</manifest>
|
@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.feature.bookmarks
|
||||
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.consumedWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.foundation.layout.windowInsetsBottomHeight
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.windowsizeclass.WindowSizeClass
|
||||
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaGradientBackground
|
||||
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTopAppBar
|
||||
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
|
||||
import com.google.samples.apps.nowinandroid.core.ui.NewsFeed
|
||||
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
|
||||
import kotlin.math.floor
|
||||
|
||||
@Composable
|
||||
fun BookmarksRoute(
|
||||
windowSizeClass: WindowSizeClass,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: BookmarksViewModel = hiltViewModel()
|
||||
) {
|
||||
val feedState by viewModel.feedState.collectAsState()
|
||||
BookmarksScreen(
|
||||
windowSizeClass = windowSizeClass,
|
||||
feedState = feedState,
|
||||
removeFromBookmarks = viewModel::removeFromSavedResources,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun BookmarksScreen(
|
||||
windowSizeClass: WindowSizeClass,
|
||||
feedState: NewsFeedUiState,
|
||||
removeFromBookmarks: (String) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
NiaGradientBackground {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
NiaTopAppBar(
|
||||
titleRes = R.string.top_app_bar_title_saved,
|
||||
navigationIcon = NiaIcons.Search,
|
||||
navigationIconContentDescription = stringResource(
|
||||
id = R.string.top_app_bar_action_search
|
||||
),
|
||||
actionIcon = NiaIcons.AccountCircle,
|
||||
actionIconContentDescription = stringResource(
|
||||
id = R.string.top_app_bar_action_menu
|
||||
),
|
||||
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
|
||||
containerColor = Color.Transparent
|
||||
),
|
||||
modifier = Modifier.windowInsetsPadding(
|
||||
WindowInsets.safeDrawing.only(WindowInsetsSides.Top)
|
||||
)
|
||||
)
|
||||
},
|
||||
containerColor = Color.Transparent
|
||||
) { innerPadding ->
|
||||
// TODO: Replace with `LazyVerticalGrid` when blocking bugs are fixed:
|
||||
// https://issuetracker.google.com/issues/230514914
|
||||
// https://issuetracker.google.com/issues/231320714
|
||||
BoxWithConstraints(
|
||||
modifier = modifier
|
||||
.padding(innerPadding)
|
||||
.consumedWindowInsets(innerPadding)
|
||||
) {
|
||||
val numberOfColumns = when (windowSizeClass.widthSizeClass) {
|
||||
WindowWidthSizeClass.Compact, WindowWidthSizeClass.Medium -> 1
|
||||
else -> floor(maxWidth / 300.dp).toInt().coerceAtLeast(1)
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.testTag("saved:feed"),
|
||||
contentPadding = PaddingValues(bottom = 16.dp)
|
||||
) {
|
||||
|
||||
NewsFeed(
|
||||
feedState = feedState,
|
||||
numberOfColumns = numberOfColumns,
|
||||
onNewsResourcesCheckedChanged = { id, _ -> removeFromBookmarks(id) },
|
||||
showLoadingUIIfLoading = true,
|
||||
loadingContentDescription = R.string.saved_loading
|
||||
)
|
||||
|
||||
item {
|
||||
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.feature.bookmarks
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.google.samples.apps.nowinandroid.core.data.repository.NewsRepository
|
||||
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
|
||||
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
|
||||
import com.google.samples.apps.nowinandroid.core.model.data.SaveableNewsResource
|
||||
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
|
||||
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState.Loading
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filterNot
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@HiltViewModel
|
||||
class BookmarksViewModel @Inject constructor(
|
||||
newsRepository: NewsRepository,
|
||||
private val userDataRepository: UserDataRepository
|
||||
) : ViewModel() {
|
||||
private val savedNewsResourcesState: StateFlow<Set<String>> =
|
||||
userDataRepository.userDataStream
|
||||
.map { userData ->
|
||||
userData.bookmarkedNewsResources
|
||||
}
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
initialValue = emptySet()
|
||||
)
|
||||
|
||||
val feedState: StateFlow<NewsFeedUiState> =
|
||||
newsRepository
|
||||
.getNewsResourcesStream()
|
||||
.mapToFeedState(savedNewsResourcesState)
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
initialValue = Loading
|
||||
)
|
||||
|
||||
private fun Flow<List<NewsResource>>.mapToFeedState(
|
||||
savedNewsResourcesState: Flow<Set<String>>
|
||||
): Flow<NewsFeedUiState> =
|
||||
filterNot { it.isEmpty() }
|
||||
.combine(savedNewsResourcesState) { newsResources, savedNewsResources ->
|
||||
newsResources
|
||||
.filter { newsResource -> savedNewsResources.contains(newsResource.id) }
|
||||
.map { SaveableNewsResource(it, true) }
|
||||
}
|
||||
.map<List<SaveableNewsResource>, NewsFeedUiState>(NewsFeedUiState::Success)
|
||||
.onStart { emit(Loading) }
|
||||
|
||||
fun removeFromSavedResources(newsResourceId: String) {
|
||||
viewModelScope.launch {
|
||||
userDataRepository.updateNewsResourceBookmark(newsResourceId, false)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.feature.bookmarks.navigation
|
||||
|
||||
import androidx.compose.material3.windowsizeclass.WindowSizeClass
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavigationDestination
|
||||
import com.google.samples.apps.nowinandroid.feature.bookmarks.BookmarksRoute
|
||||
|
||||
object BookmarksDestination : NiaNavigationDestination {
|
||||
override val route = "bookmarks_route"
|
||||
override val destination = "bookmarks_destination"
|
||||
}
|
||||
|
||||
fun NavGraphBuilder.bookmarksGraph(
|
||||
windowSizeClass: WindowSizeClass
|
||||
) {
|
||||
composable(route = BookmarksDestination.route) {
|
||||
BookmarksRoute(windowSizeClass)
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2022 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
<string name="saved_loading">Loading saved…</string>
|
||||
<string name="top_app_bar_title_saved">Saved</string>
|
||||
<string name="top_app_bar_action_search">Search</string>
|
||||
<string name="top_app_bar_action_menu">Menu</string>
|
||||
</resources>
|
@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.feature.bookmarks
|
||||
|
||||
import com.google.samples.apps.nowinandroid.core.model.data.previewNewsResources
|
||||
import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepository
|
||||
import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository
|
||||
import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule
|
||||
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState.Loading
|
||||
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState.Success
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* To learn more about how this test handles Flows created with stateIn, see
|
||||
* https://developer.android.com/kotlin/flow/test#statein
|
||||
*/
|
||||
class BookmarksViewModelTest {
|
||||
@get:Rule
|
||||
val dispatcherRule = MainDispatcherRule()
|
||||
|
||||
private val userDataRepository = TestUserDataRepository()
|
||||
private val newsRepository = TestNewsRepository()
|
||||
private lateinit var viewModel: BookmarksViewModel
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
viewModel = BookmarksViewModel(
|
||||
userDataRepository = userDataRepository,
|
||||
newsRepository = newsRepository
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun stateIsInitiallyLoading() = runTest {
|
||||
assertEquals(Loading, viewModel.feedState.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun oneBookmark_showsInFeed() = runTest {
|
||||
val collectJob = launch(UnconfinedTestDispatcher()) { viewModel.feedState.collect() }
|
||||
|
||||
newsRepository.sendNewsResources(previewNewsResources)
|
||||
userDataRepository.updateNewsResourceBookmark(previewNewsResources[0].id, true)
|
||||
val item = viewModel.feedState.value
|
||||
assertTrue(item is Success)
|
||||
assertEquals((item as Success).feed.size, 1)
|
||||
|
||||
collectJob.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun oneBookmark_whenRemoving_removesFromFeed() = runTest {
|
||||
val collectJob = launch(UnconfinedTestDispatcher()) { viewModel.feedState.collect() }
|
||||
// Set the news resources to be used by this test
|
||||
newsRepository.sendNewsResources(previewNewsResources)
|
||||
// Start with the resource saved
|
||||
userDataRepository.updateNewsResourceBookmark(previewNewsResources[0].id, true)
|
||||
// Use viewModel to remove saved resource
|
||||
viewModel.removeFromSavedResources(previewNewsResources[0].id)
|
||||
// Verify list of saved resources is now empty
|
||||
val item = viewModel.feedState.value
|
||||
assertTrue(item is Success)
|
||||
assertEquals((item as Success).feed.size, 0)
|
||||
|
||||
collectJob.cancel()
|
||||
}
|
||||
}
|
Loading…
Reference in new issue