diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 81dd4c4c..4b685495 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -87,8 +87,9 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1") // Networking/API - implementation("com.squareup.okhttp3:okhttp:4.12.0") - implementation("com.squareup.okhttp3:logging-interceptor:4.12.0") + implementation("com.squareup.okhttp3:okhttp:5.3.2") + implementation("com.squareup.okhttp3:okhttp-coroutines:5.3.2") + implementation("com.squareup.okhttp3:logging-interceptor:5.3.2") // Coroutines implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.1") @@ -108,7 +109,7 @@ dependencies { testImplementation("org.robolectric:robolectric:4.14.1") testImplementation("org.mockito:mockito-core:5.14.2") testImplementation("com.nhaarman:mockito-kotlin:1.6.0") - testImplementation("com.squareup.okhttp3:mockwebserver:4.12.0") + testImplementation("com.squareup.okhttp3:mockwebserver:5.3.2") testImplementation("junit:junit:4.13.2") } diff --git a/core/src/main/kotlin/dev/hotwire/core/turbo/config/PathConfiguration.kt b/core/src/main/kotlin/dev/hotwire/core/turbo/config/PathConfiguration.kt index b7d890da..562907a0 100644 --- a/core/src/main/kotlin/dev/hotwire/core/turbo/config/PathConfiguration.kt +++ b/core/src/main/kotlin/dev/hotwire/core/turbo/config/PathConfiguration.kt @@ -4,31 +4,46 @@ import android.annotation.SuppressLint import android.content.Context import android.net.Uri import androidx.core.net.toUri -import com.google.gson.annotations.SerializedName +import dev.hotwire.core.logging.logEvent +import dev.hotwire.core.turbo.config.PathConfigurationLoadState.Loaded +import dev.hotwire.core.turbo.config.PathConfigurationLoadState.NotLoaded import dev.hotwire.core.turbo.nav.Presentation import dev.hotwire.core.turbo.nav.PresentationContext import dev.hotwire.core.turbo.nav.QueryStringPresentation -import java.net.URL +import dev.hotwire.core.turbo.util.dispatcherProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch /** * Provides the ability to load, parse, and retrieve url path * properties from the app's JSON configuration file. */ -class PathConfiguration { +class PathConfiguration internal constructor() { private val cachedProperties: HashMap = hashMapOf() + private val _loadState = MutableStateFlow(NotLoaded) + private val loadingScope: CoroutineScope = CoroutineScope(dispatcherProvider.io + SupervisorJob()) + private var loadingJob: Job? = null - internal var loader: PathConfigurationLoader? = null + internal var loader = PathConfigurationLoader() - @SerializedName("rules") - internal var rules: List = emptyList() + /** + * A [StateFlow] that emits the current state of the path configuration + * loading process. Observe this to know when the configuration has been + * loaded and from which source (bundled asset, cached remote, or fresh remote). + */ + val loadState: StateFlow = _loadState.asStateFlow() /** * Gets the top-level settings specified in the app's path configuration. * The settings are map of key/value `String` items. */ - @SerializedName("settings") - var settings: PathConfigurationSettings = PathConfigurationSettings() - private set + val settings: PathConfigurationSettings + get() = synchronized(this) { currentConfiguration.settings } /** * Represents the location of the app's path configuration JSON file(s). @@ -73,14 +88,21 @@ class PathConfiguration { location: Location, options: LoaderOptions ) { - if (loader == null) { - loader = PathConfigurationLoader(context.applicationContext) + logEvent("pathConfigurationLoading", location.toString()) + + val appContext = context.applicationContext + loadingJob?.cancel() + + loader.loadCachedOrBundledConfiguration(appContext, location)?.let { + applyLoadedState(it) } - loader?.load(location, options) { - cachedProperties.clear() - rules = it.rules + historicalLocationRules - settings = it.settings + loadingJob = loadingScope.launch { + location.remoteFileUrl?.let { url -> + loader.loadRemoteConfigurationForUrl(appContext, url, options)?.let { + applyLoadedState(it) + } + } } } @@ -94,28 +116,31 @@ class PathConfiguration { * @return The map of key/value `String` properties */ fun properties(location: String): PathConfigurationProperties { - cachedProperties[location]?.let { return it } + synchronized(this) { + cachedProperties[location]?.let { return it } - val properties = PathConfigurationProperties() - val path = path(location) + val properties = currentConfiguration.properties(location) + cachedProperties[location] = properties - for (rule in rules) { - if (rule.matches(path)) properties.putAll(rule.properties) + return properties } - - cachedProperties[location] = properties - - return properties } - private fun path(location: String): String { - val url = URL(location) - - return when (url.query) { - null -> url.path - else -> "${url.path}?${url.query}" - } + private fun applyLoadedState(state: Loaded) = synchronized(this) { + cachedProperties.clear() + _loadState.value = state + + logEvent( + "pathConfigurationUpdated", listOf( + "source" to state.javaClass.simpleName, + "rules" to state.configuration.rules.size, + "settings" to state.configuration.settings.size + ) + ) } + + private val currentConfiguration: PathConfigurationData + get() = (_loadState.value as? Loaded)?.configuration ?: PathConfigurationData() } typealias PathConfigurationProperties = HashMap diff --git a/core/src/main/kotlin/dev/hotwire/core/turbo/config/PathConfigurationData.kt b/core/src/main/kotlin/dev/hotwire/core/turbo/config/PathConfigurationData.kt new file mode 100644 index 00000000..f54b5519 --- /dev/null +++ b/core/src/main/kotlin/dev/hotwire/core/turbo/config/PathConfigurationData.kt @@ -0,0 +1,47 @@ +package dev.hotwire.core.turbo.config + +import com.google.gson.annotations.SerializedName +import java.net.URL + +@ConsistentCopyVisibility +data class PathConfigurationData internal constructor( + @SerializedName("rules") + internal val rules: List = emptyList(), + + /** + * Gets the top-level settings specified in the app's path configuration. + * The settings are map of key/value `String` items. + */ + @SerializedName("settings") + val settings: PathConfigurationSettings = PathConfigurationSettings() +) { + /** + * Retrieve the path properties based on the cascading rules in your + * path configuration. + * + * @param location The absolute url to match against the configuration's + * rules. Only the url's relative path will be used to find the matching + * regex rules. + * @return The map of key/value `String` properties + */ + fun properties(location: String): PathConfigurationProperties { + val properties = PathConfigurationProperties() + val path = path(location) + + for (rule in rules + historicalLocationRules) { + if (rule.matches(path)) properties.putAll(rule.properties) + } + + return properties + } + + private fun path(location: String): String { + val url = URL(location) + + return if (url.query == null) { + url.path + } else { + "${url.path}?${url.query}" + } + } +} diff --git a/core/src/main/kotlin/dev/hotwire/core/turbo/config/PathConfigurationLoadState.kt b/core/src/main/kotlin/dev/hotwire/core/turbo/config/PathConfigurationLoadState.kt new file mode 100644 index 00000000..2d614df8 --- /dev/null +++ b/core/src/main/kotlin/dev/hotwire/core/turbo/config/PathConfigurationLoadState.kt @@ -0,0 +1,37 @@ +package dev.hotwire.core.turbo.config + +/** + * Represents the current state of the path configuration loading process. + * Observe [PathConfiguration.loadState] to receive updates as the configuration + * is loaded from each source. + */ +sealed interface PathConfigurationLoadState { + /** + * The initial state before any configuration has been loaded. + */ + data object NotLoaded : PathConfigurationLoadState + + /** + * The configuration was successfully loaded from a source. Check the + * specific subclass to determine the source: [BundledAssetLoaded], + * [CachedRemoteLoaded], or [RemoteLoaded]. + */ + sealed interface Loaded : PathConfigurationLoadState { + val configuration: PathConfigurationData + + /** + * The configuration was loaded from the locally bundled asset file. + */ + data class BundledAssetLoaded(override val configuration: PathConfigurationData) : Loaded + + /** + * The configuration was loaded from a previously cached remote file. + */ + data class CachedRemoteLoaded(override val configuration: PathConfigurationData) : Loaded + + /** + * The configuration was freshly loaded from the remote server. + */ + data class RemoteLoaded(override val configuration: PathConfigurationData) : Loaded + } +} diff --git a/core/src/main/kotlin/dev/hotwire/core/turbo/config/PathConfigurationLoader.kt b/core/src/main/kotlin/dev/hotwire/core/turbo/config/PathConfigurationLoader.kt index 296ca731..36af6512 100644 --- a/core/src/main/kotlin/dev/hotwire/core/turbo/config/PathConfigurationLoader.kt +++ b/core/src/main/kotlin/dev/hotwire/core/turbo/config/PathConfigurationLoader.kt @@ -4,84 +4,84 @@ import android.content.Context import com.google.gson.reflect.TypeToken import dev.hotwire.core.logging.logError import dev.hotwire.core.logging.logEvent -import dev.hotwire.core.turbo.util.dispatcherProvider import dev.hotwire.core.turbo.util.toObject -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch -import kotlin.coroutines.CoroutineContext -internal class PathConfigurationLoader(val context: Context) : CoroutineScope { +internal class PathConfigurationLoader { internal var repository = PathConfigurationRepository() - override val coroutineContext: CoroutineContext - get() = dispatcherProvider.io + Job() - - fun load( - location: PathConfiguration.Location, - options: PathConfiguration.LoaderOptions, - onCompletion: (PathConfiguration) -> Unit - ) { - location.assetFilePath?.let { - loadBundledAssetConfiguration(it, onCompletion) + fun loadCachedOrBundledConfiguration( + context: Context, + location: PathConfiguration.Location + ): PathConfigurationLoadState.Loaded? { + // Attempt to load the cached remote configuration for the url, if available + if (location.remoteFileUrl != null) { + loadCachedConfigurationForUrl(context, location.remoteFileUrl) + ?.let { return it } } - location.remoteFileUrl?.let { - downloadRemoteConfiguration(it, options, onCompletion) - } - } - - private fun downloadRemoteConfiguration( - url: String, - options: PathConfiguration.LoaderOptions, - onCompletion: (PathConfiguration) -> Unit - ) { - // Always load the previously cached version first, if available - loadCachedConfigurationForUrl(url, onCompletion) - - launch { - repository.getRemoteConfiguration(url, options)?.let { json -> - load(json)?.let { - logEvent("remotePathConfigurationLoaded", url) - onCompletion(it) - cacheConfigurationForUrl(url, it) - } - } - } + // Fall back to the bundled config when a cached config is not available + return location.assetFilePath?.let { loadBundledAssetConfiguration(context, it) } } private fun loadBundledAssetConfiguration( - filePath: String, - onCompletion: (PathConfiguration) -> Unit - ) { + context: Context, + filePath: String + ): PathConfigurationLoadState.Loaded.BundledAssetLoaded? { + logEvent("bundledPathConfigurationLoading", filePath) + val json = repository.getBundledConfiguration(context, filePath) - load(json)?.let { + return load(json)?.let { logEvent("bundledPathConfigurationLoaded", filePath) - onCompletion(it) + PathConfigurationLoadState.Loaded.BundledAssetLoaded(it) } } private fun loadCachedConfigurationForUrl( + context: Context, + url: String + ): PathConfigurationLoadState.Loaded.CachedRemoteLoaded? { + logEvent("cachedPathConfigurationLoading", url) + + val json = repository.getCachedConfigurationForUrl(context, url) + val config = json?.let { load(it) } + + return if (config == null) { + logEvent("cachedPathConfigurationFailedToLoad", url) + null + } else { + logEvent("cachedPathConfigurationLoaded", url) + PathConfigurationLoadState.Loaded.CachedRemoteLoaded(config) + } + } + + suspend fun loadRemoteConfigurationForUrl( + context: Context, url: String, - onCompletion: (PathConfiguration) -> Unit - ) { - repository.getCachedConfigurationForUrl(context, url)?.let { json -> - load(json)?.let { - logEvent("cachedPathConfigurationLoaded", url) - onCompletion(it) - } + options: PathConfiguration.LoaderOptions + ): PathConfigurationLoadState.Loaded.RemoteLoaded? { + logEvent("remotePathConfigurationLoading", url) + + val config = repository.getRemoteConfiguration(url, options)?.let { json -> load(json) } + + return if (config == null) { + null + } else { + logEvent("remotePathConfigurationLoaded", url) + cacheConfigurationForUrl(context, url, config) + PathConfigurationLoadState.Loaded.RemoteLoaded(config) } } private fun cacheConfigurationForUrl( + context: Context, url: String, - pathConfiguration: PathConfiguration + pathConfiguration: PathConfigurationData ) { repository.cacheConfigurationForUrl(context, url, pathConfiguration) } private fun load(json: String) = try { - json.toObject(object : TypeToken() {}) + json.toObject(object : TypeToken() {}) } catch(e: Exception) { logError("pathConfiguredFailedToParse", e) null diff --git a/core/src/main/kotlin/dev/hotwire/core/turbo/config/PathConfigurationRepository.kt b/core/src/main/kotlin/dev/hotwire/core/turbo/config/PathConfigurationRepository.kt index 3958df12..18c4cd48 100644 --- a/core/src/main/kotlin/dev/hotwire/core/turbo/config/PathConfigurationRepository.kt +++ b/core/src/main/kotlin/dev/hotwire/core/turbo/config/PathConfigurationRepository.kt @@ -7,8 +7,10 @@ import dev.hotwire.core.logging.logError import dev.hotwire.core.turbo.http.HotwireHttpClient import dev.hotwire.core.turbo.util.dispatcherProvider import dev.hotwire.core.turbo.util.toJson +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.withContext import okhttp3.Request +import okhttp3.coroutines.executeAsync internal class PathConfigurationRepository { private val cacheFile = "turbo" @@ -24,10 +26,7 @@ internal class PathConfigurationRepository { } val request = requestBuilder.build() - - return withContext(dispatcherProvider.io) { - issueRequest(request) - } + return issueRequest(request) } fun getBundledConfiguration( @@ -47,20 +46,20 @@ internal class PathConfigurationRepository { fun cacheConfigurationForUrl( context: Context, url: String, - pathConfiguration: PathConfiguration + pathConfiguration: PathConfigurationData ) { prefs(context).edit { putString(url, pathConfiguration.toJson()) } } - private fun issueRequest(request: Request): String? { - return try { - val call = HotwireHttpClient.instance.newCall(request) + private suspend fun issueRequest(request: Request): String? = try { + val call = HotwireHttpClient.instance.newCall(request) - call.execute().use { response -> + call.executeAsync().use { response -> + withContext(dispatcherProvider.io) { if (response.isSuccessful) { - response.body?.string() + response.body.string() } else { logError( "remotePathConfigurationFailure", @@ -69,10 +68,12 @@ internal class PathConfigurationRepository { null } } - } catch (e: Exception) { - logError("remotePathConfigurationException", e) - null } + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + logError("remotePathConfigurationException", e) + null } private fun prefs(context: Context): SharedPreferences { diff --git a/core/src/test/kotlin/dev/hotwire/core/turbo/config/PathConfigurationRepositoryTest.kt b/core/src/test/kotlin/dev/hotwire/core/turbo/config/PathConfigurationRepositoryTest.kt index 058a5250..a7759358 100644 --- a/core/src/test/kotlin/dev/hotwire/core/turbo/config/PathConfigurationRepositoryTest.kt +++ b/core/src/test/kotlin/dev/hotwire/core/turbo/config/PathConfigurationRepositoryTest.kt @@ -88,8 +88,8 @@ class PathConfigurationRepositoryTest : BaseRepositoryTest() { } } - private fun load(json: String?): PathConfiguration? { - return json?.toObject(object : TypeToken() {}) + private fun load(json: String?): PathConfigurationData? { + return json?.toObject(object : TypeToken() {}) } private fun json(): String { diff --git a/core/src/test/kotlin/dev/hotwire/core/turbo/config/PathConfigurationTest.kt b/core/src/test/kotlin/dev/hotwire/core/turbo/config/PathConfigurationTest.kt index 9b54bac9..56549ad7 100644 --- a/core/src/test/kotlin/dev/hotwire/core/turbo/config/PathConfigurationTest.kt +++ b/core/src/test/kotlin/dev/hotwire/core/turbo/config/PathConfigurationTest.kt @@ -6,6 +6,7 @@ import androidx.test.core.app.ApplicationProvider import com.google.gson.annotations.SerializedName import com.google.gson.reflect.TypeToken import com.nhaarman.mockito_kotlin.any +import com.nhaarman.mockito_kotlin.doReturn import com.nhaarman.mockito_kotlin.eq import com.nhaarman.mockito_kotlin.mock import com.nhaarman.mockito_kotlin.never @@ -58,7 +59,20 @@ class PathConfigurationTest : BaseRepositoryTest() { @Test fun assetConfigurationIsLoaded() { - assertThat(pathConfiguration.rules.size).isGreaterThan(0) + val state = pathConfiguration.loadState.value as PathConfigurationLoadState.Loaded + assertThat(state.configuration.rules.size).isGreaterThan(0) + } + + @Test + fun assetConfigurationIsAvailableImmediatelyAfterLoadReturns() { + val freshConfig = PathConfiguration() + freshConfig.load( + context = context, + location = Location(assetFilePath = "json/test-configuration.json"), + options = options + ) + + assertThat(freshConfig.properties("$url/new").context).isEqualTo(PresentationContext.MODAL) } @Test @@ -73,7 +87,7 @@ class PathConfigurationTest : BaseRepositoryTest() { @Test fun remoteConfigurationIsFetched() { - pathConfiguration.loader = PathConfigurationLoader(context).apply { + pathConfiguration.loader = PathConfigurationLoader().apply { repository = mockRepository } @@ -90,7 +104,7 @@ class PathConfigurationTest : BaseRepositoryTest() { @Test fun validConfigurationIsCached() { - pathConfiguration.loader = PathConfigurationLoader(context).apply { + pathConfiguration.loader = PathConfigurationLoader().apply { repository = mockRepository } @@ -110,7 +124,7 @@ class PathConfigurationTest : BaseRepositoryTest() { @Test fun malformedConfigurationIsNotCached() { - pathConfiguration.loader = PathConfigurationLoader(context).apply { + pathConfiguration.loader = PathConfigurationLoader().apply { repository = mockRepository } @@ -193,6 +207,113 @@ class PathConfigurationTest : BaseRepositoryTest() { assertThat((pathConfiguration.properties("$url/custom/tabs").getTabs()?.first()?.label)).isEqualTo("Tab 1") } + @Test + fun cachedConfigurationSkipsBundled() { + val remoteUrl = "$url/demo/configurations/android-v1.json" + + val config = PathConfiguration().apply { + loader = PathConfigurationLoader().apply { + repository = mock { + on { getBundledConfiguration(any(), eq("json/test-configuration.json")) } doReturn BUNDLED_JSON + on { getCachedConfigurationForUrl(any(), eq(remoteUrl)) } doReturn CACHED_JSON + } + } + } + + config.load( + context = context, + location = Location( + assetFilePath = "json/test-configuration.json", + remoteFileUrl = remoteUrl + ), + options = LoaderOptions() + ) + + assertThat(config.loadState.value).isEqualTo( + PathConfigurationLoadState.Loaded.CachedRemoteLoaded(load(CACHED_JSON)) + ) + } + + @Test + fun malformedCachedConfigurationFallsBackToBundled() { + val remoteUrl = "$url/demo/configurations/android-v1.json" + + val config = PathConfiguration().apply { + loader = PathConfigurationLoader().apply { + repository = mock { + on { getBundledConfiguration(any(), eq("json/test-configuration.json")) } doReturn BUNDLED_JSON + on { getCachedConfigurationForUrl(any(), eq(remoteUrl)) } doReturn "malformed-json" + } + } + } + + config.load( + context = context, + location = Location( + assetFilePath = "json/test-configuration.json", + remoteFileUrl = remoteUrl + ), + options = LoaderOptions() + ) + + assertThat(config.loadState.value).isEqualTo( + PathConfigurationLoadState.Loaded.BundledAssetLoaded(load(BUNDLED_JSON)) + ) + } + + @Test + fun noCachedConfigurationLoadsBundled() { + val remoteUrl = "$url/demo/configurations/android-v1.json" + + val config = PathConfiguration().apply { + loader = PathConfigurationLoader().apply { + repository = mock { + on { getBundledConfiguration(any(), eq("json/test-configuration.json")) } doReturn BUNDLED_JSON + on { getCachedConfigurationForUrl(any(), eq(remoteUrl)) } doReturn null + } + } + } + + config.load( + context = context, + location = Location( + assetFilePath = "json/test-configuration.json", + remoteFileUrl = remoteUrl + ), + options = LoaderOptions() + ) + + assertThat(config.loadState.value).isEqualTo( + PathConfigurationLoadState.Loaded.BundledAssetLoaded(load(BUNDLED_JSON)) + ) + } + + @Test + fun loadStateAndPropertiesStayInSync() { + val remoteUrl = "$url/demo/configurations/android-v1.json" + val config = PathConfiguration().apply { + loader = PathConfigurationLoader().apply { + repository = mock { + on { getCachedConfigurationForUrl(any(), eq(remoteUrl)) } doReturn CACHED_JSON + } + } + } + + config.load( + context = context, + location = Location(remoteFileUrl = remoteUrl), + options = LoaderOptions() + ) + + val state = config.loadState.value as PathConfigurationLoadState.Loaded.CachedRemoteLoaded + assertThat(state.configuration).isEqualTo(load(CACHED_JSON)) + assertThat(config.properties("$url/new").context).isEqualTo(PresentationContext.MODAL) + } + + private fun load(json: String): PathConfigurationData { + return json.toObject(object : TypeToken() {}) + } + // Extension functions to show support for deserializing custom properties/settings private fun PathConfigurationProperties.getTabs(): List? { @@ -212,4 +333,9 @@ class PathConfigurationTest : BaseRepositoryTest() { @SerializedName("marketing_site") val marketingSite: String, @SerializedName("demo_site") val demoSite: String ) + + companion object { + private const val BUNDLED_JSON = """{ "settings": {}, "rules": [{"patterns": [".+"], "properties": {"context": "default"}}] }""" + private const val CACHED_JSON = """{ "settings": {}, "rules": [{"patterns": [".+"], "properties": {"context": "default"}}, {"patterns": ["/new$"], "properties": {"context": "modal"}}] }""" + } } diff --git a/demo/src/main/kotlin/dev/hotwire/demo/DemoApplication.kt b/demo/src/main/kotlin/dev/hotwire/demo/DemoApplication.kt index c7595761..6d7505c3 100644 --- a/demo/src/main/kotlin/dev/hotwire/demo/DemoApplication.kt +++ b/demo/src/main/kotlin/dev/hotwire/demo/DemoApplication.kt @@ -24,15 +24,6 @@ class DemoApplication : Application() { } private fun configureApp() { - // Loads the path configuration - Hotwire.loadPathConfiguration( - context = this, - location = PathConfiguration.Location( - assetFilePath = "json/path-configuration.json", - remoteFileUrl = "${Demo.current.url}/configurations/android_v1.json" - ) - ) - // Set the default fragment destination Hotwire.defaultFragmentDestination = WebFragment::class @@ -56,5 +47,14 @@ class DemoApplication : Application() { Hotwire.config.webViewDebuggingEnabled = BuildConfig.DEBUG Hotwire.config.jsonConverter = KotlinXJsonConverter() Hotwire.config.applicationUserAgentPrefix = "Hotwire Demo;" + + // Loads the path configuration + Hotwire.loadPathConfiguration( + context = this, + location = PathConfiguration.Location( + assetFilePath = "json/path-configuration.json", + remoteFileUrl = "${Demo.current.url}/configurations/android_v1.json" + ) + ) } } diff --git a/navigation-fragments/src/test/kotlin/dev/hotwire/navigation/navigator/NavigatorRuleTest.kt b/navigation-fragments/src/test/kotlin/dev/hotwire/navigation/navigator/NavigatorRuleTest.kt index 4369d059..863f7a32 100644 --- a/navigation-fragments/src/test/kotlin/dev/hotwire/navigation/navigator/NavigatorRuleTest.kt +++ b/navigation-fragments/src/test/kotlin/dev/hotwire/navigation/navigator/NavigatorRuleTest.kt @@ -12,7 +12,7 @@ import androidx.navigation.navOptions import androidx.navigation.testing.TestNavHostController import androidx.navigation.ui.R import androidx.test.core.app.ApplicationProvider -import dev.hotwire.core.turbo.config.PathConfiguration +import dev.hotwire.core.config.Hotwire import dev.hotwire.core.turbo.config.PathConfiguration.Location import dev.hotwire.core.turbo.nav.Presentation import dev.hotwire.core.turbo.nav.PresentationContext @@ -33,7 +33,7 @@ import org.robolectric.annotation.Config class NavigatorRuleTest { private lateinit var context: Context private lateinit var controller: TestNavHostController - private lateinit var pathConfiguration: PathConfiguration + private val pathConfiguration get() = Hotwire.config.pathConfiguration private val homeUrl = "https://hotwired.dev/home" private val newHomeUrl = "https://hotwired.dev/new-home" @@ -72,13 +72,10 @@ class NavigatorRuleTest { fun setup() { context = ApplicationProvider.getApplicationContext() controller = buildControllerWithGraph() - pathConfiguration = PathConfiguration().apply { - load( - context = context, - location = Location(assetFilePath = "json/test-configuration.json"), - options = PathConfiguration.LoaderOptions() - ) - } + Hotwire.loadPathConfiguration( + context = context, + location = Location(assetFilePath = "json/test-configuration.json") + ) } @Test