Skip to content

Commit

Permalink
Navigation common UI tests (#1596)
Browse files Browse the repository at this point in the history
Move a bunch of Navigation UI tests to common.

Fixes [CMP-6689](https://youtrack.jetbrains.com/issue/CMP-6689) iOS
Navigation Crash Nested Graphs popBackStack

## Release Notes
### Fixes - Navigation
- Fixed [CMP-6689](https://youtrack.jetbrains.com/issue/CMP-6689) iOS
Navigation Crash Nested Graphs popBackStack
  • Loading branch information
terrakok authored Sep 26, 2024
1 parent 8b2b54d commit 43d0247
Show file tree
Hide file tree
Showing 35 changed files with 1,400 additions and 1,021 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
public final class androidx/lifecycle/testing/TestLifecycleOwner : androidx/lifecycle/LifecycleOwner {
public fun <init> ()V
public fun <init> (Landroidx/lifecycle/Lifecycle$State;)V
public fun <init> (Landroidx/lifecycle/Lifecycle$State;Lkotlinx/coroutines/CoroutineDispatcher;)V
public synthetic fun <init> (Landroidx/lifecycle/Lifecycle$State;Lkotlinx/coroutines/CoroutineDispatcher;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getCurrentState ()Landroidx/lifecycle/Lifecycle$State;
public synthetic fun getLifecycle ()Landroidx/lifecycle/Lifecycle;
public fun getLifecycle ()Landroidx/lifecycle/LifecycleRegistry;
public final fun getObserverCount ()I
public final fun handleLifecycleEvent (Landroidx/lifecycle/Lifecycle$Event;)V
public final fun setCurrentState (Landroidx/lifecycle/Lifecycle$State;)V
public final fun setCurrentState (Landroidx/lifecycle/Lifecycle$State;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}

6 changes: 6 additions & 0 deletions lifecycle/lifecycle-runtime-testing/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,21 @@
* modifying its settings.
*/


import androidx.build.AndroidXComposePlugin
import androidx.build.JetbrainsAndroidXPlugin
import androidx.build.PlatformIdentifier
import androidx.build.LibraryType
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType

plugins {
id("AndroidXPlugin")
id("com.android.library")
id("JetbrainsAndroidXPlugin")
}

JetbrainsAndroidXPlugin.applyAndConfigure(project)

androidXMultiplatform {
android()
desktop()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ public class androidx/navigation/NavDestination {
public static final fun hasRoute (Landroidx/navigation/NavDestination;Lkotlin/reflect/KClass;)Z
public final fun hasRoute (Ljava/lang/String;Landroidx/core/bundle/Bundle;)Z
public fun hashCode ()I
public fun matchDeepLink (Ljava/lang/String;)Landroidx/navigation/NavDestination$DeepLinkMatch;
public final fun matchDeepLink (Ljava/lang/String;)Landroidx/navigation/NavDestination$DeepLinkMatch;
public final fun removeArgument (Ljava/lang/String;)V
public final fun setId (I)V
public final fun setLabel (Ljava/lang/CharSequence;)V
Expand Down Expand Up @@ -211,8 +211,9 @@ public class androidx/navigation/NavGraph : androidx/navigation/NavDestination,
public final fun getStartDestinationRoute ()Ljava/lang/String;
public fun hashCode ()I
public final fun iterator ()Ljava/util/Iterator;
public fun matchDeepLink (Ljava/lang/String;)Landroidx/navigation/NavDestination$DeepLinkMatch;
public final fun matchDeepLinkComprehensive (Ljava/lang/String;ZZLandroidx/navigation/NavDestination;)Landroidx/navigation/NavDestination$DeepLinkMatch;
public fun matchDeepLinkRequest (Ljava/lang/String;)Landroidx/navigation/NavDestination$DeepLinkMatch;
public synthetic fun matchDeepLinkRequest$navigation_common (Ljava/lang/String;)Landroidx/navigation/NavDestination$DeepLinkMatch;
public final fun remove (Landroidx/navigation/NavDestination;)V
public final fun setStartDestination (I)V
public final fun setStartDestination (Ljava/lang/Object;)V
Expand Down
29 changes: 12 additions & 17 deletions navigation/navigation-common/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -45,25 +45,10 @@ androidXMultiplatform {
mac()
linux()
ios()
js()
wasm()
js { browser() }
wasm { browser() }

defaultPlatform(PlatformIdentifier.ANDROID)
}

kotlin {
js {
browser()
}
wasmJs()

watchosArm64()
watchosArm32()
watchosX64()
watchosSimulatorArm64()
tvosArm64()
tvosX64()
tvosSimulatorArm64()

sourceSets {
commonMain {
Expand Down Expand Up @@ -171,6 +156,16 @@ kotlin {
}
}

kotlin {
watchosArm64()
watchosArm32()
watchosX64()
watchosSimulatorArm64()
tvosArm64()
tvosX64()
tvosSimulatorArm64()
}

android {
buildTypes.configureEach {
consumerProguardFiles "proguard-rules.pro"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ import kotlinx.serialization.serializer
public actual open class NavGraph actual constructor(navGraphNavigator: Navigator<out NavGraph>) :
NavDestination(navGraphNavigator), Iterable<NavDestination> {

public val nodes: SparseArrayCompat<NavDestination> = SparseArrayCompat<NavDestination>()
public actual val nodes: SparseArrayCompat<NavDestination> = SparseArrayCompat<NavDestination>()
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) get

private var startDestId = 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
package androidx.navigation

import androidx.annotation.RestrictTo
import androidx.collection.SparseArrayCompat
import androidx.navigation.serialization.generateRouteWithArgs
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
Expand Down Expand Up @@ -47,6 +48,8 @@ import kotlinx.serialization.serializer
public expect open class NavGraph(
navGraphNavigator: Navigator<out NavGraph>
) : NavDestination, Iterable<NavDestination> {
val nodes: SparseArrayCompat<NavDestination>
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) get

/**
* Adds a destination to this NavGraph. The destination must have a route set.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -954,10 +954,8 @@ class NavArgumentGeneratorTest {
assertThat(exception.message)
.isEqualTo(
"Cannot generate NavArguments for polymorphic serializer " +
"kotlinx.serialization.PolymorphicSerializer(baseClass: " +
"class androidx.navigation.serialization." +
"NavArgumentGeneratorTest\$abstractClassInvalid\$TestClass (Kotlin reflection " +
"is not available)). Arguments can only be generated from concrete classes " +
"kotlinx.serialization.PolymorphicSerializer(baseClass: ${TestClass::class})." +
" Arguments can only be generated from concrete classes " +
"or objects."
)
}
Expand Down Expand Up @@ -1001,6 +999,7 @@ class NavArgumentGeneratorTest {
// and hashcode which will need to be public api.
private fun assertThat(actual: List<NamedNavArgument>) = actual

@Serializable
enum class TestEnum {
TEST
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import kotlin.test.assertFailsWith
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
Expand Down Expand Up @@ -467,18 +468,22 @@ class NavTypeConverterTest {
fun matchCustomTypeNativeTypeParam() {
@Serializable class TestClass<T : SerialDescriptor>

val descriptor = serializer<TestClass<SerialDescriptor>>().descriptor
val kType = typeOf<TestClass<SerialDescriptor>>()
assertThat(descriptor.matchKType(kType)).isTrue()
ignoreWasmAndNativeSerializationException {
val descriptor = serializer<TestClass<SerialDescriptor>>().descriptor
val kType = typeOf<TestClass<SerialDescriptor>>()
assertThat(descriptor.matchKType(kType)).isTrue()
}
}

@Test
fun matchCustomTypeArgNativeTypeParam() {
@Serializable class TestClass<T : SerialDescriptor>(val arg: Int)

val descriptor = serializer<TestClass<SerialDescriptor>>().descriptor
val kType = typeOf<TestClass<SerialDescriptor>>()
assertThat(descriptor.matchKType(kType)).isTrue()
ignoreWasmAndNativeSerializationException {
val descriptor = serializer<TestClass<SerialDescriptor>>().descriptor
val kType = typeOf<TestClass<SerialDescriptor>>()
assertThat(descriptor.matchKType(kType)).isTrue()
}
}

@Test
Expand All @@ -487,9 +492,11 @@ class NavTypeConverterTest {

@Serializable class TestClass<T : SerialDescriptor>(val arg: MyArg)

val descriptor = serializer<TestClass<SerialDescriptor>>().descriptor
val kType = typeOf<TestClass<SerialDescriptor>>()
assertThat(descriptor.matchKType(kType)).isTrue()
ignoreWasmAndNativeSerializationException {
val descriptor = serializer<TestClass<SerialDescriptor>>().descriptor
val kType = typeOf<TestClass<SerialDescriptor>>()
assertThat(descriptor.matchKType(kType)).isTrue()
}
}

@Test
Expand All @@ -498,9 +505,25 @@ class NavTypeConverterTest {

@Serializable class TestClass<T : SerialDescriptor>(val arg: Int, val arg2: MyArg)

val descriptor = serializer<TestClass<SerialDescriptor>>().descriptor
val kType = typeOf<TestClass<SerialDescriptor>>()
assertThat(descriptor.matchKType(kType)).isTrue()
ignoreWasmAndNativeSerializationException {
val descriptor = serializer<TestClass<SerialDescriptor>>().descriptor
val kType = typeOf<TestClass<SerialDescriptor>>()
assertThat(descriptor.matchKType(kType)).isTrue()
}
}

private inline fun ignoreWasmAndNativeSerializationException(block: () -> Unit) {
try {
block()
} catch (e: SerializationException) {
val message = e.message.orEmpty()
if (
message.contains("On Kotlin/Wasm explicitly declared serializer should be used for interfaces and enums without @Serializable annotation") ||
message.contains("To get interface serializer on Kotlin/Native, use PolymorphicSerializer() constructor function.")
) {
// ignore
} else throw e
}
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -533,7 +533,8 @@ internal sealed class SealedClass {
@Serializable(with = CustomSerializer::class)
internal open class CustomSerializerClass(val longArg: Long)

internal class CustomSerializer : KSerializer<CustomSerializerClass> {
//the object class is required here because of https://youtrack.jetbrains.com/issue/KT-71530
internal object CustomSerializer : KSerializer<CustomSerializerClass> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("Date", PrimitiveKind.LONG)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ import kotlin.reflect.KType
import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.serializer

// TODO: Use [RegexOption.DOT_MATCHES_ALL] once available in common
// https://youtrack.jetbrains.com/issue/KT-67574
private const val ANY_SYMBOLS_IN_THE_TAIL = "([\\s\\S]+?)?"

public actual class NavDeepLink
internal actual constructor(
public actual val uriPattern: String?,
Expand Down Expand Up @@ -244,8 +248,6 @@ internal actual constructor(
inputParams?.forEach { inputParam ->
val argMatchResult =
storedParam.paramRegex?.let {
// TODO: Use [RegexOption.DOT_MATCHES_ALL] once available in common
// https://youtrack.jetbrains.com/issue/KT-67574
Regex(it).find(inputParam)
}
// check if this particular arg value matches the expected regex.
Expand Down Expand Up @@ -466,7 +468,7 @@ internal actual constructor(
}
// we need to specifically escape any .* instances to ensure
// they are still treated as wildcards in our final regex
pathRegex = uriRegex.toString().replace(".*", "\\E.*\\Q")
pathRegex = uriRegex.toString().saveWildcardInRegex()
}

private fun parseQuery(): MutableMap<String, ParamQuery> {
Expand Down Expand Up @@ -496,9 +498,7 @@ internal actual constructor(
val inputLiteral = queryParam.substring(appendPos, result.range.first)
argRegex.append(Regex.escape(inputLiteral))
}
// TODO: Revert to "(.+?)?" when [RegexOption.DOT_MATCHES_ALL] will be available
// https://youtrack.jetbrains.com/issue/KT-67574
argRegex.append("([\\s\\S]+?)?")
argRegex.append(ANY_SYMBOLS_IN_THE_TAIL)
appendPos = result.range.last + 1
result = result.next()
}
Expand All @@ -509,7 +509,7 @@ internal actual constructor(

// Save the regex with wildcards unquoted, and add the param to the map with its
// name as the key
param.paramRegex = argRegex.toString().replace(".*", "\\E.*\\Q")
param.paramRegex = argRegex.toString().saveWildcardInRegex()
paramArgMap[paramName] = param
}
return paramArgMap
Expand Down Expand Up @@ -543,6 +543,15 @@ internal actual constructor(
return fragArgs to fragRegex.toString()
}

//for more info see #Regexp.escape platform actuals
private fun String.saveWildcardInRegex(): String =
//non-js regex escaping
if (this.contains("\\Q") && this.contains("\\E")) replace(".*", "\\E.*\\Q")
//js regex escaping
else if (this.contains("\\.\\*")) replace("\\.\\*", ".*")
//fallback
else this

init {
parsePath()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ public actual open class NavDestination actual constructor(
}

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public actual open fun matchDeepLink(route: String): DeepLinkMatch? {
public actual fun matchDeepLink(route: String): DeepLinkMatch? {
val matchingDeepLink =
if (this is NavGraph) {
matchDeepLinkComprehensive(
Expand All @@ -139,7 +139,7 @@ public actual open class NavDestination actual constructor(
return matchingDeepLink
}

internal fun matchDeepLinkRequest(route: String): DeepLinkMatch? {
internal open fun matchDeepLinkRequest(route: String): DeepLinkMatch? {
if (deepLinks.isEmpty()) {
return null
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import kotlinx.serialization.serializer
public actual open class NavGraph actual constructor(navGraphNavigator: Navigator<out NavGraph>) :
NavDestination(navGraphNavigator), Iterable<NavDestination> {

public val nodes: SparseArrayCompat<NavDestination> = SparseArrayCompat<NavDestination>()
public actual val nodes: SparseArrayCompat<NavDestination> = SparseArrayCompat<NavDestination>()
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) get

private var startDestId = 0
Expand All @@ -47,13 +47,13 @@ public actual open class NavGraph actual constructor(navGraphNavigator: Navigato
lastVisited: NavDestination
): DeepLinkMatch? {
// First search through any deep links directly added to this NavGraph
val bestMatch = matchDeepLinkRequest(route)
val bestMatch = super.matchDeepLinkRequest(route)

// If searchChildren is true, search through all child destinations for a matching deeplink
val bestChildMatch =
if (searchChildren) {
mapNotNull { child ->
if (child != lastVisited) child.matchDeepLink(route) else null
if (child != lastVisited) child.matchDeepLinkRequest(route) else null
}
.maxOrNull()
} else null
Expand All @@ -70,7 +70,7 @@ public actual open class NavGraph actual constructor(navGraphNavigator: Navigato
}

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public override fun matchDeepLink(route: String): DeepLinkMatch? =
public override fun matchDeepLinkRequest(route: String): DeepLinkMatch? =
matchDeepLinkComprehensive(
route,
searchChildren = true,
Expand Down
Loading

0 comments on commit 43d0247

Please sign in to comment.