Merge branch 'main' into introduce_screenshot_a11y_tests

pull/1708/head
Don Turner 2 weeks ago committed by GitHub
commit a258794dc6
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=true
ij_kotlin_allow_trailing_comma_on_call_site=true ij_kotlin_allow_trailing_comma_on_call_site=true
ktlint_function_naming_ignore_when_annotated_with=Composable, Test 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

@ -93,7 +93,9 @@ jobs:
continue-on-error: false continue-on-error: false
if: steps.screenshotsverify.outcome == 'failure' && github.event.pull_request.head.repo.full_name != github.repository if: steps.screenshotsverify.outcome == 'failure' && github.event.pull_request.head.repo.full_name != github.repository
run: | run: |
echo "::error::Screenshot tests failed, please create a PR in your fork first." && exit 1 echo "::error::Screenshot tests failed, please create a PR in your fork first."
echo "Your fork's CI will take screenshots for your fork."
exit 1
# Runs if previous job failed # Runs if previous job failed
- name: Generate new screenshots if verification failed and it's a PR - name: Generate new screenshots if verification failed and it's a PR
@ -166,7 +168,7 @@ jobs:
timeout-minutes: 55 timeout-minutes: 55
strategy: strategy:
matrix: matrix:
api-level: [26, 30] api-level: [26, 34]
steps: steps:
- name: Delete unnecessary tools 🔧 - name: Delete unnecessary tools 🔧
@ -235,7 +237,7 @@ jobs:
- name: Display local test coverage (only API 30) - name: Display local test coverage (only API 30)
if: matrix.api-level == 30 if: matrix.api-level == 30
id: jacoco id: jacoco
uses: madrapps/jacoco-report@v1.7.0 uses: madrapps/jacoco-report@v1.7.1
with: with:
title: Combined test coverage report title: Combined test coverage report
min-coverage-overall: 40 min-coverage-overall: 40

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

@ -89,38 +89,38 @@ androidx.hilt:hilt-navigation:1.2.0
androidx.hilt:hilt-work:1.2.0 androidx.hilt:hilt-work:1.2.0
androidx.interpolator:interpolator:1.0.0 androidx.interpolator:interpolator:1.0.0
androidx.legacy:legacy-support-core-utils:1.0.0 androidx.legacy:legacy-support-core-utils:1.0.0
androidx.lifecycle:lifecycle-common-java8:2.8.6 androidx.lifecycle:lifecycle-common-java8:2.8.7
androidx.lifecycle:lifecycle-common-jvm:2.8.6 androidx.lifecycle:lifecycle-common-jvm:2.8.7
androidx.lifecycle:lifecycle-common:2.8.6 androidx.lifecycle:lifecycle-common:2.8.7
androidx.lifecycle:lifecycle-livedata-core-ktx:2.8.6 androidx.lifecycle:lifecycle-livedata-core-ktx:2.8.7
androidx.lifecycle:lifecycle-livedata-core:2.8.6 androidx.lifecycle:lifecycle-livedata-core:2.8.7
androidx.lifecycle:lifecycle-livedata:2.8.6 androidx.lifecycle:lifecycle-livedata:2.8.7
androidx.lifecycle:lifecycle-process:2.8.6 androidx.lifecycle:lifecycle-process:2.8.7
androidx.lifecycle:lifecycle-runtime-android:2.8.6 androidx.lifecycle:lifecycle-runtime-android:2.8.7
androidx.lifecycle:lifecycle-runtime-compose-android:2.8.6 androidx.lifecycle:lifecycle-runtime-compose-android:2.8.7
androidx.lifecycle:lifecycle-runtime-compose:2.8.6 androidx.lifecycle:lifecycle-runtime-compose:2.8.7
androidx.lifecycle:lifecycle-runtime-ktx-android:2.8.6 androidx.lifecycle:lifecycle-runtime-ktx-android:2.8.7
androidx.lifecycle:lifecycle-runtime-ktx:2.8.6 androidx.lifecycle:lifecycle-runtime-ktx:2.8.7
androidx.lifecycle:lifecycle-runtime:2.8.6 androidx.lifecycle:lifecycle-runtime:2.8.7
androidx.lifecycle:lifecycle-service:2.8.6 androidx.lifecycle:lifecycle-service:2.8.7
androidx.lifecycle:lifecycle-viewmodel-android:2.8.6 androidx.lifecycle:lifecycle-viewmodel-android:2.8.7
androidx.lifecycle:lifecycle-viewmodel-compose-android:2.8.6 androidx.lifecycle:lifecycle-viewmodel-compose-android:2.8.7
androidx.lifecycle:lifecycle-viewmodel-compose:2.8.6 androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7
androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.6 androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7
androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.6 androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.7
androidx.lifecycle:lifecycle-viewmodel:2.8.6 androidx.lifecycle:lifecycle-viewmodel:2.8.7
androidx.loader:loader:1.0.0 androidx.loader:loader:1.0.0
androidx.localbroadcastmanager:localbroadcastmanager:1.0.0 androidx.localbroadcastmanager:localbroadcastmanager:1.0.0
androidx.metrics:metrics-performance:1.0.0-beta01 androidx.metrics:metrics-performance:1.0.0-beta01
androidx.navigation:navigation-common-ktx:2.8.0 androidx.navigation:navigation-common-ktx:2.8.4
androidx.navigation:navigation-common:2.8.0 androidx.navigation:navigation-common:2.8.4
androidx.navigation:navigation-compose:2.8.0 androidx.navigation:navigation-compose:2.8.4
androidx.navigation:navigation-runtime-ktx:2.8.0 androidx.navigation:navigation-runtime-ktx:2.8.4
androidx.navigation:navigation-runtime:2.8.0 androidx.navigation:navigation-runtime:2.8.4
androidx.print:print:1.0.0 androidx.print:print:1.0.0
androidx.privacysandbox.ads:ads-adservices-java:1.0.0-beta05 androidx.privacysandbox.ads:ads-adservices-java:1.0.0-beta05
androidx.privacysandbox.ads:ads-adservices:1.0.0-beta05 androidx.privacysandbox.ads:ads-adservices:1.0.0-beta05
androidx.profileinstaller:profileinstaller:1.3.1 androidx.profileinstaller:profileinstaller:1.4.1
androidx.resourceinspection:resourceinspection-annotation:1.0.1 androidx.resourceinspection:resourceinspection-annotation:1.0.1
androidx.room:room-common:2.6.1 androidx.room:room-common:2.6.1
androidx.room:room-ktx:2.6.1 androidx.room:room-ktx:2.6.1
@ -141,11 +141,11 @@ androidx.window.extensions.core:core:1.0.0
androidx.window:window-core-android:1.3.0 androidx.window:window-core-android:1.3.0
androidx.window:window-core:1.3.0 androidx.window:window-core:1.3.0
androidx.window:window:1.3.0 androidx.window:window:1.3.0
androidx.work:work-runtime-ktx:2.9.0 androidx.work:work-runtime-ktx:2.9.1
androidx.work:work-runtime:2.9.0 androidx.work:work-runtime:2.9.1
com.caverock:androidsvg-aar:1.4 com.caverock:androidsvg-aar:1.4
com.google.accompanist:accompanist-drawablepainter:0.32.0 com.google.accompanist:accompanist-drawablepainter:0.32.0
com.google.accompanist:accompanist-permissions:0.34.0 com.google.accompanist:accompanist-permissions:0.36.0
com.google.android.datatransport:transport-api:3.2.0 com.google.android.datatransport:transport-api:3.2.0
com.google.android.datatransport:transport-backend-cct:3.3.0 com.google.android.datatransport:transport-backend-cct:3.3.0
com.google.android.datatransport:transport-runtime:3.3.0 com.google.android.datatransport:transport-runtime:3.3.0
@ -153,12 +153,12 @@ com.google.android.gms:play-services-ads-identifier:18.0.0
com.google.android.gms:play-services-base:18.5.0 com.google.android.gms:play-services-base:18.5.0
com.google.android.gms:play-services-basement:18.4.0 com.google.android.gms:play-services-basement:18.4.0
com.google.android.gms:play-services-cloud-messaging:17.2.0 com.google.android.gms:play-services-cloud-messaging:17.2.0
com.google.android.gms:play-services-measurement-api:22.1.0 com.google.android.gms:play-services-measurement-api:22.1.2
com.google.android.gms:play-services-measurement-base:22.1.0 com.google.android.gms:play-services-measurement-base:22.1.2
com.google.android.gms:play-services-measurement-impl:22.1.0 com.google.android.gms:play-services-measurement-impl:22.1.2
com.google.android.gms:play-services-measurement-sdk-api:22.1.0 com.google.android.gms:play-services-measurement-sdk-api:22.1.2
com.google.android.gms:play-services-measurement-sdk:22.1.0 com.google.android.gms:play-services-measurement-sdk:22.1.2
com.google.android.gms:play-services-measurement:22.1.0 com.google.android.gms:play-services-measurement:22.1.2
com.google.android.gms:play-services-oss-licenses:17.1.0 com.google.android.gms:play-services-oss-licenses:17.1.0
com.google.android.gms:play-services-stats:17.0.2 com.google.android.gms:play-services-stats:17.0.2
com.google.android.gms:play-services-tasks:18.2.0 com.google.android.gms:play-services-tasks:18.2.0
@ -169,15 +169,15 @@ com.google.dagger:hilt-android:2.52
com.google.dagger:hilt-core:2.52 com.google.dagger:hilt-core:2.52
com.google.errorprone:error_prone_annotations:2.26.0 com.google.errorprone:error_prone_annotations:2.26.0
com.google.firebase:firebase-abt:21.1.1 com.google.firebase:firebase-abt:21.1.1
com.google.firebase:firebase-analytics:22.1.0 com.google.firebase:firebase-analytics:22.1.2
com.google.firebase:firebase-annotations:16.2.0 com.google.firebase:firebase-annotations:16.2.0
com.google.firebase:firebase-bom:33.3.0 com.google.firebase:firebase-bom:33.7.0
com.google.firebase:firebase-common-ktx:21.0.0 com.google.firebase:firebase-common-ktx:21.0.0
com.google.firebase:firebase-common:21.0.0 com.google.firebase:firebase-common:21.0.0
com.google.firebase:firebase-components:18.0.0 com.google.firebase:firebase-components:18.0.0
com.google.firebase:firebase-config-interop:16.0.1 com.google.firebase:firebase-config-interop:16.0.1
com.google.firebase:firebase-config:22.0.0 com.google.firebase:firebase-config:22.0.1
com.google.firebase:firebase-crashlytics:19.1.0 com.google.firebase:firebase-crashlytics:19.3.0
com.google.firebase:firebase-datatransport:19.0.0 com.google.firebase:firebase-datatransport:19.0.0
com.google.firebase:firebase-encoders-json:18.0.1 com.google.firebase:firebase-encoders-json:18.0.1
com.google.firebase:firebase-encoders-proto:16.0.0 com.google.firebase:firebase-encoders-proto:16.0.0
@ -186,16 +186,15 @@ com.google.firebase:firebase-iid-interop:17.1.0
com.google.firebase:firebase-installations-interop:17.2.0 com.google.firebase:firebase-installations-interop:17.2.0
com.google.firebase:firebase-installations:18.0.0 com.google.firebase:firebase-installations:18.0.0
com.google.firebase:firebase-measurement-connector:20.0.1 com.google.firebase:firebase-measurement-connector:20.0.1
com.google.firebase:firebase-messaging:24.0.1 com.google.firebase:firebase-messaging:24.1.0
com.google.firebase:firebase-perf:21.0.1 com.google.firebase:firebase-perf:21.0.3
com.google.firebase:firebase-sessions:2.0.4 com.google.firebase:firebase-sessions:2.0.7
com.google.firebase:protolite-well-known-types:18.0.0
com.google.guava:failureaccess:1.0.1 com.google.guava:failureaccess:1.0.1
com.google.guava:guava:31.1-android com.google.guava:guava:31.1-android
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava
com.google.j2objc:j2objc-annotations:1.3 com.google.j2objc:j2objc-annotations:1.3
com.google.protobuf:protobuf-javalite:4.26.1 com.google.protobuf:protobuf-javalite:4.28.2
com.google.protobuf:protobuf-kotlin-lite:4.26.1 com.google.protobuf:protobuf-kotlin-lite:4.28.2
com.squareup.okhttp3:logging-interceptor:4.12.0 com.squareup.okhttp3:logging-interceptor:4.12.0
com.squareup.okhttp3:okhttp:4.12.0 com.squareup.okhttp3:okhttp:4.12.0
com.squareup.okio:okio-jvm:3.9.0 com.squareup.okio:okio-jvm:3.9.0

@ -37,7 +37,6 @@ import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult.ActionPerformed import androidx.compose.material3.SnackbarResult.ActionPerformed
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.WindowAdaptiveInfo import androidx.compose.material3.adaptive.WindowAdaptiveInfo
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -76,7 +75,6 @@ import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination
import kotlin.reflect.KClass import kotlin.reflect.KClass
import com.google.samples.apps.nowinandroid.feature.settings.R as settingsR import com.google.samples.apps.nowinandroid.feature.settings.R as settingsR
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable @Composable
fun NiaApp( fun NiaApp(
appState: NiaAppState, appState: NiaAppState,
@ -126,7 +124,6 @@ fun NiaApp(
@OptIn( @OptIn(
ExperimentalMaterial3Api::class, ExperimentalMaterial3Api::class,
ExperimentalComposeUiApi::class, ExperimentalComposeUiApi::class,
ExperimentalMaterial3AdaptiveApi::class,
) )
internal fun NiaApp( internal fun NiaApp(
appState: NiaAppState, appState: NiaAppState,

@ -18,6 +18,8 @@ package com.google.samples.apps.nowinandroid.ui
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.navigation.NavController import androidx.navigation.NavController
@ -25,7 +27,6 @@ import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hasRoute import androidx.navigation.NavDestination.Companion.hasRoute
import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import androidx.navigation.navOptions import androidx.navigation.navOptions
import androidx.tracing.trace import androidx.tracing.trace
@ -83,9 +84,21 @@ class NiaAppState(
userNewsResourceRepository: UserNewsResourceRepository, userNewsResourceRepository: UserNewsResourceRepository,
timeZoneMonitor: TimeZoneMonitor, timeZoneMonitor: TimeZoneMonitor,
) { ) {
private val previousDestination = mutableStateOf<NavDestination?>(null)
val currentDestination: NavDestination? val currentDestination: NavDestination?
@Composable get() = navController @Composable get() {
.currentBackStackEntryAsState().value?.destination // Collect the currentBackStackEntryFlow as a state
val currentEntry = navController.currentBackStackEntryFlow
.collectAsState(initial = null)
// Fallback to previousDestination if currentEntry is null
return currentEntry.value?.destination.also { destination ->
if (destination != null) {
previousDestination.value = destination
}
} ?: previousDestination.value
}
val currentTopLevelDestination: TopLevelDestination? val currentTopLevelDestination: TopLevelDestination?
@Composable get() { @Composable get() {

@ -16,7 +16,6 @@
package com.google.samples.apps.nowinandroid.ui package com.google.samples.apps.nowinandroid.ui
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.Posture import androidx.compose.material3.adaptive.Posture
import androidx.compose.material3.adaptive.WindowAdaptiveInfo import androidx.compose.material3.adaptive.WindowAdaptiveInfo
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
@ -113,7 +112,6 @@ class NiaAppScreenSizesScreenshotTests {
TimeZone.setDefault(TimeZone.getTimeZone("UTC")) TimeZone.setDefault(TimeZone.getTimeZone("UTC"))
} }
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
private fun testNiaAppScreenshotWithSize(width: Dp, height: Dp, screenshotName: String) { private fun testNiaAppScreenshotWithSize(width: Dp, height: Dp, screenshotName: String) {
composeTestRule.setContent { composeTestRule.setContent {
CompositionLocalProvider( CompositionLocalProvider(

@ -34,7 +34,6 @@ import androidx.compose.foundation.layout.windowInsetsStartWidth
import androidx.compose.foundation.layout.windowInsetsTopHeight import androidx.compose.foundation.layout.windowInsetsTopHeight
import androidx.compose.material3.SnackbarDuration.Indefinite import androidx.compose.material3.SnackbarDuration.Indefinite
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.Posture import androidx.compose.material3.adaptive.Posture
import androidx.compose.material3.adaptive.WindowAdaptiveInfo import androidx.compose.material3.adaptive.WindowAdaptiveInfo
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -209,7 +208,6 @@ class SnackbarInsetsScreenshotTests {
} }
} }
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
private fun testSnackbarScreenshotWithSize( private fun testSnackbarScreenshotWithSize(
snackbarHostState: SnackbarHostState, snackbarHostState: SnackbarHostState,
width: Dp, width: Dp,

@ -19,7 +19,6 @@ package com.google.samples.apps.nowinandroid.ui
import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.material3.SnackbarDuration.Indefinite import androidx.compose.material3.SnackbarDuration.Indefinite
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.Posture import androidx.compose.material3.adaptive.Posture
import androidx.compose.material3.adaptive.WindowAdaptiveInfo import androidx.compose.material3.adaptive.WindowAdaptiveInfo
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
@ -182,7 +181,6 @@ class SnackbarScreenshotTests {
} }
} }
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
private fun testSnackbarScreenshotWithSize( private fun testSnackbarScreenshotWithSize(
snackbarHostState: SnackbarHostState, snackbarHostState: SnackbarHostState,
width: Dp, width: Dp,

@ -21,6 +21,7 @@ import org.gradle.api.Plugin
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.exclude
class AndroidApplicationFirebaseConventionPlugin : Plugin<Project> { class AndroidApplicationFirebaseConventionPlugin : Plugin<Project> {
override fun apply(target: Project) { override fun apply(target: Project) {
@ -35,7 +36,16 @@ class AndroidApplicationFirebaseConventionPlugin : Plugin<Project> {
val bom = libs.findLibrary("firebase-bom").get() val bom = libs.findLibrary("firebase-bom").get()
add("implementation", platform(bom)) add("implementation", platform(bom))
"implementation"(libs.findLibrary("firebase.analytics").get()) "implementation"(libs.findLibrary("firebase.analytics").get())
"implementation"(libs.findLibrary("firebase.performance").get()) "implementation"(libs.findLibrary("firebase.performance").get()) {
/*
Exclusion of protobuf / protolite dependencies is necessary as the
datastore-proto brings in protobuf dependencies. These are the source of truth
for Now in Android.
That's why the duplicate classes from below dependencies are excluded.
*/
exclude(group = "com.google.protobuf", module = "protobuf-javalite")
exclude(group = "com.google.firebase", module = "protolite-well-known-types")
}
"implementation"(libs.findLibrary("firebase.crashlytics").get()) "implementation"(libs.findLibrary("firebase.crashlytics").get())
} }

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

@ -15,6 +15,7 @@
*/ */
import com.google.samples.apps.nowinandroid.configureKotlinJvm import com.google.samples.apps.nowinandroid.configureKotlinJvm
import com.google.samples.apps.nowinandroid.libs
import org.gradle.api.Plugin import org.gradle.api.Plugin
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.kotlin.dsl.dependencies import org.gradle.kotlin.dsl.dependencies
@ -29,7 +30,7 @@ class JvmLibraryConventionPlugin : Plugin<Project> {
} }
configureKotlinJvm() configureKotlinJvm()
dependencies { dependencies {
add("testImplementation", kotlin("test")) add("testImplementation", libs.findLibrary("kotlin.test").get())
} }
} }
} }

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

@ -90,5 +90,19 @@ private inline fun <reified T : KotlinTopLevelExtension> Project.configureKotlin
// Enable experimental coroutines APIs, including Flow // Enable experimental coroutines APIs, including Flow
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
) )
freeCompilerArgs.add(
/**
* Remove this args after Phase 3.
* https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-consistent-copy-visibility/#deprecation-timeline
*
* Deprecation timeline
* Phase 3. (Supposedly Kotlin 2.2 or Kotlin 2.3).
* The default changes.
* Unless ExposedCopyVisibility is used, the generated 'copy' method has the same visibility as the primary constructor.
* The binary signature changes. The error on the declaration is no longer reported.
* '-Xconsistent-data-class-copy-visibility' compiler flag and ConsistentCopyVisibility annotation are now unnecessary.
*/
"-Xconsistent-data-class-copy-visibility"
)
} }
} }

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

@ -16,7 +16,13 @@
buildscript { buildscript {
repositories { repositories {
google() google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral() mavenCentral()
// Android Build Server // Android Build Server

@ -28,10 +28,8 @@ import androidx.compose.material3.NavigationRail
import androidx.compose.material3.NavigationRailItem import androidx.compose.material3.NavigationRailItem
import androidx.compose.material3.NavigationRailItemDefaults import androidx.compose.material3.NavigationRailItemDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.WindowAdaptiveInfo import androidx.compose.material3.adaptive.WindowAdaptiveInfo
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteDefaults import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteDefaults
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteItemColors import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteItemColors
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold
@ -184,10 +182,6 @@ fun NiaNavigationRail(
* @param windowAdaptiveInfo The window adaptive info. * @param windowAdaptiveInfo The window adaptive info.
* @param content The app content inside the scaffold. * @param content The app content inside the scaffold.
*/ */
@OptIn(
ExperimentalMaterial3AdaptiveNavigationSuiteApi::class,
ExperimentalMaterial3AdaptiveApi::class,
)
@Composable @Composable
fun NiaNavigationSuiteScaffold( fun NiaNavigationSuiteScaffold(
navigationSuiteItems: NiaNavigationSuiteScope.() -> Unit, navigationSuiteItems: NiaNavigationSuiteScope.() -> Unit,
@ -242,7 +236,6 @@ fun NiaNavigationSuiteScaffold(
/** /**
* A wrapper around [NavigationSuiteScope] to declare navigation items. * A wrapper around [NavigationSuiteScope] to declare navigation items.
*/ */
@OptIn(ExperimentalMaterial3AdaptiveNavigationSuiteApi::class)
class NiaNavigationSuiteScope internal constructor( class NiaNavigationSuiteScope internal constructor(
private val navigationSuiteScope: NavigationSuiteScope, private val navigationSuiteScope: NavigationSuiteScope,
private val navigationSuiteItemColors: NavigationSuiteItemColors, private val navigationSuiteItemColors: NavigationSuiteItemColors,

@ -22,7 +22,6 @@ import kotlinx.datetime.Instant
* A [NewsResource] with additional user information such as whether the user is following the * A [NewsResource] with additional user information such as whether the user is following the
* news resource's topics and whether they have saved (bookmarked) this news resource. * news resource's topics and whether they have saved (bookmarked) this news resource.
*/ */
@ConsistentCopyVisibility
data class UserNewsResource internal constructor( data class UserNewsResource internal constructor(
val id: String, val id: String,
val title: String, val title: String,

@ -21,7 +21,6 @@ import android.net.Uri
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.browser.customtabs.CustomTabColorSchemeParams import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsIntent
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridScope import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridScope
@ -45,7 +44,6 @@ import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource
* An extension on [LazyListScope] defining a feed with news resources. * An extension on [LazyListScope] defining a feed with news resources.
* Depending on the [feedState], this might emit no items. * Depending on the [feedState], this might emit no items.
*/ */
@OptIn(ExperimentalFoundationApi::class)
fun LazyStaggeredGridScope.newsFeed( fun LazyStaggeredGridScope.newsFeed(
feedState: NewsFeedUiState, feedState: NewsFeedUiState,
onNewsResourcesCheckedChanged: (String, Boolean) -> Unit, onNewsResourcesCheckedChanged: (String, Boolean) -> Unit,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 370 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 245 KiB

After

Width:  |  Height:  |  Size: 302 KiB

@ -18,7 +18,6 @@ package com.google.samples.apps.nowinandroid.feature.bookmarks
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.test.assertCountEquals import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.assertHasClickAction import androidx.compose.ui.test.assertHasClickAction
import androidx.compose.ui.test.filter import androidx.compose.ui.test.filter
@ -33,6 +32,7 @@ import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollToNode import androidx.compose.ui.test.performScrollToNode
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.testing.TestLifecycleOwner import androidx.lifecycle.testing.TestLifecycleOwner
import com.google.samples.apps.nowinandroid.core.testing.data.userNewsResourcesTestData import com.google.samples.apps.nowinandroid.core.testing.data.userNewsResourcesTestData
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState

@ -29,7 +29,7 @@ dependencies {
implementation(libs.accompanist.permissions) implementation(libs.accompanist.permissions)
implementation(projects.core.data) implementation(projects.core.data)
implementation(projects.core.domain) implementation(projects.core.domain)
implementation(project(":core:notifications")) implementation(projects.core.notifications)
testImplementation(libs.hilt.android.testing) testImplementation(libs.hilt.android.testing)
testImplementation(libs.robolectric) testImplementation(libs.robolectric)

@ -522,6 +522,7 @@ private fun SearchTextField(
.focusRequester(focusRequester) .focusRequester(focusRequester)
.onKeyEvent { .onKeyEvent {
if (it.key == Key.Enter) { if (it.key == Key.Enter) {
if (searchQuery.isBlank()) return@onKeyEvent false
onSearchExplicitlyTriggered() onSearchExplicitlyTriggered()
true true
} else { } else {
@ -536,6 +537,7 @@ private fun SearchTextField(
), ),
keyboardActions = KeyboardActions( keyboardActions = KeyboardActions(
onSearch = { onSearch = {
if (searchQuery.isBlank()) return@KeyboardActions
onSearchExplicitlyTriggered() onSearchExplicitlyTriggered()
}, },
), ),

@ -59,7 +59,7 @@ class SearchViewModel @Inject constructor(
flowOf(SearchResultUiState.SearchNotReady) flowOf(SearchResultUiState.SearchNotReady)
} else { } else {
searchQuery.flatMapLatest { query -> searchQuery.flatMapLatest { query ->
if (query.length < SEARCH_QUERY_MIN_LENGTH) { if (query.trim().length < SEARCH_QUERY_MIN_LENGTH) {
flowOf(SearchResultUiState.EmptyQuery) flowOf(SearchResultUiState.EmptyQuery)
} else { } else {
getSearchContentsUseCase(query) getSearchContentsUseCase(query)
@ -102,6 +102,7 @@ class SearchViewModel @Inject constructor(
* search query in the search text field, defining this method. * search query in the search text field, defining this method.
*/ */
fun onSearchTriggered(query: String) { fun onSearchTriggered(query: String) {
if (query.isBlank()) return
viewModelScope.launch { viewModelScope.launch {
recentSearchRepository.insertOrReplaceRecentSearch(searchQuery = query) recentSearchRepository.insertOrReplaceRecentSearch(searchQuery = query)
} }

@ -21,10 +21,12 @@ import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions import androidx.navigation.NavOptions
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import com.google.samples.apps.nowinandroid.feature.search.SearchRoute import com.google.samples.apps.nowinandroid.feature.search.SearchRoute
import kotlinx.serialization.Serializable
const val SEARCH_ROUTE = "search_route" @Serializable data object SearchRoute
fun NavController.navigateToSearch(navOptions: NavOptions? = null) = navigate(SEARCH_ROUTE, navOptions) fun NavController.navigateToSearch(navOptions: NavOptions? = null) =
navigate(SearchRoute, navOptions)
fun NavGraphBuilder.searchScreen( fun NavGraphBuilder.searchScreen(
onBackClick: () -> Unit, onBackClick: () -> Unit,
@ -33,7 +35,7 @@ fun NavGraphBuilder.searchScreen(
) { ) {
// TODO: Handle back stack for each top-level destination. At the moment each top-level // TODO: Handle back stack for each top-level destination. At the moment each top-level
// destination may have own search screen's back stack. // destination may have own search screen's back stack.
composable(route = SEARCH_ROUTE) { composable<SearchRoute> {
SearchRoute( SearchRoute(
onBackClick = onBackClick, onBackClick = onBackClick,
onInterestsClick = onInterestsClick, onInterestsClick = onInterestsClick,

@ -41,6 +41,7 @@ import org.junit.Rule
import org.junit.Test import org.junit.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertIs import kotlin.test.assertIs
import kotlin.test.assertNull
/** /**
* To learn more about how this test handles Flows created with stateIn, see * To learn more about how this test handles Flows created with stateIn, see
@ -122,6 +123,43 @@ class SearchViewModelTest {
assertEquals(SearchNotReady, viewModel.searchResultUiState.value) assertEquals(SearchNotReady, viewModel.searchResultUiState.value)
} }
@Test
fun emptySearchText_isNotAddedToRecentSearches() = runTest {
viewModel.onSearchTriggered("")
val recentSearchQueriesStream = getRecentQueryUseCase()
val recentSearchQueries = recentSearchQueriesStream.first()
val recentSearchQuery = recentSearchQueries.firstOrNull()
assertNull(recentSearchQuery)
}
@Test
fun searchTextWithThreeSpaces_isEmptyQuery() = runTest {
searchContentsRepository.addNewsResources(newsResourcesTestData)
searchContentsRepository.addTopics(topicsTestData)
val collectJob = launch(UnconfinedTestDispatcher()) { viewModel.searchResultUiState.collect() }
viewModel.onSearchQueryChanged(" ")
assertIs<EmptyQuery>(viewModel.searchResultUiState.value)
collectJob.cancel()
}
@Test
fun searchTextWithThreeSpacesAndOneLetter_isEmptyQuery() = runTest {
searchContentsRepository.addNewsResources(newsResourcesTestData)
searchContentsRepository.addTopics(topicsTestData)
val collectJob = launch(UnconfinedTestDispatcher()) { viewModel.searchResultUiState.collect() }
viewModel.onSearchQueryChanged(" a")
assertIs<EmptyQuery>(viewModel.searchResultUiState.value)
collectJob.cancel()
}
@Test @Test
fun whenToggleNewsResourceSavedIsCalled_bookmarkStateIsUpdated() = runTest { fun whenToggleNewsResourceSavedIsCalled_bookmarkStateIsUpdated() = runTest {
val newsResourceId = "123" val newsResourceId = "123"

@ -27,7 +27,8 @@ then
echo "The 'dot' command is not found. This is required to generate SVGs from the Graphviz files." echo "The 'dot' command is not found. This is required to generate SVGs from the Graphviz files."
echo "Installation instructions:" echo "Installation instructions:"
echo " - On macOS: You can install Graphviz using Homebrew with the command: 'brew install graphviz'" echo " - On macOS: You can install Graphviz using Homebrew with the command: 'brew install graphviz'"
echo " - On Ubuntu: You can install Graphviz using APT with the command: 'sudo apt-get install graphviz'" echo " - On Ubuntu: You can install Graphviz using APT with the command: 'sudo apt install graphviz'"
echo " - Others: Visit https://graphviz.org/download/"
exit 1 exit 1
fi fi

@ -14,10 +14,10 @@
* limitations under the License. * limitations under the License.
*/ */
val ktlintVersion = "1.0.1" val ktlintVersion = "1.4.0"
initscript { initscript {
val spotlessVersion = "6.23.3" val spotlessVersion = "6.25.0"
repositories { repositories {
mavenCentral() mavenCentral()

@ -1,9 +1,9 @@
[versions] [versions]
accompanist = "0.34.0" accompanist = "0.36.0"
androidDesugarJdkLibs = "2.0.4" androidDesugarJdkLibs = "2.1.3"
# AGP and tools should be updated together # AGP and tools should be updated together
androidGradlePlugin = "8.6.1" androidGradlePlugin = "8.6.1"
androidTools = "31.7.2" androidTools = "31.7.3"
androidxActivity = "1.9.3" androidxActivity = "1.9.3"
androidxAppCompat = "1.7.0" androidxAppCompat = "1.7.0"
androidxBrowser = "1.8.0" androidxBrowser = "1.8.0"
@ -14,11 +14,11 @@ androidxCoreSplashscreen = "1.0.1"
androidxDataStore = "1.1.1" androidxDataStore = "1.1.1"
androidxEspresso = "3.6.1" androidxEspresso = "3.6.1"
androidxHiltNavigationCompose = "1.2.0" androidxHiltNavigationCompose = "1.2.0"
androidxLifecycle = "2.8.6" androidxLifecycle = "2.8.7"
androidxMacroBenchmark = "1.3.0" androidxMacroBenchmark = "1.3.3"
androidxMetrics = "1.0.0-beta01" androidxMetrics = "1.0.0-beta01"
androidxNavigation = "2.8.0" androidxNavigation = "2.8.4"
androidxProfileinstaller = "1.3.1" androidxProfileinstaller = "1.4.1"
androidxTestCore = "1.6.1" androidxTestCore = "1.6.1"
androidxTestExt = "1.2.1" androidxTestExt = "1.2.1"
androidxTestRules = "1.6.1" androidxTestRules = "1.6.1"
@ -26,13 +26,13 @@ androidxTestRunner = "1.6.2"
androidxTracing = "1.3.0-alpha02" androidxTracing = "1.3.0-alpha02"
androidxUiAutomator = "2.3.0" androidxUiAutomator = "2.3.0"
androidxWindowManager = "1.3.0" androidxWindowManager = "1.3.0"
androidxWork = "2.9.0" androidxWork = "2.9.1"
coil = "2.7.0" coil = "2.7.0"
dependencyGuard = "0.5.0" dependencyGuard = "0.5.0"
firebaseBom = "33.3.0" firebaseBom = "33.7.0"
firebaseCrashlyticsPlugin = "2.9.9" firebaseCrashlyticsPlugin = "3.0.2"
firebasePerfPlugin = "1.4.2" firebasePerfPlugin = "1.4.2"
gmsPlugin = "4.4.1" gmsPlugin = "4.4.2"
googleOss = "17.1.0" googleOss = "17.1.0"
googleOssPlugin = "0.10.6" googleOssPlugin = "0.10.6"
hilt = "2.52" hilt = "2.52"
@ -46,15 +46,15 @@ kotlinxSerializationJson = "1.6.3"
ksp = "2.0.20-1.0.25" ksp = "2.0.20-1.0.25"
moduleGraph = "2.7.1" moduleGraph = "2.7.1"
okhttp = "4.12.0" okhttp = "4.12.0"
protobuf = "4.26.1" protobuf = "4.28.2"
protobufPlugin = "0.9.4" protobufPlugin = "0.9.4"
retrofit = "2.11.0" retrofit = "2.11.0"
retrofitKotlinxSerializationJson = "1.0.0" retrofitKotlinxSerializationJson = "1.0.0"
robolectric = "4.14.1" robolectric = "4.14.1"
roborazzi = "1.33.0" roborazzi = "1.36.0"
room = "2.6.1" room = "2.6.1"
secrets = "2.0.1" secrets = "2.0.1"
truth = "1.4.2" truth = "1.4.4"
turbine = "1.1.0" turbine = "1.1.0"
[bundles] [bundles]

@ -38,7 +38,7 @@ kotlin {
dependencies { dependencies {
compileOnly(libs.kotlin.stdlib) compileOnly(libs.kotlin.stdlib)
compileOnly(libs.lint.api) compileOnly(libs.lint.api)
testImplementation(libs.kotlin.test)
testImplementation(libs.lint.checks) testImplementation(libs.lint.checks)
testImplementation(libs.lint.tests) testImplementation(libs.lint.tests)
testImplementation(kotlin("test"))
} }

@ -17,7 +17,13 @@
pluginManagement { pluginManagement {
includeBuild("build-logic") includeBuild("build-logic")
repositories { repositories {
google() google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral() mavenCentral()
gradlePluginPortal() gradlePluginPortal()
} }
@ -26,7 +32,13 @@ pluginManagement {
dependencyResolutionManagement { dependencyResolutionManagement {
repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
repositories { repositories {
google() google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral() mavenCentral()
} }
} }

Loading…
Cancel
Save