Merge branch 'main' into dependabot/gradle/protobuf-4.28.2

pull/1630/head
Ben Weiss 10 months ago committed by GitHub
commit 2f0940d3c9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -5,3 +5,13 @@
ij_kotlin_allow_trailing_comma=true
ij_kotlin_allow_trailing_comma_on_call_site=true
ktlint_function_naming_ignore_when_annotated_with=Composable, Test
ktlint_standard_backing-property-naming = disabled
ktlint_standard_binary-expression-wrapping = disabled
ktlint_standard_chain-method-continuation = disabled
ktlint_standard_class-signature = disabled
ktlint_standard_condition-wrapping = disabled
ktlint_standard_function-expression-body = disabled
ktlint_standard_function-literal = disabled
ktlint_standard_function-type-modifier-spacing = disabled
ktlint_standard_multiline-loop = disabled
ktlint_standard_function-signature = disabled

@ -116,7 +116,7 @@ To run the tests execute the following gradle tasks:
- `connectedDemoDebugAndroidTest` run all instrumented tests against the `demoDebug` variant.
**Note:** You should not run `./gradlew test` or `./gradlew connectedAndroidTest` as this will execute
tests against _all_ build variants which is both unecessary and will result in failures as only the
tests against _all_ build variants which is both unnecessary and will result in failures as only the
`demoDebug` variant is supported. No other variants have any tests (although this might change in future).
## Screenshot tests

@ -1,56 +1,56 @@
androidx.activity:activity-compose:1.9.2
androidx.activity:activity-ktx:1.9.2
androidx.activity:activity:1.9.2
androidx.annotation:annotation-experimental:1.4.0
androidx.annotation:annotation-jvm:1.8.0
androidx.annotation:annotation:1.8.0
androidx.activity:activity-compose:1.9.3
androidx.activity:activity-ktx:1.9.3
androidx.activity:activity:1.9.3
androidx.annotation:annotation-experimental:1.4.1
androidx.annotation:annotation-jvm:1.8.1
androidx.annotation:annotation:1.8.1
androidx.appcompat:appcompat-resources:1.6.1
androidx.arch.core:core-common:2.2.0
androidx.arch.core:core-runtime:2.2.0
androidx.autofill:autofill:1.0.0
androidx.browser:browser:1.8.0
androidx.collection:collection-jvm:1.4.0
androidx.collection:collection-ktx:1.4.0
androidx.collection:collection:1.4.0
androidx.compose.animation:animation-android:1.7.0
androidx.compose.animation:animation-core-android:1.7.0
androidx.compose.animation:animation-core:1.7.0
androidx.compose.animation:animation:1.7.0
androidx.compose.foundation:foundation-android:1.7.0
androidx.compose.foundation:foundation-layout-android:1.7.0
androidx.compose.foundation:foundation-layout:1.7.0
androidx.compose.foundation:foundation:1.7.0
androidx.collection:collection-jvm:1.4.4
androidx.collection:collection-ktx:1.4.4
androidx.collection:collection:1.4.4
androidx.compose.animation:animation-android:1.7.5
androidx.compose.animation:animation-core-android:1.7.5
androidx.compose.animation:animation-core:1.7.5
androidx.compose.animation:animation:1.7.5
androidx.compose.foundation:foundation-android:1.7.5
androidx.compose.foundation:foundation-layout-android:1.7.5
androidx.compose.foundation:foundation-layout:1.7.5
androidx.compose.foundation:foundation:1.7.5
androidx.compose.material3.adaptive:adaptive-android:1.0.0
androidx.compose.material3.adaptive:adaptive:1.0.0
androidx.compose.material3:material3-adaptive-navigation-suite-android:1.3.0
androidx.compose.material3:material3-adaptive-navigation-suite:1.3.0
androidx.compose.material3:material3-android:1.3.0
androidx.compose.material3:material3:1.3.0
androidx.compose.material:material-icons-core-android:1.7.0
androidx.compose.material:material-icons-core:1.7.0
androidx.compose.material:material-icons-extended-android:1.7.0
androidx.compose.material:material-icons-extended:1.7.0
androidx.compose.material:material-ripple-android:1.7.0
androidx.compose.material:material-ripple:1.7.0
androidx.compose.runtime:runtime-android:1.7.0
androidx.compose.runtime:runtime-saveable-android:1.7.0
androidx.compose.runtime:runtime-saveable:1.7.0
androidx.compose.runtime:runtime:1.7.0
androidx.compose.ui:ui-android:1.7.0
androidx.compose.ui:ui-geometry-android:1.7.0
androidx.compose.ui:ui-geometry:1.7.0
androidx.compose.ui:ui-graphics-android:1.7.0
androidx.compose.ui:ui-graphics:1.7.0
androidx.compose.ui:ui-text-android:1.7.0
androidx.compose.ui:ui-text:1.7.0
androidx.compose.ui:ui-tooling-preview-android:1.7.0
androidx.compose.ui:ui-tooling-preview:1.7.0
androidx.compose.ui:ui-unit-android:1.7.0
androidx.compose.ui:ui-unit:1.7.0
androidx.compose.ui:ui-util-android:1.7.0
androidx.compose.ui:ui-util:1.7.0
androidx.compose.ui:ui:1.7.0
androidx.compose:compose-bom:2024.09.00
androidx.compose.material3:material3-adaptive-navigation-suite-android:1.3.1
androidx.compose.material3:material3-adaptive-navigation-suite:1.3.1
androidx.compose.material3:material3-android:1.3.1
androidx.compose.material3:material3:1.3.1
androidx.compose.material:material-icons-core-android:1.7.5
androidx.compose.material:material-icons-core:1.7.5
androidx.compose.material:material-icons-extended-android:1.7.5
androidx.compose.material:material-icons-extended:1.7.5
androidx.compose.material:material-ripple-android:1.7.5
androidx.compose.material:material-ripple:1.7.5
androidx.compose.runtime:runtime-android:1.7.5
androidx.compose.runtime:runtime-saveable-android:1.7.5
androidx.compose.runtime:runtime-saveable:1.7.5
androidx.compose.runtime:runtime:1.7.5
androidx.compose.ui:ui-android:1.7.5
androidx.compose.ui:ui-geometry-android:1.7.5
androidx.compose.ui:ui-geometry:1.7.5
androidx.compose.ui:ui-graphics-android:1.7.5
androidx.compose.ui:ui-graphics:1.7.5
androidx.compose.ui:ui-text-android:1.7.5
androidx.compose.ui:ui-text:1.7.5
androidx.compose.ui:ui-tooling-preview-android:1.7.5
androidx.compose.ui:ui-tooling-preview:1.7.5
androidx.compose.ui:ui-unit-android:1.7.5
androidx.compose.ui:ui-unit:1.7.5
androidx.compose.ui:ui-util-android:1.7.5
androidx.compose.ui:ui-util:1.7.5
androidx.compose.ui:ui:1.7.5
androidx.compose:compose-bom:2024.11.00
androidx.concurrent:concurrent-futures:1.1.0
androidx.core:core-ktx:1.13.1
androidx.core:core:1.13.1

@ -114,6 +114,7 @@ dependencies {
kspTest(libs.hilt.compiler)
testImplementation(projects.core.dataTest)
testImplementation(projects.core.datastoreTest)
testImplementation(libs.hilt.android.testing)
testImplementation(projects.sync.syncTest)
testImplementation(libs.kotlin.test)
@ -122,7 +123,6 @@ dependencies {
testDemoImplementation(libs.roborazzi)
testDemoImplementation(projects.core.screenshotTesting)
androidTestImplementation(kotlin("test"))
androidTestImplementation(projects.core.testing)
androidTestImplementation(projects.core.dataTest)
androidTestImplementation(projects.core.datastoreTest)
@ -130,6 +130,7 @@ dependencies {
androidTestImplementation(libs.androidx.navigation.testing)
androidTestImplementation(libs.androidx.compose.ui.test)
androidTestImplementation(libs.hilt.android.testing)
androidTestImplementation(libs.kotlin.test)
baselineProfile(projects.benchmarks)
}

@ -1,6 +1,6 @@
androidx.activity:activity-compose:1.9.2
androidx.activity:activity-ktx:1.9.2
androidx.activity:activity:1.9.2
androidx.activity:activity-compose:1.9.3
androidx.activity:activity-ktx:1.9.3
androidx.activity:activity:1.9.3
androidx.annotation:annotation-experimental:1.4.1
androidx.annotation:annotation-jvm:1.8.1
androidx.annotation:annotation:1.8.1
@ -10,55 +10,55 @@ androidx.arch.core:core-common:2.2.0
androidx.arch.core:core-runtime:2.2.0
androidx.autofill:autofill:1.0.0
androidx.browser:browser:1.8.0
androidx.collection:collection-jvm:1.4.2
androidx.collection:collection-ktx:1.4.2
androidx.collection:collection:1.4.2
androidx.compose.animation:animation-android:1.7.0
androidx.compose.animation:animation-core-android:1.7.0
androidx.compose.animation:animation-core:1.7.0
androidx.compose.animation:animation:1.7.0
androidx.compose.foundation:foundation-android:1.7.0
androidx.compose.foundation:foundation-layout-android:1.7.0
androidx.compose.foundation:foundation-layout:1.7.0
androidx.compose.foundation:foundation:1.7.0
androidx.collection:collection-jvm:1.4.4
androidx.collection:collection-ktx:1.4.4
androidx.collection:collection:1.4.4
androidx.compose.animation:animation-android:1.7.5
androidx.compose.animation:animation-core-android:1.7.5
androidx.compose.animation:animation-core:1.7.5
androidx.compose.animation:animation:1.7.5
androidx.compose.foundation:foundation-android:1.7.5
androidx.compose.foundation:foundation-layout-android:1.7.5
androidx.compose.foundation:foundation-layout:1.7.5
androidx.compose.foundation:foundation:1.7.5
androidx.compose.material3.adaptive:adaptive-android:1.0.0
androidx.compose.material3.adaptive:adaptive-layout-android:1.0.0
androidx.compose.material3.adaptive:adaptive-layout:1.0.0
androidx.compose.material3.adaptive:adaptive-navigation-android:1.0.0
androidx.compose.material3.adaptive:adaptive-navigation:1.0.0
androidx.compose.material3.adaptive:adaptive:1.0.0
androidx.compose.material3:material3-adaptive-navigation-suite-android:1.3.0
androidx.compose.material3:material3-adaptive-navigation-suite:1.3.0
androidx.compose.material3:material3-android:1.3.0
androidx.compose.material3:material3-window-size-class-android:1.3.0
androidx.compose.material3:material3-window-size-class:1.3.0
androidx.compose.material3:material3:1.3.0
androidx.compose.material:material-icons-core-android:1.7.0
androidx.compose.material:material-icons-core:1.7.0
androidx.compose.material:material-icons-extended-android:1.7.0
androidx.compose.material:material-icons-extended:1.7.0
androidx.compose.material:material-ripple-android:1.7.0
androidx.compose.material:material-ripple:1.7.0
androidx.compose.runtime:runtime-android:1.7.1
androidx.compose.runtime:runtime-saveable-android:1.7.1
androidx.compose.runtime:runtime-saveable:1.7.1
androidx.compose.runtime:runtime-tracing:1.0.0-beta01
androidx.compose.runtime:runtime:1.7.1
androidx.compose.ui:ui-android:1.7.0
androidx.compose.ui:ui-geometry-android:1.7.0
androidx.compose.ui:ui-geometry:1.7.0
androidx.compose.ui:ui-graphics-android:1.7.0
androidx.compose.ui:ui-graphics:1.7.0
androidx.compose.ui:ui-text-android:1.7.0
androidx.compose.ui:ui-text:1.7.0
androidx.compose.ui:ui-tooling-preview-android:1.7.0
androidx.compose.ui:ui-tooling-preview:1.7.0
androidx.compose.ui:ui-unit-android:1.7.0
androidx.compose.ui:ui-unit:1.7.0
androidx.compose.ui:ui-util-android:1.7.0
androidx.compose.ui:ui-util:1.7.0
androidx.compose.ui:ui:1.7.0
androidx.compose:compose-bom:2024.09.00
androidx.compose.material3:material3-adaptive-navigation-suite-android:1.3.1
androidx.compose.material3:material3-adaptive-navigation-suite:1.3.1
androidx.compose.material3:material3-android:1.3.1
androidx.compose.material3:material3-window-size-class-android:1.3.1
androidx.compose.material3:material3-window-size-class:1.3.1
androidx.compose.material3:material3:1.3.1
androidx.compose.material:material-icons-core-android:1.7.5
androidx.compose.material:material-icons-core:1.7.5
androidx.compose.material:material-icons-extended-android:1.7.5
androidx.compose.material:material-icons-extended:1.7.5
androidx.compose.material:material-ripple-android:1.7.5
androidx.compose.material:material-ripple:1.7.5
androidx.compose.runtime:runtime-android:1.7.5
androidx.compose.runtime:runtime-saveable-android:1.7.5
androidx.compose.runtime:runtime-saveable:1.7.5
androidx.compose.runtime:runtime-tracing:1.7.5
androidx.compose.runtime:runtime:1.7.5
androidx.compose.ui:ui-android:1.7.5
androidx.compose.ui:ui-geometry-android:1.7.5
androidx.compose.ui:ui-geometry:1.7.5
androidx.compose.ui:ui-graphics-android:1.7.5
androidx.compose.ui:ui-graphics:1.7.5
androidx.compose.ui:ui-text-android:1.7.5
androidx.compose.ui:ui-text:1.7.5
androidx.compose.ui:ui-tooling-preview-android:1.7.5
androidx.compose.ui:ui-tooling-preview:1.7.5
androidx.compose.ui:ui-unit-android:1.7.5
androidx.compose.ui:ui-unit:1.7.5
androidx.compose.ui:ui-util-android:1.7.5
androidx.compose.ui:ui-util:1.7.5
androidx.compose.ui:ui:1.7.5
androidx.compose:compose-bom:2024.11.00
androidx.concurrent:concurrent-futures:1.1.0
androidx.core:core-ktx:1.13.1
androidx.core:core-splashscreen:1.0.1

@ -105,9 +105,9 @@ application-icon-640:'res/mipmap-anydpi-v26/ic_launcher.xml'
application-icon-65534:'res/mipmap-anydpi-v26/ic_launcher.xml'
application: label='Now in Android' icon='res/mipmap-anydpi-v26/ic_launcher.xml'
launchable-activity: name='com.google.samples.apps.nowinandroid.MainActivity' label='' icon=''
uses-library-not-required:'android.ext.adservices'
uses-library-not-required:'androidx.window.extensions'
uses-library-not-required:'androidx.window.sidecar'
uses-library-not-required:'android.ext.adservices'
feature-group: label=''
uses-feature: name='android.hardware.faketouch'
uses-implied-feature: name='android.hardware.faketouch' reason='default feature for all apps'

@ -16,13 +16,16 @@
package com.google.samples.apps.nowinandroid.ui
import androidx.compose.ui.semantics.SemanticsActions.ScrollBy
import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.assertIsOn
import androidx.compose.ui.test.assertIsSelected
import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onAllNodesWithTag
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onFirst
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
@ -32,10 +35,10 @@ import androidx.test.espresso.Espresso
import androidx.test.espresso.NoActivityResumedException
import com.google.samples.apps.nowinandroid.MainActivity
import com.google.samples.apps.nowinandroid.R
import com.google.samples.apps.nowinandroid.core.data.repository.NewsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository
import com.google.samples.apps.nowinandroid.core.model.data.Topic
import com.google.samples.apps.nowinandroid.core.rules.GrantPostNotificationsPermissionRule
import dagger.hilt.android.testing.BindValue
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.flow.first
@ -43,7 +46,6 @@ import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import javax.inject.Inject
import com.google.samples.apps.nowinandroid.feature.bookmarks.R as BookmarksR
import com.google.samples.apps.nowinandroid.feature.foryou.R as FeatureForyouR
@ -62,29 +64,24 @@ class NavigationTest {
@get:Rule(order = 0)
val hiltRule = HiltAndroidRule(this)
/**
* Create a temporary folder used to create a Data Store file. This guarantees that
* the file is removed in between each test, preventing a crash.
*/
@BindValue
@get:Rule(order = 1)
val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build()
/**
* Grant [android.Manifest.permission.POST_NOTIFICATIONS] permission.
*/
@get:Rule(order = 2)
@get:Rule(order = 1)
val postNotificationsPermission = GrantPostNotificationsPermissionRule()
/**
* Use the primary activity to initialize the app normally.
*/
@get:Rule(order = 3)
@get:Rule(order = 2)
val composeTestRule = createAndroidComposeRule<MainActivity>()
@Inject
lateinit var topicsRepository: TopicsRepository
@Inject
lateinit var newsRepository: NewsRepository
// The strings used for matching in these tests
private val navigateUp by composeTestRule.stringResource(FeatureForyouR.string.feature_foryou_navigate_up)
private val forYou by composeTestRule.stringResource(FeatureForyouR.string.feature_foryou_title)
@ -277,4 +274,44 @@ class NavigationTest {
onNodeWithTag("topic:${topic.id}").assertExists()
}
}
@Test
fun navigatingToTopicFromForYou_showsTopicDetails() {
composeTestRule.apply {
// Get the first news resource
val newsResource = runBlocking {
newsRepository.getNewsResources().first().first()
}
// Get its first topic and follow it
val topic = newsResource.topics.first()
onNodeWithText(topic.name).performClick()
// Get the news feed and scroll to the news resource
// Note: Possible flakiness. If the content of the news resource is long then the topic
// tag might not be visible meaning it cannot be clicked
onNodeWithTag("forYou:feed")
.performScrollToNode(hasTestTag("newsResourceCard:${newsResource.id}"))
.fetchSemanticsNode()
.apply {
val newsResourceCardNode = onNodeWithTag("newsResourceCard:${newsResource.id}")
.fetchSemanticsNode()
config[ScrollBy].action?.invoke(
0f,
// to ensure the bottom of the card is visible,
// manually scroll the difference between the height of
// the scrolling node and the height of the card
(newsResourceCardNode.size.height - size.height).coerceAtLeast(0).toFloat(),
)
}
// Click the first topic tag
onAllNodesWithTag("topicTag:${topic.id}", useUnmergedTree = true)
.onFirst()
.performClick()
// Verify that we're on the correct topic details screen
onNodeWithTag("topic:${topic.id}").assertExists()
}
}
}

@ -20,10 +20,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.navigation.compose.NavHost
import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.bookmarksScreen
import com.google.samples.apps.nowinandroid.feature.foryou.navigation.ForYouRoute
import com.google.samples.apps.nowinandroid.feature.foryou.navigation.forYouScreen
import com.google.samples.apps.nowinandroid.feature.foryou.navigation.ForYouBaseRoute
import com.google.samples.apps.nowinandroid.feature.foryou.navigation.forYouSection
import com.google.samples.apps.nowinandroid.feature.interests.navigation.navigateToInterests
import com.google.samples.apps.nowinandroid.feature.search.navigation.searchScreen
import com.google.samples.apps.nowinandroid.feature.topic.navigation.navigateToTopic
import com.google.samples.apps.nowinandroid.feature.topic.navigation.topicScreen
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination.INTERESTS
import com.google.samples.apps.nowinandroid.ui.NiaAppState
import com.google.samples.apps.nowinandroid.ui.interests2pane.interestsListDetailScreen
@ -44,10 +46,18 @@ fun NiaNavHost(
val navController = appState.navController
NavHost(
navController = navController,
startDestination = ForYouRoute,
startDestination = ForYouBaseRoute,
modifier = modifier,
) {
forYouScreen(onTopicClick = navController::navigateToInterests)
forYouSection(
onTopicClick = navController::navigateToTopic,
) {
topicScreen(
showBackButton = true,
onBackClick = navController::popBackStack,
onTopicClick = navController::navigateToTopic,
)
}
bookmarksScreen(
onTopicClick = navController::navigateToInterests,
onShowSnackbar = onShowSnackbar,

@ -21,6 +21,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
import com.google.samples.apps.nowinandroid.R
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.BookmarksRoute
import com.google.samples.apps.nowinandroid.feature.foryou.navigation.ForYouBaseRoute
import com.google.samples.apps.nowinandroid.feature.foryou.navigation.ForYouRoute
import com.google.samples.apps.nowinandroid.feature.interests.navigation.InterestsRoute
import kotlin.reflect.KClass
@ -29,9 +30,18 @@ import com.google.samples.apps.nowinandroid.feature.foryou.R as forYouR
import com.google.samples.apps.nowinandroid.feature.search.R as searchR
/**
* Type for the top level destinations in the application. Each of these destinations
* can contain one or more screens (based on the window size). Navigation from one screen to the
* next within a single destination will be handled directly in composables.
* Type for the top level destinations in the application. Contains metadata about the destination
* that is used in the top app bar and common navigation UI.
*
* @param selectedIcon The icon to be displayed in the navigation UI when this destination is
* selected.
* @param unselectedIcon The icon to be displayed in the navigation UI when this destination is
* not selected.
* @param iconTextId Text that to be displayed in the navigation UI.
* @param titleTextId Text that is displayed on the top app bar.
* @param route The route to use when navigating to this destination.
* @param baseRoute The highest ancestor of this destination. Defaults to [route], meaning that
* there is a single destination in that section of the app (no nested destinations).
*/
enum class TopLevelDestination(
val selectedIcon: ImageVector,
@ -39,6 +49,7 @@ enum class TopLevelDestination(
@StringRes val iconTextId: Int,
@StringRes val titleTextId: Int,
val route: KClass<*>,
val baseRoute: KClass<*> = route,
) {
FOR_YOU(
selectedIcon = NiaIcons.Upcoming,
@ -46,6 +57,7 @@ enum class TopLevelDestination(
iconTextId = forYouR.string.feature_foryou_title,
titleTextId = R.string.app_name,
route = ForYouRoute::class,
baseRoute = ForYouBaseRoute::class,
),
BOOKMARKS(
selectedIcon = NiaIcons.Bookmarks,

@ -152,7 +152,7 @@ internal fun NiaApp(
appState.topLevelDestinations.forEach { destination ->
val hasUnread = unreadDestinations.contains(destination)
val selected = currentDestination
.isRouteInHierarchy(destination.route)
.isRouteInHierarchy(destination.baseRoute)
item(
selected = selected,
onClick = { appState.navigateToTopLevelDestination(destination) },

@ -90,7 +90,7 @@ class NiaAppState(
val currentTopLevelDestination: TopLevelDestination?
@Composable get() {
return TopLevelDestination.entries.firstOrNull { topLevelDestination ->
currentDestination?.hasRoute(route = topLevelDestination.route) ?: false
currentDestination?.hasRoute(route = topLevelDestination.route) == true
}
}

@ -15,9 +15,6 @@
limitations under the License.
-->
<resources>
<!-- Status bar -->
<color name="black30">#4D000000</color>
<color name="ic_launcher_background_tint">#000000</color>
<color name="ic_launcher_foreground_tint">#FCFCFC</color>
</resources>

@ -31,7 +31,6 @@ import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.model.data.Topic
import com.google.samples.apps.nowinandroid.ui.interests2pane.InterestsListDetailScreen
import com.google.samples.apps.nowinandroid.uitesthiltmanifest.HiltComponentActivity
import dagger.hilt.android.testing.BindValue
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.HiltTestApplication
@ -40,7 +39,6 @@ import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@ -60,11 +58,7 @@ class InterestsListDetailScreenTest {
@get:Rule(order = 0)
val hiltRule = HiltAndroidRule(this)
@BindValue
@get:Rule(order = 1)
val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build()
@get:Rule(order = 2)
val composeTestRule = createAndroidComposeRule<HiltComponentActivity>()
@Inject

@ -38,7 +38,6 @@ import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.testing.util.DefaultRoborazziOptions
import com.google.samples.apps.nowinandroid.uitesthiltmanifest.HiltComponentActivity
import dagger.hilt.android.testing.BindValue
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.HiltTestApplication
@ -47,7 +46,6 @@ import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@ -74,18 +72,10 @@ class NiaAppScreenSizesScreenshotTests {
@get:Rule(order = 0)
val hiltRule = HiltAndroidRule(this)
/**
* Create a temporary folder used to create a Data Store file. This guarantees that
* the file is removed in between each test, preventing a crash.
*/
@BindValue
@get:Rule(order = 1)
val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build()
/**
* Use a test activity to set the content on.
*/
@get:Rule(order = 2)
@get:Rule(order = 1)
val composeTestRule = createAndroidComposeRule<HiltComponentActivity>()
@Inject

@ -69,7 +69,6 @@ import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.testing.util.DefaultRoborazziOptions
import com.google.samples.apps.nowinandroid.uitesthiltmanifest.HiltComponentActivity
import dagger.hilt.android.testing.BindValue
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.HiltTestApplication
@ -80,7 +79,6 @@ import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@ -107,18 +105,10 @@ class SnackbarInsetsScreenshotTests {
@get:Rule(order = 0)
val hiltRule = HiltAndroidRule(this)
/**
* Create a temporary folder used to create a Data Store file. This guarantees that
* the file is removed in between each test, preventing a crash.
*/
@BindValue
@get:Rule(order = 1)
val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build()
/**
* Use a test activity to set the content on.
*/
@get:Rule(order = 2)
@get:Rule(order = 1)
val composeTestRule = createAndroidComposeRule<HiltComponentActivity>()
@Inject

@ -42,7 +42,6 @@ import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.testing.util.DefaultRoborazziOptions
import com.google.samples.apps.nowinandroid.uitesthiltmanifest.HiltComponentActivity
import dagger.hilt.android.testing.BindValue
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.HiltTestApplication
@ -53,7 +52,6 @@ import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@ -80,18 +78,10 @@ class SnackbarScreenshotTests {
@get:Rule(order = 0)
val hiltRule = HiltAndroidRule(this)
/**
* Create a temporary folder used to create a Data Store file. This guarantees that
* the file is removed in between each test, preventing a crash.
*/
@BindValue
@get:Rule(order = 1)
val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build()
/**
* Use a test activity to set the content on.
*/
@get:Rule(order = 2)
@get:Rule(order = 1)
val composeTestRule = createAndroidComposeRule<HiltComponentActivity>()
@Inject

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 260 KiB

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 KiB

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 211 KiB

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 94 KiB

@ -53,8 +53,8 @@ class AndroidLibraryConventionPlugin : Plugin<Project> {
disableUnnecessaryAndroidTests(target)
}
dependencies {
add("androidTestImplementation", kotlin("test"))
add("testImplementation", kotlin("test"))
add("androidTestImplementation", libs.findLibrary("kotlin.test").get())
add("testImplementation", libs.findLibrary("kotlin.test").get())
add("implementation", libs.findLibrary("androidx.tracing.ktx").get())
}

@ -26,7 +26,13 @@ class HiltConventionPlugin : Plugin<Project> {
pluginManager.apply("com.google.devtools.ksp")
dependencies {
add("ksp", libs.findLibrary("hilt.compiler").get())
add("implementation", libs.findLibrary("hilt.core").get())
}
// Add support for Jvm Module, base on org.jetbrains.kotlin.jvm
pluginManager.withPlugin("org.jetbrains.kotlin.jvm") {
dependencies {
add("implementation", libs.findLibrary("hilt.core").get())
}
}
/** Add support for Android modules, based on [AndroidBasePlugin] */

@ -15,8 +15,11 @@
*/
import com.google.samples.apps.nowinandroid.configureKotlinJvm
import com.google.samples.apps.nowinandroid.libs
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.kotlin
class JvmLibraryConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
@ -26,6 +29,9 @@ class JvmLibraryConventionPlugin : Plugin<Project> {
apply("nowinandroid.android.lint")
}
configureKotlinJvm()
dependencies {
add("testImplementation", libs.findLibrary("kotlin.test").get())
}
}
}
}

@ -30,6 +30,6 @@ import org.gradle.api.Project
internal fun LibraryAndroidComponentsExtension.disableUnnecessaryAndroidTests(
project: Project,
) = beforeVariants {
it.enableAndroidTest = it.enableAndroidTest
it.androidTest.enable = it.androidTest.enable
&& project.projectDir.resolve("src/androidTest").exists()
}

@ -16,7 +16,13 @@
dependencyResolutionManagement {
repositories {
google()
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
}
versionCatalogs {

@ -16,7 +16,13 @@
buildscript {
repositories {
google()
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
// Android Build Server
@ -30,7 +36,13 @@ buildscript {
}
// Lists all plugins used throughout the project
/*
* By listing all the plugins used throughout all subprojects in the root project build script, it
* ensures that the build script classpath remains the same for all projects. This avoids potential
* problems with mismatching versions of transitive plugin dependencies. A subproject that applies
* an unlisted plugin will have that plugin and its dependencies _appended_ to the classpath, not
* replacing pre-existing dependencies.
*/
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.library) apply false

@ -1,5 +1,5 @@
// This file contains classes (with possible wildcards) that the Compose Compiler will treat as stable.
// It allows us to define classes that our not part of our codebase without wrapping them in a stable class.
// It allows us to define classes that are not part of our codebase without wrapping them in a stable class.
// For more information, check https://developer.android.com/jetpack/compose/performance/stability/fix#configuration-file
// We always use immutable classes for our data model, to avoid running the Compose compiler

@ -17,16 +17,13 @@
package com.google.samples.apps.nowinandroid.core.data.test.repository
import com.google.samples.apps.nowinandroid.core.data.Synchronizer
import com.google.samples.apps.nowinandroid.core.data.model.asEntity
import com.google.samples.apps.nowinandroid.core.data.model.asExternalModel
import com.google.samples.apps.nowinandroid.core.data.repository.NewsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.NewsResourceQuery
import com.google.samples.apps.nowinandroid.core.database.model.NewsResourceEntity
import com.google.samples.apps.nowinandroid.core.database.model.asExternalModel
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.network.Dispatcher
import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.IO
import com.google.samples.apps.nowinandroid.core.network.demo.DemoNiaNetworkDataSource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
@ -48,9 +45,11 @@ class FakeNewsRepository @Inject constructor(
query: NewsResourceQuery,
): Flow<List<NewsResource>> =
flow {
val newsResources = datasource.getNewsResources()
val topics = datasource.getTopics()
emit(
datasource
.getNewsResources()
newsResources
.filter { networkNewsResource ->
// Filter out any news resources which don't match the current query.
// If no query parameters (filterTopicIds or filterNewsIds) are specified
@ -64,8 +63,7 @@ class FakeNewsRepository @Inject constructor(
)
.all(true::equals)
}
.map(NetworkNewsResource::asEntity)
.map(NewsResourceEntity::asExternalModel),
.map { it.asExternalModel(topics) },
)
}.flowOn(ioDispatcher)

@ -19,8 +19,10 @@ package com.google.samples.apps.nowinandroid.core.data.model
import com.google.samples.apps.nowinandroid.core.database.model.NewsResourceEntity
import com.google.samples.apps.nowinandroid.core.database.model.NewsResourceTopicCrossRef
import com.google.samples.apps.nowinandroid.core.database.model.TopicEntity
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResourceExpanded
import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
import com.google.samples.apps.nowinandroid.core.network.model.asExternalModel
fun NetworkNewsResource.asEntity() = NewsResourceEntity(
id = id,
@ -32,16 +34,6 @@ fun NetworkNewsResource.asEntity() = NewsResourceEntity(
type = type,
)
fun NetworkNewsResourceExpanded.asEntity() = NewsResourceEntity(
id = id,
title = title,
content = content,
url = url,
headerImageUrl = headerImageUrl,
publishDate = publishDate,
type = type,
)
/**
* A shell [TopicEntity] to fulfill the foreign key constraint when inserting
* a [NewsResourceEntity] into the DB
@ -65,3 +57,17 @@ fun NetworkNewsResource.topicCrossReferences(): List<NewsResourceTopicCrossRef>
topicId = topicId,
)
}
fun NetworkNewsResource.asExternalModel(topics: List<NetworkTopic>) =
NewsResource(
id = id,
title = title,
content = content,
url = url,
headerImageUrl = headerImageUrl,
publishDate = publishDate,
type = type,
topics = topics
.filter { networkTopic -> this.topics.contains(networkTopic.id) }
.map(NetworkTopic::asExternalModel),
)

@ -1,91 +0,0 @@
/*
* 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.core.data.model
import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResourceExpanded
import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
import kotlinx.datetime.Instant
import org.junit.Test
import kotlin.test.assertEquals
class NetworkEntityKtTest {
@Test
fun network_topic_can_be_mapped_to_topic_entity() {
val networkModel = NetworkTopic(
id = "0",
name = "Test",
shortDescription = "short description",
longDescription = "long description",
url = "URL",
imageUrl = "image URL",
)
val entity = networkModel.asEntity()
assertEquals("0", entity.id)
assertEquals("Test", entity.name)
assertEquals("short description", entity.shortDescription)
assertEquals("long description", entity.longDescription)
assertEquals("URL", entity.url)
assertEquals("image URL", entity.imageUrl)
}
@Test
fun network_news_resource_can_be_mapped_to_news_resource_entity() {
val networkModel =
NetworkNewsResource(
id = "0",
title = "title",
content = "content",
url = "url",
headerImageUrl = "headerImageUrl",
publishDate = Instant.fromEpochMilliseconds(1),
type = "Article 📚",
)
val entity = networkModel.asEntity()
assertEquals("0", entity.id)
assertEquals("title", entity.title)
assertEquals("content", entity.content)
assertEquals("url", entity.url)
assertEquals("headerImageUrl", entity.headerImageUrl)
assertEquals(Instant.fromEpochMilliseconds(1), entity.publishDate)
assertEquals("Article 📚", entity.type)
val expandedNetworkModel =
NetworkNewsResourceExpanded(
id = "0",
title = "title",
content = "content",
url = "url",
headerImageUrl = "headerImageUrl",
publishDate = Instant.fromEpochMilliseconds(1),
type = "Article 📚",
)
val entityFromExpanded = expandedNetworkModel.asEntity()
assertEquals("0", entityFromExpanded.id)
assertEquals("title", entityFromExpanded.title)
assertEquals("content", entityFromExpanded.content)
assertEquals("url", entityFromExpanded.url)
assertEquals("headerImageUrl", entityFromExpanded.headerImageUrl)
assertEquals(Instant.fromEpochMilliseconds(1), entityFromExpanded.publishDate)
assertEquals("Article 📚", entityFromExpanded.type)
}
}

@ -0,0 +1,140 @@
/*
* 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.core.data.model
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.model.data.Topic
import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
import com.google.samples.apps.nowinandroid.core.network.model.asExternalModel
import kotlinx.datetime.Instant
import org.junit.Test
import kotlin.test.assertEquals
class NetworkEntityTest {
@Test
fun networkTopicMapsToDatabaseModel() {
val networkModel = NetworkTopic(
id = "0",
name = "Test",
shortDescription = "short description",
longDescription = "long description",
url = "URL",
imageUrl = "image URL",
)
val entity = networkModel.asEntity()
assertEquals("0", entity.id)
assertEquals("Test", entity.name)
assertEquals("short description", entity.shortDescription)
assertEquals("long description", entity.longDescription)
assertEquals("URL", entity.url)
assertEquals("image URL", entity.imageUrl)
}
@Test
fun networkNewsResourceMapsToDatabaseModel() {
val networkModel =
NetworkNewsResource(
id = "0",
title = "title",
content = "content",
url = "url",
headerImageUrl = "headerImageUrl",
publishDate = Instant.fromEpochMilliseconds(1),
type = "Article 📚",
)
val entity = networkModel.asEntity()
assertEquals("0", entity.id)
assertEquals("title", entity.title)
assertEquals("content", entity.content)
assertEquals("url", entity.url)
assertEquals("headerImageUrl", entity.headerImageUrl)
assertEquals(Instant.fromEpochMilliseconds(1), entity.publishDate)
assertEquals("Article 📚", entity.type)
}
@Test
fun networkTopicMapsToExternalModel() {
val networkTopic = NetworkTopic(
id = "0",
name = "Test",
shortDescription = "short description",
longDescription = "long description",
url = "URL",
imageUrl = "imageUrl",
)
val expected = Topic(
id = "0",
name = "Test",
shortDescription = "short description",
longDescription = "long description",
url = "URL",
imageUrl = "imageUrl",
)
assertEquals(expected, networkTopic.asExternalModel())
}
@Test
fun networkNewsResourceMapsToExternalModel() {
val networkNewsResource = NetworkNewsResource(
id = "0",
title = "title",
content = "content",
url = "url",
headerImageUrl = "headerImageUrl",
publishDate = Instant.fromEpochMilliseconds(1),
type = "Article 📚",
topics = listOf("1", "2"),
)
val networkTopics = listOf(
NetworkTopic(
id = "1",
name = "Test 1",
shortDescription = "short description 1",
longDescription = "long description 1",
url = "url 1",
imageUrl = "imageUrl 1",
),
NetworkTopic(
id = "2",
name = "Test 2",
shortDescription = "short description 2",
longDescription = "long description 2",
url = "url 2",
imageUrl = "imageUrl 2",
),
)
val expected = NewsResource(
id = "0",
title = "title",
content = "content",
url = "url",
headerImageUrl = "headerImageUrl",
publishDate = Instant.fromEpochMilliseconds(1),
type = "Article 📚",
topics = networkTopics.map(NetworkTopic::asExternalModel),
)
assertEquals(expected, networkNewsResource.asExternalModel(networkTopics))
}
}

@ -32,7 +32,8 @@ import com.google.samples.apps.nowinandroid.core.database.model.PopulatedNewsRes
import com.google.samples.apps.nowinandroid.core.database.model.TopicEntity
import com.google.samples.apps.nowinandroid.core.database.model.asExternalModel
import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferencesDataSource
import com.google.samples.apps.nowinandroid.core.datastore.test.testUserPreferencesDataStore
import com.google.samples.apps.nowinandroid.core.datastore.UserPreferences
import com.google.samples.apps.nowinandroid.core.datastore.test.InMemoryDataStore
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.model.data.Topic
import com.google.samples.apps.nowinandroid.core.network.model.NetworkChangeList
@ -43,9 +44,7 @@ import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import kotlin.test.assertEquals
import kotlin.test.assertTrue
@ -67,14 +66,9 @@ class OfflineFirstNewsRepositoryTest {
private lateinit var synchronizer: Synchronizer
@get:Rule
val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build()
@Before
fun setup() {
niaPreferencesDataSource = NiaPreferencesDataSource(
tmpFolder.testUserPreferencesDataStore(testScope),
)
niaPreferencesDataSource = NiaPreferencesDataSource(InMemoryDataStore(UserPreferences.getDefaultInstance()))
newsResourceDao = TestNewsResourceDao()
topicDao = TestTopicDao()
network = TestNiaNetworkDataSource()

@ -25,7 +25,8 @@ import com.google.samples.apps.nowinandroid.core.database.dao.TopicDao
import com.google.samples.apps.nowinandroid.core.database.model.TopicEntity
import com.google.samples.apps.nowinandroid.core.database.model.asExternalModel
import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferencesDataSource
import com.google.samples.apps.nowinandroid.core.datastore.test.testUserPreferencesDataStore
import com.google.samples.apps.nowinandroid.core.datastore.UserPreferences
import com.google.samples.apps.nowinandroid.core.datastore.test.InMemoryDataStore
import com.google.samples.apps.nowinandroid.core.model.data.Topic
import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
import kotlinx.coroutines.flow.first
@ -33,9 +34,7 @@ import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import kotlin.test.assertEquals
class OfflineFirstTopicsRepositoryTest {
@ -52,16 +51,11 @@ class OfflineFirstTopicsRepositoryTest {
private lateinit var synchronizer: Synchronizer
@get:Rule
val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build()
@Before
fun setup() {
topicDao = TestTopicDao()
network = TestNiaNetworkDataSource()
niaPreferences = NiaPreferencesDataSource(
tmpFolder.testUserPreferencesDataStore(testScope),
)
niaPreferences = NiaPreferencesDataSource(InMemoryDataStore(UserPreferences.getDefaultInstance()))
synchronizer = TestSynchronizer(niaPreferences)
subject = OfflineFirstTopicsRepository(

@ -18,7 +18,8 @@ package com.google.samples.apps.nowinandroid.core.data.repository
import com.google.samples.apps.nowinandroid.core.analytics.NoOpAnalyticsHelper
import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferencesDataSource
import com.google.samples.apps.nowinandroid.core.datastore.test.testUserPreferencesDataStore
import com.google.samples.apps.nowinandroid.core.datastore.UserPreferences
import com.google.samples.apps.nowinandroid.core.datastore.test.InMemoryDataStore
import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig
import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand
import com.google.samples.apps.nowinandroid.core.model.data.UserData
@ -28,9 +29,7 @@ import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
@ -45,14 +44,9 @@ class OfflineFirstUserDataRepositoryTest {
private val analyticsHelper = NoOpAnalyticsHelper()
@get:Rule
val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build()
@Before
fun setup() {
niaPreferencesDataSource = NiaPreferencesDataSource(
tmpFolder.testUserPreferencesDataStore(testScope),
)
niaPreferencesDataSource = NiaPreferencesDataSource(InMemoryDataStore(UserPreferences.getDefaultInstance()))
subject = OfflineFirstUserDataRepository(
niaPreferencesDataSource = niaPreferencesDataSource,

@ -0,0 +1,28 @@
/*
* Copyright 2024 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.core.datastore.test
import androidx.datastore.core.DataStore
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.updateAndGet
class InMemoryDataStore<T>(initialValue: T) : DataStore<T> {
override val data = MutableStateFlow(initialValue)
override suspend fun updateData(
transform: suspend (it: T) -> T,
) = data.updateAndGet { transform(it) }
}

@ -17,17 +17,13 @@
package com.google.samples.apps.nowinandroid.core.datastore.test
import androidx.datastore.core.DataStore
import androidx.datastore.core.DataStoreFactory
import com.google.samples.apps.nowinandroid.core.datastore.UserPreferences
import com.google.samples.apps.nowinandroid.core.datastore.UserPreferencesSerializer
import com.google.samples.apps.nowinandroid.core.datastore.di.DataStoreModule
import com.google.samples.apps.nowinandroid.core.network.di.ApplicationScope
import dagger.Module
import dagger.Provides
import dagger.hilt.components.SingletonComponent
import dagger.hilt.testing.TestInstallIn
import kotlinx.coroutines.CoroutineScope
import org.junit.rules.TemporaryFolder
import javax.inject.Singleton
@Module
@ -36,26 +32,9 @@ import javax.inject.Singleton
replaces = [DataStoreModule::class],
)
internal object TestDataStoreModule {
@Provides
@Singleton
fun providesUserPreferencesDataStore(
@ApplicationScope scope: CoroutineScope,
userPreferencesSerializer: UserPreferencesSerializer,
tmpFolder: TemporaryFolder,
): DataStore<UserPreferences> =
tmpFolder.testUserPreferencesDataStore(
coroutineScope = scope,
userPreferencesSerializer = userPreferencesSerializer,
)
}
fun TemporaryFolder.testUserPreferencesDataStore(
coroutineScope: CoroutineScope,
userPreferencesSerializer: UserPreferencesSerializer = UserPreferencesSerializer(),
) = DataStoreFactory.create(
serializer = userPreferencesSerializer,
scope = coroutineScope,
) {
newFile("user_preferences_test.pb")
serializer: UserPreferencesSerializer,
): DataStore<UserPreferences> = InMemoryDataStore(serializer.defaultValue)
}

@ -16,15 +16,13 @@
package com.google.samples.apps.nowinandroid.core.datastore
import com.google.samples.apps.nowinandroid.core.datastore.test.testUserPreferencesDataStore
import com.google.samples.apps.nowinandroid.core.datastore.test.InMemoryDataStore
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import kotlin.test.assertFalse
import kotlin.test.assertTrue
@ -34,14 +32,9 @@ class NiaPreferencesDataSourceTest {
private lateinit var subject: NiaPreferencesDataSource
@get:Rule
val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build()
@Before
fun setup() {
subject = NiaPreferencesDataSource(
tmpFolder.testUserPreferencesDataStore(testScope),
)
subject = NiaPreferencesDataSource(InMemoryDataStore(UserPreferences.getDefaultInstance()))
}
@Test

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 527 B

After

Width:  |  Height:  |  Size: 575 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 527 B

After

Width:  |  Height:  |  Size: 575 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 527 B

After

Width:  |  Height:  |  Size: 575 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 527 B

After

Width:  |  Height:  |  Size: 575 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 527 B

After

Width:  |  Height:  |  Size: 575 B

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save