From 474dc79a8a9b586f2324f2a6fe48baade57dc63f Mon Sep 17 00:00:00 2001 From: Jose Alcerreca Date: Fri, 30 Oct 2020 17:14:09 +0100 Subject: [PATCH 01/11] [Owl] Migrates to navigation-compose --- Owl/app/build.gradle | 1 + .../main/java/com/example/owl/ui/NavGraph.kt | 31 ++++-------- .../main/java/com/example/owl/ui/OwlApp.kt | 34 +++++++------ .../com/example/owl/ui/utils/Navigation.kt | 49 ------------------- .../com/example/owl/buildsrc/Dependencies.kt | 1 + 5 files changed, 30 insertions(+), 86 deletions(-) diff --git a/Owl/app/build.gradle b/Owl/app/build.gradle index 98a6ec6a70..74a09a5aec 100644 --- a/Owl/app/build.gradle +++ b/Owl/app/build.gradle @@ -80,6 +80,7 @@ dependencies { implementation Libs.Kotlin.stdlib implementation Libs.AndroidX.coreKtx + implementation Libs.AndroidX.navigation implementation Libs.AndroidX.Compose.runtime implementation Libs.AndroidX.Compose.foundation diff --git a/Owl/app/src/main/java/com/example/owl/ui/NavGraph.kt b/Owl/app/src/main/java/com/example/owl/ui/NavGraph.kt index 4d2f8842b3..4ca76ee387 100644 --- a/Owl/app/src/main/java/com/example/owl/ui/NavGraph.kt +++ b/Owl/app/src/main/java/com/example/owl/ui/NavGraph.kt @@ -16,37 +16,26 @@ package com.example.owl.ui -import android.os.Parcelable -import androidx.compose.runtime.Immutable -import com.example.owl.ui.utils.Navigator -import kotlinx.android.parcel.Parcelize +import androidx.navigation.NavHostController +import androidx.navigation.compose.navigate -/** - * Models the screens in the app and any arguments they require. - */ -sealed class Destination : Parcelable { - @Parcelize - object Onboarding : Destination() - - @Parcelize - object Courses : Destination() - - @Immutable - @Parcelize - data class Course(val courseId: Long) : Destination() +object Destinations { + const val Onboarding = "Onboarding_dest_key" + const val Courses = "Courses_dest_key" + const val CourseDetails = "Courses_dest_key" } /** * Models the navigation actions in the app. */ -class Actions(navigator: Navigator) { +class Actions(navController: NavHostController) { val onboardingComplete: () -> Unit = { - navigator.navigate(Destination.Courses) + navController.navigate(Destinations.Courses) } val selectCourse: (Long) -> Unit = { courseId: Long -> - navigator.navigate(Destination.Course(courseId)) + navController.navigate(Destinations.CourseDetails + "/$courseId") } val upPress: () -> Unit = { - navigator.back() + navController.popBackStack() } } diff --git a/Owl/app/src/main/java/com/example/owl/ui/OwlApp.kt b/Owl/app/src/main/java/com/example/owl/ui/OwlApp.kt index cc6b3e8a5f..054ab3e66d 100644 --- a/Owl/app/src/main/java/com/example/owl/ui/OwlApp.kt +++ b/Owl/app/src/main/java/com/example/owl/ui/OwlApp.kt @@ -17,38 +17,40 @@ package com.example.owl.ui import androidx.activity.OnBackPressedDispatcher -import androidx.compose.animation.Crossfade import androidx.compose.runtime.Composable import androidx.compose.runtime.Providers import androidx.compose.runtime.remember -import androidx.compose.runtime.savedinstancestate.rememberSavedInstanceState +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController import com.example.owl.ui.course.CourseDetails import com.example.owl.ui.courses.Courses import com.example.owl.ui.onboarding.Onboarding import com.example.owl.ui.utils.AmbientBackDispatcher -import com.example.owl.ui.utils.Navigator import com.example.owl.ui.utils.ProvideDisplayInsets import com.example.owl.ui.utils.ProvideImageLoader @Composable fun OwlApp(backDispatcher: OnBackPressedDispatcher) { - @Suppress("RemoveExplicitTypeArguments") - val navigator: Navigator = rememberSavedInstanceState( - saver = Navigator.saver(backDispatcher) - ) { - Navigator(Destination.Onboarding, backDispatcher) - } - val actions = remember(navigator) { Actions(navigator) } Providers(AmbientBackDispatcher provides backDispatcher) { ProvideDisplayInsets { ProvideImageLoader { - Crossfade(navigator.current) { destination -> - when (destination) { - Destination.Onboarding -> Onboarding(actions.onboardingComplete) - Destination.Courses -> Courses(actions.selectCourse) - is Destination.Course -> CourseDetails( - destination.courseId, + val navController = rememberNavController() + + val actions = remember(navController) { Actions(navController) } + NavHost(navController = navController, startDestination = Destinations.Onboarding) { + composable(Destinations.Onboarding) { + Onboarding(actions.onboardingComplete) + } + composable(Destinations.Courses) { + Courses(actions.selectCourse) + } + composable(Destinations.CourseDetails + "/{courseId}") { backStackEntry -> + val courseId = backStackEntry.arguments?.getLong("courseId") + ?: throw IllegalArgumentException("No course ID provided") + CourseDetails( + courseId, actions.selectCourse, actions.upPress ) diff --git a/Owl/app/src/main/java/com/example/owl/ui/utils/Navigation.kt b/Owl/app/src/main/java/com/example/owl/ui/utils/Navigation.kt index 44234e20a9..0be19dbd9d 100644 --- a/Owl/app/src/main/java/com/example/owl/ui/utils/Navigation.kt +++ b/Owl/app/src/main/java/com/example/owl/ui/utils/Navigation.kt @@ -16,61 +16,12 @@ package com.example.owl.ui.utils -import android.os.Parcelable import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedDispatcher import androidx.compose.runtime.Composable import androidx.compose.runtime.onCommit import androidx.compose.runtime.remember -import androidx.compose.runtime.savedinstancestate.listSaver import androidx.compose.runtime.staticAmbientOf -import androidx.compose.runtime.toMutableStateList - -/** - * A simple navigator which maintains a back stack. - */ -class Navigator private constructor( - initialBackStack: List, - backDispatcher: OnBackPressedDispatcher -) { - constructor( - initial: T, - backDispatcher: OnBackPressedDispatcher - ) : this(listOf(initial), backDispatcher) - - private val backStack = initialBackStack.toMutableStateList() - private val backCallback = object : OnBackPressedCallback(canGoBack()) { - override fun handleOnBackPressed() { - back() - } - }.also { callback -> - backDispatcher.addCallback(callback) - } - val current: T get() = backStack.last() - - fun back() { - backStack.removeAt(backStack.lastIndex) - backCallback.isEnabled = canGoBack() - } - - fun navigate(destination: T) { - backStack += destination - backCallback.isEnabled = canGoBack() - } - - private fun canGoBack(): Boolean = backStack.size > 1 - - companion object { - /** - * Serialize the back stack to save to instance state. - */ - fun saver(backDispatcher: OnBackPressedDispatcher) = - listSaver, T>( - save = { navigator -> navigator.backStack.toList() }, - restore = { backstack -> Navigator(backstack, backDispatcher) } - ) - } -} /** * An effect for handling presses of the device back button. diff --git a/Owl/buildSrc/src/main/java/com/example/owl/buildsrc/Dependencies.kt b/Owl/buildSrc/src/main/java/com/example/owl/buildsrc/Dependencies.kt index bf9d478d07..ce3b4c55d9 100644 --- a/Owl/buildSrc/src/main/java/com/example/owl/buildsrc/Dependencies.kt +++ b/Owl/buildSrc/src/main/java/com/example/owl/buildsrc/Dependencies.kt @@ -45,6 +45,7 @@ object Libs { object AndroidX { const val coreKtx = "androidx.core:core-ktx:1.5.0-alpha04" + const val navigation = "androidx.navigation:navigation-compose:1.0.0-alpha01" object Compose { const val snapshot = "" From eb9c08b7eca63bdeba415f6b436c46278b745b18 Mon Sep 17 00:00:00 2001 From: Jose Alcerreca Date: Tue, 3 Nov 2020 16:23:49 +0100 Subject: [PATCH 02/11] Adressing comments Change-Id: Ie5f550e3ea9eb6c8c3838358ad401794b6242314 --- Owl/app/src/main/java/com/example/owl/ui/NavGraph.kt | 11 +++++++---- Owl/app/src/main/java/com/example/owl/ui/OwlApp.kt | 7 ++++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Owl/app/src/main/java/com/example/owl/ui/NavGraph.kt b/Owl/app/src/main/java/com/example/owl/ui/NavGraph.kt index 4ca76ee387..6a72e6b2c7 100644 --- a/Owl/app/src/main/java/com/example/owl/ui/NavGraph.kt +++ b/Owl/app/src/main/java/com/example/owl/ui/NavGraph.kt @@ -20,9 +20,12 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.navigate object Destinations { - const val Onboarding = "Onboarding_dest_key" - const val Courses = "Courses_dest_key" - const val CourseDetails = "Courses_dest_key" + const val Onboarding = "onboarding" + const val Courses = "courses" + + object CoursesArgs { + const val CourseId = "courseId" + } } /** @@ -33,7 +36,7 @@ class Actions(navController: NavHostController) { navController.navigate(Destinations.Courses) } val selectCourse: (Long) -> Unit = { courseId: Long -> - navController.navigate(Destinations.CourseDetails + "/$courseId") + navController.navigate(Destinations.Courses + "/$courseId") } val upPress: () -> Unit = { navController.popBackStack() diff --git a/Owl/app/src/main/java/com/example/owl/ui/OwlApp.kt b/Owl/app/src/main/java/com/example/owl/ui/OwlApp.kt index 054ab3e66d..0031760532 100644 --- a/Owl/app/src/main/java/com/example/owl/ui/OwlApp.kt +++ b/Owl/app/src/main/java/com/example/owl/ui/OwlApp.kt @@ -46,9 +46,10 @@ fun OwlApp(backDispatcher: OnBackPressedDispatcher) { composable(Destinations.Courses) { Courses(actions.selectCourse) } - composable(Destinations.CourseDetails + "/{courseId}") { backStackEntry -> - val courseId = backStackEntry.arguments?.getLong("courseId") - ?: throw IllegalArgumentException("No course ID provided") + composable(Destinations.Courses + "/{${Destinations.CoursesArgs.CourseId}}") { + backStackEntry -> + val courseId = requireNotNull(backStackEntry.arguments) + .getLong(Destinations.CoursesArgs.CourseId) CourseDetails( courseId, actions.selectCourse, From 88db781642c2ba9f34b6712a44aeacbf1aab79ff Mon Sep 17 00:00:00 2001 From: Jose Alcerreca Date: Tue, 3 Nov 2020 17:17:50 +0100 Subject: [PATCH 03/11] Syntax Change-Id: I1203d3a7f5c58770d6f544ad2606c5b2828cabdf --- Owl/app/src/main/java/com/example/owl/ui/OwlApp.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Owl/app/src/main/java/com/example/owl/ui/OwlApp.kt b/Owl/app/src/main/java/com/example/owl/ui/OwlApp.kt index 0031760532..fe1d26c37b 100644 --- a/Owl/app/src/main/java/com/example/owl/ui/OwlApp.kt +++ b/Owl/app/src/main/java/com/example/owl/ui/OwlApp.kt @@ -46,8 +46,9 @@ fun OwlApp(backDispatcher: OnBackPressedDispatcher) { composable(Destinations.Courses) { Courses(actions.selectCourse) } - composable(Destinations.Courses + "/{${Destinations.CoursesArgs.CourseId}}") { - backStackEntry -> + composable( + Destinations.Courses + "/{${Destinations.CoursesArgs.CourseId}}" + ) { backStackEntry -> val courseId = requireNotNull(backStackEntry.arguments) .getLong(Destinations.CoursesArgs.CourseId) CourseDetails( From 67b00c8657bb745d5a49d11850204fd2324a38b5 Mon Sep 17 00:00:00 2001 From: Jose Alcerreca Date: Thu, 5 Nov 2020 14:49:49 +0100 Subject: [PATCH 04/11] Navigation cleanup, safe arguments Change-Id: Id41b29bc4174a694d1c1abee9fd37819912b54e8 --- Owl/app/build.gradle | 8 +++++ .../main/java/com/example/owl/ui/NavGraph.kt | 33 ++++++++++++++----- .../main/java/com/example/owl/ui/OwlApp.kt | 27 ++++++++------- .../example/owl/ui/course/CourseDetails.kt | 13 +++++++- 4 files changed, 59 insertions(+), 22 deletions(-) diff --git a/Owl/app/build.gradle b/Owl/app/build.gradle index 74a09a5aec..71d633572a 100644 --- a/Owl/app/build.gradle +++ b/Owl/app/build.gradle @@ -92,4 +92,12 @@ dependencies { implementation Libs.AndroidX.Compose.tooling implementation Libs.Accompanist.coil + + androidTestImplementation Libs.junit + androidTestImplementation Libs.AndroidX.Test.core + androidTestImplementation Libs.AndroidX.Test.espressoCore + androidTestImplementation Libs.AndroidX.Test.rules + androidTestImplementation Libs.AndroidX.Test.Ext.junit + androidTestImplementation Libs.AndroidX.Compose.uiTest + } diff --git a/Owl/app/src/main/java/com/example/owl/ui/NavGraph.kt b/Owl/app/src/main/java/com/example/owl/ui/NavGraph.kt index 6a72e6b2c7..ce49e203fd 100644 --- a/Owl/app/src/main/java/com/example/owl/ui/NavGraph.kt +++ b/Owl/app/src/main/java/com/example/owl/ui/NavGraph.kt @@ -16,27 +16,42 @@ package com.example.owl.ui +import android.os.Bundle import androidx.navigation.NavHostController +import androidx.navigation.NavType +import androidx.navigation.compose.navArgument import androidx.navigation.compose.navigate -object Destinations { - const val Onboarding = "onboarding" - const val Courses = "courses" +open class Destination(open val route: String) +open class DestinationSingleArg( + private val rootRoute: String, + private val argName: String, + argType: NavType<*> +) { + val args = listOf(navArgument(argName) { type = argType }) + val route = "$rootRoute/{$argName}" + fun getRouteWithArg(courseId: Long) = "$rootRoute/$courseId" + fun getArgFromBundle(args: Bundle) = args.getLong(argName) +} - object CoursesArgs { - const val CourseId = "courseId" - } +/** + * Destinations used in the main screen ([OwlApp]). + */ +object MainDestinations { + val Onboarding = Destination("onboarding") + val Courses = Destination("courses") + val CourseDetail = DestinationSingleArg("courses", "courseId", NavType.LongType) } /** * Models the navigation actions in the app. */ -class Actions(navController: NavHostController) { +class MainActions(navController: NavHostController) { val onboardingComplete: () -> Unit = { - navController.navigate(Destinations.Courses) + navController.navigate(MainDestinations.Courses.route) } val selectCourse: (Long) -> Unit = { courseId: Long -> - navController.navigate(Destinations.Courses + "/$courseId") + navController.navigate(MainDestinations.CourseDetail.getRouteWithArg(courseId)) } val upPress: () -> Unit = { navController.popBackStack() diff --git a/Owl/app/src/main/java/com/example/owl/ui/OwlApp.kt b/Owl/app/src/main/java/com/example/owl/ui/OwlApp.kt index fe1d26c37b..930fbfa0ea 100644 --- a/Owl/app/src/main/java/com/example/owl/ui/OwlApp.kt +++ b/Owl/app/src/main/java/com/example/owl/ui/OwlApp.kt @@ -38,23 +38,26 @@ fun OwlApp(backDispatcher: OnBackPressedDispatcher) { ProvideImageLoader { val navController = rememberNavController() - val actions = remember(navController) { Actions(navController) } - NavHost(navController = navController, startDestination = Destinations.Onboarding) { - composable(Destinations.Onboarding) { - Onboarding(actions.onboardingComplete) + val actions = remember(navController) { MainActions(navController) } + NavHost( + navController = navController, + startDestination = MainDestinations.Onboarding.route + ) { + composable(MainDestinations.Onboarding.route) { + Onboarding(onboardingComplete = actions.onboardingComplete) } - composable(Destinations.Courses) { - Courses(actions.selectCourse) + composable(MainDestinations.Courses.route) { + Courses(selectCourse = actions.selectCourse) } composable( - Destinations.Courses + "/{${Destinations.CoursesArgs.CourseId}}" + MainDestinations.CourseDetail.route, + arguments = MainDestinations.CourseDetail.args ) { backStackEntry -> - val courseId = requireNotNull(backStackEntry.arguments) - .getLong(Destinations.CoursesArgs.CourseId) + val arguments = requireNotNull(backStackEntry.arguments) CourseDetails( - courseId, - actions.selectCourse, - actions.upPress + courseId = MainDestinations.CourseDetail.getArgFromBundle(arguments), + selectCourse = actions.selectCourse, + upPress = actions.upPress ) } } diff --git a/Owl/app/src/main/java/com/example/owl/ui/course/CourseDetails.kt b/Owl/app/src/main/java/com/example/owl/ui/course/CourseDetails.kt index 1046aa011d..8043826832 100644 --- a/Owl/app/src/main/java/com/example/owl/ui/course/CourseDetails.kt +++ b/Owl/app/src/main/java/com/example/owl/ui/course/CourseDetails.kt @@ -93,14 +93,25 @@ import com.example.owl.ui.utils.toPaddingValues private val FabSize = 56.dp private const val ExpandedSheetAlpha = 0.96f -@OptIn(ExperimentalMaterialApi::class) @Composable fun CourseDetails( courseId: Long, selectCourse: (Long) -> Unit, upPress: () -> Unit ) { + // Simplified for the sample val course = remember(courseId) { CourseRepo.getCourse(courseId) } + // TODO: Show error if course not found. + CourseDetails(course, selectCourse, upPress) +} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun CourseDetails( + course: Course, + selectCourse: (Long) -> Unit, + upPress: () -> Unit +) { PinkTheme { WithConstraints { val sheetState = rememberSwipeableState(SheetState.Closed) From 2cb58c28ca4a078601b04b2c576bf980967997d6 Mon Sep 17 00:00:00 2001 From: Jose Alcerreca Date: Thu, 5 Nov 2020 18:33:12 +0100 Subject: [PATCH 05/11] [Owl] Initial nav tests Change-Id: I6fb4a08ee99b96438dc28d25b91abd32f9e0dbc6 --- .../java/com/example/owl/ui/NavigationTest.kt | 112 ++++++++++++++++++ .../owl/ui/fakes/ProvideTestImageLoader.kt | 78 ++++++++++++ Owl/app/src/main/AndroidManifest.xml | 1 + .../main/java/com/example/owl/ui/NavGraph.kt | 37 ++++++ .../main/java/com/example/owl/ui/OwlApp.kt | 33 +----- .../example/owl/ui/courses/FeaturedCourses.kt | 7 +- .../example/owl/ui/onboarding/Onboarding.kt | 9 +- .../com/example/owl/ui/utils/NetworkImage.kt | 2 +- Owl/app/src/main/res/values/strings.xml | 1 + .../com/example/owl/buildsrc/Dependencies.kt | 9 +- 10 files changed, 248 insertions(+), 41 deletions(-) create mode 100644 Owl/app/src/androidTest/java/com/example/owl/ui/NavigationTest.kt create mode 100644 Owl/app/src/androidTest/java/com/example/owl/ui/fakes/ProvideTestImageLoader.kt diff --git a/Owl/app/src/androidTest/java/com/example/owl/ui/NavigationTest.kt b/Owl/app/src/androidTest/java/com/example/owl/ui/NavigationTest.kt new file mode 100644 index 0000000000..9d5025fa56 --- /dev/null +++ b/Owl/app/src/androidTest/java/com/example/owl/ui/NavigationTest.kt @@ -0,0 +1,112 @@ +/* + * Copyright 2020 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.example.owl.ui + +import androidx.activity.ComponentActivity +import androidx.compose.runtime.Providers +import androidx.test.platform.app.InstrumentationRegistry +import androidx.ui.test.createAndroidComposeRule +import androidx.ui.test.onNodeWithLabel +import androidx.ui.test.onNodeWithSubstring +import androidx.ui.test.performClick +import com.example.owl.R +import com.example.owl.model.courses +import com.example.owl.ui.fakes.ProvideTestImageLoader +import com.example.owl.ui.utils.AmbientBackDispatcher +import org.junit.Rule +import org.junit.Test + +/** + * Checks that the navigation flows in the app are correct. + */ +class NavigationTest { + + @get:Rule + val composeTestRule = createAndroidComposeRule() + lateinit var activity: ComponentActivity + + private fun startActivity(startDestination: String? = null) { + composeTestRule.activityRule.scenario.onActivity { + activity = it + } + composeTestRule.setContent { + Providers(AmbientBackDispatcher provides activity.onBackPressedDispatcher) { + ProvideTestImageLoader { + if (startDestination == null) { + NavGraph() + } else { + NavGraph(startDestination) + } + } + } + } + } + + @Test + fun firstScreenIsOnboarding() { + // When the app is open + startActivity() + // The first screen should be the onboarding screen. + // Assert that the FAB label for the onboarding screen exists: + composeTestRule.onNodeWithLabel(getOnboardingFabLabel()).assertExists() + } + + @Test + fun onboardingToCourses() { + // Given the app in the onboarding screen + startActivity() + + // Navigate to the next screen by clicking on the FAB + val fabLabel = getOnboardingFabLabel() + composeTestRule.onNodeWithLabel(fabLabel).performClick() + + // The first course should be shown + composeTestRule.onNodeWithSubstring(courses.first().name).assertExists() + } + + @Test + fun coursesToDetail() { + // Given the app in the courses screen + startActivity(MainDestinations.Courses.route) + + // Navigate to the first course + composeTestRule.onNodeWithSubstring(courses.first().name).performClick() + + // Assert navigated to the course details + composeTestRule.onNodeWithSubstring(getCourseDesc().take(15)).assertExists() + } + + @Test + fun coursesToDetailAndBack() { + coursesToDetail() + composeTestRule.runOnUiThread { + activity.onBackPressed() + } + + // The first course should be shown + composeTestRule.onNodeWithSubstring(courses.first().name).assertExists() + } + + private fun getOnboardingFabLabel(): String { + return InstrumentationRegistry.getInstrumentation().targetContext.resources + .getString(R.string.continue_to_courses) + } + private fun getCourseDesc(): String { + return InstrumentationRegistry.getInstrumentation().targetContext.resources + .getString(R.string.course_desc) + } +} diff --git a/Owl/app/src/androidTest/java/com/example/owl/ui/fakes/ProvideTestImageLoader.kt b/Owl/app/src/androidTest/java/com/example/owl/ui/fakes/ProvideTestImageLoader.kt new file mode 100644 index 0000000000..b062e0b818 --- /dev/null +++ b/Owl/app/src/androidTest/java/com/example/owl/ui/fakes/ProvideTestImageLoader.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2020 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.example.owl.ui.fakes + +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Providers +import coil.ImageLoader +import coil.annotation.ExperimentalCoilApi +import coil.bitmap.BitmapPool +import coil.decode.DataSource +import coil.memory.MemoryCache +import coil.request.DefaultRequestOptions +import coil.request.Disposable +import coil.request.ImageRequest +import coil.request.ImageResult +import coil.request.SuccessResult +import com.example.owl.ui.utils.AmbientImageLoader + +@OptIn(ExperimentalCoilApi::class) +@Composable +fun ProvideTestImageLoader(content: @Composable () -> Unit) { + + // From https://coil-kt.github.io/coil/image_loaders/ + val loader = object : ImageLoader { + private val drawable = ColorDrawable(Color.BLACK) + + private val disposable = object : Disposable { + override val isDisposed get() = true + override fun dispose() {} + override suspend fun await() {} + } + + override val bitmapPool: BitmapPool = BitmapPool(0) + + override val defaults: DefaultRequestOptions = DefaultRequestOptions() + override val memoryCache: MemoryCache + get() = TODO("Not yet implemented") + + override fun enqueue(request: ImageRequest): Disposable { + // Always call onStart before onSuccess. + request.target?.onStart(drawable) + request.target?.onSuccess(drawable) + return disposable + } + + override suspend fun execute(request: ImageRequest): ImageResult { + return SuccessResult( + drawable = drawable, + request = request, + metadata = ImageResult.Metadata( + memoryCacheKey = MemoryCache.Key(""), + isSampled = false, + dataSource = DataSource.MEMORY_CACHE, + isPlaceholderMemoryCacheKeyPresent = false + ) + ) + } + + override fun shutdown() { } + } + Providers(AmbientImageLoader provides loader, children = content) +} diff --git a/Owl/app/src/main/AndroidManifest.xml b/Owl/app/src/main/AndroidManifest.xml index 855b3b5422..ebb670f495 100644 --- a/Owl/app/src/main/AndroidManifest.xml +++ b/Owl/app/src/main/AndroidManifest.xml @@ -32,6 +32,7 @@ + diff --git a/Owl/app/src/main/java/com/example/owl/ui/NavGraph.kt b/Owl/app/src/main/java/com/example/owl/ui/NavGraph.kt index ce49e203fd..284fbc9670 100644 --- a/Owl/app/src/main/java/com/example/owl/ui/NavGraph.kt +++ b/Owl/app/src/main/java/com/example/owl/ui/NavGraph.kt @@ -17,10 +17,18 @@ package com.example.owl.ui import android.os.Bundle +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.navigation.NavHostController import androidx.navigation.NavType +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable import androidx.navigation.compose.navArgument import androidx.navigation.compose.navigate +import androidx.navigation.compose.rememberNavController +import com.example.owl.ui.course.CourseDetails +import com.example.owl.ui.courses.Courses +import com.example.owl.ui.onboarding.Onboarding open class Destination(open val route: String) open class DestinationSingleArg( @@ -57,3 +65,32 @@ class MainActions(navController: NavHostController) { navController.popBackStack() } } + +@Composable +fun NavGraph(startDestination: String = MainDestinations.Onboarding.route) { + val navController = rememberNavController() + + val actions = remember(navController) { MainActions(navController) } + NavHost( + navController = navController, + startDestination = startDestination + ) { + composable(MainDestinations.Onboarding.route) { + Onboarding(onboardingComplete = actions.onboardingComplete) + } + composable(MainDestinations.Courses.route) { + Courses(selectCourse = actions.selectCourse) + } + composable( + MainDestinations.CourseDetail.route, + arguments = MainDestinations.CourseDetail.args + ) { backStackEntry -> + val arguments = requireNotNull(backStackEntry.arguments) + CourseDetails( + courseId = MainDestinations.CourseDetail.getArgFromBundle(arguments), + selectCourse = actions.selectCourse, + upPress = actions.upPress + ) + } + } +} diff --git a/Owl/app/src/main/java/com/example/owl/ui/OwlApp.kt b/Owl/app/src/main/java/com/example/owl/ui/OwlApp.kt index 930fbfa0ea..7e26d5a53b 100644 --- a/Owl/app/src/main/java/com/example/owl/ui/OwlApp.kt +++ b/Owl/app/src/main/java/com/example/owl/ui/OwlApp.kt @@ -19,13 +19,6 @@ package com.example.owl.ui import androidx.activity.OnBackPressedDispatcher import androidx.compose.runtime.Composable import androidx.compose.runtime.Providers -import androidx.compose.runtime.remember -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController -import com.example.owl.ui.course.CourseDetails -import com.example.owl.ui.courses.Courses -import com.example.owl.ui.onboarding.Onboarding import com.example.owl.ui.utils.AmbientBackDispatcher import com.example.owl.ui.utils.ProvideDisplayInsets import com.example.owl.ui.utils.ProvideImageLoader @@ -36,31 +29,7 @@ fun OwlApp(backDispatcher: OnBackPressedDispatcher) { Providers(AmbientBackDispatcher provides backDispatcher) { ProvideDisplayInsets { ProvideImageLoader { - val navController = rememberNavController() - - val actions = remember(navController) { MainActions(navController) } - NavHost( - navController = navController, - startDestination = MainDestinations.Onboarding.route - ) { - composable(MainDestinations.Onboarding.route) { - Onboarding(onboardingComplete = actions.onboardingComplete) - } - composable(MainDestinations.Courses.route) { - Courses(selectCourse = actions.selectCourse) - } - composable( - MainDestinations.CourseDetail.route, - arguments = MainDestinations.CourseDetail.args - ) { backStackEntry -> - val arguments = requireNotNull(backStackEntry.arguments) - CourseDetails( - courseId = MainDestinations.CourseDetail.getArgFromBundle(arguments), - selectCourse = actions.selectCourse, - upPress = actions.upPress - ) - } - } + NavGraph() } } } diff --git a/Owl/app/src/main/java/com/example/owl/ui/courses/FeaturedCourses.kt b/Owl/app/src/main/java/com/example/owl/ui/courses/FeaturedCourses.kt index 215797d4cc..bf23531861 100644 --- a/Owl/app/src/main/java/com/example/owl/ui/courses/FeaturedCourses.kt +++ b/Owl/app/src/main/java/com/example/owl/ui/courses/FeaturedCourses.kt @@ -79,9 +79,10 @@ fun FeaturedCourse( shape = MaterialTheme.shapes.medium ) { ConstraintLayout( - modifier = Modifier.clickable( - onClick = { selectCourse(course.id) } - ) + modifier = Modifier + .clickable( + onClick = { selectCourse(course.id) } + ) ) { val (image, avatar, subject, name, steps, icon) = createRefs() NetworkImage( diff --git a/Owl/app/src/main/java/com/example/owl/ui/onboarding/Onboarding.kt b/Owl/app/src/main/java/com/example/owl/ui/onboarding/Onboarding.kt index f1e7925754..10da1073b4 100644 --- a/Owl/app/src/main/java/com/example/owl/ui/onboarding/Onboarding.kt +++ b/Owl/app/src/main/java/com/example/owl/ui/onboarding/Onboarding.kt @@ -59,6 +59,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.drawLayer import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.semantics.accessibilityLabel +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.ui.tooling.preview.Preview @@ -80,9 +82,14 @@ fun Onboarding(onboardingComplete: () -> Unit) { topBar = { AppBar() }, backgroundColor = MaterialTheme.colors.primarySurface, floatingActionButton = { + val fabLabel = stringResource(id = R.string.continue_to_courses) FloatingActionButton( onClick = onboardingComplete, - modifier = Modifier.navigationBarsPadding() + modifier = Modifier + .navigationBarsPadding() + .semantics { + accessibilityLabel = fabLabel + } ) { Icon(Icons.Rounded.Explore) } diff --git a/Owl/app/src/main/java/com/example/owl/ui/utils/NetworkImage.kt b/Owl/app/src/main/java/com/example/owl/ui/utils/NetworkImage.kt index 5d9ee0ef2e..2e30e8d4f6 100644 --- a/Owl/app/src/main/java/com/example/owl/ui/utils/NetworkImage.kt +++ b/Owl/app/src/main/java/com/example/owl/ui/utils/NetworkImage.kt @@ -64,7 +64,7 @@ fun NetworkImage( ) } -private val AmbientImageLoader = staticAmbientOf { +val AmbientImageLoader = staticAmbientOf { error("No loader provided") } diff --git a/Owl/app/src/main/res/values/strings.xml b/Owl/app/src/main/res/values/strings.xml index 93d6521be8..7d50fece2f 100644 --- a/Owl/app/src/main/res/values/strings.xml +++ b/Owl/app/src/main/res/values/strings.xml @@ -17,6 +17,7 @@ Choose topics that interest you + Continue to courses My Courses diff --git a/Owl/buildSrc/src/main/java/com/example/owl/buildsrc/Dependencies.kt b/Owl/buildSrc/src/main/java/com/example/owl/buildsrc/Dependencies.kt index ce3b4c55d9..7f99140b3d 100644 --- a/Owl/buildSrc/src/main/java/com/example/owl/buildsrc/Dependencies.kt +++ b/Owl/buildSrc/src/main/java/com/example/owl/buildsrc/Dependencies.kt @@ -51,14 +51,15 @@ object Libs { const val snapshot = "" const val version = "1.0.0-alpha06" - const val runtime = "androidx.compose.runtime:runtime:$version" + const val animation = "androidx.compose.animation:animation:$version" const val foundation = "androidx.compose.foundation:foundation:$version" const val layout = "androidx.compose.foundation:foundation-layout:$version" - const val ui = "androidx.compose.ui:ui:$version" + const val iconsExtended = "androidx.compose.material:material-icons-extended:$version" const val material = "androidx.compose.material:material:$version" - const val animation = "androidx.compose.animation:animation:$version" + const val runtime = "androidx.compose.runtime:runtime:$version" const val tooling = "androidx.ui:ui-tooling:$version" - const val iconsExtended = "androidx.compose.material:material-icons-extended:$version" + const val ui = "androidx.compose.ui:ui:$version" + const val uiTest = "androidx.ui:ui-test:$version" } object Test { From 73657aa249ea391d1a0c7de8ba542c0c17df3f36 Mon Sep 17 00:00:00 2001 From: Jose Alcerreca Date: Fri, 6 Nov 2020 13:47:59 +0100 Subject: [PATCH 06/11] Test cleanup Change-Id: I9c77838e006c8d6e45b62117daa7332d34754a4a --- .../java/com/example/owl/ui/NavigationTest.kt | 15 ++++++++++++++- .../com/example/owl/ui/courses/FeaturedCourses.kt | 8 ++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/Owl/app/src/androidTest/java/com/example/owl/ui/NavigationTest.kt b/Owl/app/src/androidTest/java/com/example/owl/ui/NavigationTest.kt index 9d5025fa56..76e3554045 100644 --- a/Owl/app/src/androidTest/java/com/example/owl/ui/NavigationTest.kt +++ b/Owl/app/src/androidTest/java/com/example/owl/ui/NavigationTest.kt @@ -20,9 +20,13 @@ import androidx.activity.ComponentActivity import androidx.compose.runtime.Providers import androidx.test.platform.app.InstrumentationRegistry import androidx.ui.test.createAndroidComposeRule +import androidx.ui.test.hasLabel +import androidx.ui.test.hasSubstring import androidx.ui.test.onNodeWithLabel import androidx.ui.test.onNodeWithSubstring +import androidx.ui.test.onRoot import androidx.ui.test.performClick +import androidx.ui.test.printToLog import com.example.owl.R import com.example.owl.model.courses import com.example.owl.ui.fakes.ProvideTestImageLoader @@ -83,8 +87,11 @@ class NavigationTest { // Given the app in the courses screen startActivity(MainDestinations.Courses.route) + composeTestRule.onRoot().printToLog("jalc") // Navigate to the first course - composeTestRule.onNodeWithSubstring(courses.first().name).performClick() + composeTestRule.onNode( + hasLabel(getFeaturedCourseLabel()).and(hasSubstring(courses.first().name)) + ).performClick() // Assert navigated to the course details composeTestRule.onNodeWithSubstring(getCourseDesc().take(15)).assertExists() @@ -105,6 +112,12 @@ class NavigationTest { return InstrumentationRegistry.getInstrumentation().targetContext.resources .getString(R.string.continue_to_courses) } + + private fun getFeaturedCourseLabel(): String { + return InstrumentationRegistry.getInstrumentation().targetContext.resources + .getString(R.string.featured) + } + private fun getCourseDesc(): String { return InstrumentationRegistry.getInstrumentation().targetContext.resources .getString(R.string.course_desc) diff --git a/Owl/app/src/main/java/com/example/owl/ui/courses/FeaturedCourses.kt b/Owl/app/src/main/java/com/example/owl/ui/courses/FeaturedCourses.kt index bf23531861..2322b816ba 100644 --- a/Owl/app/src/main/java/com/example/owl/ui/courses/FeaturedCourses.kt +++ b/Owl/app/src/main/java/com/example/owl/ui/courses/FeaturedCourses.kt @@ -33,10 +33,14 @@ import androidx.compose.material.icons.rounded.OndemandVideo import androidx.compose.runtime.Composable import androidx.compose.ui.Layout import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.accessibilityLabel +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.ui.tooling.preview.Preview +import com.example.owl.R import com.example.owl.model.Course import com.example.owl.model.courses import com.example.owl.ui.common.OutlinedAvatar @@ -78,11 +82,15 @@ fun FeaturedCourse( elevation = OwlTheme.elevations.card, shape = MaterialTheme.shapes.medium ) { + val featuredString = stringResource(id = R.string.featured) ConstraintLayout( modifier = Modifier .clickable( onClick = { selectCourse(course.id) } ) + .semantics { + accessibilityLabel = featuredString + } ) { val (image, avatar, subject, name, steps, icon) = createRefs() NetworkImage( From 97ed75862cda0bc97283ffc7f714efa99a1b6c2c Mon Sep 17 00:00:00 2001 From: Jose Alcerreca Date: Fri, 20 Nov 2020 15:22:12 +0100 Subject: [PATCH 07/11] Reverts navigation abstraction and fixes tests Change-Id: I632f72c50f8ecf19f497fa126ec73ce1a1b64e94 --- Owl/app/build.gradle | 6 ++ .../java/com/example/owl/ui/NavigationTest.kt | 31 ++++----- .../owl/ui/fakes/ProvideTestImageLoader.kt | 5 +- .../main/java/com/example/owl/ui/NavGraph.kt | 63 ++++++++----------- .../main/java/com/example/owl/ui/OwlApp.kt | 4 +- .../example/owl/ui/courses/FeaturedCourses.kt | 2 +- .../com/example/owl/buildsrc/Dependencies.kt | 2 +- 7 files changed, 57 insertions(+), 56 deletions(-) diff --git a/Owl/app/build.gradle b/Owl/app/build.gradle index 3457a8b77f..410d01c3fa 100644 --- a/Owl/app/build.gradle +++ b/Owl/app/build.gradle @@ -74,6 +74,12 @@ android { kotlinCompilerVersion Libs.Kotlin.version kotlinCompilerExtensionVersion Libs.AndroidX.Compose.version } + + packagingOptions { + exclude "META-INF/licenses/**" + exclude "META-INF/AL2.0" + exclude "META-INF/LGPL2.1" + } } dependencies { diff --git a/Owl/app/src/androidTest/java/com/example/owl/ui/NavigationTest.kt b/Owl/app/src/androidTest/java/com/example/owl/ui/NavigationTest.kt index 76e3554045..b0b7134fb1 100644 --- a/Owl/app/src/androidTest/java/com/example/owl/ui/NavigationTest.kt +++ b/Owl/app/src/androidTest/java/com/example/owl/ui/NavigationTest.kt @@ -18,19 +18,20 @@ package com.example.owl.ui import androidx.activity.ComponentActivity import androidx.compose.runtime.Providers +import androidx.compose.ui.test.hasLabel +import androidx.compose.ui.test.hasSubstring +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithLabel +import androidx.compose.ui.test.onNodeWithSubstring +import androidx.compose.ui.test.onRoot +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.printToLog import androidx.test.platform.app.InstrumentationRegistry -import androidx.ui.test.createAndroidComposeRule -import androidx.ui.test.hasLabel -import androidx.ui.test.hasSubstring -import androidx.ui.test.onNodeWithLabel -import androidx.ui.test.onNodeWithSubstring -import androidx.ui.test.onRoot -import androidx.ui.test.performClick -import androidx.ui.test.printToLog import com.example.owl.R import com.example.owl.model.courses import com.example.owl.ui.fakes.ProvideTestImageLoader import com.example.owl.ui.utils.AmbientBackDispatcher +import dev.chrisbanes.accompanist.insets.ProvideWindowInsets import org.junit.Rule import org.junit.Test @@ -49,11 +50,13 @@ class NavigationTest { } composeTestRule.setContent { Providers(AmbientBackDispatcher provides activity.onBackPressedDispatcher) { - ProvideTestImageLoader { - if (startDestination == null) { - NavGraph() - } else { - NavGraph(startDestination) + ProvideWindowInsets { + ProvideTestImageLoader { + if (startDestination == null) { + NavGraph() + } else { + NavGraph(startDestination) + } } } } @@ -85,7 +88,7 @@ class NavigationTest { @Test fun coursesToDetail() { // Given the app in the courses screen - startActivity(MainDestinations.Courses.route) + startActivity(MainDestinations.COURSES_ROUTE) composeTestRule.onRoot().printToLog("jalc") // Navigate to the first course diff --git a/Owl/app/src/androidTest/java/com/example/owl/ui/fakes/ProvideTestImageLoader.kt b/Owl/app/src/androidTest/java/com/example/owl/ui/fakes/ProvideTestImageLoader.kt index b062e0b818..f137764510 100644 --- a/Owl/app/src/androidTest/java/com/example/owl/ui/fakes/ProvideTestImageLoader.kt +++ b/Owl/app/src/androidTest/java/com/example/owl/ui/fakes/ProvideTestImageLoader.kt @@ -30,8 +30,11 @@ import coil.request.Disposable import coil.request.ImageRequest import coil.request.ImageResult import coil.request.SuccessResult -import com.example.owl.ui.utils.AmbientImageLoader +import dev.chrisbanes.accompanist.coil.AmbientImageLoader +/** + * Replaces all images with a simple black drawable to make testing faster and hermetic. + */ @OptIn(ExperimentalCoilApi::class) @Composable fun ProvideTestImageLoader(content: @Composable () -> Unit) { diff --git a/Owl/app/src/main/java/com/example/owl/ui/NavGraph.kt b/Owl/app/src/main/java/com/example/owl/ui/NavGraph.kt index 284fbc9670..685a74cfa3 100644 --- a/Owl/app/src/main/java/com/example/owl/ui/NavGraph.kt +++ b/Owl/app/src/main/java/com/example/owl/ui/NavGraph.kt @@ -16,7 +16,6 @@ package com.example.owl.ui -import android.os.Bundle import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.navigation.NavHostController @@ -26,48 +25,23 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.navArgument import androidx.navigation.compose.navigate import androidx.navigation.compose.rememberNavController +import com.example.owl.ui.MainDestinations.COURSE_DETAIL_ID_KEY import com.example.owl.ui.course.CourseDetails import com.example.owl.ui.courses.Courses import com.example.owl.ui.onboarding.Onboarding -open class Destination(open val route: String) -open class DestinationSingleArg( - private val rootRoute: String, - private val argName: String, - argType: NavType<*> -) { - val args = listOf(navArgument(argName) { type = argType }) - val route = "$rootRoute/{$argName}" - fun getRouteWithArg(courseId: Long) = "$rootRoute/$courseId" - fun getArgFromBundle(args: Bundle) = args.getLong(argName) -} - /** * Destinations used in the main screen ([OwlApp]). */ object MainDestinations { - val Onboarding = Destination("onboarding") - val Courses = Destination("courses") - val CourseDetail = DestinationSingleArg("courses", "courseId", NavType.LongType) -} - -/** - * Models the navigation actions in the app. - */ -class MainActions(navController: NavHostController) { - val onboardingComplete: () -> Unit = { - navController.navigate(MainDestinations.Courses.route) - } - val selectCourse: (Long) -> Unit = { courseId: Long -> - navController.navigate(MainDestinations.CourseDetail.getRouteWithArg(courseId)) - } - val upPress: () -> Unit = { - navController.popBackStack() - } + const val ONBOARDING_ROUTE = "onboarding" + const val COURSES_ROUTE = "courses" + const val COURSE_DETAIL_ROUTE = "course" + const val COURSE_DETAIL_ID_KEY = "courseId" } @Composable -fun NavGraph(startDestination: String = MainDestinations.Onboarding.route) { +fun NavGraph(startDestination: String = MainDestinations.ONBOARDING_ROUTE) { val navController = rememberNavController() val actions = remember(navController) { MainActions(navController) } @@ -75,22 +49,37 @@ fun NavGraph(startDestination: String = MainDestinations.Onboarding.route) { navController = navController, startDestination = startDestination ) { - composable(MainDestinations.Onboarding.route) { + composable(MainDestinations.ONBOARDING_ROUTE) { Onboarding(onboardingComplete = actions.onboardingComplete) } - composable(MainDestinations.Courses.route) { + composable(MainDestinations.COURSES_ROUTE) { Courses(selectCourse = actions.selectCourse) } composable( - MainDestinations.CourseDetail.route, - arguments = MainDestinations.CourseDetail.args + "${MainDestinations.COURSE_DETAIL_ROUTE}/{$COURSE_DETAIL_ID_KEY}", + arguments = listOf(navArgument(COURSE_DETAIL_ID_KEY) { type = NavType.LongType }) ) { backStackEntry -> val arguments = requireNotNull(backStackEntry.arguments) CourseDetails( - courseId = MainDestinations.CourseDetail.getArgFromBundle(arguments), + courseId = arguments.getLong(COURSE_DETAIL_ID_KEY), selectCourse = actions.selectCourse, upPress = actions.upPress ) } } } + +/** + * Models the navigation actions in the app. + */ +class MainActions(navController: NavHostController) { + val onboardingComplete: () -> Unit = { + navController.navigate(MainDestinations.COURSES_ROUTE) + } + val selectCourse: (Long) -> Unit = { courseId: Long -> + navController.navigate("${MainDestinations.COURSE_DETAIL_ROUTE}/$courseId") + } + val upPress: () -> Unit = { + navController.popBackStack() + } +} diff --git a/Owl/app/src/main/java/com/example/owl/ui/OwlApp.kt b/Owl/app/src/main/java/com/example/owl/ui/OwlApp.kt index 7e26d5a53b..11285dde1a 100644 --- a/Owl/app/src/main/java/com/example/owl/ui/OwlApp.kt +++ b/Owl/app/src/main/java/com/example/owl/ui/OwlApp.kt @@ -20,14 +20,14 @@ import androidx.activity.OnBackPressedDispatcher import androidx.compose.runtime.Composable import androidx.compose.runtime.Providers import com.example.owl.ui.utils.AmbientBackDispatcher -import com.example.owl.ui.utils.ProvideDisplayInsets import com.example.owl.ui.utils.ProvideImageLoader +import dev.chrisbanes.accompanist.insets.ProvideWindowInsets @Composable fun OwlApp(backDispatcher: OnBackPressedDispatcher) { Providers(AmbientBackDispatcher provides backDispatcher) { - ProvideDisplayInsets { + ProvideWindowInsets { ProvideImageLoader { NavGraph() } diff --git a/Owl/app/src/main/java/com/example/owl/ui/courses/FeaturedCourses.kt b/Owl/app/src/main/java/com/example/owl/ui/courses/FeaturedCourses.kt index 261e20678c..b88c404c6c 100644 --- a/Owl/app/src/main/java/com/example/owl/ui/courses/FeaturedCourses.kt +++ b/Owl/app/src/main/java/com/example/owl/ui/courses/FeaturedCourses.kt @@ -31,8 +31,8 @@ import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.OndemandVideo import androidx.compose.runtime.Composable -import androidx.compose.ui.layout.Layout import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.Layout import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.accessibilityLabel import androidx.compose.ui.semantics.semantics diff --git a/Owl/buildSrc/src/main/java/com/example/owl/buildsrc/Dependencies.kt b/Owl/buildSrc/src/main/java/com/example/owl/buildsrc/Dependencies.kt index b8191ef973..334ec15b87 100644 --- a/Owl/buildSrc/src/main/java/com/example/owl/buildsrc/Dependencies.kt +++ b/Owl/buildSrc/src/main/java/com/example/owl/buildsrc/Dependencies.kt @@ -46,7 +46,7 @@ object Libs { object AndroidX { const val coreKtx = "androidx.core:core-ktx:1.5.0-alpha04" - const val navigation = "androidx.navigation:navigation-compose:1.0.0-alpha01" + const val navigation = "androidx.navigation:navigation-compose:1.0.0-alpha02" object Compose { const val snapshot = "" From 3bc7c1fe156006a9df1a21d94bb38b435856fe1e Mon Sep 17 00:00:00 2001 From: Jose Alcerreca Date: Fri, 20 Nov 2020 15:27:19 +0100 Subject: [PATCH 08/11] Enables tests for Owl in GHA Change-Id: I787b7863ccec5ff180630bb631740424e9a7e3f5 --- .github/workflows/Owl.yaml | 47 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/.github/workflows/Owl.yaml b/.github/workflows/Owl.yaml index 3b52f1166d..23bb761116 100644 --- a/.github/workflows/Owl.yaml +++ b/.github/workflows/Owl.yaml @@ -57,3 +57,50 @@ jobs: with: name: build-reports path: ${{ env.SAMPLE_PATH }}/app/build/reports + + test: + needs: build + runs-on: macOS-latest # enables hardware acceleration in the virtual machine + timeout-minutes: 30 + strategy: + matrix: + api-level: [23, 26, 29] + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Copy CI gradle.properties + run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties + + - name: Set up JDK 1.8 + uses: actions/setup-java@v1 + with: + java-version: 1.8 + + - name: Generate cache key + run: ./scripts/checksum.sh $SAMPLE_PATH checksum.txt + + - uses: actions/cache@v2 + with: + path: | + ~/.gradle/caches/modules-* + ~/.gradle/caches/jars-* + ~/.gradle/caches/build-cache-* + key: gradle-${{ hashFiles('checksum.txt') }} + + - name: Run instrumentation tests + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ matrix.api-level }} + arch: x86 + disable-animations: true + script: ./gradlew connectedCheck --stacktrace + working-directory: ${{ env.SAMPLE_PATH }} + + - name: Upload test reports + if: always() + uses: actions/upload-artifact@v2 + with: + name: test-reports + path: ${{ env.SAMPLE_PATH }}/app/build/reports From d984804f85b949a29e51d72fd96fb0f2f225e5b3 Mon Sep 17 00:00:00 2001 From: Jose Alcerreca Date: Fri, 20 Nov 2020 18:28:58 +0100 Subject: [PATCH 09/11] Adressing review comments Change-Id: Id15eb3775869d8914e49d73f5fbc66f05ba4e09c --- .../java/com/example/owl/ui/NavigationTest.kt | 26 +++++++++++-------- .../owl/ui/fakes/ProvideTestImageLoader.kt | 2 +- Owl/app/src/debug/AndroidManifest.xml | 19 ++++++++++++++ Owl/app/src/main/AndroidManifest.xml | 1 - .../main/java/com/example/owl/ui/NavGraph.kt | 2 +- 5 files changed, 36 insertions(+), 14 deletions(-) create mode 100644 Owl/app/src/debug/AndroidManifest.xml diff --git a/Owl/app/src/androidTest/java/com/example/owl/ui/NavigationTest.kt b/Owl/app/src/androidTest/java/com/example/owl/ui/NavigationTest.kt index b0b7134fb1..486e7559b1 100644 --- a/Owl/app/src/androidTest/java/com/example/owl/ui/NavigationTest.kt +++ b/Owl/app/src/androidTest/java/com/example/owl/ui/NavigationTest.kt @@ -21,11 +21,11 @@ import androidx.compose.runtime.Providers import androidx.compose.ui.test.hasLabel import androidx.compose.ui.test.hasSubstring import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithLabel import androidx.compose.ui.test.onNodeWithSubstring import androidx.compose.ui.test.onRoot import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.printToLog import androidx.test.platform.app.InstrumentationRegistry import com.example.owl.R import com.example.owl.model.courses @@ -40,6 +40,11 @@ import org.junit.Test */ class NavigationTest { + /** + * Using an empty activity to have control of the content that is set. + * + * This activity must be declared in the manifest (see src/debug/AndroidManifest.xml) + */ @get:Rule val composeTestRule = createAndroidComposeRule() lateinit var activity: ComponentActivity @@ -47,15 +52,15 @@ class NavigationTest { private fun startActivity(startDestination: String? = null) { composeTestRule.activityRule.scenario.onActivity { activity = it - } - composeTestRule.setContent { - Providers(AmbientBackDispatcher provides activity.onBackPressedDispatcher) { - ProvideWindowInsets { - ProvideTestImageLoader { - if (startDestination == null) { - NavGraph() - } else { - NavGraph(startDestination) + composeTestRule.setContent { + Providers(AmbientBackDispatcher provides activity.onBackPressedDispatcher) { + ProvideWindowInsets { + ProvideTestImageLoader { + if (startDestination == null) { + NavGraph() + } else { + NavGraph(startDestination) + } } } } @@ -90,7 +95,6 @@ class NavigationTest { // Given the app in the courses screen startActivity(MainDestinations.COURSES_ROUTE) - composeTestRule.onRoot().printToLog("jalc") // Navigate to the first course composeTestRule.onNode( hasLabel(getFeaturedCourseLabel()).and(hasSubstring(courses.first().name)) diff --git a/Owl/app/src/androidTest/java/com/example/owl/ui/fakes/ProvideTestImageLoader.kt b/Owl/app/src/androidTest/java/com/example/owl/ui/fakes/ProvideTestImageLoader.kt index f137764510..f3371499b3 100644 --- a/Owl/app/src/androidTest/java/com/example/owl/ui/fakes/ProvideTestImageLoader.kt +++ b/Owl/app/src/androidTest/java/com/example/owl/ui/fakes/ProvideTestImageLoader.kt @@ -33,7 +33,7 @@ import coil.request.SuccessResult import dev.chrisbanes.accompanist.coil.AmbientImageLoader /** - * Replaces all images with a simple black drawable to make testing faster and hermetic. + * Replaces all remote images with a simple black drawable to make testing faster and hermetic. */ @OptIn(ExperimentalCoilApi::class) @Composable diff --git a/Owl/app/src/debug/AndroidManifest.xml b/Owl/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000000..aa21dd579b --- /dev/null +++ b/Owl/app/src/debug/AndroidManifest.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/Owl/app/src/main/AndroidManifest.xml b/Owl/app/src/main/AndroidManifest.xml index ebb670f495..855b3b5422 100644 --- a/Owl/app/src/main/AndroidManifest.xml +++ b/Owl/app/src/main/AndroidManifest.xml @@ -32,7 +32,6 @@ - diff --git a/Owl/app/src/main/java/com/example/owl/ui/NavGraph.kt b/Owl/app/src/main/java/com/example/owl/ui/NavGraph.kt index 685a74cfa3..a02b83284f 100644 --- a/Owl/app/src/main/java/com/example/owl/ui/NavGraph.kt +++ b/Owl/app/src/main/java/com/example/owl/ui/NavGraph.kt @@ -31,7 +31,7 @@ import com.example.owl.ui.courses.Courses import com.example.owl.ui.onboarding.Onboarding /** - * Destinations used in the main screen ([OwlApp]). + * Destinations used in the ([OwlApp]). */ object MainDestinations { const val ONBOARDING_ROUTE = "onboarding" From 0ea81d883f5ae76e0e240ae25ba13c580617f90e Mon Sep 17 00:00:00 2001 From: Jose Alcerreca Date: Fri, 20 Nov 2020 18:39:25 +0100 Subject: [PATCH 10/11] Spotless ~_~ Change-Id: I04c12559c2f1df2c2af5a143d9615ac47fbaa53b --- .../src/androidTest/java/com/example/owl/ui/NavigationTest.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/Owl/app/src/androidTest/java/com/example/owl/ui/NavigationTest.kt b/Owl/app/src/androidTest/java/com/example/owl/ui/NavigationTest.kt index 486e7559b1..ab6c9eaaf4 100644 --- a/Owl/app/src/androidTest/java/com/example/owl/ui/NavigationTest.kt +++ b/Owl/app/src/androidTest/java/com/example/owl/ui/NavigationTest.kt @@ -21,10 +21,8 @@ import androidx.compose.runtime.Providers import androidx.compose.ui.test.hasLabel import androidx.compose.ui.test.hasSubstring import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithLabel import androidx.compose.ui.test.onNodeWithSubstring -import androidx.compose.ui.test.onRoot import androidx.compose.ui.test.performClick import androidx.test.platform.app.InstrumentationRegistry import com.example.owl.R From b3aa4763775fc69fe290b1a2e594ad21061e04ef Mon Sep 17 00:00:00 2001 From: Jose Alcerreca Date: Tue, 24 Nov 2020 15:57:29 +0100 Subject: [PATCH 11/11] Replaces popBackStack with navigateUp Change-Id: I3ebf727b390b72c89ba3dd5be21e084347bc2840 --- Owl/app/src/main/java/com/example/owl/ui/NavGraph.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Owl/app/src/main/java/com/example/owl/ui/NavGraph.kt b/Owl/app/src/main/java/com/example/owl/ui/NavGraph.kt index a02b83284f..04038c3f4f 100644 --- a/Owl/app/src/main/java/com/example/owl/ui/NavGraph.kt +++ b/Owl/app/src/main/java/com/example/owl/ui/NavGraph.kt @@ -80,6 +80,6 @@ class MainActions(navController: NavHostController) { navController.navigate("${MainDestinations.COURSE_DETAIL_ROUTE}/$courseId") } val upPress: () -> Unit = { - navController.popBackStack() + navController.navigateUp() } }