From eb50a1d133c4e22184f452a4851c00403b3d6855 Mon Sep 17 00:00:00 2001 From: Jay Ohms Date: Wed, 5 Nov 2025 17:40:36 -0500 Subject: [PATCH 1/2] Fix restorationIdentifier + "restore" logic so that "replace" visit actions don't get unintentionally rewritten --- .../dev/hotwire/core/turbo/session/Session.kt | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/core/src/main/kotlin/dev/hotwire/core/turbo/session/Session.kt b/core/src/main/kotlin/dev/hotwire/core/turbo/session/Session.kt index 06fdd75..7cb9d91 100644 --- a/core/src/main/kotlin/dev/hotwire/core/turbo/session/Session.kt +++ b/core/src/main/kotlin/dev/hotwire/core/turbo/session/Session.kt @@ -623,25 +623,27 @@ class Session( // Private private fun visitLocation(visit: Visit) { - val restorationIdentifier = when (visit.options.action) { - VisitAction.RESTORE -> restorationIdentifiers[visit.destinationIdentifier] ?: "" - VisitAction.ADVANCE -> "" - else -> "" + val restorationIdentifier = if (visit.options.action == VisitAction.RESTORE) { + restorationIdentifiers[visit.destinationIdentifier] + } else { + null } - val options = when (restorationIdentifier) { - "" -> visit.options.copy(action = VisitAction.ADVANCE) - else -> visit.options + // Only initiate a restore visit if a restorationIdentifier is available + val options = if (visit.options.action == VisitAction.RESTORE && restorationIdentifier == null) { + visit.options.copy(action = VisitAction.ADVANCE) + } else { + visit.options } logEvent( "visitLocation", "location" to visit.location, "options" to options, - "restorationIdentifier" to restorationIdentifier + "restorationIdentifier" to (restorationIdentifier ?: "") ) - webView.visitLocation(visit.location, options, restorationIdentifier) + webView.visitLocation(visit.location, options, restorationIdentifier ?: "") } private fun visitLocationAsColdBoot(visit: Visit) { From 0fd112097ce087dc7555eea845ea7a0519ab22b2 Mon Sep 17 00:00:00 2001 From: Jay Ohms Date: Thu, 6 Nov 2025 09:59:42 -0500 Subject: [PATCH 2/2] Add Session tests to cover visits with restorationIdentifier logic --- .../hotwire/core/turbo/session/SessionTest.kt | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/core/src/test/kotlin/dev/hotwire/core/turbo/session/SessionTest.kt b/core/src/test/kotlin/dev/hotwire/core/turbo/session/SessionTest.kt index 9f7f58c..be30ed1 100644 --- a/core/src/test/kotlin/dev/hotwire/core/turbo/session/SessionTest.kt +++ b/core/src/test/kotlin/dev/hotwire/core/turbo/session/SessionTest.kt @@ -12,6 +12,7 @@ import dev.hotwire.core.turbo.errors.LoadError import dev.hotwire.core.turbo.errors.WebError import dev.hotwire.core.turbo.util.toJson import dev.hotwire.core.turbo.visit.Visit +import dev.hotwire.core.turbo.visit.VisitAction import dev.hotwire.core.turbo.visit.VisitDestination import dev.hotwire.core.turbo.visit.VisitOptions import dev.hotwire.core.turbo.webview.HotwireWebView @@ -290,6 +291,84 @@ class SessionTest : BaseRepositoryTest() { verify(callback, times(1)).visitCompleted(false) } + @Test + fun `restore visit with restoration identifier uses restore visit`() { + val visitIdentifier = "12345" + val restorationIdentifier = "67890" + val restoreVisit = visit.copy( + options = VisitOptions(action = VisitAction.RESTORE) + ) + + session.currentVisit = visit.copy(identifier = visitIdentifier) + session.turboIsReady(true) + session.pageLoaded(restorationIdentifier) + session.visit(restoreVisit) + + verify(webView, times(1)).visitLocation( + location = restoreVisit.location, + options = restoreVisit.options, + restorationIdentifier = "67890" + ) + } + + @Test + fun `restore visit with no restoration identifier uses advance visit`() { + val visitIdentifier = "12345" + val restoreVisit = visit.copy( + options = VisitOptions(action = VisitAction.RESTORE) + ) + + session.currentVisit = visit.copy(identifier = visitIdentifier) + session.turboIsReady(true) + session.visit(restoreVisit) + + verify(webView, times(1)).visitLocation( + location = restoreVisit.location, + options = restoreVisit.options.copy(action = VisitAction.ADVANCE), + restorationIdentifier = "" + ) + } + + @Test + fun `advance visit does not use restoration identifier`() { + val visitIdentifier = "12345" + val restorationIdentifier = "67890" + val advanceVisit = visit.copy( + options = VisitOptions(action = VisitAction.ADVANCE) + ) + + session.currentVisit = visit.copy(identifier = visitIdentifier) + session.turboIsReady(true) + session.pageLoaded(restorationIdentifier) + session.visit(advanceVisit) + + verify(webView, times(1)).visitLocation( + location = advanceVisit.location, + options = advanceVisit.options, + restorationIdentifier = "" + ) + } + + @Test + fun `replace visit does not use restoration identifier`() { + val visitIdentifier = "12345" + val restorationIdentifier = "67890" + val replaceVisit = visit.copy( + options = VisitOptions(action = VisitAction.REPLACE) + ) + + session.currentVisit = visit.copy(identifier = visitIdentifier) + session.turboIsReady(true) + session.pageLoaded(restorationIdentifier) + session.visit(replaceVisit) + + verify(webView, times(1)).visitLocation( + location = replaceVisit.location, + options = replaceVisit.options, + restorationIdentifier = "" + ) + } + @Test fun `restore current visit fails with session not ready`() { val visitIdentifier = "12345"