Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move EnsureSupportForCustomWebViewClients to Appium #24167

Merged
merged 2 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 174 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue16032.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
#if ANDROID
using Microsoft.Maui;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Controls.Internals;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Handlers;
using AWebView = Android.Webkit.WebView;
using IWebResourceRequest = Android.Webkit.IWebResourceRequest;
using Microsoft.Maui.Platform;
using WebResourceResponse = Android.Webkit.WebResourceResponse;

namespace Maui.Controls.Sample.Issues
{
[Preserve(AllMembers = true)]
[Issue(IssueTracker.Github, 16032, "Improve the customization of WebView on Android", PlatformAffected.Android)]
public class Issue16032 : ContentPage
{
public Issue16032()
{
Content = new VerticalStackLayout()
{
new Issue16032WebView
{
Background = new SolidPaint(Colors.Red),
WidthRequest = 300,
HeightRequest = 300
}
};
}

class Issue16032WebView : WebView, IPropertyMapperView
{
PropertyMapper<Issue16032WebView, WebViewHandler> TestMapper =
new PropertyMapper<Issue16032WebView, WebViewHandler>(WebViewHandler.Mapper);

public Issue16032WebView()
{
TestMapper.ModifyMapping(
"WebViewClient",
(handler, view, setter) =>
{
WebClient ??= new CustomWebClient((WebViewHandler)handler);
handler.PlatformView.SetWebViewClient(WebClient);
});

}

protected async override void OnHandlerChanged()
{
if (Handler is not WebViewHandler webViewHandler)
{
return;
}

var platformWebView = webViewHandler.PlatformView;
platformWebView.Settings.AllowFileAccess = true;

Source = new UrlWebViewSource { Url = "extracontent.html" };

var tcsLoaded = new TaskCompletionSource<bool>();
var tcsNavigating = new TaskCompletionSource();
var tcsRequested = new TaskCompletionSource();

// if the timeout happens, cancel everything
var pageLoadTimeout = TimeSpan.FromSeconds(30);
var ctsTimeout = new CancellationTokenSource(pageLoadTimeout);
ctsTimeout.Token.Register(() =>
{
tcsRequested.TrySetException(new TimeoutException($"Failed to request the image"));
tcsNavigating.TrySetException(new TimeoutException($"Failed to navigate to the loaded page"));
tcsLoaded.TrySetException(new TimeoutException($"Failed to load HTML"));
});

// attach some event handlers to track things
var navigatingCount = 0;
Navigating += ((sender, args) =>
{
navigatingCount++;

if (args.Url == "file:///android_asset/extracontent.html")
tcsNavigating.TrySetResult();
});
var shouldRequestCount = 0;
ShouldInterceptRequestDelegate = new((view, request) =>
{
shouldRequestCount++;

if (request.Url.ToString().StartsWith("https://raw.githubusercontent.com/dotnet/maui/4c096c1f17e9a23bf3961ba5778d3936039ad881/Assets/icon.png"))
tcsRequested.TrySetResult();
});

// set up a task to wait for the page to load
Navigated += (sender, args) =>
{
// Set success when we have a successful nav result
if (args.Result == WebNavigationResult.Success && args.Url == "file:///android_asset/extracontent.html")
tcsLoaded.TrySetResult(args.Result == WebNavigationResult.Success);
};

string failureMessage = String.Empty;

try
{

if (WebClient is not CustomWebClient)
{
throw new Exception("CustomWebClient was not set");
}

// wait for the navigation to complete
await tcsNavigating.Task;
if (navigatingCount < 1)
{
throw new Exception("Navigating event did not fire");
}

if ((!await tcsLoaded.Task))
{
throw new Exception("HTML Source Failed to Load");
}

// wait for the image to be requested
await tcsRequested.Task;

if (shouldRequestCount != 1)
{
throw new Exception("only 1 request for the image to load");
}
}
catch(Exception ex)
{
failureMessage = ex.Message;
}

if (String.IsNullOrEmpty(failureMessage))
{
((VerticalStackLayout)Parent).Insert(0, new Label { Text = "All Expectations Have Been Met", AutomationId = "Success" });
}
else
{
((VerticalStackLayout)Parent).Insert(0, new Label { Text = failureMessage });
}
}

PropertyMapper IPropertyMapperView.GetPropertyMapperOverrides() => TestMapper;

CustomWebClient WebClient { get; set; }

public Action<AWebView, IWebResourceRequest> ShouldInterceptRequestDelegate { get; set; }

class CustomWebClient : MauiWebViewClient
{
WebViewHandler _handler;

public CustomWebClient(WebViewHandler handler)
: base(handler)
{
_handler = handler;
}

public override WebResourceResponse ShouldInterceptRequest(AWebView view, IWebResourceRequest request)
{
if (_handler.VirtualView is Issue16032WebView customWebView)
customWebView.ShouldInterceptRequestDelegate(view, request);

return base.ShouldInterceptRequest(view, request);
}
}
}
}
}


#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#if ANDROID
using NUnit.Framework;
using UITest.Appium;
using UITest.Core;

namespace Microsoft.Maui.TestCases.Tests.Issues
{
public class Issue16032 : _IssuesUITest
{
public Issue16032(TestDevice device)
: base(device)
{ }

public override string Issue => "Improve the customization of WebView on Android";

[Test]
[Category(UITestCategories.WebView)]
public void EnsureSupportForCustomWebViewClients()
{
App.WaitForElement("Success");
}
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -10,145 +10,10 @@ namespace Microsoft.Maui.DeviceTests
{
public partial class WebViewHandlerTests
{
[Fact]
public Task EnsureSupportForCustomWebViewClients() =>
InvokeOnMainThreadAsync(async () =>
{
// create the cross-platform view
var webView = new WebViewStub
{
Width = 300,
Height = 200,
Background = new SolidPaint(Colors.Red),
};

// create the platform view
await AttachAndRun<CustomWebViewHandler>(webView, async webViewHandler =>
{
var platformWebView = webViewHandler.PlatformView;

var tcsLoaded = new TaskCompletionSource<bool>();
var tcsNavigating = new TaskCompletionSource();
var tcsRequested = new TaskCompletionSource();

// if the timeout happens, cancel everything
var pageLoadTimeout = TimeSpan.FromSeconds(30);
var ctsTimeout = new CancellationTokenSource(pageLoadTimeout);
ctsTimeout.Token.Register(() =>
{
tcsLoaded.TrySetException(new TimeoutException($"Failed to load HTML"));
tcsNavigating.TrySetException(new TimeoutException($"Failed to navigate to the loaded page"));
tcsRequested.TrySetException(new TimeoutException($"Failed to request the image"));
});

// attach some event handlers to track things
var navigatingCount = 0;
webView.NavigatingDelegate = new((evnt, url) =>
{
navigatingCount++;

if (url == "file:///android_asset/extracontent.html")
tcsNavigating.TrySetResult();

return false; // do not cancel the navigation
});
var shouldRequestCount = 0;
webViewHandler.ShouldInterceptRequestDelegate = new((view, request) =>
{
shouldRequestCount++;

if (request.Url.ToString().StartsWith("https://raw.githubusercontent.com/dotnet/maui/4c096c1f17e9a23bf3961ba5778d3936039ad881/Assets/icon.png"))
tcsRequested.TrySetResult();
});

// set up a task to wait for the page to load
webView.NavigatedDelegate = (evnt, url, result) =>
{
// Set success when we have a successful nav result
if (result == WebNavigationResult.Success && url == "file:///android_asset/extracontent.html")
tcsLoaded.TrySetResult(result == WebNavigationResult.Success);
};

// load the page
webView.Source = new UrlWebViewSourceStub { Url = "extracontent.html" };
webViewHandler.UpdateValue(nameof(IWebView.Source));

// wait for the loaded event
Assert.True(await tcsLoaded.Task, "HTML Source Failed to Load");

// make sure the mapper override fired at least once
Assert.IsType<CustomWebClient>(webViewHandler.CustomWebClient);

// wait for the navigation to complete
await tcsNavigating.Task;
Assert.True(navigatingCount > 1); // at least 1 navigation, Android seems to do a few

// wait for the image to be requested
await tcsRequested.Task;
Assert.Equal(1, shouldRequestCount); // only 1 request for the image to load
});
});

AWebView GetNativeWebView(WebViewHandler webViewHandler) =>
webViewHandler.PlatformView;

string GetNativeSource(WebViewHandler webViewHandler) =>
GetNativeWebView(webViewHandler).Url;

class CustomWebViewHandler : WebViewHandler
{
// make a copy of the Core mappers because we don't want any Controls changes or to override us
static IPropertyMapper<IWebView, IWebViewHandler> TestMapper =
new PropertyMapper<IWebView, IWebViewHandler>(WebViewHandler.Mapper);
static CommandMapper<IWebView, IWebViewHandler> TestCommandMapper =
new(WebViewHandler.CommandMapper);

static CustomWebViewHandler()
{
// this is part of the test: testing the modify/replace the existing mapper
TestMapper.ModifyMapping(
nameof(WebViewClient),
(handler, view, setter) =>
{
if (handler is not CustomWebViewHandler custom)
throw new Exception("The CustomWebViewHandler.TestMapper is only meant to be used with the CustomWebViewHandler tests.");

if (custom.CustomWebClient is not null)
throw new Exception("The [WebViewClient] mapper method is only supposed to be called once.");

custom.CustomWebClient = new CustomWebClient((CustomWebViewHandler)handler);

handler.PlatformView.SetWebViewClient(custom.CustomWebClient);
});
}

// make sure to use the Core mappers
public CustomWebViewHandler()
: base(TestMapper, TestCommandMapper)
{
}

public CustomWebClient CustomWebClient { get; private set; }

public Action<AWebView, IWebResourceRequest> ShouldInterceptRequestDelegate { get; set; }
}

class CustomWebClient : MauiWebViewClient
{
CustomWebViewHandler _handler;

public CustomWebClient(CustomWebViewHandler handler)
: base(handler)
{
_handler = handler;
}

public override WebResourceResponse ShouldInterceptRequest(AWebView view, IWebResourceRequest request)
{
_handler.ShouldInterceptRequestDelegate(view, request);

return base.ShouldInterceptRequest(view, request);
}
}
}
}
Loading