diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue35788.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue35788.cs new file mode 100644 index 000000000000..12c95524a5b8 --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue35788.cs @@ -0,0 +1,51 @@ +namespace Maui.Controls.Sample.Issues; + +[Issue(IssueTracker.Github, 35788, "[Android] WebView CanGoBack returns true unexpectedly on first page due to spurious about:blank history entry", PlatformAffected.Android)] +public class Issue35788 : ContentPage +{ + const string StatusLabelId = "Issue35788StatusLabel"; + const string NavigateButtonId = "Issue35788NavigateButton"; + + readonly Label _statusLabel; + readonly WebView _webView; + + public Issue35788() + { + _statusLabel = new Label + { + AutomationId = StatusLabelId, + Text = "Waiting" + }; + + _webView = new WebView + { + HeightRequest = 300 + }; + + _webView.Navigated += OnWebViewNavigated; + + var navigateButton = new Button + { + AutomationId = NavigateButtonId, + Text = "Load Page" + }; + + navigateButton.Clicked += (s, e) => + _webView.Source = new HtmlWebViewSource { Html = "

Hello

" }; + + Content = new VerticalStackLayout + { + Padding = 20, + Spacing = 10, + Children = { navigateButton, _statusLabel, _webView } + }; + } + + void OnWebViewNavigated(object sender, WebNavigatedEventArgs e) + { + if (e.Result == WebNavigationResult.Success) + _statusLabel.Text = _webView.CanGoBack ? "CanGoBack=True" : "CanGoBack=False"; + else + _statusLabel.Text = $"NavFailed:{e.Result}"; + } +} diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue35788.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue35788.cs new file mode 100644 index 000000000000..42c5dadc5025 --- /dev/null +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue35788.cs @@ -0,0 +1,32 @@ +#if ANDROID +using NUnit.Framework; +using UITest.Appium; +using UITest.Core; + +namespace Microsoft.Maui.TestCases.Tests.Issues; + +public class Issue35788 : _IssuesUITest +{ + public Issue35788(TestDevice device) : base(device) + { + } + + public override string Issue => "[Android] WebView CanGoBack returns true unexpectedly on first page due to spurious about:blank history entry"; + + [Test] + [Category(UITestCategories.WebView)] + public void WebViewCanGoBackShouldBeFalseOnFirstPage() + { + App.WaitForElement("Issue35788NavigateButton"); + App.Tap("Issue35788NavigateButton"); + + App.WaitForTextToBePresentInElement("Issue35788StatusLabel", "CanGoBack="); + + var statusText = App.FindElement("Issue35788StatusLabel").GetText(); + + Assert.That(statusText, Is.EqualTo("CanGoBack=False"), + "WebView.CanGoBack should be false on the first navigated page. " + + "If true, the about:blank layout entry was not cleared from the native history stack."); + } +} +#endif diff --git a/src/Core/src/Handlers/WebView/WebViewHandler.Android.cs b/src/Core/src/Handlers/WebView/WebViewHandler.Android.cs index 12fbc975f6d7..4dff350474a7 100644 --- a/src/Core/src/Handlers/WebView/WebViewHandler.Android.cs +++ b/src/Core/src/Handlers/WebView/WebViewHandler.Android.cs @@ -64,6 +64,14 @@ protected override void DisconnectHandler(AWebView platformView) webChromeClient.Disconnect(); } + // Reset layout flag so a stale true value does not trigger ClearHistory() + // if this handler is re-connected (e.g., Shell tab switch). (#35788) + if (platformView is MauiWebView mauiWebView) + { + mauiWebView.IsLoadingForLayout = false; + } + + platformView.SetWebViewClient(null!); platformView.SetWebChromeClient(null); platformView.StopLoading(); diff --git a/src/Core/src/Platform/Android/MauiWebView.cs b/src/Core/src/Platform/Android/MauiWebView.cs index 5ccc05cdfeef..c1abe5859adb 100644 --- a/src/Core/src/Platform/Android/MauiWebView.cs +++ b/src/Core/src/Platform/Android/MauiWebView.cs @@ -21,6 +21,11 @@ public class MauiWebView : WebView, IWebViewDelegate // https://github.com/dotnet/maui/issues/35771 bool _isAutoSizing; + // Tracks whether about:blank was loaded synthetically for layout (null source). + // MauiWebViewClient clears this entry from the native back stack once the real URL loads, + // preventing CanGoBack() returning true unexpectedly. Fixes #35788. + internal bool IsLoadingForLayout { get; set; } + public MauiWebView(WebViewHandler handler, Context context) : base(context) { _handler = handler ?? throw new ArgumentNullException(nameof(handler)); diff --git a/src/Core/src/Platform/Android/MauiWebViewClient.cs b/src/Core/src/Platform/Android/MauiWebViewClient.cs index aa554dc0e2d5..260f59ba44bd 100644 --- a/src/Core/src/Platform/Android/MauiWebViewClient.cs +++ b/src/Core/src/Platform/Android/MauiWebViewClient.cs @@ -56,20 +56,42 @@ public override void OnPageStarted(WebView? view, string? url, Bitmap? favicon) public override void OnPageFinished(WebView? view, string? url) { if (!_handler.TryGetTarget(out var handler) || handler.VirtualView == null || string.IsNullOrWhiteSpace(url)) + { return; + } bool navigate = _navigationResult != WebNavigationResult.Failure || !GetValidUrl(url).Equals(_lastUrlNavigatedCancel, StringComparison.OrdinalIgnoreCase); _lastUrlNavigatedCancel = _navigationResult == WebNavigationResult.Cancel ? url : null; + var mauiWebView = view as MauiWebView; + bool isLayoutLoad = mauiWebView?.IsLoadingForLayout == true; + // Skip Navigated event for about:blank to prevent unwanted events when Source is null if (navigate && !IsBlankNavigation(url)) { - handler.VirtualView.Navigated(handler.CurrentNavigationEvent, GetValidUrl(url), _navigationResult); + // Clear the synthetic about:blank entry (loaded for layout, see #32030) now that + // the real URL is current. ClearHistory() removes all entries except the current + // page, ensuring CanGoBack() is already false when Navigated fires (#35788). + if (isLayoutLoad) + { + mauiWebView!.ClearHistory(); + mauiWebView!.IsLoadingForLayout = false; + // Called BEFORE Navigated fires so user handlers observe CanGoBack=false immediately. + handler?.PlatformView?.UpdateCanGoBackForward(handler.VirtualView); + } + + handler!.VirtualView.Navigated(handler.CurrentNavigationEvent, GetValidUrl(url), _navigationResult); + } + else if (isLayoutLoad && (_navigationResult == WebNavigationResult.Failure || _navigationResult == WebNavigationResult.Cancel)) + { + // Navigation failed or canceled — reset the layout flag so a subsequent successful load + // does not incorrectly trigger ClearHistory() (#35788). + mauiWebView!.IsLoadingForLayout = false; } handler.SyncPlatformCookiesToVirtualView(url); - handler?.PlatformView.UpdateCanGoBackForward(handler.VirtualView); + handler?.PlatformView?.UpdateCanGoBackForward(handler.VirtualView); // Only inject the scroll-capture observer when the WebView is hosted inside // a RefreshView – avoids unnecessary JS overhead for standalone WebViews. diff --git a/src/Core/src/Platform/Android/WebViewExtensions.cs b/src/Core/src/Platform/Android/WebViewExtensions.cs index f09cd7397f23..448248e7f5e2 100644 --- a/src/Core/src/Platform/Android/WebViewExtensions.cs +++ b/src/Core/src/Platform/Android/WebViewExtensions.cs @@ -27,8 +27,16 @@ public static void UpdateSource(this AWebView platformWebView, IWebView webView, } else { - // Load about:blank when source is null to ensure proper layout bounds - platformWebView.LoadUrl("about:blank"); + // Load about:blank to constrain the WebView within its layout bounds when source + // is null, preventing overflow in grid cells (#32030). Flag IsLoadingForLayout so + // MauiWebViewClient can remove this synthetic entry from the native back stack + // once the real URL loads, fixing CanGoBack() returning true unexpectedly (#35788). + // Guard: skip when real history exists so ClearHistory() does not destroy back entries. + if (platformWebView is MauiWebView mauiWebView && !platformWebView.CanGoBack() && !platformWebView.CanGoForward()) + { + mauiWebView.IsLoadingForLayout = true; + platformWebView.LoadUrl("about:blank"); + } } }