Change-Id: I5535315c6159e3b70e8591b60a50d2e0607efe97 # Conflicts: # core/designsystem/build.gradle.ktspull/1437/head
@ -0,0 +1,59 @@
|
||||
name: NightlyBaselineProfiles
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '42 4 * * *'
|
||||
|
||||
jobs:
|
||||
baseline_profiles:
|
||||
name: "Generate Baseline Profiles"
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Enable KVM group perms
|
||||
run: |
|
||||
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
|
||||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger --name-match=kvm
|
||||
ls /dev/kvm
|
||||
|
||||
- name: Copy CI gradle.properties
|
||||
run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: 17
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
|
||||
- name: Setup Android SDK
|
||||
uses: android-actions/setup-android@v3
|
||||
|
||||
- name: Accept licenses
|
||||
run: yes | sdkmanager --licenses || true
|
||||
|
||||
- name: Check build-logic
|
||||
run: ./gradlew check -p build-logic
|
||||
|
||||
- name: Setup GMD
|
||||
run: ./gradlew :benchmarks:pixel6Api33Setup
|
||||
--info
|
||||
-Pandroid.experimental.testOptions.managedDevices.emulator.showKernelLogging=true
|
||||
-Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect"
|
||||
|
||||
- name: Build all build type and flavor permutations including baseline profiles
|
||||
run: ./gradlew :app:assemble
|
||||
-Pandroid.testInstrumentationRunnerArguments.androidx.benchmark.enabledRules=baselineprofile
|
||||
-Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect"
|
||||
-Pandroid.experimental.testOptions.managedDevices.emulator.showKernelLogging=true
|
@ -1,2 +1,2 @@
|
||||
# This file can be used to trigger an internal build by changing the number below
|
||||
3
|
||||
2
|
||||
|
@ -1,19 +0,0 @@
|
||||
-dontwarn org.bouncycastle.jsse.BCSSLParameters
|
||||
-dontwarn org.bouncycastle.jsse.BCSSLSocket
|
||||
-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
|
||||
-dontwarn org.conscrypt.Conscrypt$Version
|
||||
-dontwarn org.conscrypt.Conscrypt
|
||||
-dontwarn org.conscrypt.ConscryptHostnameVerifier
|
||||
-dontwarn org.openjsse.javax.net.ssl.SSLParameters
|
||||
-dontwarn org.openjsse.javax.net.ssl.SSLSocket
|
||||
-dontwarn org.openjsse.net.ssl.OpenJSSE
|
||||
|
||||
# Fix for Retrofit issue https://github.com/square/retrofit/issues/3751
|
||||
# Keep generic signature of Call, Response (R8 full mode strips signatures from non-kept items).
|
||||
-keep,allowobfuscation,allowshrinking interface retrofit2.Call
|
||||
-keep,allowobfuscation,allowshrinking class retrofit2.Response
|
||||
|
||||
# With R8 full mode generic signatures are stripped for classes that are not
|
||||
# kept. Suspend functions are wrapped in continuations where the type argument
|
||||
# is used.
|
||||
-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation
|
@ -1,247 +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.ui
|
||||
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
|
||||
import androidx.compose.material3.windowsizeclass.WindowSizeClass
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.test.DeviceConfigurationOverride
|
||||
import androidx.compose.ui.test.ForcedSize
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.google.samples.apps.nowinandroid.core.data.repository.CompositeUserNewsResourceRepository
|
||||
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
|
||||
import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor
|
||||
import com.google.samples.apps.nowinandroid.core.rules.GrantPostNotificationsPermissionRule
|
||||
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.uitesthiltmanifest.HiltComponentActivity
|
||||
import dagger.hilt.android.testing.BindValue
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Tests that the navigation UI is rendered correctly on different screen sizes.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
|
||||
@HiltAndroidTest
|
||||
class NavigationUiTest {
|
||||
|
||||
/**
|
||||
* Manages the components' state and is used to perform injection on your test
|
||||
*/
|
||||
@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)
|
||||
val postNotificationsPermission = GrantPostNotificationsPermissionRule()
|
||||
|
||||
/**
|
||||
* Use a test activity to set the content on.
|
||||
*/
|
||||
@get:Rule(order = 3)
|
||||
val composeTestRule = createAndroidComposeRule<HiltComponentActivity>()
|
||||
|
||||
val userNewsResourceRepository = CompositeUserNewsResourceRepository(
|
||||
newsRepository = TestNewsRepository(),
|
||||
userDataRepository = TestUserDataRepository(),
|
||||
)
|
||||
|
||||
@Inject
|
||||
lateinit var networkMonitor: NetworkMonitor
|
||||
|
||||
@Inject
|
||||
lateinit var timeZoneMonitor: TimeZoneMonitor
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
hiltRule.inject()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun compactWidth_compactHeight_showsNavigationBar() {
|
||||
composeTestRule.setContent {
|
||||
DeviceConfigurationOverride(
|
||||
DeviceConfigurationOverride.ForcedSize(DpSize(400.dp, 400.dp)),
|
||||
) {
|
||||
BoxWithConstraints {
|
||||
NiaApp(fakeAppState(maxWidth, maxHeight))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag("NiaBottomBar").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTag("NiaNavRail").assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun mediumWidth_compactHeight_showsNavigationRail() {
|
||||
composeTestRule.setContent {
|
||||
DeviceConfigurationOverride(
|
||||
DeviceConfigurationOverride.ForcedSize(DpSize(610.dp, 400.dp)),
|
||||
) {
|
||||
BoxWithConstraints {
|
||||
NiaApp(fakeAppState(maxWidth, maxHeight))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag("NiaNavRail").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTag("NiaBottomBar").assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun expandedWidth_compactHeight_showsNavigationRail() {
|
||||
composeTestRule.setContent {
|
||||
DeviceConfigurationOverride(
|
||||
DeviceConfigurationOverride.ForcedSize(DpSize(900.dp, 400.dp)),
|
||||
) {
|
||||
BoxWithConstraints {
|
||||
NiaApp(fakeAppState(maxWidth, maxHeight))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag("NiaNavRail").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTag("NiaBottomBar").assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun compactWidth_mediumHeight_showsNavigationBar() {
|
||||
composeTestRule.setContent {
|
||||
DeviceConfigurationOverride(
|
||||
DeviceConfigurationOverride.ForcedSize(DpSize(400.dp, 500.dp)),
|
||||
) {
|
||||
BoxWithConstraints {
|
||||
NiaApp(fakeAppState(maxWidth, maxHeight))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag("NiaBottomBar").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTag("NiaNavRail").assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun mediumWidth_mediumHeight_showsNavigationRail() {
|
||||
composeTestRule.setContent {
|
||||
DeviceConfigurationOverride(
|
||||
DeviceConfigurationOverride.ForcedSize(DpSize(610.dp, 500.dp)),
|
||||
) {
|
||||
BoxWithConstraints {
|
||||
NiaApp(fakeAppState(maxWidth, maxHeight))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag("NiaNavRail").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTag("NiaBottomBar").assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun expandedWidth_mediumHeight_showsNavigationRail() {
|
||||
composeTestRule.setContent {
|
||||
DeviceConfigurationOverride(
|
||||
DeviceConfigurationOverride.ForcedSize(DpSize(900.dp, 500.dp)),
|
||||
) {
|
||||
BoxWithConstraints {
|
||||
NiaApp(fakeAppState(maxWidth, maxHeight))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag("NiaNavRail").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTag("NiaBottomBar").assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun compactWidth_expandedHeight_showsNavigationBar() {
|
||||
composeTestRule.setContent {
|
||||
DeviceConfigurationOverride(
|
||||
DeviceConfigurationOverride.ForcedSize(DpSize(400.dp, 1000.dp)),
|
||||
) {
|
||||
BoxWithConstraints {
|
||||
NiaApp(fakeAppState(maxWidth, maxHeight))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag("NiaBottomBar").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTag("NiaNavRail").assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun mediumWidth_expandedHeight_showsNavigationRail() {
|
||||
composeTestRule.setContent {
|
||||
DeviceConfigurationOverride(
|
||||
DeviceConfigurationOverride.ForcedSize(DpSize(610.dp, 1000.dp)),
|
||||
) {
|
||||
BoxWithConstraints {
|
||||
NiaApp(fakeAppState(maxWidth, maxHeight))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag("NiaNavRail").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTag("NiaBottomBar").assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun expandedWidth_expandedHeight_showsNavigationRail() {
|
||||
composeTestRule.setContent {
|
||||
DeviceConfigurationOverride(
|
||||
DeviceConfigurationOverride.ForcedSize(DpSize(900.dp, 1000.dp)),
|
||||
) {
|
||||
BoxWithConstraints {
|
||||
NiaApp(fakeAppState(maxWidth, maxHeight))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag("NiaNavRail").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTag("NiaBottomBar").assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun fakeAppState(maxWidth: Dp, maxHeight: Dp) = rememberNiaAppState(
|
||||
windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(maxWidth, maxHeight)),
|
||||
networkMonitor = networkMonitor,
|
||||
userNewsResourceRepository = userNewsResourceRepository,
|
||||
timeZoneMonitor = timeZoneMonitor,
|
||||
)
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
/*
|
||||
* 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.ui
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||
import kotlin.properties.ReadOnlyProperty
|
||||
|
||||
fun AndroidComposeTestRule<*, *>.stringResource(
|
||||
@StringRes resId: Int,
|
||||
): ReadOnlyProperty<Any, String> =
|
||||
ReadOnlyProperty { _, _ -> activity.getString(resId) }
|
@ -0,0 +1,67 @@
|
||||
/*
|
||||
* 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.ui
|
||||
|
||||
import android.view.WindowInsets
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.platform.AbstractComposeView
|
||||
import androidx.compose.ui.test.DeviceConfigurationOverride
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.children
|
||||
|
||||
/**
|
||||
* A [DeviceConfigurationOverride] that overrides the window insets for the contained content.
|
||||
*/
|
||||
@Suppress("ktlint:standard:function-naming")
|
||||
fun DeviceConfigurationOverride.Companion.WindowInsets(
|
||||
windowInsets: WindowInsetsCompat,
|
||||
): DeviceConfigurationOverride = DeviceConfigurationOverride { contentUnderTest ->
|
||||
val currentContentUnderTest by rememberUpdatedState(contentUnderTest)
|
||||
val currentWindowInsets by rememberUpdatedState(windowInsets)
|
||||
AndroidView(
|
||||
factory = { context ->
|
||||
object : AbstractComposeView(context) {
|
||||
@Composable
|
||||
override fun Content() {
|
||||
currentContentUnderTest()
|
||||
}
|
||||
|
||||
override fun dispatchApplyWindowInsets(insets: WindowInsets): WindowInsets {
|
||||
children.forEach {
|
||||
it.dispatchApplyWindowInsets(
|
||||
WindowInsets(currentWindowInsets.toWindowInsets()),
|
||||
)
|
||||
}
|
||||
return WindowInsetsCompat.CONSUMED.toWindowInsets()!!
|
||||
}
|
||||
|
||||
/**
|
||||
* Deprecated, but intercept the `requestApplyInsets` call via the deprecated
|
||||
* method.
|
||||
*/
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun requestFitSystemWindows() {
|
||||
dispatchApplyWindowInsets(WindowInsets(currentWindowInsets.toWindowInsets()!!))
|
||||
}
|
||||
}
|
||||
},
|
||||
update = { with(currentWindowInsets) { it.requestApplyInsets() } },
|
||||
)
|
||||
}
|
@ -0,0 +1,204 @@
|
||||
/*
|
||||
* 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.ui
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.assertIsNotDisplayed
|
||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.test.espresso.Espresso
|
||||
import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository
|
||||
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.HiltAndroidRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import dagger.hilt.android.testing.HiltTestApplication
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
import javax.inject.Inject
|
||||
import kotlin.properties.ReadOnlyProperty
|
||||
import kotlin.test.assertTrue
|
||||
import com.google.samples.apps.nowinandroid.feature.topic.R as FeatureTopicR
|
||||
|
||||
private const val EXPANDED_WIDTH = "w1200dp-h840dp"
|
||||
private const val COMPACT_WIDTH = "w412dp-h915dp"
|
||||
|
||||
@HiltAndroidTest
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(application = HiltTestApplication::class)
|
||||
class InterestsListDetailScreenTest {
|
||||
|
||||
@get:Rule(order = 0)
|
||||
val hiltRule = HiltAndroidRule(this)
|
||||
|
||||
@get:Rule(order = 1)
|
||||
val composeTestRule = createAndroidComposeRule<HiltComponentActivity>()
|
||||
|
||||
@Inject
|
||||
lateinit var topicsRepository: TopicsRepository
|
||||
|
||||
/** Convenience function for getting all topics during tests, */
|
||||
private fun getTopics(): List<Topic> = runBlocking {
|
||||
topicsRepository.getTopics().first().sortedBy { it.name }
|
||||
}
|
||||
|
||||
// The strings used for matching in these tests.
|
||||
private val placeholderText by composeTestRule.stringResource(FeatureTopicR.string.feature_topic_select_an_interest)
|
||||
private val listPaneTag = "interests:topics"
|
||||
|
||||
private val Topic.testTag
|
||||
get() = "topic:${this.id}"
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
hiltRule.inject()
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(qualifiers = EXPANDED_WIDTH)
|
||||
fun expandedWidth_initialState_showsTwoPanesWithPlaceholder() {
|
||||
composeTestRule.apply {
|
||||
setContent {
|
||||
NiaTheme {
|
||||
InterestsListDetailScreen()
|
||||
}
|
||||
}
|
||||
|
||||
onNodeWithTag(listPaneTag).assertIsDisplayed()
|
||||
onNodeWithText(placeholderText).assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(qualifiers = COMPACT_WIDTH)
|
||||
fun compactWidth_initialState_showsListPane() {
|
||||
composeTestRule.apply {
|
||||
setContent {
|
||||
NiaTheme {
|
||||
InterestsListDetailScreen()
|
||||
}
|
||||
}
|
||||
|
||||
onNodeWithTag(listPaneTag).assertIsDisplayed()
|
||||
onNodeWithText(placeholderText).assertIsNotDisplayed()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(qualifiers = EXPANDED_WIDTH)
|
||||
fun expandedWidth_topicSelected_updatesDetailPane() {
|
||||
composeTestRule.apply {
|
||||
setContent {
|
||||
NiaTheme {
|
||||
InterestsListDetailScreen()
|
||||
}
|
||||
}
|
||||
|
||||
val firstTopic = getTopics().first()
|
||||
onNodeWithText(firstTopic.name).performClick()
|
||||
|
||||
onNodeWithTag(listPaneTag).assertIsDisplayed()
|
||||
onNodeWithText(placeholderText).assertIsNotDisplayed()
|
||||
onNodeWithTag(firstTopic.testTag).assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(qualifiers = COMPACT_WIDTH)
|
||||
fun compactWidth_topicSelected_showsTopicDetailPane() {
|
||||
composeTestRule.apply {
|
||||
setContent {
|
||||
NiaTheme {
|
||||
InterestsListDetailScreen()
|
||||
}
|
||||
}
|
||||
|
||||
val firstTopic = getTopics().first()
|
||||
onNodeWithText(firstTopic.name).performClick()
|
||||
|
||||
onNodeWithTag(listPaneTag).assertIsNotDisplayed()
|
||||
onNodeWithText(placeholderText).assertIsNotDisplayed()
|
||||
onNodeWithTag(firstTopic.testTag).assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(qualifiers = EXPANDED_WIDTH)
|
||||
fun expandedWidth_backPressFromTopicDetail_leavesInterests() {
|
||||
var unhandledBackPress = false
|
||||
composeTestRule.apply {
|
||||
setContent {
|
||||
NiaTheme {
|
||||
// Back press should not be handled by the two pane layout, and thus
|
||||
// "fall through" to this BackHandler.
|
||||
BackHandler {
|
||||
unhandledBackPress = true
|
||||
}
|
||||
InterestsListDetailScreen()
|
||||
}
|
||||
}
|
||||
|
||||
val firstTopic = getTopics().first()
|
||||
onNodeWithText(firstTopic.name).performClick()
|
||||
|
||||
waitForIdle()
|
||||
Espresso.pressBack()
|
||||
|
||||
assertTrue(unhandledBackPress)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(qualifiers = COMPACT_WIDTH)
|
||||
fun compactWidth_backPressFromTopicDetail_showsListPane() {
|
||||
composeTestRule.apply {
|
||||
setContent {
|
||||
NiaTheme {
|
||||
InterestsListDetailScreen()
|
||||
}
|
||||
}
|
||||
|
||||
val firstTopic = getTopics().first()
|
||||
onNodeWithText(firstTopic.name).performClick()
|
||||
|
||||
waitForIdle()
|
||||
Espresso.pressBack()
|
||||
|
||||
onNodeWithTag(listPaneTag).assertIsDisplayed()
|
||||
onNodeWithText(placeholderText).assertIsNotDisplayed()
|
||||
onNodeWithTag(firstTopic.testTag).assertIsNotDisplayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun AndroidComposeTestRule<*, *>.stringResource(
|
||||
@StringRes resId: Int,
|
||||
): ReadOnlyProperty<Any, String> =
|
||||
ReadOnlyProperty { _, _ -> activity.getString(resId) }
|
@ -0,0 +1,337 @@
|
||||
/*
|
||||
* Copyright 2023 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.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.foundation.layout.windowInsetsBottomHeight
|
||||
import androidx.compose.foundation.layout.windowInsetsEndWidth
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.layout.windowInsetsStartWidth
|
||||
import androidx.compose.foundation.layout.windowInsetsTopHeight
|
||||
import androidx.compose.material3.SnackbarDuration.Indefinite
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.adaptive.Posture
|
||||
import androidx.compose.material3.adaptive.WindowAdaptiveInfo
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toAndroidRect
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.test.DeviceConfigurationOverride
|
||||
import androidx.compose.ui.test.ForcedSize
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpRect
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.roundToIntRect
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.window.core.layout.WindowSizeClass
|
||||
import com.github.takahirom.roborazzi.captureRoboImage
|
||||
import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository
|
||||
import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository
|
||||
import com.google.samples.apps.nowinandroid.core.data.test.repository.FakeUserDataRepository
|
||||
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
|
||||
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.HiltAndroidRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import dagger.hilt.android.testing.HiltTestApplication
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
import org.robolectric.annotation.GraphicsMode
|
||||
import org.robolectric.annotation.LooperMode
|
||||
import java.util.TimeZone
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Tests that the Snackbar is correctly displayed on different screen sizes.
|
||||
*/
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@GraphicsMode(GraphicsMode.Mode.NATIVE)
|
||||
// Configure Robolectric to use a very large screen size that can fit all of the test sizes.
|
||||
// This allows enough room to render the content under test without clipping or scaling.
|
||||
@Config(application = HiltTestApplication::class, qualifiers = "w1000dp-h1000dp-480dpi")
|
||||
@LooperMode(LooperMode.Mode.PAUSED)
|
||||
@HiltAndroidTest
|
||||
class SnackbarInsetsScreenshotTests {
|
||||
|
||||
/**
|
||||
* Manages the components' state and is used to perform injection on your test
|
||||
*/
|
||||
@get:Rule(order = 0)
|
||||
val hiltRule = HiltAndroidRule(this)
|
||||
|
||||
/**
|
||||
* Use a test activity to set the content on.
|
||||
*/
|
||||
@get:Rule(order = 1)
|
||||
val composeTestRule = createAndroidComposeRule<HiltComponentActivity>()
|
||||
|
||||
@Inject
|
||||
lateinit var networkMonitor: NetworkMonitor
|
||||
|
||||
@Inject
|
||||
lateinit var timeZoneMonitor: TimeZoneMonitor
|
||||
|
||||
@Inject
|
||||
lateinit var userDataRepository: FakeUserDataRepository
|
||||
|
||||
@Inject
|
||||
lateinit var topicsRepository: TopicsRepository
|
||||
|
||||
@Inject
|
||||
lateinit var userNewsResourceRepository: UserNewsResourceRepository
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
hiltRule.inject()
|
||||
|
||||
// Configure user data
|
||||
runBlocking {
|
||||
userDataRepository.setShouldHideOnboarding(true)
|
||||
|
||||
userDataRepository.setFollowedTopicIds(
|
||||
setOf(topicsRepository.getTopics().first().first().id),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setTimeZone() {
|
||||
// Make time zone deterministic in tests
|
||||
TimeZone.setDefault(TimeZone.getTimeZone("UTC"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun phone_noSnackbar() {
|
||||
val snackbarHostState = SnackbarHostState()
|
||||
testSnackbarScreenshotWithSize(
|
||||
snackbarHostState,
|
||||
400.dp,
|
||||
500.dp,
|
||||
"insets_snackbar_compact_medium_noSnackbar",
|
||||
action = { },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun snackbarShown_phone() {
|
||||
val snackbarHostState = SnackbarHostState()
|
||||
testSnackbarScreenshotWithSize(
|
||||
snackbarHostState,
|
||||
400.dp,
|
||||
500.dp,
|
||||
"insets_snackbar_compact_medium",
|
||||
) {
|
||||
snackbarHostState.showSnackbar(
|
||||
"This is a test snackbar message",
|
||||
actionLabel = "Action Label",
|
||||
duration = Indefinite,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun snackbarShown_foldable() {
|
||||
val snackbarHostState = SnackbarHostState()
|
||||
testSnackbarScreenshotWithSize(
|
||||
snackbarHostState,
|
||||
600.dp,
|
||||
600.dp,
|
||||
"insets_snackbar_medium_medium",
|
||||
) {
|
||||
snackbarHostState.showSnackbar(
|
||||
"This is a test snackbar message",
|
||||
actionLabel = "Action Label",
|
||||
duration = Indefinite,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun snackbarShown_tablet() {
|
||||
val snackbarHostState = SnackbarHostState()
|
||||
testSnackbarScreenshotWithSize(
|
||||
snackbarHostState,
|
||||
900.dp,
|
||||
900.dp,
|
||||
"insets_snackbar_expanded_expanded",
|
||||
) {
|
||||
snackbarHostState.showSnackbar(
|
||||
"This is a test snackbar message",
|
||||
actionLabel = "Action Label",
|
||||
duration = Indefinite,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun testSnackbarScreenshotWithSize(
|
||||
snackbarHostState: SnackbarHostState,
|
||||
width: Dp,
|
||||
height: Dp,
|
||||
screenshotName: String,
|
||||
action: suspend () -> Unit,
|
||||
) {
|
||||
lateinit var scope: CoroutineScope
|
||||
composeTestRule.setContent {
|
||||
CompositionLocalProvider(
|
||||
// Replaces images with placeholders
|
||||
LocalInspectionMode provides true,
|
||||
) {
|
||||
scope = rememberCoroutineScope()
|
||||
|
||||
DeviceConfigurationOverride(
|
||||
DeviceConfigurationOverride.ForcedSize(DpSize(width, height)),
|
||||
) {
|
||||
DeviceConfigurationOverride(
|
||||
DeviceConfigurationOverride.WindowInsets(
|
||||
WindowInsetsCompat.Builder()
|
||||
.setInsets(
|
||||
WindowInsetsCompat.Type.statusBars(),
|
||||
DpRect(
|
||||
left = 0.dp,
|
||||
top = 64.dp,
|
||||
right = 0.dp,
|
||||
bottom = 0.dp,
|
||||
).toInsets(),
|
||||
)
|
||||
.setInsets(
|
||||
WindowInsetsCompat.Type.navigationBars(),
|
||||
DpRect(
|
||||
left = 64.dp,
|
||||
top = 0.dp,
|
||||
right = 64.dp,
|
||||
bottom = 64.dp,
|
||||
).toInsets(),
|
||||
)
|
||||
.build(),
|
||||
),
|
||||
) {
|
||||
BoxWithConstraints(Modifier.testTag("root")) {
|
||||
NiaTheme {
|
||||
val appState = rememberNiaAppState(
|
||||
networkMonitor = networkMonitor,
|
||||
userNewsResourceRepository = userNewsResourceRepository,
|
||||
timeZoneMonitor = timeZoneMonitor,
|
||||
)
|
||||
NiaApp(
|
||||
appState = appState,
|
||||
snackbarHostState = snackbarHostState,
|
||||
showSettingsDialog = false,
|
||||
onSettingsDismissed = {},
|
||||
onTopAppBarActionClick = {},
|
||||
windowAdaptiveInfo = WindowAdaptiveInfo(
|
||||
windowSizeClass = WindowSizeClass.compute(
|
||||
maxWidth.value,
|
||||
maxHeight.value,
|
||||
),
|
||||
windowPosture = Posture(),
|
||||
),
|
||||
)
|
||||
DebugVisibleWindowInsets()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
action()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag("root")
|
||||
.captureRoboImage(
|
||||
"src/testDemo/screenshots/$screenshotName.png",
|
||||
roborazziOptions = DefaultRoborazziOptions,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DebugVisibleWindowInsets(
|
||||
modifier: Modifier = Modifier,
|
||||
debugColor: Color = Color.Magenta.copy(alpha = 0.5f),
|
||||
) {
|
||||
Box(modifier = modifier.fillMaxSize()) {
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterStart)
|
||||
.fillMaxHeight()
|
||||
.windowInsetsStartWidth(WindowInsets.safeDrawing)
|
||||
.windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Vertical))
|
||||
.background(debugColor),
|
||||
)
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterEnd)
|
||||
.fillMaxHeight()
|
||||
.windowInsetsEndWidth(WindowInsets.safeDrawing)
|
||||
.windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Vertical))
|
||||
.background(debugColor),
|
||||
)
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.fillMaxWidth()
|
||||
.windowInsetsTopHeight(WindowInsets.safeDrawing)
|
||||
.background(debugColor),
|
||||
)
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.fillMaxWidth()
|
||||
.windowInsetsBottomHeight(WindowInsets.safeDrawing)
|
||||
.background(debugColor),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DpRect.toInsets() = toInsets(LocalDensity.current)
|
||||
|
||||
private fun DpRect.toInsets(density: Density) =
|
||||
Insets.of(with(density) { toRect() }.roundToIntRect().toAndroidRect())
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 112 KiB |
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
After Width: | Height: | Size: 76 KiB |
Before Width: | Height: | Size: 102 KiB |
Before Width: | Height: | Size: 260 KiB After Width: | Height: | Size: 251 KiB |
Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 139 KiB |
After Width: | Height: | Size: 33 KiB |
After Width: | Height: | Size: 26 KiB |
After Width: | Height: | Size: 197 KiB |
After Width: | Height: | Size: 78 KiB |
After Width: | Height: | Size: 59 KiB |
Before Width: | Height: | Size: 78 KiB |
Before Width: | Height: | Size: 173 KiB After Width: | Height: | Size: 162 KiB |
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 210 KiB After Width: | Height: | Size: 211 KiB |
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 94 KiB |
@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import androidx.benchmark.macro.ExperimentalMetricApi
|
||||
import androidx.benchmark.macro.StartupTimingMetric
|
||||
import androidx.benchmark.macro.TraceSectionMetric
|
||||
|
||||
/**
|
||||
* Custom Metrics to measure baseline profile effectiveness.
|
||||
*/
|
||||
class BaselineProfileMetrics {
|
||||
companion object {
|
||||
/**
|
||||
* A [TraceSectionMetric] that tracks the time spent in JIT compilation.
|
||||
*
|
||||
* This number should go down when a baseline profile is applied properly.
|
||||
*/
|
||||
@OptIn(ExperimentalMetricApi::class)
|
||||
val jitCompilationMetric = TraceSectionMetric("JIT Compiling %", label = "JIT compilation")
|
||||
|
||||
/**
|
||||
* A [TraceSectionMetric] that tracks the time spent in class initialization.
|
||||
*
|
||||
* This number should go down when a baseline profile is applied properly.
|
||||
*/
|
||||
@OptIn(ExperimentalMetricApi::class)
|
||||
val classInitMetric = TraceSectionMetric("L%/%;", label = "ClassInit")
|
||||
|
||||
/**
|
||||
* Metrics relevant to startup and baseline profile effectiveness measurement.
|
||||
*/
|
||||
@OptIn(ExperimentalMetricApi::class)
|
||||
val allMetrics = listOf(StartupTimingMetric(), jitCompilationMetric, classInitMetric)
|
||||
}
|
||||
}
|
@ -1,38 +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.
|
||||
*/
|
||||
|
||||
import com.google.samples.apps.nowinandroid.libs
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.dependencies
|
||||
|
||||
class AndroidHiltConventionPlugin : Plugin<Project> {
|
||||
override fun apply(target: Project) {
|
||||
with(target) {
|
||||
with(pluginManager) {
|
||||
apply("com.google.devtools.ksp")
|
||||
apply("dagger.hilt.android.plugin")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
"implementation"(libs.findLibrary("hilt.android").get())
|
||||
"ksp"(libs.findLibrary("hilt.compiler").get())
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2023 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.
|
||||
*/
|
||||
|
||||
import com.android.build.gradle.api.AndroidBasePlugin
|
||||
import com.google.samples.apps.nowinandroid.libs
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.dependencies
|
||||
|
||||
class HiltConventionPlugin : Plugin<Project> {
|
||||
override fun apply(target: Project) {
|
||||
with(target) {
|
||||
pluginManager.apply("com.google.devtools.ksp")
|
||||
dependencies {
|
||||
add("ksp", libs.findLibrary("hilt.compiler").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] */
|
||||
pluginManager.withPlugin("com.android.base") {
|
||||
pluginManager.apply("dagger.hilt.android.plugin")
|
||||
dependencies {
|
||||
add("implementation", libs.findLibrary("hilt.android").get())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
<?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 />
|
@ -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))
|
||||
}
|
||||
}
|
@ -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) }
|
||||
}
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |